diff --git a/.gitmodules b/.gitmodules index 90468044..98b1cb5e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,7 @@ url = https://github.com/LPGhatguy/roblox-lua-promise.git [submodule "plugin/modules/t"] path = plugin/modules/t - url = https://github.com/osyrisrblx/t.git \ No newline at end of file + url = https://github.com/osyrisrblx/t.git +[submodule "plugin/modules/flipper"] + path = plugin/modules/flipper + url = https://github.com/Reselim/Flipper diff --git a/build.rs b/build.rs index 343c151a..fe2a840a 100644 --- a/build.rs +++ b/build.rs @@ -62,6 +62,9 @@ fn main() -> Result<(), anyhow::Error> { "t" => VfsSnapshot::dir(hashmap! { "lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))? }), + "flipper" => VfsSnapshot::dir(hashmap! { + "src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))? + }), }), }); diff --git a/foreman.toml b/foreman.toml index 53464e41..7fb33cb9 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] -rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.1" } +rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.3" } run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } diff --git a/plugin/default.project.json b/plugin/default.project.json index c1a85c9b..bdaadab3 100644 --- a/plugin/default.project.json +++ b/plugin/default.project.json @@ -25,6 +25,9 @@ }, "t": { "$path": "modules/t/lib" + }, + "Flipper": { + "$path": "modules/flipper/src" } } } \ No newline at end of file diff --git a/plugin/modules/flipper b/plugin/modules/flipper new file mode 160000 index 00000000..4cf7a03c --- /dev/null +++ b/plugin/modules/flipper @@ -0,0 +1 @@ +Subproject commit 4cf7a03cb6776ec0af41b6a50988aa70e5c9cc3c diff --git a/plugin/src/App/Components/BorderedContainer.lua b/plugin/src/App/Components/BorderedContainer.lua new file mode 100644 index 00000000..78ceb426 --- /dev/null +++ b/plugin/src/App/Components/BorderedContainer.lua @@ -0,0 +1,41 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Theme = require(Plugin.App.Theme) +local Assets = require(Plugin.Assets) + +local SlicedImage = require(script.Parent.SlicedImage) + +local e = Roact.createElement + +local function BorderedContainer(props) + return Theme.with(function(theme) + return e(SlicedImage, { + slice = Assets.Slices.RoundedBackground, + color = theme.BorderedContainer.BackgroundColor, + transparency = props.transparency, + + size = props.size, + position = props.position, + anchorPoint = props.anchorPoint, + layoutOrder = props.layoutOrder, + }, { + Content = e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, props[Roact.Children]), + + Border = e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.BorderedContainer.BorderColor, + transparency = props.transparency, + + size = UDim2.new(1, 0, 1, 0), + }), + }) + end) +end + +return BorderedContainer \ No newline at end of file diff --git a/plugin/src/App/Components/Checkbox.lua b/plugin/src/App/Components/Checkbox.lua new file mode 100644 index 00000000..21c2c544 --- /dev/null +++ b/plugin/src/App/Components/Checkbox.lua @@ -0,0 +1,96 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Flipper = require(Rojo.Flipper) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) +local bindingUtil = require(Plugin.App.bindingUtil) + +local SlicedImage = require(script.Parent.SlicedImage) + +local e = Roact.createElement + +local Checkbox = Roact.Component:extend("Checkbox") + +function Checkbox:init() + self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0) + self.binding = bindingUtil.fromMotor(self.motor) +end + +function Checkbox:didUpdate(lastProps) + if lastProps.active ~= self.props.active then + self.motor:setGoal( + Flipper.Spring.new(self.props.active and 1 or 0, { + frequency = 6, + dampingRatio = 1.1, + }) + ) + end +end + +function Checkbox:render() + return Theme.with(function(theme) + theme = theme.Checkbox + + local activeTransparency = Roact.joinBindings({ + self.binding:map(function(value) + return 1 - value + end), + self.props.transparency, + }):map(bindingUtil.blendAlpha) + + return e("ImageButton", { + Size = UDim2.new(0, 28, 0, 28), + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + ZIndex = self.props.zIndex, + BackgroundTransparency = 1, + + [Roact.Event.Activated] = self.props.onClick, + }, { + Active = e(SlicedImage, { + slice = Assets.Slices.RoundedBackground, + color = theme.Active.BackgroundColor, + transparency = activeTransparency, + size = UDim2.new(1, 0, 1, 0), + zIndex = 2, + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Checkbox.Active, + ImageColor3 = theme.Active.IconColor, + ImageTransparency = activeTransparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }), + + Inactive = e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.Inactive.BorderColor, + transparency = self.props.transparency, + size = UDim2.new(1, 0, 1, 0), + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Checkbox.Inactive, + ImageColor3 = theme.Inactive.IconColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }), + }) + end) +end + +return Checkbox \ No newline at end of file diff --git a/plugin/src/App/Components/Header.lua b/plugin/src/App/Components/Header.lua new file mode 100644 index 00000000..fdc5e5d1 --- /dev/null +++ b/plugin/src/App/Components/Header.lua @@ -0,0 +1,55 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Theme = require(Plugin.App.Theme) +local Assets = require(Plugin.Assets) +local Config = require(Plugin.Config) +local Version = require(Plugin.Version) + +local e = Roact.createElement + +local function Header(props) + return Theme.with(function(theme) + return e("Frame", { + Size = UDim2.new(1, 0, 0, 32), + LayoutOrder = props.layoutOrder, + BackgroundTransparency = 1, + }, { + Logo = e("ImageLabel", { + Image = Assets.Images.Logo, + ImageColor3 = theme.Header.LogoColor, + ImageTransparency = props.transparency, + + Size = UDim2.new(0, 60, 0, 27), + + LayoutOrder = 1, + BackgroundTransparency = 1, + }), + + Version = e("TextLabel", { + Text = Version.display(Config.version), + Font = Enum.Font.Gotham, + TextSize = 14, + TextColor3 = theme.Header.VersionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = props.transparency, + + Size = UDim2.new(1, 0, 0, 14), + + LayoutOrder = 2, + BackgroundTransparency = 1, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 15), + }), + }) + end) +end + +return Header \ No newline at end of file diff --git a/plugin/src/App/Components/IconButton.lua b/plugin/src/App/Components/IconButton.lua new file mode 100644 index 00000000..388a1fe0 --- /dev/null +++ b/plugin/src/App/Components/IconButton.lua @@ -0,0 +1,79 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Flipper = require(Rojo.Flipper) + +local Assets = require(Plugin.Assets) +local bindingUtil = require(Plugin.App.bindingUtil) + +local HOVER_SPRING_PROPS = { + frequency = 5, + dampingRatio = 1.1, +} + +local e = Roact.createElement + +local IconButton = Roact.Component:extend("IconButton") + +function IconButton:init() + self.motor = Flipper.SingleMotor.new(0) + self.binding = bindingUtil.fromMotor(self.motor) +end + +function IconButton:render() + local iconSize = self.props.iconSize + + return e("ImageButton", { + Size = UDim2.new(0, iconSize * 1.5, 0, iconSize * 1.5), + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + + LayoutOrder = self.props.layoutOrder, + ZIndex = self.props.zIndex, + BackgroundTransparency = 1, + + [Roact.Event.Activated] = self.props.onClick, + + [Roact.Event.MouseEnter] = function() + self.motor:setGoal( + Flipper.Spring.new(1, HOVER_SPRING_PROPS) + ) + end, + + [Roact.Event.MouseLeave] = function() + self.motor:setGoal( + Flipper.Spring.new(0, HOVER_SPRING_PROPS) + ) + end, + }, { + Icon = e("ImageLabel", { + Image = self.props.icon, + ImageColor3 = self.props.color, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + + HoverCircle = e("ImageLabel", { + Image = Assets.Images.Circles[128], + ImageColor3 = self.props.color, + ImageTransparency = Roact.joinBindings({ + hover = self.binding, + transparency = self.props.transparency, + }):map(function(values) + return bindingUtil.blendAlpha({ 0.85, 1 - values.hover, values.transparency }) + end), + + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + }), + }) +end + +return IconButton \ No newline at end of file diff --git a/plugin/src/App/Components/ScrollingFrame.lua b/plugin/src/App/Components/ScrollingFrame.lua new file mode 100644 index 00000000..87d10226 --- /dev/null +++ b/plugin/src/App/Components/ScrollingFrame.lua @@ -0,0 +1,42 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) +local bindingUtil = require(Plugin.App.bindingUtil) + +local e = Roact.createElement + +local function ScrollingFrame(props) + return Theme.with(function(theme) + return e("ScrollingFrame", { + ScrollBarThickness = 9, + ScrollBarImageColor3 = theme.ScrollBarColor, + ScrollBarImageTransparency = props.transparency:map(function(value) + return bindingUtil.blendAlpha({ 0.65, value }) + end), + TopImage = Assets.Images.ScrollBar.Top, + MidImage = Assets.Images.ScrollBar.Middle, + BottomImage = Assets.Images.ScrollBar.Bottom, + + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollingDirection = Enum.ScrollingDirection.Y, + + Size = props.size, + Position = props.position, + AnchorPoint = props.anchorPoint, + CanvasSize = props.contentSize:map(function(value) + return UDim2.new(0, 0, 0, value.Y) + end), + + BorderSizePixel = 0, + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize] + }, props[Roact.Children]) + end) +end + +return ScrollingFrame \ No newline at end of file diff --git a/plugin/src/App/Components/SlicedImage.lua b/plugin/src/App/Components/SlicedImage.lua new file mode 100644 index 00000000..8c03c877 --- /dev/null +++ b/plugin/src/App/Components/SlicedImage.lua @@ -0,0 +1,29 @@ +local Rojo = script:FindFirstAncestor("Rojo") + +local Roact = require(Rojo.Roact) + +local e = Roact.createElement + +local function SlicedImage(props) + local slice = props.slice + + return e("ImageLabel", { + Image = slice.Image, + ImageColor3 = props.color, + ImageTransparency = props.transparency, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = slice.Center, + SliceScale = slice.Scale, + + Size = props.size, + Position = props.position, + AnchorPoint = props.anchorPoint, + + ZIndex = props.zIndex, + LayoutOrder = props.layoutOrder, + BackgroundTransparency = 1, + }, props[Roact.Children]) +end + +return SlicedImage \ No newline at end of file diff --git a/plugin/src/App/Components/Spinner.lua b/plugin/src/App/Components/Spinner.lua new file mode 100644 index 00000000..562bd679 --- /dev/null +++ b/plugin/src/App/Components/Spinner.lua @@ -0,0 +1,66 @@ +local RunService = game:GetService("RunService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Theme = require(Plugin.App.Theme) +local Assets = require(Plugin.Assets) + +local ROTATIONS_PER_SECOND = 1.75 + +local e = Roact.createElement + +local Spinner = Roact.PureComponent:extend("Spinner") + +function Spinner:init() + self.rotation, self.setRotation = Roact.createBinding(0) +end + +function Spinner:render() + return Theme.with(function(theme) + return e("ImageLabel", { + Image = Assets.Images.Spinner.Background, + ImageColor3 = theme.Spinner.BackgroundColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 24, 0, 24), + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + + LayoutOrder = self.props.layoutOrder, + BackgroundTransparency = 1, + }, { + Foreground = e("ImageLabel", { + Image = Assets.Images.Spinner.Foreground, + ImageColor3 = theme.Spinner.ForegroundColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(1, 0, 1, 0), + Rotation = self.rotation:map(function(value) + return value * 360 + end), + + BackgroundTransparency = 1, + }), + }) + end) +end + +function Spinner:didMount() + self.stepper = RunService.RenderStepped:Connect(function(deltaTime) + local rotation = self.rotation:getValue() + + rotation = rotation + deltaTime * ROTATIONS_PER_SECOND + rotation = rotation % 1 + + self.setRotation(rotation) + end) +end + +function Spinner:willUnmount() + self.stepper:Disconnect() +end + +return Spinner \ No newline at end of file diff --git a/plugin/src/App/Components/Studio/StudioPluginContext.lua b/plugin/src/App/Components/Studio/StudioPluginContext.lua new file mode 100644 index 00000000..978f628f --- /dev/null +++ b/plugin/src/App/Components/Studio/StudioPluginContext.lua @@ -0,0 +1,7 @@ +local Rojo = script:FindFirstAncestor("Rojo") + +local Roact = require(Rojo.Roact) + +local StudioPluginContext = Roact.createContext(nil) + +return StudioPluginContext \ No newline at end of file diff --git a/plugin/src/App/Components/Studio/StudioPluginGui.lua b/plugin/src/App/Components/Studio/StudioPluginGui.lua new file mode 100644 index 00000000..6bd37511 --- /dev/null +++ b/plugin/src/App/Components/Studio/StudioPluginGui.lua @@ -0,0 +1,84 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Dictionary = require(Plugin.Dictionary) + +local StudioPluginContext = require(script.Parent.StudioPluginContext) + +local e = Roact.createElement + +local StudioPluginGui = Roact.PureComponent:extend("StudioPluginGui") + +StudioPluginGui.defaultProps = { + initDockState = Enum.InitialDockState.Right, + active = false, + overridePreviousState = false, + floatingSize = Vector2.new(0, 0), + minimumSize = Vector2.new(0, 0), + zIndexBehavior = Enum.ZIndexBehavior.Sibling, +} + +function StudioPluginGui:init() + local floatingSize = self.props.floatingSize + local minimumSize = self.props.minimumSize + + local dockWidgetPluginGuiInfo = DockWidgetPluginGuiInfo.new( + self.props.initDockState, + self.props.active, + self.props.overridePreviousState, + floatingSize.X, floatingSize.Y, + minimumSize.X, minimumSize.Y + ) + + local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo) + + pluginGui.Name = self.props.id + pluginGui.Title = self.props.title + pluginGui.ZIndexBehavior = self.props.zIndexBehavior + + if self.props.onInitialState then + self.props.onInitialState(pluginGui.Enabled) + end + + pluginGui:BindToClose(function() + if self.props.onClose then + self.props.onClose() + else + pluginGui.Enabled = false + end + end) + + self.pluginGui = pluginGui +end + +function StudioPluginGui:render() + return e(Roact.Portal, { + target = self.pluginGui, + }, self.props[Roact.Children]) +end + +function StudioPluginGui:didUpdate(lastProps) + if self.props.active ~= lastProps.active then + -- This is intentionally in didUpdate to make sure the initial active state + -- (if the PluginGui is open initially) is preserved. + self.pluginGui.Enabled = self.props.active + end +end + +function StudioPluginGui:willUnmount() + self.pluginGui:Destroy() +end + +local function StudioPluginGuiWrapper(props) + return e(StudioPluginContext.Consumer, { + render = function(plugin) + return e(StudioPluginGui, Dictionary.merge(props, { + plugin = plugin, + })) + end, + }) +end + +return StudioPluginGuiWrapper \ No newline at end of file diff --git a/plugin/src/App/Components/Studio/StudioToggleButton.lua b/plugin/src/App/Components/Studio/StudioToggleButton.lua new file mode 100644 index 00000000..1970ff92 --- /dev/null +++ b/plugin/src/App/Components/Studio/StudioToggleButton.lua @@ -0,0 +1,66 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Dictionary = require(Plugin.Dictionary) + +local StudioToolbarContext = require(script.Parent.StudioToolbarContext) + +local e = Roact.createElement + +local StudioToggleButton = Roact.Component:extend("StudioToggleButton") + +StudioToggleButton.defaultProps = { + enabled = true, + active = false, +} + +function StudioToggleButton:init() + local button = self.props.toolbar:CreateButton( + self.props.name, + self.props.tooltip, + self.props.icon, + self.props.text + ) + + button.Click:Connect(function() + if self.props.onClick then + self.props.onClick() + end + end) + + button.ClickableWhenViewportHidden = true + + self.button = button +end + +function StudioToggleButton:render() + return nil +end + +function StudioToggleButton:didUpdate(lastProps) + if self.props.enabled ~= lastProps.enabled then + self.button.Enabled = self.props.enabled + end + + if self.props.active ~= lastProps.active then + self.button:SetActive(self.props.active) + end +end + +function StudioToggleButton:willUnmount() + self.button:Destroy() +end + +local function StudioToggleButtonWrapper(props) + return e(StudioToolbarContext.Consumer, { + render = function(toolbar) + return e(StudioToggleButton, Dictionary.merge(props, { + toolbar = toolbar, + })) + end, + }) +end + +return StudioToggleButtonWrapper \ No newline at end of file diff --git a/plugin/src/App/Components/Studio/StudioToolbar.lua b/plugin/src/App/Components/Studio/StudioToolbar.lua new file mode 100644 index 00000000..c03b20c0 --- /dev/null +++ b/plugin/src/App/Components/Studio/StudioToolbar.lua @@ -0,0 +1,45 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Dictionary = require(Plugin.Dictionary) + +local StudioToolbarContext = require(script.Parent.StudioToolbarContext) +local StudioPluginContext = require(script.Parent.StudioPluginContext) + +local e = Roact.createElement + +local StudioToolbar = Roact.Component:extend("StudioToolbar") + +function StudioToolbar:init() + self.toolbar = self.props.plugin:CreateToolbar(self.props.name) +end + +function StudioToolbar:render() + return e(StudioToolbarContext.Provider, { + value = self.toolbar, + }, self.props[Roact.Children]) +end + +function StudioToolbar:didUpdate(lastProps) + if self.props.name ~= lastProps.name then + self.toolbar.Name = self.props.name + end +end + +function StudioToolbar:willUnmount() + self.toolbar:Destroy() +end + +local function StudioToolbarWrapper(props) + return e(StudioPluginContext.Consumer, { + render = function(plugin) + return e(StudioToolbar, Dictionary.merge(props, { + plugin = plugin, + })) + end, + }) +end + +return StudioToolbarWrapper \ No newline at end of file diff --git a/plugin/src/App/Components/Studio/StudioToolbarContext.lua b/plugin/src/App/Components/Studio/StudioToolbarContext.lua new file mode 100644 index 00000000..925b7e0c --- /dev/null +++ b/plugin/src/App/Components/Studio/StudioToolbarContext.lua @@ -0,0 +1,7 @@ +local Rojo = script:FindFirstAncestor("Rojo") + +local Roact = require(Rojo.Roact) + +local StudioToolbarContext = Roact.createContext(nil) + +return StudioToolbarContext \ No newline at end of file diff --git a/plugin/src/App/Components/TextButton.lua b/plugin/src/App/Components/TextButton.lua new file mode 100644 index 00000000..e4e97e71 --- /dev/null +++ b/plugin/src/App/Components/TextButton.lua @@ -0,0 +1,137 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Flipper = require(Rojo.Flipper) + +local Theme = require(Plugin.App.Theme) +local Assets = require(Plugin.Assets) +local bindingUtil = require(Plugin.App.bindingUtil) + +local SlicedImage = require(script.Parent.SlicedImage) +local TouchRipple = require(script.Parent.TouchRipple) + +local SPRING_PROPS = { + frequency = 5, + dampingRatio = 1, +} + +local e = Roact.createElement + +local TextButton = Roact.Component:extend("TextButton") + +function TextButton:init() + self.motor = Flipper.GroupMotor.new({ + hover = 0, + enabled = self.props.enabled and 1 or 0, + }) + self.binding = bindingUtil.fromMotor(self.motor) +end + +function TextButton:didUpdate(lastProps) + if lastProps.enabled ~= self.props.enabled then + self.motor:setGoal({ + enabled = Flipper.Spring.new(self.props.enabled and 1 or 0), + }) + end +end + +function TextButton:render() + return Theme.with(function(theme) + local textSize = TextService:GetTextSize( + self.props.text, 18, Enum.Font.GothamSemibold, + Vector2.new(math.huge, math.huge) + ) + + local style = self.props.style + + theme = theme.Button[style] + + local bindingHover = bindingUtil.deriveProperty(self.binding, "hover") + local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled") + + return e("ImageButton", { + Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34), + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + + LayoutOrder = self.props.layoutOrder, + BackgroundTransparency = 1, + + [Roact.Event.Activated] = self.props.onClick, + + [Roact.Event.MouseEnter] = function() + self.motor:setGoal({ + hover = Flipper.Spring.new(1, SPRING_PROPS), + }) + end, + + [Roact.Event.MouseLeave] = function() + self.motor:setGoal({ + hover = Flipper.Spring.new(0, SPRING_PROPS), + }) + end, + }, { + TouchRipple = e(TouchRipple, { + color = theme.ActionFillColor, + transparency = self.props.transparency:map(function(value) + return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value }) + end), + zIndex = 2, + }), + + Text = e("TextLabel", { + Text = self.props.text, + Font = Enum.Font.GothamSemibold, + TextSize = 18, + TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor), + TextTransparency = self.props.transparency, + + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + }), + + Border = style == "Bordered" and e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor), + transparency = self.props.transparency, + + size = UDim2.new(1, 0, 1, 0), + + zIndex = 0, + }), + + HoverOverlay = e(SlicedImage, { + slice = Assets.Slices.RoundedBackground, + color = theme.ActionFillColor, + transparency = Roact.joinBindings({ + hover = bindingHover:map(function(value) + return 1 - value + end), + transparency = self.props.transparency, + }):map(function(values) + return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency }) + end), + + size = UDim2.new(1, 0, 1, 0), + + zIndex = -1, + }), + + Background = style == "Solid" and e(SlicedImage, { + slice = Assets.Slices.RoundedBackground, + color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor), + transparency = self.props.transparency, + + size = UDim2.new(1, 0, 1, 0), + + zIndex = -2, + }), + }) + end) +end + +return TextButton \ No newline at end of file diff --git a/plugin/src/App/Components/TouchRipple.lua b/plugin/src/App/Components/TouchRipple.lua new file mode 100644 index 00000000..955d0716 --- /dev/null +++ b/plugin/src/App/Components/TouchRipple.lua @@ -0,0 +1,145 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Flipper = require(Rojo.Flipper) + +local Assets = require(Plugin.Assets) +local bindingUtil = require(Plugin.App.bindingUtil) + +local EXPAND_SPRING = { + frequency = 7, + dampingRatio = 2, +} + +local TouchRipple = Roact.Component:extend("TouchRipple") + +function TouchRipple:init() + self.ref = Roact.createRef() + + self.motor = Flipper.GroupMotor.new({ + scale = 0, + opacity = 0, + }) + self.binding = bindingUtil.fromMotor(self.motor) + + self.position, self.setPosition = Roact.createBinding( + Vector2.new(0, 0) + ) +end + +function TouchRipple:reset() + self.motor:setGoal({ + scale = Flipper.Instant.new(0), + opacity = Flipper.Instant.new(0), + }) + + -- Forces motor to update + self.motor:step(0) +end + +function TouchRipple:calculateRadius(position) + local container = self.ref.current + + if container then + local corner = Vector2.new( + math.floor((1 - position.X) + 0.5), + math.floor((1 - position.Y) + 0.5) + ) + + local size = container.AbsoluteSize + local ratio = size / math.min(size.X, size.Y) + + return ((corner * ratio) - (position * ratio)).Magnitude + else + return 0 + end +end + +function TouchRipple:render() + local scale = bindingUtil.deriveProperty(self.binding, "scale") + local transparency = bindingUtil.deriveProperty(self.binding, "opacity"):map(function(value) + return 1 - value + end) + + transparency = Roact.joinBindings({ + transparency, + self.props.transparency, + }):map(bindingUtil.blendAlpha) + + return Roact.createElement("Frame", { + ClipsDescendants = true, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = self.props.zIndex, + BackgroundTransparency = 1, + + [Roact.Ref] = self.ref, + + [Roact.Event.InputBegan] = function(object, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:reset() + + local position = Vector2.new(input.Position.X, input.Position.Y) + local relativePosition = (position - object.AbsolutePosition) / object.AbsoluteSize + + self.setPosition(relativePosition) + + self.motor:setGoal({ + scale = Flipper.Spring.new(1, EXPAND_SPRING), + opacity = Flipper.Spring.new(1, EXPAND_SPRING), + }) + + input:GetPropertyChangedSignal("UserInputState"):Connect(function() + local userInputState = input.UserInputState + + if + userInputState == Enum.UserInputState.Cancel + or userInputState == Enum.UserInputState.End + then + self.motor:setGoal({ + opacity = Flipper.Spring.new(0, { + frequency = 5, + dampingRatio = 1, + }), + }) + end + end) + end + end, + }, { + Circle = Roact.createElement("ImageLabel", { + Image = Assets.Images.Circles[500], + ImageColor3 = self.props.color, + ImageTransparency = transparency, + + Size = Roact.joinBindings({ + scale = scale, + position = self.position, + }):map(function(values) + local targetSize = self:calculateRadius(values.position) * 2 + local currentSize = targetSize * values.scale + + local container = self.ref.current + + if container then + local containerSize = container.AbsoluteSize + local containerAspect = containerSize.X / containerSize.Y + + return UDim2.new( + currentSize / math.max(containerAspect, 1), 0, + currentSize * math.min(containerAspect, 1), 0 + ) + end + end), + + Position = self.position:map(function(value) + return UDim2.new(value.X, 0, value.Y, 0) + end), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }) +end + +return TouchRipple \ No newline at end of file diff --git a/plugin/src/App/Page.lua b/plugin/src/App/Page.lua new file mode 100644 index 00000000..99b5a942 --- /dev/null +++ b/plugin/src/App/Page.lua @@ -0,0 +1,70 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Flipper = require(Rojo.Flipper) + +local Dictionary = require(Plugin.Dictionary) + +local bindingUtil = require(script.Parent.bindingUtil) + +local e = Roact.createElement + +local Page = Roact.Component:extend("Page") + +function Page:init() + self:setState({ + rendered = self.props.active + }) + + self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0) + self.binding = bindingUtil.fromMotor(self.motor) + + self.motor:onStep(function(value) + local rendered = value > 0 + + self:setState(function(state) + if state.rendered ~= rendered then + return { + rendered = rendered, + } + end + end) + end) +end + +function Page:render() + if not self.state.rendered then + return nil + end + + local transparency = self.binding:map(function(value) + return 1 - value + end) + + return e("Frame", { + Position = transparency:map(function(value) + value = self.props.active and value or -value + return UDim2.new(0, value * 30, 0, 0) + end), + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Component = e(self.props.component, Dictionary.merge(self.props, { + transparency = transparency, + })) + }) +end + +function Page:didUpdate(lastProps) + if self.props.active ~= lastProps.active then + self.motor:setGoal( + Flipper.Spring.new(self.props.active and 1 or 0, { + frequency = 6, + dampingRatio = 1, + }) + ) + end +end + +return Page \ No newline at end of file diff --git a/plugin/src/Components/PluginSettings.lua b/plugin/src/App/PluginSettings.lua similarity index 100% rename from plugin/src/Components/PluginSettings.lua rename to plugin/src/App/PluginSettings.lua diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua new file mode 100644 index 00000000..5793c6ba --- /dev/null +++ b/plugin/src/App/StatusPages/Connected.lua @@ -0,0 +1,125 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Theme = require(Plugin.App.Theme) +local Assets = require(Plugin.Assets) + +local Header = require(Plugin.App.Components.Header) +local IconButton = require(Plugin.App.Components.IconButton) +local BorderedContainer = require(Plugin.App.Components.BorderedContainer) + +local e = Roact.createElement + +local function ConnectionDetails(props) + return Theme.with(function(theme) + return e(BorderedContainer, { + transparency = props.transparency, + size = UDim2.new(1, 0, 0, 70), + layoutOrder = props.layoutOrder, + }, { + TextContainer = e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1 + }, { + ProjectName = e("TextLabel", { + Text = props.projectName, + Font = Enum.Font.GothamBold, + TextSize = 20, + TextColor3 = theme.ConnectionDetails.ProjectNameColor, + TextTransparency = props.transparency, + TextXAlignment = Enum.TextXAlignment.Left, + + Size = UDim2.new(1, 0, 0, 20), + + LayoutOrder = 1, + BackgroundTransparency = 1, + }), + + Address = e("TextLabel", { + Text = props.address, + Font = Enum.Font.Code, + TextSize = 15, + TextColor3 = theme.ConnectionDetails.AddressColor, + TextTransparency = props.transparency, + TextXAlignment = Enum.TextXAlignment.Left, + + Size = UDim2.new(1, 0, 0, 15), + + LayoutOrder = 2, + BackgroundTransparency = 1, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 6), + }), + }), + + Disconnect = e(IconButton, { + icon = Assets.Images.Icons.Close, + iconSize = 24, + color = theme.ConnectionDetails.DisconnectColor, + transparency = props.transparency, + + position = UDim2.new(1, 0, 0.5, 0), + anchorPoint = Vector2.new(1, 0.5), + + onClick = props.onDisconnect, + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 17), + PaddingRight = UDim.new(0, 15), + }), + }) + end) +end + +local ConnectedPage = Roact.Component:extend("ConnectedPage") + +function ConnectedPage:render() + return Roact.createFragment({ + Header = e(Header, { + transparency = self.props.transparency, + layoutOrder = 1, + }), + + ConnectionDetails = e(ConnectionDetails, { + projectName = self.state.projectName, + address = self.state.address, + transparency = self.props.transparency, + layoutOrder = 2, + + onDisconnect = self.props.onDisconnect, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), + }) +end + +function ConnectedPage.getDerivedStateFromProps(props) + -- If projectName or address ever get removed from props, make sure we still have + -- the properties! The component still needs to have its data for it to be properly + -- animated out without the labels changing. + + return { + projectName = props.projectName, + address = props.address, + } +end + +return ConnectedPage \ No newline at end of file diff --git a/plugin/src/App/StatusPages/Connecting.lua b/plugin/src/App/StatusPages/Connecting.lua new file mode 100644 index 00000000..e45c3c53 --- /dev/null +++ b/plugin/src/App/StatusPages/Connecting.lua @@ -0,0 +1,20 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Spinner = require(Plugin.App.Components.Spinner) + +local e = Roact.createElement + +local ConnectingPage = Roact.Component:extend("ConnectingPage") + +function ConnectingPage:render() + return e(Spinner, { + position = UDim2.new(0.5, 0, 0.5, 0), + anchorPoint = Vector2.new(0.5, 0.5), + transparency = self.props.transparency, + }) +end + +return ConnectingPage \ No newline at end of file diff --git a/plugin/src/App/StatusPages/Error.lua b/plugin/src/App/StatusPages/Error.lua new file mode 100644 index 00000000..01ad1091 --- /dev/null +++ b/plugin/src/App/StatusPages/Error.lua @@ -0,0 +1,153 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Theme = require(Plugin.App.Theme) + +local TextButton = require(Plugin.App.Components.TextButton) +local BorderedContainer = require(Plugin.App.Components.BorderedContainer) +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) + +local e = Roact.createElement + +local ERROR_PADDING = Vector2.new(13, 10) + +local Error = Roact.Component:extend("Error") + +function Error:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function Error:render() + return e(BorderedContainer, { + size = Roact.joinBindings({ + containerSize = self.props.containerSize, + contentSize = self.contentSize, + }):map(function(values) + local maximumSize = values.containerSize + maximumSize -= Vector2.new(14, 14) * 2 -- Page padding + maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing + + local outerSize = values.contentSize + ERROR_PADDING * 2 + + return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y)) + end), + transparency = self.props.transparency, + }, { + ScrollingFrame = e(ScrollingFrame, { + size = UDim2.new(1, 0, 1, 0), + contentSize = self.contentSize:map(function(value) + return value + ERROR_PADDING * 2 + end), + transparency = self.props.transparency, + + [Roact.Change.AbsoluteSize] = function(object) + local containerSize = object.AbsoluteSize - ERROR_PADDING * 2 + + local textBounds = TextService:GetTextSize( + self.props.errorMessage, 16, Enum.Font.Code, + Vector2.new(containerSize.X, math.huge) + ) + + self.setContentSize(Vector2.new(containerSize.X, textBounds.Y)) + end, + }, { + ErrorMessage = Theme.with(function(theme) + return e("TextLabel", { + Text = self.props.errorMessage, + Font = Enum.Font.Code, + TextSize = 16, + TextColor3 = theme.ErrorColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + }) + end), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, ERROR_PADDING.X), + PaddingRight = UDim.new(0, ERROR_PADDING.X), + PaddingTop = UDim.new(0, ERROR_PADDING.Y), + PaddingBottom = UDim.new(0, ERROR_PADDING.Y), + }), + }), + }) +end + +local ErrorPage = Roact.Component:extend("ErrorPage") + +function ErrorPage:init() + self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function ErrorPage:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = function(object) + self.setContainerSize(object.AbsoluteSize) + end, + }, { + Error = e(Error, { + errorMessage = self.state.errorMessage, + containerSize = self.containerSize, + transparency = self.props.transparency, + layoutOrder = 1, + }), + + Buttons = e("Frame", { + Size = UDim2.new(1, 0, 0, 35), + LayoutOrder = 2, + BackgroundTransparency = 1, + }, { + Close = e(TextButton, { + text = "Okay", + style = "Bordered", + transparency = self.props.transparency, + layoutOrder = 1, + onClick = self.props.onClose, + }), + + Layout = e("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Right, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 14), + PaddingRight = UDim.new(0, 14), + PaddingTop = UDim.new(0, 14), + PaddingBottom = UDim.new(0, 14), + }), + }) +end + +function ErrorPage.getDerivedStateFromProps(props) + -- If errorMessage ever gets removed from props, make sure we still have the + -- property! The component still needs to have its data for it to be properly + -- animated out without the labels changing. + + return { + errorMessage = props.errorMessage, + } +end + +return ErrorPage \ No newline at end of file diff --git a/plugin/src/App/StatusPages/NotConnected.lua b/plugin/src/App/StatusPages/NotConnected.lua new file mode 100644 index 00000000..10988424 --- /dev/null +++ b/plugin/src/App/StatusPages/NotConnected.lua @@ -0,0 +1,154 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Config = require(Plugin.Config) + +local Theme = require(Plugin.App.Theme) +local BorderedContainer = require(Plugin.App.Components.BorderedContainer) +local TextButton = require(Plugin.App.Components.TextButton) +local Header = require(Plugin.App.Components.Header) + +local PORT_WIDTH = 74 +local DIVIDER_WIDTH = 1 +local HOST_OFFSET = 12 + +local e = Roact.createElement + +local function AddressEntry(props) + return Theme.with(function(theme) + return e(BorderedContainer, { + transparency = props.transparency, + size = UDim2.new(1, 0, 0, 36), + layoutOrder = props.layoutOrder, + }, { + Host = e("TextBox", { + Text = "", + Font = Enum.Font.Code, + TextSize = 18, + TextColor3 = theme.AddressEntry.TextColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = props.transparency, + PlaceholderText = Config.defaultHost, + PlaceholderColor3 = theme.AddressEntry.PlaceholderColor, + + Size = UDim2.new(1, -(HOST_OFFSET + DIVIDER_WIDTH + PORT_WIDTH), 1, 0), + Position = UDim2.new(0, HOST_OFFSET, 0, 0), + + ClipsDescendants = true, + BackgroundTransparency = 1, + + [Roact.Ref] = props.hostRef, + }), + + Port = e("TextBox", { + Text = "", + Font = Enum.Font.Code, + TextSize = 18, + TextColor3 = theme.AddressEntry.TextColor, + TextTransparency = props.transparency, + PlaceholderText = Config.defaultPort, + PlaceholderColor3 = theme.AddressEntry.PlaceholderColor, + + Size = UDim2.new(0, PORT_WIDTH, 1, 0), + Position = UDim2.new(1, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + + ClipsDescendants = true, + BackgroundTransparency = 1, + + [Roact.Ref] = props.portRef, + + [Roact.Change.Text] = function(object) + local text = object.Text + text = text:gsub("%D", "") + object.Text = text + end, + }, { + Divider = e("Frame", { + BackgroundColor3 = theme.BorderedContainer.BorderColor, + BackgroundTransparency = props.transparency, + Size = UDim2.new(0, DIVIDER_WIDTH, 1, 0), + AnchorPoint = Vector2.new(1, 0), + BorderSizePixel = 0, + }), + }), + }) + end) +end + +local NotConnectedPage = Roact.Component:extend("NotConnectedPage") + +function NotConnectedPage:init() + self.hostRef = Roact.createRef() + self.portRef = Roact.createRef() +end + +function NotConnectedPage:render() + return Roact.createFragment({ + Header = e(Header, { + transparency = self.props.transparency, + layoutOrder = 1, + }), + + AddressEntry = e(AddressEntry, { + hostRef = self.hostRef, + portRef = self.portRef, + transparency = self.props.transparency, + layoutOrder = 2, + }), + + Buttons = e("Frame", { + Size = UDim2.new(1, 0, 0, 34), + LayoutOrder = 3, + BackgroundTransparency = 1, + }, { + Settings = e(TextButton, { + text = "Settings", + style = "Bordered", + transparency = self.props.transparency, + layoutOrder = 1, + onClick = self.props.onNavigateSettings, + }), + + Connect = e(TextButton, { + text = "Connect", + style = "Solid", + transparency = self.props.transparency, + layoutOrder = 2, + onClick = function() + local hostText = self.hostRef.current.Text + local portText = self.portRef.current.Text + + self.props.onConnect( + #hostText > 0 and hostText or Config.defaultHost, + #portText > 0 and portText or Config.defaultPort + ) + end, + }), + + Layout = e("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Right, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + }), + + Layout = e("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), + }) +end + +return NotConnectedPage \ No newline at end of file diff --git a/plugin/src/App/StatusPages/Settings.lua b/plugin/src/App/StatusPages/Settings.lua new file mode 100644 index 00000000..1b933252 --- /dev/null +++ b/plugin/src/App/StatusPages/Settings.lua @@ -0,0 +1,230 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) +local PluginSettings = require(Plugin.App.PluginSettings) + +local Checkbox = require(Plugin.App.Components.Checkbox) +local IconButton = require(Plugin.App.Components.IconButton) +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) + +local e = Roact.createElement + +local DIVIDER_FADE_SIZE = 0.1 + +local function getTextBounds(text, textSize, font, lineHeight, bounds) + local textBounds = TextService:GetTextSize(text, textSize, font, bounds) + + local lineCount = textBounds.Y / textSize + local lineHeightAbsolute = textSize * lineHeight + + return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize)) +end + +local function Navbar(props) + return Theme.with(function(theme) + theme = theme.Settings.Navbar + + return e("Frame", { + Size = UDim2.new(1, 0, 0, 46), + LayoutOrder = props.layoutOrder, + BackgroundTransparency = 1, + }, { + Back = e(IconButton, { + icon = Assets.Images.Icons.Back, + iconSize = 24, + color = theme.BackButtonColor, + transparency = props.transparency, + + position = UDim2.new(0, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 0.5), + + onClick = props.onBack, + }), + + Text = e("TextLabel", { + Text = "Settings", + Font = Enum.Font.Gotham, + TextSize = 18, + TextColor3 = theme.TextColor, + TextTransparency = props.transparency, + + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + }) + }) + end) +end + +local Setting = Roact.Component:extend("Setting") + +function Setting:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function Setting:render() + return Theme.with(function(theme) + theme = theme.Settings + + return PluginSettings.with(function(settings) + return e("Frame", { + Size = self.contentSize:map(function(value) + return UDim2.new(1, 0, 0, 20 + value.Y + 20) + end), + LayoutOrder = self.props.layoutOrder, + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = function(object) + self.setContainerSize(object.AbsoluteSize) + end, + }, { + Checkbox = e(Checkbox, { + active = settings:get(self.props.id), + transparency = self.props.transparency, + position = UDim2.new(1, 0, 0.5, 0), + anchorPoint = Vector2.new(1, 0.5), + onClick = function() + local currentValue = settings:get(self.props.id) + settings:set(self.props.id, not currentValue) + end, + }), + + Text = e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Name = e("TextLabel", { + Text = self.props.name, + Font = Enum.Font.GothamBold, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + + Size = UDim2.new(1, 0, 0, 17), + + LayoutOrder = 1, + BackgroundTransparency = 1, + }), + + Description = e("TextLabel", { + Text = self.props.description, + Font = Enum.Font.Gotham, + LineHeight = 1.2, + TextSize = 14, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = self.containerSize:map(function(value) + local textBounds = getTextBounds( + self.props.description, 14, Enum.Font.Gotham, 1.2, + Vector2.new(value.X - 50, math.huge) + ) + return UDim2.new(1, -50, 0, textBounds.Y) + end), + + LayoutOrder = 2, + BackgroundTransparency = 1, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 6), + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + + Padding = e("UIPadding", { + PaddingTop = UDim.new(0, 20), + PaddingBottom = UDim.new(0, 20), + }), + }), + + Divider = e("Frame", { + BackgroundColor3 = theme.DividerColor, + BackgroundTransparency = self.props.transparency, + Size = UDim2.new(1, 0, 0, 1), + BorderSizePixel = 0, + }, { + Gradient = e("UIGradient", { + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1), + NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1, 1), + }), + }), + }), + }) + end) + end) +end + +local SettingsPage = Roact.Component:extend("SettingsPage") + +function SettingsPage:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function SettingsPage:render() + return Theme.with(function(theme) + theme = theme.Settings + + return e(ScrollingFrame, { + size = UDim2.new(1, 0, 1, 0), + contentSize = self.contentSize, + transparency = self.props.transparency, + }, { + Navbar = e(Navbar, { + onBack = self.props.onBack, + transparency = self.props.transparency, + layoutOrder = 0, + }), + + OpenScriptsExternally = e(Setting, { + id = "openScriptsExternally", + name = "Open Scripts Externally", + description = "Attempt to open scripts in an external editor", + transparency = self.props.transparency, + layoutOrder = 1, + }), + + TwoWaySync = e(Setting, { + id = "twoWaySync", + name = "Two-Way Sync", + description = "Editing files in Studio will sync them into the filesystem", + transparency = self.props.transparency, + layoutOrder = 2, + }), + + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), + }) + end) +end + +return SettingsPage \ No newline at end of file diff --git a/plugin/src/App/StatusPages/init.lua b/plugin/src/App/StatusPages/init.lua new file mode 100644 index 00000000..03d64e45 --- /dev/null +++ b/plugin/src/App/StatusPages/init.lua @@ -0,0 +1,7 @@ +return { + NotConnected = require(script.NotConnected), + Settings = require(script.Settings), + Connecting = require(script.Connecting), + Connected = require(script.Connected), + Error = require(script.Error), +} \ No newline at end of file diff --git a/plugin/src/App/Theme.lua b/plugin/src/App/Theme.lua new file mode 100644 index 00000000..2829cf4e --- /dev/null +++ b/plugin/src/App/Theme.lua @@ -0,0 +1,239 @@ +--[[ + Theming system taking advantage of Roact's new context API. + Doesn't use colors provided by Studio and instead just branches on theme + name. This isn't exactly best practice. +]] + +-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it +-- when possible. +local _Studio +local function getStudio() + if _Studio == nil then + _Studio = settings():GetService("Studio") + end + + return _Studio +end + +local Rojo = script:FindFirstAncestor("Rojo") + +local Roact = require(Rojo.Roact) +local Log = require(Rojo.Log) + +local strict = require(script.Parent.Parent.strict) + +-- Copying hex colors back and forth from design programs is faster +local function hexColor(decimal) + local red = bit32.band(bit32.rshift(decimal, 16), 2^8 - 1) + local green = bit32.band(bit32.rshift(decimal, 8), 2^8 - 1) + local blue = bit32.band(decimal, 2^8 - 1) + + return Color3.fromRGB(red, green, blue) +end + +local BRAND_COLOR = hexColor(0xE13835) + +local lightTheme = strict("LightTheme", { + BackgroundColor = hexColor(0xF0F0F0), + Button = { + Solid = { + ActionFillColor = hexColor(0xFFFFFF), + ActionFillTransparency = 0.8, + Enabled = { + TextColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + Disabled = { + TextColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + }, + Bordered = { + ActionFillColor = hexColor(0x000000), + ActionFillTransparency = 0.9, + Enabled = { + TextColor = hexColor(0x393939), + BorderColor = hexColor(0xACACAC), + }, + Disabled = { + TextColor = hexColor(0x393939), + BorderColor = hexColor(0xACACAC), + }, + }, + }, + Checkbox = { + Active = { + IconColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + Inactive = { + IconColor = hexColor(0xCACACA), + BorderColor = hexColor(0xAFAFAF), + }, + }, + AddressEntry = { + TextColor = hexColor(0x000000), + PlaceholderColor = hexColor(0x8C8C8C) + }, + BorderedContainer = { + BorderColor = hexColor(0xCBCBCB), + BackgroundColor = hexColor(0xE0E0E0), + }, + Spinner = { + ForegroundColor = BRAND_COLOR, + BackgroundColor = hexColor(0xE0E0E0), + }, + ConnectionDetails = { + ProjectNameColor = hexColor(0x00000), + AddressColor = hexColor(0x00000), + DisconnectColor = BRAND_COLOR, + }, + Settings = { + DividerColor = hexColor(0xCBCBCB), + Navbar = { + BackButtonColor = hexColor(0x000000), + TextColor = hexColor(0x000000), + }, + Setting = { + NameColor = hexColor(0x000000), + DescriptionColor = hexColor(0x5F5F5F), + }, + }, + Header = { + LogoColor = BRAND_COLOR, + VersionColor = hexColor(0x727272), + }, + ErrorColor = hexColor(0x000000), + ScrollBarColor = hexColor(0x000000), +}) + +local darkTheme = strict("DarkTheme", { + BackgroundColor = hexColor(0x272727), + Button = { + Solid = { + ActionFillColor = hexColor(0xFFFFFF), + ActionFillTransparency = 0.8, + Enabled = { + TextColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + Disabled = { + TextColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + }, + Bordered = { + ActionFillColor = hexColor(0xFFFFFF), + ActionFillTransparency = 0.9, + Enabled = { + TextColor = hexColor(0xDBDBDB), + BorderColor = hexColor(0x535353), + }, + Disabled = { + TextColor = hexColor(0xDBDBDB), + BorderColor = hexColor(0x535353), + }, + }, + }, + Checkbox = { + Active = { + IconColor = hexColor(0xFFFFFF), + BackgroundColor = BRAND_COLOR, + }, + Inactive = { + IconColor = hexColor(0x484848), + BorderColor = hexColor(0x5A5A5A), + }, + }, + AddressEntry = { + TextColor = hexColor(0xFFFFFF), + PlaceholderColor = hexColor(0x717171) + }, + BorderedContainer = { + BorderColor = hexColor(0x535353), + BackgroundColor = hexColor(0x323232), + }, + Spinner = { + ForegroundColor = BRAND_COLOR, + BackgroundColor = hexColor(0x323232), + }, + ConnectionDetails = { + ProjectNameColor = hexColor(0xFFFFFF), + AddressColor = hexColor(0xFFFFFF), + DisconnectColor = hexColor(0xFFFFFF), + }, + Settings = { + DividerColor = hexColor(0x535353), + Navbar = { + BackButtonColor = hexColor(0xFFFFFF), + TextColor = hexColor(0xFFFFFF), + }, + Setting = { + NameColor = hexColor(0xFFFFFF), + DescriptionColor = hexColor(0xD3D3D3), + }, + }, + Header = { + LogoColor = hexColor(0xFFFFFF), + VersionColor = hexColor(0xD3D3D3) + }, + ErrorColor = hexColor(0xFFFFFF), + ScrollBarColor = hexColor(0xFFFFFF), +}) + +local Context = Roact.createContext(lightTheme) + +local StudioProvider = Roact.Component:extend("StudioProvider") + +-- Pull the current theme from Roblox Studio and update state with it. +function StudioProvider:updateTheme() + local studioTheme = getStudio().Theme + + if studioTheme.Name == "Light" then + self:setState({ + theme = lightTheme, + }) + elseif studioTheme.Name == "Dark" then + self:setState({ + theme = darkTheme, + }) + else + Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name) + + self:setState({ + theme = lightTheme, + }) + end +end + +function StudioProvider:init() + self:updateTheme() +end + +function StudioProvider:render() + return Roact.createElement(Context.Provider, { + value = self.state.theme, + }, self.props[Roact.Children]) +end + +function StudioProvider:didMount() + self.connection = getStudio().ThemeChanged:Connect(function() + self:updateTheme() + end) +end + +function StudioProvider:willUnmount() + self.connection:Disconnect() +end + +local function with(callback) + return Roact.createElement(Context.Consumer, { + render = callback, + }) +end + +return { + StudioProvider = StudioProvider, + Consumer = Context.Consumer, + with = with, +} \ No newline at end of file diff --git a/plugin/src/App/bindingUtil.lua b/plugin/src/App/bindingUtil.lua new file mode 100644 index 00000000..daabd711 --- /dev/null +++ b/plugin/src/App/bindingUtil.lua @@ -0,0 +1,58 @@ +local Rojo = script:FindFirstAncestor("Rojo") + +local Roact = require(Rojo.Roact) +local Log = require(Rojo.Log) + +local LERP_DATA_TYPES = { + Color3 = true, + UDim = true, + UDim2 = true, + Vector2 = true, + Vector3 = true, +} + +local function fromMotor(motor) + local motorBinding, setMotorBinding = Roact.createBinding(motor:getValue()) + motor:onStep(setMotorBinding) + return motorBinding +end + +local function mapLerp(binding, value1, value2) + local valueType = typeof(value1) + if valueType ~= typeof(value2) then + Log.error("Type mismatch between values ({}, {}})", valueType, typeof(value2)) + end + + return binding:map(function(position) + if valueType == "number" then + return value1 - (value2 - value1) * position + elseif LERP_DATA_TYPES[valueType] then + return value1:lerp(value2, position) + else + Log.error("Unable to interpolate type {}", valueType) + end + end) +end + +local function deriveProperty(binding, propertyName) + return binding:map(function(values) + return values[propertyName] + end) +end + +local function blendAlpha(alphaValues) + local alpha = 0 + + for _, value in pairs(alphaValues) do + alpha = alpha + (1 - alpha) * value + end + + return alpha +end + +return { + fromMotor = fromMotor, + mapLerp = mapLerp, + deriveProperty = deriveProperty, + blendAlpha = blendAlpha, +} \ No newline at end of file diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua new file mode 100644 index 00000000..b4d01f5b --- /dev/null +++ b/plugin/src/App/init.lua @@ -0,0 +1,226 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin + +local Roact = require(Rojo.Roact) +local Log = require(Rojo.Log) + +local Assets = require(Plugin.Assets) +local Version = require(Plugin.Version) +local Config = require(Plugin.Config) +local strict = require(Plugin.strict) +local Dictionary = require(Plugin.Dictionary) +local ServeSession = require(Plugin.ServeSession) +local ApiContext = require(Plugin.ApiContext) +local preloadAssets = require(Plugin.preloadAssets) +local Theme = require(script.Theme) +local PluginSettings = require(script.PluginSettings) + +local Page = require(script.Page) +local StudioToolbar = require(script.Components.Studio.StudioToolbar) +local StudioToggleButton = require(script.Components.Studio.StudioToggleButton) +local StudioPluginGui = require(script.Components.Studio.StudioPluginGui) +local StudioPluginContext = require(script.Components.Studio.StudioPluginContext) +local StatusPages = require(script.StatusPages) + +local AppStatus = strict("AppStatus", { + NotConnected = "NotConnected", + Settings = "Settings", + Connecting = "Connecting", + Connected = "Connected", + Error = "Error", +}) + +local e = Roact.createElement + +local App = Roact.Component:extend("App") + +function App:init() + preloadAssets() + + self:setState({ + appStatus = AppStatus.NotConnected, + guiEnabled = false, + }) +end + +function App:startSession(host, port, sessionOptions) + local baseUrl = ("http://%s:%s"):format(host, port) + local apiContext = ApiContext.new(baseUrl) + + local serveSession = ServeSession.new({ + apiContext = apiContext, + openScriptsExternally = sessionOptions.openScriptsExternally, + twoWaySync = sessionOptions.twoWaySync, + }) + + serveSession:onStatusChanged(function(status, details) + if status == ServeSession.Status.Connecting then + self:setState({ + appStatus = AppStatus.Connecting, + }) + elseif status == ServeSession.Status.Connected then + local address = ("%s:%s"):format(host, port) + self:setState({ + appStatus = AppStatus.Connected, + projectName = details, + address = address, + }) + elseif status == ServeSession.Status.Disconnected then + self.serveSession = nil + + -- Details being present indicates that this + -- disconnection was from an error. + if details ~= nil then + Log.warn("Disconnected from an error: {}", details) + + self:setState({ + appStatus = AppStatus.Error, + errorMessage = tostring(details), + }) + else + self:setState({ + appStatus = AppStatus.NotConnected, + }) + end + end + end) + + serveSession:start() + + self.serveSession = serveSession +end + +function App:render() + local pluginName = "Rojo " .. Version.display(Config.version) + + local function createPageElement(appStatus, additionalProps) + additionalProps = additionalProps or {} + + local props = Dictionary.merge(additionalProps, { + component = StatusPages[appStatus], + active = self.state.appStatus == appStatus, + }) + + return e(Page, props) + end + + return e(StudioPluginContext.Provider, { + value = self.props.plugin, + }, { + e(Theme.StudioProvider, nil, { + e(PluginSettings.StudioProvider, { + plugin = self.props.plugin, + }, { + gui = e(StudioPluginGui, { + id = pluginName, + title = pluginName, + active = self.state.guiEnabled, + + initDockState = Enum.InitialDockState.Right, + initEnabled = false, + overridePreviousState = false, + floatingSize = Vector2.new(300, 200), + minimumSize = Vector2.new(300, 200), + + zIndexBehavior = Enum.ZIndexBehavior.Sibling, + + onInitialState = function(initialState) + self:setState({ + guiEnabled = initialState, + }) + end, + + onClose = function() + self:setState({ + guiEnabled = false, + }) + end, + }, { + NotConnectedPage = PluginSettings.with(function(settings) + return createPageElement(AppStatus.NotConnected, { + onConnect = function(host, port) + self:startSession(host, port, { + openScriptsExternally = settings:get("openScriptsExternally"), + twoWaySync = settings:get("twoWaySync"), + }) + end, + + onNavigateSettings = function() + self:setState({ + appStatus = AppStatus.Settings, + }) + end, + }) + end), + + Connecting = createPageElement(AppStatus.Connecting), + + Connected = createPageElement(AppStatus.Connected, { + projectName = self.state.projectName, + address = self.state.address, + + onDisconnect = function() + Log.trace("Disconnecting session") + + self.serveSession:stop() + self.serveSession = nil + self:setState({ + appStatus = AppStatus.NotConnected, + }) + + Log.trace("Session terminated by user") + end, + }), + + Settings = createPageElement(AppStatus.Settings, { + onBack = function() + self:setState({ + appStatus = AppStatus.NotConnected, + }) + end, + }), + + Error = createPageElement(AppStatus.Error, { + errorMessage = self.state.errorMessage, + + onClose = function() + self:setState({ + appStatus = AppStatus.NotConnected, + }) + end, + }), + + Background = Theme.with(function(theme) + return e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = theme.BackgroundColor, + ZIndex = 0, + BorderSizePixel = 0, + }) + end), + }), + + toolbar = e(StudioToolbar, { + name = pluginName, + }, { + button = e(StudioToggleButton, { + name = "Rojo", + tooltip = "Show or hide the Rojo panel", + icon = Assets.Images.PluginButton, + active = self.state.guiEnabled, + enabled = true, + onClick = function() + self:setState(function(state) + return { + guiEnabled = not state.guiEnabled, + } + end) + end, + }) + }), + }), + }), + }) +end + +return App \ No newline at end of file diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 86b8500c..beb4bad5 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -3,16 +3,45 @@ local strict = require(script.Parent.strict) local Assets = { Sprites = {}, Slices = { - RoundBox = { - asset = "rbxassetid://2773204550", - offset = Vector2.new(0, 0), - size = Vector2.new(32, 32), - center = Rect.new(4, 4, 4, 4), + RoundedBackground = { + Image = "rbxassetid://5981360418", + Center = Rect.new(10, 10, 10, 10), + Scale = 0.5, + }, + + RoundedBorder = { + Image = "rbxassetid://5981360137", + Center = Rect.new(10, 10, 10, 10), + Scale = 0.5, }, }, Images = { - Logo = "rbxassetid://3405346157", - Icon = "rbxassetid://3405341609", + Logo = "rbxassetid://5990772764", + PluginButton = "rbxassetid://3405341609", + Icons = { + Close = "rbxassetid://6012985953", + Back = "rbxassetid://6017213752", + }, + Checkbox = { + Active = "rbxassetid://6016251644", + Inactive = "rbxassetid://6016251963", + }, + Spinner = { + Foreground = "rbxassetid://3222731032", + Background = "rbxassetid://3222730627", + }, + ScrollBar = { + Top = "rbxassetid://6017290134", + Middle = "rbxassetid://6017289904", + Bottom = "rbxassetid://6017289712", + }, + Circles = { + [16] = "rbxassetid://3056541177", + [32] = "rbxassetid://3088713341", + [64] = "rbxassetid://4918677124", + [128] = "rbxassetid://2600845734", + [500] = "rbxassetid://2609138523" + }, }, StartSession = "", SessionActive = "", diff --git a/plugin/src/Components/App.lua b/plugin/src/Components/App.lua deleted file mode 100644 index 8a195235..00000000 --- a/plugin/src/Components/App.lua +++ /dev/null @@ -1,241 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) -local Log = require(Rojo.Log) - -local ApiContext = require(Plugin.ApiContext) -local Assets = require(Plugin.Assets) -local Config = require(Plugin.Config) -local DevSettings = require(Plugin.DevSettings) -local ServeSession = require(Plugin.ServeSession) -local Version = require(Plugin.Version) -local preloadAssets = require(Plugin.preloadAssets) -local strict = require(Plugin.strict) - -local ConnectPanel = require(Plugin.Components.ConnectPanel) -local ConnectingPanel = require(Plugin.Components.ConnectingPanel) -local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel) -local ErrorPanel = require(Plugin.Components.ErrorPanel) -local SettingsPanel = require(Plugin.Components.SettingsPanel) - -local e = Roact.createElement - -local function showUpgradeMessage(lastVersion) - local message = ( - "Rojo detected an upgrade from version %s to version %s." .. - "\nMake sure you have also upgraded your server!" .. - "\n\nRojo plugin version %s is intended for use with server version %s." - ):format( - Version.display(lastVersion), Version.display(Config.version), - Version.display(Config.version), Config.expectedServerVersionString - ) - - Log.info(message) -end - ---[[ - Check if the user is using a newer version of Rojo than last time. If they - are, show them a reminder to make sure they check their server version. -]] -local function checkUpgrade(plugin) - -- When developing Rojo, there's no use in doing version checks - if DevSettings:isEnabled() then - return - end - - local lastVersion = plugin:GetSetting("LastRojoVersion") - - if lastVersion then - local wasUpgraded = Version.compare(Config.version, lastVersion) == 1 - - if wasUpgraded then - showUpgradeMessage(lastVersion) - end - end - - plugin:SetSetting("LastRojoVersion", Config.version) -end - -local AppStatus = strict("AppStatus", { - NotStarted = "NotStarted", - Connecting = "Connecting", - Connected = "Connected", - Error = "Error", - Settings = "Settings", -}) - -local App = Roact.Component:extend("App") - -function App:init() - self:setState({ - appStatus = AppStatus.NotStarted, - errorMessage = nil, - }) - - self.signals = {} - self.serveSession = nil - self.displayedVersion = Version.display(Config.version) - - local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion) - - self.toggleButton = toolbar:CreateButton( - "Rojo", - "Show or hide the Rojo panel", - Assets.Images.Icon) - self.toggleButton.ClickableWhenViewportHidden = true - self.toggleButton.Click:Connect(function() - self.dockWidget.Enabled = not self.dockWidget.Enabled - end) - - local widgetInfo = DockWidgetPluginGuiInfo.new( - Enum.InitialDockState.Right, - false, -- Initially enabled state - false, -- Whether to override the widget's previous state - 360, 190, -- Floating size - 360, 190 -- Minimum size - ) - - self.dockWidget = self.props.plugin:CreateDockWidgetPluginGui("Rojo-" .. self.displayedVersion, widgetInfo) - self.dockWidget.Name = "Rojo " .. self.displayedVersion - self.dockWidget.Title = "Rojo " .. self.displayedVersion - self.dockWidget.AutoLocalize = false - self.dockWidget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling - - self.signals.dockWidgetEnabled = self.dockWidget:GetPropertyChangedSignal("Enabled"):Connect(function() - self.toggleButton:SetActive(self.dockWidget.Enabled) - end) -end - -function App:startSession(address, port, sessionOptions) - Log.trace("Starting new session") - - local baseUrl = ("http://%s:%s"):format(address, port) - self.serveSession = ServeSession.new({ - apiContext = ApiContext.new(baseUrl), - openScriptsExternally = sessionOptions.openScriptsExternally, - twoWaySync = sessionOptions.twoWaySync, - }) - - self.serveSession:onStatusChanged(function(status, details) - if status == ServeSession.Status.Connecting then - self:setState({ - appStatus = AppStatus.Connecting, - }) - elseif status == ServeSession.Status.Connected then - self:setState({ - appStatus = AppStatus.Connected, - }) - elseif status == ServeSession.Status.Disconnected then - self.serveSession = nil - - -- Details being present indicates that this - -- disconnection was from an error. - if details ~= nil then - Log.warn("Disconnected from an error: {}", details) - - self:setState({ - appStatus = AppStatus.Error, - errorMessage = tostring(details), - }) - else - self:setState({ - appStatus = AppStatus.NotStarted, - }) - end - end - end) - - self.serveSession:start() -end - -function App:render() - local children - - if self.state.appStatus == AppStatus.NotStarted then - children = { - ConnectPanel = e(ConnectPanel, { - startSession = function(address, port, settings) - self:startSession(address, port, settings) - end, - openSettings = function() - self:setState({ - appStatus = AppStatus.Settings, - }) - end, - cancel = function() - Log.trace("Canceling session configuration") - - self:setState({ - appStatus = AppStatus.NotStarted, - }) - end, - }), - } - elseif self.state.appStatus == AppStatus.Connecting then - children = { - ConnectingPanel = e(ConnectingPanel), - } - elseif self.state.appStatus == AppStatus.Connected then - children = { - ConnectionActivePanel = e(ConnectionActivePanel, { - stopSession = function() - Log.trace("Disconnecting session") - - self.serveSession:stop() - self.serveSession = nil - self:setState({ - appStatus = AppStatus.NotStarted, - }) - - Log.trace("Session terminated by user") - end, - }), - } - elseif self.state.appStatus == AppStatus.Settings then - children = { - e(SettingsPanel, { - back = function() - self:setState({ - appStatus = AppStatus.NotStarted, - }) - end, - }), - } - elseif self.state.appStatus == AppStatus.Error then - children = { - ErrorPanel = e(ErrorPanel, { - errorMessage = self.state.errorMessage, - onDismiss = function() - self:setState({ - appStatus = AppStatus.NotStarted, - }) - end, - }), - } - end - - return e(Roact.Portal, { - target = self.dockWidget, - }, children) -end - -function App:didMount() - Log.trace("Rojo {} initializing", self.displayedVersion) - - checkUpgrade(self.props.plugin) - preloadAssets() -end - -function App:willUnmount() - if self.serveSession ~= nil then - self.serveSession:stop() - self.serveSession = nil - end - - for _, signal in pairs(self.signals) do - signal:Disconnect() - end -end - -return App \ No newline at end of file diff --git a/plugin/src/Components/Checkbox.lua b/plugin/src/Components/Checkbox.lua deleted file mode 100644 index 0969b8e4..00000000 --- a/plugin/src/Components/Checkbox.lua +++ /dev/null @@ -1,39 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) - -local Theme = require(Plugin.Components.Theme) - -local e = Roact.createElement - -local function Checkbox(props) - local checked = props.checked - local layoutOrder = props.layoutOrder - local onChange = props.onChange - - return Theme.with(function(theme) - return e("ImageButton", { - LayoutOrder = layoutOrder, - Size = UDim2.new(0, 20, 0, 20), - BorderSizePixel = 2, - BorderColor3 = theme.Text2, - BackgroundColor3 = theme.Background2, - - [Roact.Event.Activated] = function() - onChange(not checked) - end, - }, { - Indicator = e("Frame", { - Size = UDim2.new(0, 18, 0, 18), - Position = UDim2.new(0.5, 0, 0.5, 0), - AnchorPoint = Vector2.new(0.5, 0.5), - BorderSizePixel = 0, - BackgroundColor3 = theme.Brand1, - BackgroundTransparency = checked and 0 or 1, - }) - }) - end) -end - -return Checkbox \ No newline at end of file diff --git a/plugin/src/Components/ConnectPanel.lua b/plugin/src/Components/ConnectPanel.lua deleted file mode 100644 index bebd67a6..00000000 --- a/plugin/src/Components/ConnectPanel.lua +++ /dev/null @@ -1,183 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) - -local Config = require(Plugin.Config) - -local Theme = require(Plugin.Components.Theme) -local Panel = require(Plugin.Components.Panel) -local FitList = require(Plugin.Components.FitList) -local FitText = require(Plugin.Components.FitText) -local FormButton = require(Plugin.Components.FormButton) -local FormTextInput = require(Plugin.Components.FormTextInput) -local PluginSettings = require(Plugin.Components.PluginSettings) - -local e = Roact.createElement - -local ConnectPanel = Roact.Component:extend("ConnectPanel") - -function ConnectPanel:init() - self:setState({ - address = "", - port = "", - }) -end - -function ConnectPanel:render() - local startSession = self.props.startSession - local openSettings = self.props.openSettings - - return Theme.with(function(theme) - return PluginSettings.with(function(settings) - return e(Panel, nil, { - Layout = e("UIListLayout", { - SortOrder = Enum.SortOrder.LayoutOrder, - HorizontalAlignment = Enum.HorizontalAlignment.Center, - VerticalAlignment = Enum.VerticalAlignment.Center, - }), - - Inputs = e(FitList, { - containerProps = { - BackgroundTransparency = 1, - LayoutOrder = 1, - }, - layoutProps = { - FillDirection = Enum.FillDirection.Horizontal, - Padding = UDim.new(0, 8), - }, - paddingProps = { - PaddingTop = UDim.new(0, 20), - PaddingBottom = UDim.new(0, 10), - PaddingLeft = UDim.new(0, 24), - PaddingRight = UDim.new(0, 24), - }, - }, { - Address = e(FitList, { - containerProps = { - LayoutOrder = 1, - BackgroundTransparency = 1, - }, - layoutProps = { - Padding = UDim.new(0, 4), - }, - }, { - Label = e(FitText, { - Kind = "TextLabel", - LayoutOrder = 1, - BackgroundTransparency = 1, - TextXAlignment = Enum.TextXAlignment.Left, - Font = theme.TitleFont, - TextSize = 20, - Text = "Address", - TextColor3 = theme.Text1, - }), - - Input = e(FormTextInput, { - layoutOrder = 2, - width = UDim.new(0, 220), - value = self.state.address, - placeholderValue = Config.defaultHost, - onValueChange = function(newValue) - self:setState({ - address = newValue, - }) - end, - }), - }), - - Port = e(FitList, { - containerProps = { - LayoutOrder = 2, - BackgroundTransparency = 1, - }, - layoutProps = { - Padding = UDim.new(0, 4), - }, - }, { - Label = e(FitText, { - Kind = "TextLabel", - LayoutOrder = 1, - BackgroundTransparency = 1, - TextXAlignment = Enum.TextXAlignment.Left, - Font = theme.TitleFont, - TextSize = 20, - Text = "Port", - TextColor3 = theme.Text1, - }), - - Input = e(FormTextInput, { - layoutOrder = 2, - width = UDim.new(0, 80), - value = self.state.port, - placeholderValue = Config.defaultPort, - onValueChange = function(newValue) - self:setState({ - port = newValue, - }) - end, - }), - }), - }), - - Buttons = e(FitList, { - fitAxes = "Y", - containerProps = { - BackgroundTransparency = 1, - LayoutOrder = 2, - Size = UDim2.new(1, 0, 0, 0), - }, - layoutProps = { - FillDirection = Enum.FillDirection.Horizontal, - HorizontalAlignment = Enum.HorizontalAlignment.Right, - Padding = UDim.new(0, 8), - }, - paddingProps = { - PaddingTop = UDim.new(0, 0), - PaddingBottom = UDim.new(0, 20), - PaddingLeft = UDim.new(0, 24), - PaddingRight = UDim.new(0, 24), - }, - }, { - e(FormButton, { - layoutOrder = 1, - text = "Settings", - secondary = true, - onClick = function() - if openSettings ~= nil then - openSettings() - end - end, - }), - - e(FormButton, { - layoutOrder = 2, - text = "Connect", - onClick = function() - if startSession ~= nil then - local address = self.state.address - if address:len() == 0 then - address = Config.defaultHost - end - - local port = self.state.port - if port:len() == 0 then - port = Config.defaultPort - end - - local sessionOptions = { - openScriptsExternally = settings:get("openScriptsExternally"), - twoWaySync = settings:get("twoWaySync"), - } - - startSession(address, port, sessionOptions) - end - end, - }), - }), - }) - end) - end) -end - -return ConnectPanel \ No newline at end of file diff --git a/plugin/src/Components/ConnectingPanel.lua b/plugin/src/Components/ConnectingPanel.lua deleted file mode 100644 index 9dd0c126..00000000 --- a/plugin/src/Components/ConnectingPanel.lua +++ /dev/null @@ -1,35 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Plugin = script:FindFirstAncestor("Plugin") - -local Theme = require(Plugin.Components.Theme) -local Panel = require(Plugin.Components.Panel) -local FitText = require(Plugin.Components.FitText) - -local e = Roact.createElement - -local ConnectingPanel = Roact.Component:extend("ConnectingPanel") - -function ConnectingPanel:render() - return Theme.with(function(theme) - return e(Panel, nil, { - Layout = Roact.createElement("UIListLayout", { - HorizontalAlignment = Enum.HorizontalAlignment.Center, - VerticalAlignment = Enum.VerticalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 8), - }), - - Text = e(FitText, { - Padding = Vector2.new(12, 6), - Font = theme.ButtonFont, - TextSize = 18, - Text = "Connecting...", - TextColor3 = theme.Text1, - BackgroundTransparency = 1, - }), - }) - end) -end - -return ConnectingPanel \ No newline at end of file diff --git a/plugin/src/Components/ConnectionActivePanel.lua b/plugin/src/Components/ConnectionActivePanel.lua deleted file mode 100644 index 437589a9..00000000 --- a/plugin/src/Components/ConnectionActivePanel.lua +++ /dev/null @@ -1,47 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Plugin = script:FindFirstAncestor("Plugin") - -local Theme = require(Plugin.Components.Theme) -local Panel = require(Plugin.Components.Panel) -local FitText = require(Plugin.Components.FitText) -local FormButton = require(Plugin.Components.FormButton) - -local e = Roact.createElement - -local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel") - -function ConnectionActivePanel:render() - local stopSession = self.props.stopSession - - return Theme.with(function(theme) - return e(Panel, nil, { - Layout = Roact.createElement("UIListLayout", { - HorizontalAlignment = Enum.HorizontalAlignment.Center, - VerticalAlignment = Enum.VerticalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 8), - }), - - Text = e(FitText, { - Padding = Vector2.new(12, 6), - Font = theme.ButtonFont, - TextSize = 18, - Text = "Connected to Live-Sync Server", - TextColor3 = theme.Text1, - BackgroundTransparency = 1, - }), - - DisconnectButton = e(FormButton, { - layoutOrder = 2, - text = "Disconnect", - secondary = true, - onClick = function() - stopSession() - end, - }), - }) - end) -end - -return ConnectionActivePanel \ No newline at end of file diff --git a/plugin/src/Components/ErrorPanel.lua b/plugin/src/Components/ErrorPanel.lua deleted file mode 100644 index e541e0d8..00000000 --- a/plugin/src/Components/ErrorPanel.lua +++ /dev/null @@ -1,70 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Plugin = script:FindFirstAncestor("Plugin") - -local Theme = require(Plugin.Components.Theme) -local Panel = require(Plugin.Components.Panel) -local FitText = require(Plugin.Components.FitText) -local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame) -local FormButton = require(Plugin.Components.FormButton) - -local e = Roact.createElement - -local BUTTON_HEIGHT = 60 -local HOR_PADDING = 8 - -local ErrorPanel = Roact.Component:extend("ErrorPanel") - -function ErrorPanel:render() - local errorMessage = self.props.errorMessage - local onDismiss = self.props.onDismiss - - return Theme.with(function(theme) - return e(Panel, nil, { - Layout = Roact.createElement("UIListLayout", { - HorizontalAlignment = Enum.HorizontalAlignment.Center, - VerticalAlignment = Enum.VerticalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 8), - }), - - ErrorContainer = e(FitScrollingFrame, { - containerProps = { - BackgroundTransparency = 1, - BorderSizePixel = 0, - Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT), - Position = UDim2.new(0, HOR_PADDING, 0, 0), - ScrollBarImageColor3 = theme.Text1, - VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar, - ScrollingDirection = Enum.ScrollingDirection.Y, - }, - }, { - Text = e(FitText, { - Size = UDim2.new(1, 0, 0, 0), - - LayoutOrder = 1, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - FitAxis = "Y", - Font = theme.ButtonFont, - TextSize = 18, - Text = errorMessage, - TextWrap = true, - TextColor3 = theme.Text1, - BackgroundTransparency = 1, - }), - }), - - DismissButton = e(FormButton, { - layoutOrder = 2, - text = "Dismiss", - secondary = true, - onClick = function() - onDismiss() - end, - }), - }) - end) -end - -return ErrorPanel \ No newline at end of file diff --git a/plugin/src/Components/FitList.lua b/plugin/src/Components/FitList.lua deleted file mode 100644 index e86e185a..00000000 --- a/plugin/src/Components/FitList.lua +++ /dev/null @@ -1,63 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Dictionary = require(script.Parent.Parent.Dictionary) - -local e = Roact.createElement - -local FitList = Roact.Component:extend("FitList") - -function FitList:init() - self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new()) -end - -function FitList:render() - local containerKind = self.props.containerKind or "Frame" - local fitAxes = self.props.fitAxes or "XY" - local containerProps = self.props.containerProps - local layoutProps = self.props.layoutProps - local paddingProps = self.props.paddingProps - - local padding - if paddingProps ~= nil then - padding = e("UIPadding", paddingProps) - end - - local children = Dictionary.merge(self.props[Roact.Children], { - ["$Layout"] = e("UIListLayout", Dictionary.merge({ - SortOrder = Enum.SortOrder.LayoutOrder, - [Roact.Change.AbsoluteContentSize] = function(instance) - local contentSize = instance.AbsoluteContentSize - - if paddingProps ~= nil then - contentSize = contentSize + Vector2.new( - paddingProps.PaddingLeft.Offset + paddingProps.PaddingRight.Offset, - paddingProps.PaddingTop.Offset + paddingProps.PaddingBottom.Offset) - end - - local combinedSize - - if fitAxes == "X" then - combinedSize = UDim2.new(0, contentSize.X, containerProps.Size.Y.Scale, containerProps.Size.Y.Offset) - elseif fitAxes == "Y" then - combinedSize = UDim2.new(containerProps.Size.X.Scale, containerProps.Size.X.Offset, 0, contentSize.Y) - elseif fitAxes == "XY" then - combinedSize = UDim2.new(0, contentSize.X, 0, contentSize.Y) - else - error("Invalid fitAxes value") - end - - self.setSize(combinedSize) - end, - }, layoutProps)), - - ["$Padding"] = padding, - }) - - local fullContainerProps = Dictionary.merge(containerProps, { - Size = self.sizeBinding, - }) - - return e(containerKind, fullContainerProps, children) -end - -return FitList \ No newline at end of file diff --git a/plugin/src/Components/FitScrollingFrame.lua b/plugin/src/Components/FitScrollingFrame.lua deleted file mode 100644 index ede927fd..00000000 --- a/plugin/src/Components/FitScrollingFrame.lua +++ /dev/null @@ -1,33 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Dictionary = require(script.Parent.Parent.Dictionary) - -local e = Roact.createElement - -local FitScrollingFrame = Roact.Component:extend("FitScrollingFrame") - -function FitScrollingFrame:init() - self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new()) -end - -function FitScrollingFrame:render() - local containerProps = self.props.containerProps - local layoutProps = self.props.layoutProps - - local children = Dictionary.merge(self.props[Roact.Children], { - ["$Layout"] = e("UIListLayout", Dictionary.merge({ - SortOrder = Enum.SortOrder.LayoutOrder, - [Roact.Change.AbsoluteContentSize] = function(instance) - self.setSize(UDim2.new(0, 0, 0, instance.AbsoluteContentSize.Y)) - end, - }, layoutProps)), - }) - - local fullContainerProps = Dictionary.merge(containerProps, { - CanvasSize = self.sizeBinding, - }) - - return e("ScrollingFrame", fullContainerProps, children) -end - -return FitScrollingFrame \ No newline at end of file diff --git a/plugin/src/Components/FitText.lua b/plugin/src/Components/FitText.lua deleted file mode 100644 index 1c036ba9..00000000 --- a/plugin/src/Components/FitText.lua +++ /dev/null @@ -1,88 +0,0 @@ -local TextService = game:GetService("TextService") - -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Dictionary = require(script.Parent.Parent.Dictionary) - -local e = Roact.createElement - -local FitText = Roact.Component:extend("FitText") - -function FitText:init() - self.ref = Roact.createRef() - self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new()) -end - -function FitText:render() - local kind = self.props.Kind or "TextLabel" - - local containerProps = Dictionary.merge(self.props, { - FitAxis = Dictionary.None, - Kind = Dictionary.None, - Padding = Dictionary.None, - MinSize = Dictionary.None, - Size = self.sizeBinding, - [Roact.Ref] = self.ref, - [Roact.Change.AbsoluteSize] = function() - self:updateTextMeasurements() - end - }) - - return e(kind, containerProps) -end - -function FitText:didMount() - self:updateTextMeasurements() -end - -function FitText:didUpdate() - self:updateTextMeasurements() -end - -function FitText:updateTextMeasurements() - local minSize = self.props.MinSize or Vector2.new(0, 0) - local padding = self.props.Padding or Vector2.new(0, 0) - local fitAxis = self.props.FitAxis or "XY" - local baseSize = self.props.Size - - local text = self.props.Text or "" - local font = self.props.Font or Enum.Font.Legacy - local textSize = self.props.TextSize or 12 - - local containerSize = self.ref.current.AbsoluteSize - - local textBounds - - if fitAxis == "XY" then - textBounds = Vector2.new(9e6, 9e6) - elseif fitAxis == "X" then - textBounds = Vector2.new(9e6, containerSize.Y - padding.Y * 2) - elseif fitAxis == "Y" then - textBounds = Vector2.new(containerSize.X - padding.X * 2, 9e6) - end - - local measuredText = TextService:GetTextSize(text, textSize, font, textBounds) - - local computedX = math.max(minSize.X, padding.X * 2 + measuredText.X) - local computedY = math.max(minSize.Y, padding.Y * 2 + measuredText.Y) - - local totalSize - - if fitAxis == "XY" then - totalSize = UDim2.new( - 0, computedX, - 0, computedY) - elseif fitAxis == "X" then - totalSize = UDim2.new( - 0, computedX, - baseSize.Y.Scale, baseSize.Y.Offset) - elseif fitAxis == "Y" then - totalSize = UDim2.new( - baseSize.X.Scale, baseSize.X.Offset, - 0, computedY) - end - - self.setSize(totalSize) -end - -return FitText \ No newline at end of file diff --git a/plugin/src/Components/FormButton.lua b/plugin/src/Components/FormButton.lua deleted file mode 100644 index 7451395a..00000000 --- a/plugin/src/Components/FormButton.lua +++ /dev/null @@ -1,64 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) - -local Assets = require(Plugin.Assets) -local Theme = require(Plugin.Components.Theme) -local FitList = require(Plugin.Components.FitList) -local FitText = require(Plugin.Components.FitText) - -local e = Roact.createElement - -local RoundBox = Assets.Slices.RoundBox - -local function FormButton(props) - local text = props.text - local layoutOrder = props.layoutOrder - local onClick = props.onClick - - local textColor - local backgroundColor - - return Theme.with(function(theme) - if props.secondary then - textColor = theme.Brand1 - backgroundColor = theme.Background2 - else - textColor = theme.TextOnAccent - backgroundColor = theme.Brand1 - end - - return e(FitList, { - containerKind = "ImageButton", - containerProps = { - LayoutOrder = layoutOrder, - BackgroundTransparency = 1, - Image = RoundBox.asset, - ImageRectOffset = RoundBox.offset, - ImageRectSize = RoundBox.size, - SliceCenter = RoundBox.center, - ScaleType = Enum.ScaleType.Slice, - ImageColor3 = backgroundColor, - - [Roact.Event.Activated] = function() - if onClick ~= nil then - onClick() - end - end, - }, - }, { - Text = e(FitText, { - Kind = "TextLabel", - Text = text, - TextSize = 18, - TextColor3 = textColor, - Font = theme.ButtonFont, - Padding = Vector2.new(16, 8), - BackgroundTransparency = 1, - }), - }) - end) -end - -return FormButton \ No newline at end of file diff --git a/plugin/src/Components/FormTextInput.lua b/plugin/src/Components/FormTextInput.lua deleted file mode 100644 index f1f4625f..00000000 --- a/plugin/src/Components/FormTextInput.lua +++ /dev/null @@ -1,82 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) - -local Assets = require(Plugin.Assets) -local Theme = require(Plugin.Components.Theme) - -local e = Roact.createElement - -local RoundBox = Assets.Slices.RoundBox - -local TEXT_SIZE = 22 -local PADDING = 8 - -local FormTextInput = Roact.Component:extend("FormTextInput") - -function FormTextInput:init() - self:setState({ - focused = false, - }) -end - -function FormTextInput:render() - local value = self.props.value - local placeholderValue = self.props.placeholderValue - local onValueChange = self.props.onValueChange - local layoutOrder = self.props.layoutOrder - local width = self.props.width - - local shownPlaceholder - if self.state.focused then - shownPlaceholder = "" - else - shownPlaceholder = placeholderValue - end - - return Theme.with(function(theme) - return e("ImageLabel", { - LayoutOrder = layoutOrder, - Image = RoundBox.asset, - ImageRectOffset = RoundBox.offset, - ImageRectSize = RoundBox.size, - ScaleType = Enum.ScaleType.Slice, - SliceCenter = RoundBox.center, - ImageColor3 = theme.Background2, - Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2), - BackgroundTransparency = 1, - }, { - InputInner = e("TextBox", { - BackgroundTransparency = 1, - Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2), - Position = UDim2.new(0.5, 0, 0.5, 0), - AnchorPoint = Vector2.new(0.5, 0.5), - Font = theme.InputFont, - ClearTextOnFocus = false, - TextXAlignment = Enum.TextXAlignment.Center, - TextSize = TEXT_SIZE, - Text = value, - PlaceholderText = shownPlaceholder, - PlaceholderColor3 = theme.Text2, - TextColor3 = theme.Text1, - - [Roact.Change.Text] = function(rbx) - onValueChange(rbx.Text) - end, - [Roact.Event.Focused] = function() - self:setState({ - focused = true, - }) - end, - [Roact.Event.FocusLost] = function() - self:setState({ - focused = false, - }) - end, - }), - }) - end) -end - -return FormTextInput \ No newline at end of file diff --git a/plugin/src/Components/Panel.lua b/plugin/src/Components/Panel.lua deleted file mode 100644 index f1961702..00000000 --- a/plugin/src/Components/Panel.lua +++ /dev/null @@ -1,38 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Plugin = script:FindFirstAncestor("Plugin") - -local RojoFooter = require(Plugin.Components.RojoFooter) -local Theme = require(Plugin.Components.Theme) - -local e = Roact.createElement - -local Panel = Roact.Component:extend("Panel") - -function Panel:init() - self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new()) -end - -function Panel:render() - return Theme.with(function(theme) - return e("Frame", { - Size = UDim2.new(1, 0, 1, 0), - BackgroundColor3 = theme.Background1, - BorderSizePixel = 1, - }, { - Layout = Roact.createElement("UIListLayout", { - HorizontalAlignment = Enum.HorizontalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - }), - - Body = e("Frame", { - Size = UDim2.new(0, 360, 1, -32), - BackgroundTransparency = 1, - }, self.props[Roact.Children]), - - Footer = e(RojoFooter), - }) - end) -end - -return Panel \ No newline at end of file diff --git a/plugin/src/Components/RojoFooter.lua b/plugin/src/Components/RojoFooter.lua deleted file mode 100644 index 3a32c610..00000000 --- a/plugin/src/Components/RojoFooter.lua +++ /dev/null @@ -1,70 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin - -local Roact = require(Rojo.Roact) - -local Config = require(Plugin.Config) -local Version = require(Plugin.Version) -local Assets = require(Plugin.Assets) -local Theme = require(Plugin.Components.Theme) - -local e = Roact.createElement - -local RojoFooter = Roact.Component:extend("RojoFooter") - -function RojoFooter:init() - self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new()) - self.footerVersionSize, self.setFooterVersionSize = Roact.createBinding(Vector2.new()) -end - -function RojoFooter:render() - return Theme.with(function(theme) - return e("Frame", { - LayoutOrder = 3, - Size = UDim2.new(1, 0, 0, 32), - BackgroundColor3 = theme.Background2, - BorderSizePixel = 0, - ZIndex = 2, - }, { - Padding = e("UIPadding", { - PaddingTop = UDim.new(0, 4), - PaddingBottom = UDim.new(0, 4), - PaddingLeft = UDim.new(0, 8), - PaddingRight = UDim.new(0, 8), - }), - - LogoContainer = e("Frame", { - BackgroundTransparency = 1, - - Size = UDim2.new(0, 0, 0, 32), - }, { - Logo = e("ImageLabel", { - Image = Assets.Images.Logo, - Size = UDim2.new(0, 80, 0, 40), - ScaleType = Enum.ScaleType.Fit, - BackgroundTransparency = 1, - Position = UDim2.new(0, 0, 1, -10), - AnchorPoint = Vector2.new(0, 1), - }), - }), - - Version = e("TextLabel", { - Position = UDim2.new(1, 0, 0, 0), - Size = UDim2.new(0, 0, 1, 0), - AnchorPoint = Vector2.new(1, 0), - Font = theme.TitleFont, - TextSize = 18, - Text = Version.display(Config.version), - TextXAlignment = Enum.TextXAlignment.Right, - TextColor3 = theme.Text2, - BackgroundTransparency = 1, - - [Roact.Change.AbsoluteSize] = function(rbx) - self.setFooterVersionSize(rbx.AbsoluteSize) - end, - }), - }) - end) -end - -return RojoFooter \ No newline at end of file diff --git a/plugin/src/Components/SettingsPanel.lua b/plugin/src/Components/SettingsPanel.lua deleted file mode 100644 index d385d05b..00000000 --- a/plugin/src/Components/SettingsPanel.lua +++ /dev/null @@ -1,119 +0,0 @@ -local Roact = require(script:FindFirstAncestor("Rojo").Roact) - -local Plugin = script:FindFirstAncestor("Plugin") - -local Checkbox = require(Plugin.Components.Checkbox) -local FitList = require(Plugin.Components.FitList) -local FitText = require(Plugin.Components.FitText) -local FormButton = require(Plugin.Components.FormButton) -local Panel = require(Plugin.Components.Panel) -local PluginSettings = require(Plugin.Components.PluginSettings) -local Theme = require(Plugin.Components.Theme) - -local e = Roact.createElement - -local SettingsPanel = Roact.Component:extend("SettingsPanel") - -function SettingsPanel:render() - local back = self.props.back - - return Theme.with(function(theme) - return PluginSettings.with(function(settings) - return e(Panel, nil, { - Layout = Roact.createElement("UIListLayout", { - HorizontalAlignment = Enum.HorizontalAlignment.Center, - VerticalAlignment = Enum.VerticalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 16), - }), - - OpenScriptsExternally = e(FitList, { - containerProps = { - LayoutOrder = 1, - BackgroundTransparency = 1, - }, - layoutProps = { - Padding = UDim.new(0, 4), - FillDirection = Enum.FillDirection.Horizontal, - HorizontalAlignment = Enum.HorizontalAlignment.Left, - VerticalAlignment = Enum.VerticalAlignment.Center, - }, - }, { - Label = e(FitText, { - Kind = "TextLabel", - LayoutOrder = 1, - BackgroundTransparency = 1, - TextXAlignment = Enum.TextXAlignment.Left, - Font = theme.MainFont, - TextSize = 16, - Text = "Open Scripts Externally", - TextColor3 = theme.Text1, - }), - - Padding = e("Frame", { - Size = UDim2.new(0, 8, 0, 0), - BackgroundTransparency = 1, - LayoutOrder = 2, - }), - - Input = e(Checkbox, { - layoutOrder = 3, - checked = settings:get("openScriptsExternally"), - onChange = function(newValue) - settings:set("openScriptsExternally", not settings:get("openScriptsExternally")) - end, - }), - }), - - TwoWaySync = e(FitList, { - containerProps = { - LayoutOrder = 2, - BackgroundTransparency = 1, - }, - layoutProps = { - Padding = UDim.new(0, 4), - FillDirection = Enum.FillDirection.Horizontal, - HorizontalAlignment = Enum.HorizontalAlignment.Left, - VerticalAlignment = Enum.VerticalAlignment.Center, - }, - }, { - Label = e(FitText, { - Kind = "TextLabel", - LayoutOrder = 1, - BackgroundTransparency = 1, - TextXAlignment = Enum.TextXAlignment.Left, - Font = theme.MainFont, - TextSize = 16, - Text = "Two-Way Sync (Experimental!)", - TextColor3 = theme.Text1, - }), - - Padding = e("Frame", { - Size = UDim2.new(0, 8, 0, 0), - BackgroundTransparency = 1, - LayoutOrder = 2, - }), - - Input = e(Checkbox, { - layoutOrder = 3, - checked = settings:get("twoWaySync"), - onChange = function(newValue) - settings:set("twoWaySync", not settings:get("twoWaySync")) - end, - }), - }), - - BackButton = e(FormButton, { - layoutOrder = 4, - text = "Okay", - secondary = true, - onClick = function() - back() - end, - }), - }) - end) - end) -end - -return SettingsPanel \ No newline at end of file diff --git a/plugin/src/Components/Theme.lua b/plugin/src/Components/Theme.lua deleted file mode 100644 index c8ee810d..00000000 --- a/plugin/src/Components/Theme.lua +++ /dev/null @@ -1,113 +0,0 @@ ---[[ - Theming system taking advantage of Roact's new context API. - - Doesn't use colors provided by Studio and instead just branches on theme - name. This isn't exactly best practice. -]] - --- Studio does not exist outside Roblox Studio, so we'll lazily initialize it --- when possible. -local _Studio -local function getStudio() - if _Studio == nil then - _Studio = settings():GetService("Studio") - end - - return _Studio -end - -local Rojo = script:FindFirstAncestor("Rojo") - -local Roact = require(Rojo.Roact) -local Log = require(Rojo.Log) - -local strict = require(script.Parent.Parent.strict) - -local lightTheme = strict("Theme", { - ButtonFont = Enum.Font.GothamSemibold, - InputFont = Enum.Font.Code, - TitleFont = Enum.Font.GothamBold, - MainFont = Enum.Font.Gotham, - - Brand1 = Color3.fromRGB(225, 56, 53), - - Text1 = Color3.fromRGB(64, 64, 64), - Text2 = Color3.fromRGB(160, 160, 160), - TextOnAccent = Color3.fromRGB(235, 235, 235), - - Background1 = Color3.fromRGB(255, 255, 255), - Background2 = Color3.fromRGB(235, 235, 235), -}) - -local darkTheme = strict("Theme", { - ButtonFont = Enum.Font.GothamSemibold, - InputFont = Enum.Font.Code, - TitleFont = Enum.Font.GothamBold, - MainFont = Enum.Font.Gotham, - - Brand1 = Color3.fromRGB(225, 56, 53), - - Text1 = Color3.fromRGB(235, 235, 235), - Text2 = Color3.fromRGB(200, 200, 200), - TextOnAccent = Color3.fromRGB(235, 235, 235), - - Background1 = Color3.fromRGB(48, 48, 48), - Background2 = Color3.fromRGB(64, 64, 64), -}) - -local Context = Roact.createContext(lightTheme) - -local StudioProvider = Roact.Component:extend("StudioProvider") - --- Pull the current theme from Roblox Studio and update state with it. -function StudioProvider:updateTheme() - local studioTheme = getStudio().Theme - - if studioTheme.Name == "Light" then - self:setState({ - theme = lightTheme, - }) - elseif studioTheme.Name == "Dark" then - self:setState({ - theme = darkTheme, - }) - else - Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name) - - self:setState({ - theme = lightTheme, - }) - end -end - -function StudioProvider:init() - self:updateTheme() -end - -function StudioProvider:render() - return Roact.createElement(Context.Provider, { - value = self.state.theme, - }, self.props[Roact.Children]) -end - -function StudioProvider:didMount() - self.connection = getStudio().ThemeChanged:Connect(function() - self:updateTheme() - end) -end - -function StudioProvider:willUnmount() - self.connection:Disconnect() -end - -local function with(callback) - return Roact.createElement(Context.Consumer, { - render = callback, - }) -end - -return { - StudioProvider = StudioProvider, - Consumer = Context.Consumer, - with = with, -} \ No newline at end of file diff --git a/plugin/src/ServeSession.lua b/plugin/src/ServeSession.lua index 4bbe726c..9e06065f 100644 --- a/plugin/src/ServeSession.lua +++ b/plugin/src/ServeSession.lua @@ -110,7 +110,7 @@ function ServeSession:start() self.__apiContext:connect() :andThen(function(serverInfo) - self:__setStatus(Status.Connected) + self:__setStatus(Status.Connected, serverInfo.projectName) local rootInstanceId = serverInfo.rootInstanceId diff --git a/plugin/src/init.server.lua b/plugin/src/init.server.lua index ca8d000f..79e2b51a 100644 --- a/plugin/src/init.server.lua +++ b/plugin/src/init.server.lua @@ -13,20 +13,11 @@ end) local Roact = require(script.Parent.Roact) local Config = require(script.Config) -local App = require(script.Components.App) -local Theme = require(script.Components.Theme) -local PluginSettings = require(script.Components.PluginSettings) +local App = require(script.App) -local app = Roact.createElement(Theme.StudioProvider, nil, { - Roact.createElement(PluginSettings.StudioProvider, { - plugin = plugin, - }, { - RojoUI = Roact.createElement(App, { - plugin = plugin, - }), - }) +local app = Roact.createElement(App, { + plugin = plugin }) - local tree = Roact.mount(app, nil, "Rojo UI") plugin.Unloading:Connect(function() diff --git a/plugin/src/preloadAssets.lua b/plugin/src/preloadAssets.lua index c23d2dd1..edbc37d7 100644 --- a/plugin/src/preloadAssets.lua +++ b/plugin/src/preloadAssets.lua @@ -4,20 +4,23 @@ local Log = require(script.Parent.Parent.Log) local Assets = require(script.Parent.Assets) +local gatherAssetUrlsRecursive +function gatherAssetUrlsRecursive(currentTable, currentUrls) + currentUrls = currentUrls or {} + + for _, value in pairs(currentTable) do + if typeof(value) == "string" then + table.insert(currentUrls, value) + elseif typeof(value) == "table" then + gatherAssetUrlsRecursive(value) + end + end + + return currentUrls +end + local function preloadAssets() - local contentUrls = {} - - for _, sprite in pairs(Assets.Sprites) do - table.insert(contentUrls, sprite.asset) - end - - for _, slice in pairs(Assets.Slices) do - table.insert(contentUrls, slice.asset) - end - - for _, url in pairs(Assets.Images) do - table.insert(contentUrls, url) - end + local contentUrls = gatherAssetUrlsRecursive(Assets) Log.trace("Preloading assets: {:?}", contentUrls)