From 4832035a18dfca311290508adab6b9921522d572 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 25 Sep 2022 16:43:02 -0400 Subject: [PATCH] Add headless API --- assets/thirdParty.png | Bin 0 -> 5899 bytes plugin/src/App/Components/TextButton.lua | 4 +- plugin/src/App/Notifications.lua | 38 +++++----- plugin/src/App/init.lua | 39 ++++++++-- plugin/src/Assets.lua | 1 + plugin/src/HeadlessAPI.lua | 92 +++++++++++++++++++++++ 6 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 assets/thirdParty.png create mode 100644 plugin/src/HeadlessAPI.lua diff --git a/assets/thirdParty.png b/assets/thirdParty.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb1a32cd4efba7260646ed89ca07030c2dc888e GIT binary patch literal 5899 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 HBJ_U%F5okJ literal 0 HcmV?d00001 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