Patch visualizer redesign (#883)

This commit is contained in:
boatbomber
2024-04-02 00:04:58 -07:00
committed by GitHub
parent 87920964d7
commit b2f133e6f1
16 changed files with 690 additions and 312 deletions

View File

@@ -0,0 +1,126 @@
local StudioService = game:GetService("StudioService")
local AssetService = game:GetService("AssetService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = require(Plugin.App.Components.EditableImage)
local imageCache = {}
local function getImageSizeAndPixels(image)
if not imageCache[image] then
local editableImage = AssetService:CreateEditableImageAsync(image)
imageCache[image] = {
Size = editableImage.Size,
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
}
end
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
end
local function getRecoloredClassIcon(className, color)
local iconProps = StudioService:GetClassIcon(className)
if iconProps and color then
local success, editableImageSize, editableImagePixels = pcall(function()
local size, pixels = getImageSizeAndPixels(iconProps.Image)
local minVal, maxVal = math.huge, -math.huge
for i = 1, #pixels, 4 do
if pixels[i + 3] == 0 then
continue
end
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
minVal = math.min(minVal, pixelVal)
maxVal = math.max(maxVal, pixelVal)
end
local hue, sat, val = color:ToHSV()
for i = 1, #pixels, 4 do
if pixels[i + 3] == 0 then
continue
end
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
local newVal = val
if minVal < maxVal then
-- Remap minVal - maxVal to val*0.9 - val
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
end
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
end
return size, pixels
end)
if success then
iconProps.EditableImagePixels = editableImagePixels
iconProps.EditableImageSize = editableImageSize
end
end
return iconProps
end
local ClassIcon = Roact.PureComponent:extend("ClassIcon")
function ClassIcon:init()
self.state = {
iconProps = nil,
}
end
function ClassIcon:updateIcon()
local props = self.props
local iconProps = getRecoloredClassIcon(props.className, props.color)
self:setState({
iconProps = iconProps,
})
end
function ClassIcon:didMount()
self:updateIcon()
end
function ClassIcon:didUpdate(lastProps)
if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
self:updateIcon()
end
end
function ClassIcon:render()
local iconProps = self.state.iconProps
if not iconProps then
return nil
end
return e(
"ImageLabel",
{
Size = self.props.size,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
AnchorPoint = self.props.anchorPoint,
ImageTransparency = self.props.transparency,
Image = iconProps.Image,
ImageRectOffset = iconProps.ImageRectOffset,
ImageRectSize = iconProps.ImageRectSize,
BackgroundTransparency = 1,
},
if iconProps.EditableImagePixels
then e(EditableImage, {
size = iconProps.EditableImageSize,
pixels = iconProps.EditableImagePixels,
})
else nil
)
end
return ClassIcon

View File

@@ -0,0 +1,41 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = Roact.PureComponent:extend("EditableImage")
function EditableImage:init()
self.ref = Roact.createRef()
end
function EditableImage:writePixels()
local image = self.ref.current
if not image then
return
end
if not self.props.pixels then
return
end
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
end
function EditableImage:render()
return e("EditableImage", {
Size = self.props.size,
[Roact.Ref] = self.ref,
})
end
function EditableImage:didMount()
self:writePixels()
end
function EditableImage:didUpdate()
self:writePixels()
end
return EditableImage

View File

@@ -155,7 +155,7 @@ function ChangeList:render()
local headerRow = changes[1]
local headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
Size = UDim2.new(1, 0, 0, 24),
BackgroundTransparency = rowTransparency,
BackgroundColor3 = theme.Diff.Row,
LayoutOrder = 0,
@@ -214,7 +214,7 @@ function ChangeList:render()
local isWarning = metadata.isWarning
rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
Size = UDim2.new(1, 0, 0, 24),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0,
@@ -269,8 +269,8 @@ function ChangeList:render()
}, {
Headers = headers,
Values = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -30),
position = UDim2.new(0, 0, 0, 30),
size = UDim2.new(1, 0, 1, -24),
position = UDim2.new(0, 0, 0, 24),
contentSize = self.contentSize,
transparency = props.transparency,
}, rows),

View File

@@ -1,5 +1,4 @@
local SelectionService = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
@@ -15,7 +14,8 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList)
local Tooltip = require(script.Parent.Parent.Tooltip)
local Tooltip = require(Plugin.App.Components.Tooltip)
local ClassIcon = require(Plugin.App.Components.ClassIcon)
local Expansion = Roact.Component:extend("Expansion")
@@ -28,8 +28,8 @@ function Expansion:render()
return e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -props.indent, 1, -30),
Position = UDim2.new(0, props.indent, 0, 30),
Size = UDim2.new(1, -props.indent, 1, -24),
Position = UDim2.new(0, props.indent, 0, 24),
}, {
ChangeList = e(ChangeList, {
changes = props.changeList,
@@ -44,7 +44,7 @@ local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init()
local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 30
self.expanded = initHeight > 24
self.motor = Flipper.SingleMotor.new(initHeight)
self.binding = bindingUtil.fromMotor(self.motor)
@@ -53,7 +53,7 @@ function DomLabel:init()
renderExpansion = self.expanded,
})
self.motor:onStep(function(value)
local renderExpansion = value > 30
local renderExpansion = value > 24
self.props.setElementHeight(value)
if self.props.updateEvent then
@@ -81,7 +81,7 @@ function DomLabel:didUpdate(prevProps)
then
-- Close the expansion when the domlabel is changed to a different thing
self.expanded = false
self.motor:setGoal(Flipper.Spring.new(30, {
self.motor:setGoal(Flipper.Spring.new(24, {
frequency = 5,
dampingRatio = 1,
}))
@@ -90,17 +90,49 @@ end
function DomLabel:render()
local props = self.props
local depth = props.depth or 1
return Theme.with(function(theme)
local iconProps = StudioService:GetClassIcon(props.className)
local indent = (props.depth or 0) * 20 + 25
local color = if props.isWarning
then theme.Diff.Warning
elseif props.patchType then theme.Diff[props.patchType]
else theme.TextColor
local indent = (depth - 1) * 12 + 15
-- Line guides help indent depth remain readable
local lineGuides = {}
for i = 1, props.depth or 0 do
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 1, 2),
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
for i = 2, depth do
if props.depthsComplete[i] then
continue
end
if props.isFinalChild and i == depth then
-- This line stops halfway down to merge with our connector for the right angle
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 0, 15),
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
else
-- All other lines go all the way
-- with the exception of the final element, which stops halfway down
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
end
end
if depth ~= 1 then
lineGuides["Connector"] = e("Frame", {
Size = UDim2.new(0, 8, 0, 2),
Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
AnchorPoint = Vector2.xAxis,
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
@@ -109,9 +141,8 @@ function DomLabel:render()
return e("Frame", {
ClipsDescendants = true,
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
BorderSizePixel = 0,
BackgroundTransparency = props.patchType and props.transparency or 1,
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
BackgroundColor3 = theme.Diff.Row,
Size = self.binding:map(function(expand)
return UDim2.new(1, 0, 0, expand)
end),
@@ -141,8 +172,8 @@ function DomLabel:render()
if props.changeList then
self.expanded = not self.expanded
local goalHeight = 30
+ (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
local goalHeight = 24
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5,
dampingRatio = 1,
@@ -174,40 +205,74 @@ function DomLabel:render()
DiffIcon = if props.patchType
then e("ImageLabel", {
Image = Assets.Images.Diff[props.patchType],
ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageColor3 = color,
ImageTransparency = props.transparency,
BackgroundTransparency = 1,
Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, 0, 0, 15),
Size = UDim2.new(0, 14, 0, 14),
Position = UDim2.new(0, 0, 0, 12),
AnchorPoint = Vector2.new(0, 0.5),
})
else nil,
ClassIcon = e("ImageLabel", {
Image = iconProps.Image,
ImageTransparency = props.transparency,
ImageRectOffset = iconProps.ImageRectOffset,
ImageRectSize = iconProps.ImageRectSize,
BackgroundTransparency = 1,
Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, indent, 0, 15),
AnchorPoint = Vector2.new(0, 0.5),
ClassIcon = e(ClassIcon, {
className = props.className,
color = color,
transparency = props.transparency,
size = UDim2.new(0, 16, 0, 16),
position = UDim2.new(0, indent + 2, 0, 12),
anchorPoint = Vector2.new(0, 0.5),
}),
InstanceName = e("TextLabel", {
Text = (if props.isWarning then "" else "") .. props.name .. (props.hint and string.format(
' <font color="#%s">%s</font>',
theme.AddressEntry.PlaceholderColor:ToHex(),
props.hint
) or ""),
Text = (if props.isWarning then "" else "") .. props.name,
RichText = true,
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = if props.isWarning then theme.Diff.Warning else theme.TextColor,
TextColor3 = color,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, -indent - 50, 0, 30),
Position = UDim2.new(0, indent + 30, 0, 0),
Size = UDim2.new(1, -indent - 50, 0, 24),
Position = UDim2.new(0, indent + 22, 0, 0),
}),
ChangeInfo = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -indent - 80, 0, 24),
Position = UDim2.new(1, -2, 0, 0),
AnchorPoint = Vector2.new(1, 0),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
}),
Edits = if props.changeInfo and props.changeInfo.edits
then e("TextLabel", {
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
BackgroundTransparency = 1,
Font = Enum.Font.Gotham,
TextSize = 14,
TextColor3 = theme.SubTextColor,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, 16),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 2,
})
else nil,
Failed = if props.changeInfo and props.changeInfo.failed
then e("TextLabel", {
Text = props.changeInfo.failed,
BackgroundTransparency = 1,
Font = Enum.Font.Gotham,
TextSize = 14,
TextColor3 = theme.Diff.Warning,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, 16),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 6,
})
else nil,
}),
LineGuides = e("Folder", nil, lineGuides),
})

View File

@@ -8,8 +8,8 @@ local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet)
local Theme = require(Plugin.App.Theme)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement
@@ -55,34 +55,60 @@ function PatchVisualizer:render()
end
-- Recusively draw tree
local scrollElements, elementHeights = {}, {}
local scrollElements, elementHeights, elementIndex = {}, {}, 0
if patchTree then
local elementTotal = patchTree:getCount()
local depthsComplete = {}
local function drawNode(node, depth)
local elementHeight, setElementHeight = Roact.createBinding(30)
table.insert(elementHeights, elementHeight)
table.insert(
scrollElements,
e(DomLabel, {
updateEvent = self.updateEvent,
elementHeight = elementHeight,
setElementHeight = setElementHeight,
patchType = node.patchType,
className = node.className,
isWarning = node.isWarning,
instance = node.instance,
name = node.name,
hint = node.hint,
changeList = node.changeList,
depth = depth,
transparency = self.props.transparency,
showStringDiff = self.props.showStringDiff,
showTableDiff = self.props.showTableDiff,
})
)
elementIndex += 1
local parentNode = patchTree:getNode(node.parentId)
local isFinalChild = true
if parentNode then
for _id, sibling in parentNode.children do
if type(sibling) == "table" and sibling.name and sibling.name > node.name then
isFinalChild = false
break
end
end
end
local elementHeight, setElementHeight = Roact.createBinding(24)
elementHeights[elementIndex] = elementHeight
scrollElements[elementIndex] = e(DomLabel, {
transparency = self.props.transparency,
showStringDiff = self.props.showStringDiff,
showTableDiff = self.props.showTableDiff,
updateEvent = self.updateEvent,
elementHeight = elementHeight,
setElementHeight = setElementHeight,
elementIndex = elementIndex,
isFinalElement = elementIndex == elementTotal,
depth = depth,
depthsComplete = table.clone(depthsComplete),
hasChildren = (node.children ~= nil and next(node.children) ~= nil),
isFinalChild = isFinalChild,
patchType = node.patchType,
className = node.className,
isWarning = node.isWarning,
instance = node.instance,
name = node.name,
changeInfo = node.changeInfo,
changeList = node.changeList,
})
if isFinalChild then
depthsComplete[depth] = true
end
end
patchTree:forEach(function(node, depth)
depthsComplete[depth] = false
for i = depth + 1, #depthsComplete do
depthsComplete[i] = nil
end
drawNode(node, depth)
end)
end
@@ -92,6 +118,7 @@ function PatchVisualizer:render()
transparency = self.props.transparency,
size = self.props.size,
position = self.props.position,
anchorPoint = self.props.anchorPoint,
layoutOrder = self.props.layoutOrder,
}, {
CleanMerge = e("TextLabel", {
@@ -106,7 +133,8 @@ function PatchVisualizer:render()
}),
VirtualScroller = e(VirtualScroller, {
size = UDim2.new(1, 0, 1, 0),
size = UDim2.new(1, 0, 1, -2),
position = UDim2.new(0, 0, 0, 2),
transparency = self.props.transparency,
count = #scrollElements,
updateEvent = self.updateEvent.Event,

View File

@@ -131,8 +131,8 @@ function VirtualScroller:render()
Position = props.position,
AnchorPoint = props.anchorPoint,
BackgroundTransparency = props.backgroundTransparency or 1,
BackgroundColor3 = props.backgroundColor3,
BorderColor3 = props.borderColor3,
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
CanvasSize = self.totalCanvas:map(function(s)
return UDim2.fromOffset(0, s)
end),

View File

@@ -9,7 +9,6 @@ local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
local Header = require(Plugin.App.Components.Header)
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
@@ -60,17 +59,11 @@ end
function ConfirmingPage:render()
return Theme.with(function(theme)
local pageContent = Roact.createFragment({
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
}),
Title = e("TextLabel", {
Text = string.format(
"Sync changes for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
),
LayoutOrder = 2,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
@@ -82,7 +75,7 @@ function ConfirmingPage:render()
}),
PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, -150),
size = UDim2.new(1, 0, 1, -100),
transparency = self.props.transparency,
layoutOrder = 3,
@@ -155,6 +148,11 @@ function ConfirmingPage:render()
}),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
@@ -163,11 +161,6 @@ function ConfirmingPage:render()
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
StringDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingStringDiff",
title = "String diff",

View File

@@ -3,9 +3,7 @@ local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local bindingUtil = require(Plugin.App.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local PatchSet = require(Plugin.PatchSet)
@@ -23,28 +21,20 @@ local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
local e = Roact.createElement
local AGE_UNITS = {
{ 31556909, "year" },
{ 2629743, "month" },
{ 604800, "week" },
{ 86400, "day" },
{ 3600, "hour" },
{
60,
"minute",
},
{ 31556909, "y" },
{ 2629743, "mon" },
{ 604800, "w" },
{ 86400, "d" },
{ 3600, "h" },
{ 60, "m" },
}
function timeSinceText(elapsed: number): string
if elapsed < 3 then
return "just now"
end
local ageText = string.format("%d seconds ago", elapsed)
local ageText = string.format("%ds", elapsed)
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
local c = math.floor(elapsed / UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
ageText = elapsed // UnitSeconds .. UnitName
break
end
end
@@ -52,49 +42,179 @@ function timeSinceText(elapsed: number): string
return ageText
end
local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
local ChangesViewer = Roact.Component:extend("ChangesViewer")
function ChangesDrawer:init()
function ChangesViewer:init()
-- Hold onto the serve session during the lifecycle of this component
-- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession
end
function ChangesDrawer:render()
if self.props.rendered == false or self.serveSession == nil then
function ChangesViewer:render()
if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then
return nil
end
local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = self.props.transparency,
size = self.props.height:map(function(y)
return UDim2.new(1, 0, y, -220 * y)
end),
position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1),
layoutOrder = self.props.layoutOrder,
}, {
Close = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0, 0),
anchorPoint = Vector2.new(1, 0),
onClick = self.props.onClose,
return Roact.createFragment({
Navbar = e("Frame", {
Size = UDim2.new(1, 0, 0, 40),
BackgroundTransparency = 1,
}, {
Tip = e(Tooltip.Trigger, {
text = "Close the patch visualizer",
Close = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.Settings.Navbar.BackButtonColor,
transparency = self.props.transparency,
position = UDim2.new(0, 0, 0.5, 0),
anchorPoint = Vector2.new(0, 0.5),
onClick = self.props.onBack,
}, {
Tip = e(Tooltip.Trigger, {
text = "Close",
}),
}),
Title = e("TextLabel", {
Text = "Sync",
Font = Enum.Font.GothamMedium,
TextSize = 17,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = theme.TextColor,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, -40, 0, 20),
Position = UDim2.new(0, 40, 0, 0),
BackgroundTransparency = 1,
}),
Subtitle = e("TextLabel", {
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
TextXAlignment = Enum.TextXAlignment.Left,
Font = Enum.Font.Gotham,
TextSize = 15,
TextColor3 = theme.SubTextColor,
TextTruncate = Enum.TextTruncate.AtEnd,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, -40, 0, 16),
Position = UDim2.new(0, 40, 0, 20),
BackgroundTransparency = 1,
}),
Info = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 10, 0, 24),
AutomaticSize = Enum.AutomaticSize.X,
Position = UDim2.new(1, -5, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
}, {
Tooltip = e(Tooltip.Trigger, {
text = `{applied} changes applied`
.. (if unapplied > 0 then `, {unapplied} changes failed` else ""),
}),
Content = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
}),
StatusIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = if unapplied > 0
then Assets.Images.Icons.SyncWarning
else Assets.Images.Icons.SyncSuccess,
ImageColor3 = if unapplied > 0 then theme.Diff.Warning else theme.TextColor,
Size = UDim2.new(0, 24, 0, 24),
LayoutOrder = 10,
}),
StatusSpacer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 6, 0, 4),
LayoutOrder = 9,
}),
AppliedIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = Assets.Images.Icons.Checkmark,
ImageColor3 = theme.TextColor,
Size = UDim2.new(0, 16, 0, 16),
LayoutOrder = 1,
}),
AppliedText = e("TextLabel", {
Text = applied,
Font = Enum.Font.Gotham,
TextSize = 15,
TextColor3 = theme.TextColor,
TextTransparency = self.props.transparency,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Warnings = if unapplied > 0
then Roact.createFragment({
WarningsSpacer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 4, 0, 4),
LayoutOrder = 3,
}),
UnappliedIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = Assets.Images.Icons.Exclamation,
ImageColor3 = theme.Diff.Warning,
Size = UDim2.new(0, 4, 0, 16),
LayoutOrder = 4,
}),
UnappliedText = e("TextLabel", {
Text = unapplied,
Font = Enum.Font.Gotham,
TextSize = 15,
TextColor3 = theme.Diff.Warning,
TextTransparency = self.props.transparency,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
LayoutOrder = 5,
}),
})
else nil,
}),
}),
Divider = e("Frame", {
BackgroundColor3 = theme.Settings.DividerColor,
BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, 0),
BorderSizePixel = 0,
}, {
Gradient = e("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.1, 0),
NumberSequenceKeypoint.new(0.9, 0),
NumberSequenceKeypoint.new(1, 1),
}),
}),
}),
}),
PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, 0),
Patch = e(PatchVisualizer, {
size = UDim2.new(1, -10, 1, -65),
position = UDim2.new(0, 5, 1, -5),
anchorPoint = Vector2.new(0, 1),
transparency = self.props.transparency,
layoutOrder = 3,
layoutOrder = self.props.layoutOrder,
patchTree = self.props.patchTree,
@@ -167,20 +287,7 @@ function ConnectedPage:getChangeInfoText()
if patchData == nil then
return ""
end
local elapsed = os.time() - patchData.timestamp
local unapplied = PatchSet.countChanges(patchData.unapplied)
return "<i>Synced "
.. timeSinceText(elapsed)
.. (if unapplied > 0
then string.format(
', <font color="#FF8E3C">but %d change%s failed to apply</font>',
unapplied,
unapplied == 1 and "" or "s"
)
else "")
.. "</i>"
return timeSinceText(DateTime.now().UnixTimestamp - patchData.timestamp)
end
function ConnectedPage:startChangeInfoTextUpdater()
@@ -190,13 +297,9 @@ function ConnectedPage:startChangeInfoTextUpdater()
-- Start a new updater
self.changeInfoTextUpdater = task.defer(function()
while true do
if self.state.hoveringChangeInfo then
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
else
self.setChangeInfoText(self:getChangeInfoText())
end
self.setChangeInfoText(self:getChangeInfoText())
local elapsed = os.time() - self.props.patchData.timestamp
local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp
local updateInterval = 1
-- Update timestamp text as frequently as currently needed
@@ -221,23 +324,6 @@ function ConnectedPage:stopChangeInfoTextUpdater()
end
function ConnectedPage:init()
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
self.changeDrawerMotor:onStep(function(value)
local renderChanges = value > 0.05
self:setState(function(state)
if state.renderChanges == renderChanges then
return nil
end
return {
renderChanges = renderChanges,
}
end)
end)
self:setState({
renderChanges = false,
hoveringChangeInfo = false,
@@ -266,6 +352,10 @@ function ConnectedPage:didUpdate(previousProps)
end
function ConnectedPage:render()
local syncWarning = self.props.patchData
and self.props.patchData.unapplied
and PatchSet.countChanges(self.props.patchData.unapplied) > 0
return Theme.with(function(theme)
return Roact.createFragment({
Padding = e("UIPadding", {
@@ -280,9 +370,88 @@ function ConnectedPage:render()
Padding = UDim.new(0, 10),
}),
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
Heading = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 32),
}, {
Header = e(Header, {
transparency = self.props.transparency,
}),
ChangeInfo = e("TextButton", {
Text = "",
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = theme.BorderedContainer.BorderedColor,
BackgroundTransparency = if self.state.hoveringChangeInfo then 0.7 else 1,
BorderSizePixel = 0,
Position = UDim2.new(1, -5, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
[Roact.Event.MouseEnter] = function()
self:setState({
hoveringChangeInfo = true,
})
end,
[Roact.Event.MouseLeave] = function()
self:setState({
hoveringChangeInfo = false,
})
end,
[Roact.Event.Activated] = function()
self:setState(function(prevState)
prevState = prevState or {}
return {
renderChanges = not prevState.renderChanges,
}
end)
end,
}, {
Corner = e("UICorner", {
CornerRadius = UDim.new(0, 5),
}),
Tooltip = e(Tooltip.Trigger, {
text = if self.state.renderChanges then "Hide changes" else "View changes",
}),
Content = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}),
Text = e("TextLabel", {
BackgroundTransparency = 1,
Text = self.changeInfoText,
Font = Enum.Font.Gotham,
TextSize = 15,
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
TextTransparency = self.props.transparency,
TextXAlignment = Enum.TextXAlignment.Right,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1,
}),
Icon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = if syncWarning
then Assets.Images.Icons.SyncWarning
else Assets.Images.Icons.SyncSuccess,
ImageColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 24, 0, 24),
LayoutOrder = 2,
}),
}),
}),
}),
ConnectionDetails = e(ConnectionDetails, {
@@ -332,84 +501,59 @@ function ConnectedPage:render()
}),
}),
ChangeInfo = e("TextButton", {
Text = self.changeInfoText,
Font = Enum.Font.Gotham,
TextSize = 14,
TextWrapped = true,
RichText = true,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency,
ChangesViewer = e(StudioPluginGui, {
id = "Rojo_ChangesViewer",
title = "View changes",
active = self.state.renderChanges,
isEphemeral = true,
Size = UDim2.new(1, 0, 0, 28),
initDockState = Enum.InitialDockState.Float,
overridePreviousState = true,
floatingSize = Vector2.new(400, 500),
minimumSize = Vector2.new(300, 300),
LayoutOrder = 4,
BackgroundTransparency = 1,
[Roact.Event.MouseEnter] = function()
self:setState({
hoveringChangeInfo = true,
})
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
end,
[Roact.Event.MouseLeave] = function()
self:setState({
hoveringChangeInfo = false,
})
self.setChangeInfoText(self:getChangeInfoText())
end,
[Roact.Event.Activated] = function()
if self.state.renderChanges then
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
frequency = 4,
dampingRatio = 1,
}))
else
self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
}))
end
end,
}, {
Tooltip = e(Tooltip.Trigger, {
text = if self.state.renderChanges then "Hide the changes" else "View the changes",
}),
}),
ChangesDrawer = e(ChangesDrawer, {
rendered = self.state.renderChanges,
transparency = self.props.transparency,
patchTree = self.props.patchTree,
serveSession = self.props.serveSession,
height = self.changeDrawerHeight,
layoutOrder = 5,
showStringDiff = function(oldString: string, newString: string)
self:setState({
showingStringDiff = true,
oldString = oldString,
newString = newString,
})
end,
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
self:setState({
showingTableDiff = true,
oldTable = oldTable,
newTable = newTable,
})
end,
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onClose = function()
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
frequency = 4,
dampingRatio = 1,
}))
self:setState({
renderChanges = false,
})
end,
}, {
TooltipsProvider = e(Tooltip.Provider, nil, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Changes = e(ChangesViewer, {
transparency = self.props.transparency,
rendered = self.state.renderChanges,
patchData = self.props.patchData,
patchTree = self.props.patchTree,
serveSession = self.props.serveSession,
showStringDiff = function(oldString: string, newString: string)
self:setState({
showingStringDiff = true,
oldString = oldString,
newString = newString,
})
end,
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
self:setState({
showingTableDiff = true,
oldTable = oldTable,
newTable = newTable,
})
end,
onBack = function()
self:setState({
renderChanges = false,
})
end,
}),
}),
}),
}),
StringDiff = e(StudioPluginGui, {

View File

@@ -32,9 +32,12 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:updateTheme()
local studioTheme = getStudio().Theme
local isDark = studioTheme.Name == "Dark"
local theme = strict(studioTheme.Name .. "Theme", {
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
SubTextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
Button = {
Solid = {
-- Solid uses brand theming, not Studio theming.
@@ -139,9 +142,10 @@ function StudioProvider:updateTheme()
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
},
Diff = {
Add = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextAdditionBackground),
Remove = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextDeletionBackground),
Edit = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffLineNumSeparatorBackground),
-- Studio doesn't have good colors since their diffs use backgrounds, not text
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
},

View File

@@ -457,7 +457,7 @@ function App:startSession()
end)
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
local now = os.time()
local now = DateTime.now().UnixTimestamp
local old = self.state.patchData
if PatchSet.isEmpty(patch) then

View File

@@ -25,6 +25,10 @@ local Assets = {
Back = "rbxassetid://6017213752",
Reset = "rbxassetid://10142422327",
Expand = "rbxassetid://12045401097",
Checkmark = "rbxassetid://16571012729",
Exclamation = "rbxassetid://16571172190",
SyncSuccess = "rbxassetid://16565035221",
SyncWarning = "rbxassetid://16565325171",
},
Diff = {
Add = "rbxassetid://10434145835",

View File

@@ -211,9 +211,11 @@ end
function PatchSet.countChanges(patch)
local count = 0
for _ in patch.added do
-- Adding an instance is 1 change
count += 1
for _, add in patch.added do
-- Adding an instance is 1 change per property
for _ in add.Properties do
count += 1
end
end
for _ in patch.removed do
-- Removing an instance is 1 change

View File

@@ -79,6 +79,15 @@ function Tree.new()
return setmetatable(tree, Tree)
end
-- Iterates over all nodes and counts them up
function Tree:getCount()
local count = 0
self:forEach(function()
count += 1
end)
return count
end
-- Iterates over all sub-nodes, depth first
-- node is where to start from, defaults to root
-- depth is used for recursion but can be used to set the starting depth
@@ -219,42 +228,14 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text
local changeList, hint = nil, nil
local changeList, changeInfo = nil, nil
if next(change.changedProperties) or change.changedName then
changeList = {}
local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
local changeIndex = 0
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
changeIndex += 1
changeList[changeIndex] = { prop, current, incoming, metadata }
if hintBufferSize < 3 then
hintBufferSize += 1
hintBuffer[hintBufferSize] = prop
return
end
-- We only want to have 3 hints
-- to keep it deterministic, we sort them alphabetically
-- Either this prop overflows, or it makes another one move to overflow
hintOverflow += 1
-- Shortcut for the common case
if hintBuffer[3] <= prop then
-- This prop is below the last hint, no need to insert
return
end
-- Find the first available spot
for i, hintItem in hintBuffer do
if prop < hintItem then
-- This prop is before the currently selected hint,
-- so take its place and then continue to find a spot for the old hint
hintBuffer[i], prop = prop, hintBuffer[i]
end
end
end
-- Gather the changes
@@ -274,8 +255,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
)
end
-- Finalize detail values
hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
changeInfo = {
edits = changeIndex,
}
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -291,7 +273,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
className = instance.ClassName,
name = instance.Name,
instance = instance,
hint = hint,
changeInfo = changeInfo,
changeList = changeList,
})
end
@@ -376,42 +358,14 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text
local changeList, hint = nil, nil
local changeList, changeInfo = nil, nil
if next(change.Properties) then
changeList = {}
local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
local changeIndex = 0
local function addProp(prop: string, incoming: any)
changeIndex += 1
changeList[changeIndex] = { prop, "N/A", incoming }
if hintBufferSize < 3 then
hintBufferSize += 1
hintBuffer[hintBufferSize] = prop
return
end
-- We only want to have 3 hints
-- to keep it deterministic, we sort them alphabetically
-- Either this prop overflows, or it makes another one move to overflow
hintOverflow += 1
-- Shortcut for the common case
if hintBuffer[3] <= prop then
-- This prop is below the last hint, no need to insert
return
end
-- Find the first available spot
for i, hintItem in hintBuffer do
if prop < hintItem then
-- This prop is before the currently selected hint,
-- so take its place and then continue to find a spot for the old hint
hintBuffer[i], prop = prop, hintBuffer[i]
end
end
end
for prop, incoming in change.Properties do
@@ -419,8 +373,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
addProp(prop, if success then incomingValue else select(2, next(incoming)))
end
-- Finalize detail values
hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
changeInfo = {
edits = changeIndex,
}
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -435,7 +390,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
patchType = "Add",
className = change.ClassName,
name = change.Name,
hint = hint,
changeInfo = changeInfo,
changeList = changeList,
instance = instanceMap.fromIds[id],
})
@@ -473,6 +428,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then
continue
end
local warnings = 0
for _, change in node.changeList do
local property = change[1]
local propertyFailedToApply = if property == "Name"
@@ -483,6 +440,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
-- This change didn't fail, no need to mark
continue
end
warnings += 1
if change[4] == nil then
change[4] = { isWarning = true }
else
@@ -490,6 +449,11 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end
Log.trace(" Marked property as warning: {}.{}", node.name, property)
end
node.changeInfo = {
edits = (node.changeInfo.edits or (#node.changeList - 1)) - warnings,
failed = if warnings > 0 then warnings else nil,
}
end
for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId)
@@ -503,6 +467,7 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then
continue
end
for _, change in node.changeList do
-- Failed addition means that all properties failed to be added
if change[4] == nil then
@@ -512,6 +477,10 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end
node.changeInfo = {
failed = node.changeInfo.edits or (#node.changeList - 1),
}
end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)