diff --git a/assets/thirdParty.png b/assets/thirdParty.png new file mode 100644 index 00000000..cdb1a32c Binary files /dev/null and b/assets/thirdParty.png differ diff --git a/plugin/src/App/Components/TextButton.lua b/plugin/src/App/Components/TextButton.lua index d33ddf9f..89fcb504 100644 --- a/plugin/src/App/Components/TextButton.lua +++ b/plugin/src/App/Components/TextButton.lua @@ -42,7 +42,7 @@ end function TextButton:render() return Theme.with(function(theme) local textSize = TextService:GetTextSize( - self.props.text, 18, Enum.Font.GothamSemibold, + self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge) ) @@ -85,7 +85,7 @@ function TextButton:render() Text = e("TextLabel", { Text = self.props.text, - Font = Enum.Font.GothamSemibold, + Font = Enum.Font.GothamMedium, TextSize = 18, TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor), TextTransparency = self.props.transparency, diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index 5e1b0882..32380c95 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -91,7 +91,7 @@ function Notification:render() local textBounds = TextService:GetTextSize( self.props.text, 15, - Enum.Font.GothamSemibold, + Enum.Font.GothamMedium, Vector2.new(350, 700) ) @@ -102,7 +102,7 @@ function Notification:render() local size = self.binding:map(function(value) return UDim2.fromOffset( (35+40+textBounds.X)*value, - math.max(14+20+textBounds.Y, 32+20) + math.max(16+20+textBounds.Y, 54) ) end) @@ -129,15 +129,15 @@ function Notification:render() }, { Logo = e("ImageLabel", { ImageTransparency = transparency, - Image = Assets.Images.PluginButton, + Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton, BackgroundTransparency = 1, Size = UDim2.new(0, 32, 0, 32), Position = UDim2.new(0, 0, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), }), - Info = e("TextLabel", { + Message = e("TextLabel", { Text = self.props.text, - Font = Enum.Font.GothamSemibold, + Font = Enum.Font.GothamMedium, TextSize = 15, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, @@ -145,21 +145,21 @@ function Notification:render() TextWrapped = true, Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = UDim2.fromOffset(35, 0), + Position = UDim2.fromOffset(38, 0), LayoutOrder = 1, BackgroundTransparency = 1, }), - Time = e("TextLabel", { - Text = time:FormatLocalTime("LTS", "en-us"), - Font = Enum.Font.Code, + Info = e("TextLabel", { + Text = if self.props.source then "From: " .. self.props.source else time:FormatLocalTime("LTS", "en-us"), + Font = Enum.Font.Gotham, TextSize = 12, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, TextXAlignment = Enum.TextXAlignment.Left, - Size = UDim2.new(1, -35, 0, 14), - Position = UDim2.new(0, 35, 1, -14), + Size = UDim2.new(1, -38, 0, 14), + Position = UDim2.new(0, 38, 1, -14), LayoutOrder = 1, BackgroundTransparency = 1, @@ -167,8 +167,8 @@ function Notification:render() }), Padding = e("UIPadding", { - PaddingLeft = UDim.new(0, 17), - PaddingRight = UDim.new(0, 15), + PaddingLeft = UDim.new(0, 12), + PaddingRight = UDim.new(0, 12), }), }) }) @@ -181,16 +181,18 @@ function Notifications:render() local notifs = {} for index, notif in ipairs(self.props.notifications) do - notifs[notif] = e(Notification, { + local props = { soundPlayer = self.props.soundPlayer, - text = notif.text, - timestamp = notif.timestamp, - timeout = notif.timeout, layoutOrder = (notif.timestamp - baseClock), onClose = function() self.props.onClose(index) end, - }) + } + for key, value in notif do + props[key] = value + end + + notifs[notif] = e(Notification, props) end return Roact.createFragment(notifs) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 9f7e2ecf..c8ea22b6 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -16,6 +16,7 @@ local strict = require(Plugin.strict) local Dictionary = require(Plugin.Dictionary) local ServeSession = require(Plugin.ServeSession) local ApiContext = require(Plugin.ApiContext) +local HeadlessAPI = require(Plugin.HeadlessAPI) local preloadAssets = require(Plugin.preloadAssets) local soundPlayer = require(Plugin.soundPlayer) local Theme = require(script.Theme) @@ -48,6 +49,11 @@ function App:init() self.host, self.setHost = Roact.createBinding(priorHost or "") self.port, self.setPort = Roact.createBinding(priorPort or "") + self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self, Config, Settings) + + -- selene: allow(global_usage) + _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + self.patchInfo, self.setPatchInfo = Roact.createBinding({ changes = 0, timestamp = os.time(), @@ -78,6 +84,25 @@ function App:addNotification(text: string, timeout: number?) }) end +function App:addThirdPartyNotification(source: string, text: string, timeout: number?) + if not Settings:get("showNotifications") then + return + end + + local notifications = table.clone(self.state.notifications) + table.insert(notifications, { + text = text, + timestamp = DateTime.now().UnixTimestampMillis, + timeout = timeout or 3, + thirdParty = true, + source = source, + }) + + self:setState({ + notifications = notifications, + }) +end + function App:closeNotification(index: number) local notifications = table.clone(self.state.notifications) table.remove(notifications, index) @@ -126,7 +151,7 @@ function App:setPriorEndpoint(host: string, port: string) Settings:set("priorEndpoints", priorEndpoints) end -function App:getHostAndPort() +function App:getHostAndPort(): (string, string) local host = self.host:getValue() local port = self.port:getValue() @@ -179,7 +204,7 @@ function App:releaseSyncLock() Log.trace("Could not relase sync lock because it is owned by {}", lock.Value) end -function App:startSession() +function App:startSession(host: string?, port: string?) local claimedLock, priorOwner = self:claimSyncLock() if not claimedLock then local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner)) @@ -195,14 +220,16 @@ function App:startSession() return end - local host, port = self:getHostAndPort() + if host == nil or port == nil then + host, port = self:getHostAndPort() + end local sessionOptions = { openScriptsExternally = Settings:get("openScriptsExternally"), twoWaySync = Settings:get("twoWaySync"), } - local baseUrl = ("http://%s:%s"):format(host, port) + local baseUrl = string.format("http://%s:%s", host :: string, port :: string) local apiContext = ApiContext.new(baseUrl) local serveSession = ServeSession.new({ @@ -240,6 +267,8 @@ function App:startSession() end) serveSession:onStatusChanged(function(status, details) + self.headlessAPI.Connected = status == ServeSession.Status.Connected + if status == ServeSession.Status.Connecting then self:setPriorEndpoint(host, port) @@ -249,7 +278,7 @@ function App:startSession() }) self:addNotification("Connecting to session...") elseif status == ServeSession.Status.Connected then - local address = ("%s:%s"):format(host, port) + local address = string.format("%s:%s", host :: string, port :: string) self:setState({ appStatus = AppStatus.Connected, projectName = details, diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 08f86b3b..a5484aa5 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -20,6 +20,7 @@ local Assets = { PluginButton = "rbxassetid://3405341609", PluginButtonConnected = "rbxassetid://9529783993", PluginButtonWarning = "rbxassetid://9529784530", + ThirdPartyPlugin = "rbxassetid://11064843298", Icons = { Close = "rbxassetid://6012985953", Back = "rbxassetid://6017213752", diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua new file mode 100644 index 00000000..0bb540a8 --- /dev/null +++ b/plugin/src/HeadlessAPI.lua @@ -0,0 +1,92 @@ +local API = {} + +function API.new(app, config, settings) + local Rojo = {} + + Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false + Rojo.Version = table.clone(config.version) + Rojo.ProtocolVersion = config.protocolVersion + + Rojo._notifRateLimit = {} + + function Rojo:Test(...) + print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) + end + + function Rojo:_getCaller() + local traceback = string.split(debug.traceback(), "\n") + local topLevel = traceback[#traceback - 1] + + local debugPlugin = string.match(topLevel, "^PluginDebugService%.user_(.-)%.") + if debugPlugin then + return debugPlugin + end + + local localPlugin = string.match(topLevel, "^user_(.-)%.") + if localPlugin then + return localPlugin + end + + local cloudPlugin = string.match(topLevel, "cloud_%d-%.(.-)%.") + if cloudPlugin then + return cloudPlugin + end + + return "Command Bar" + end + + function Rojo:ConnectAsync(host: string?, port: number?) + app:startSession(host, port) + end + + function Rojo:DisconnectAsync() + app:endSession() + end + + function Rojo:GetSetting(setting: string): any + return settings:get(setting) + end + + function Rojo:SetSetting(setting: string, value: any) + return settings:set(setting, value) + end + + function Rojo:Notify(msg: string, timeout: number?) + local source = Rojo:_getCaller() + + if Rojo._notifRateLimit[source] == nil then + Rojo._notifRateLimit[source] = 0 + elseif Rojo._notifRateLimit[source] > 45 then + return -- Rate limited + end + + Rojo._notifRateLimit[source] += 1 + task.delay(30, function() + Rojo._notifRateLimit[source] -= 1 + end) + + app:addThirdPartyNotification(source, msg, timeout) + return + end + + function Rojo:GetHostAndPort(): (string, string) + return app:getHostAndPort() + end + + local ReadOnly = setmetatable({}, { + __index = function(_, key) + if string.find(key, "^_") then + return nil -- Don't expose private members + end + return Rojo[key] + end, + __newindex = function(_, key, value) + error(string.format("Attempted to set Rojo.%s to %q but it's a read-only value", key, value), 2) + return + end, + }) + + return Rojo, ReadOnly +end + +return API