diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua new file mode 100644 index 00000000..4f2e1072 --- /dev/null +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -0,0 +1,164 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Settings = require(Plugin.Settings) +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) + +local Checkbox = require(Plugin.App.Components.Checkbox) +local Dropdown = require(Plugin.App.Components.Dropdown) +local IconButton = require(Plugin.App.Components.IconButton) +local SlicedImage = require(Plugin.App.Components.SlicedImage) + +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 Listing = Roact.Component:extend("Listing") + +function Listing:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function Listing:render() + return Theme.with(function(theme) + theme = theme.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, + ZIndex = -self.props.layoutOrder, + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = function(object) + self.setContainerSize(object.AbsoluteSize) + end, + }, { + Settings = e("TextButton", { + Text = "", + BackgroundTransparency = 1, + Size = UDim2.fromOffset(28, 28), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + + [Roact.Event.Activated] = function() + self.props.onClick() + end, + }, { + Button = Theme.with(function(theme) + theme = theme.Checkbox + return 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.Icons.Settings, + 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), + }), + + 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 - 40, math.huge) + ) + return UDim2.new(1, - 40, 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 + +return Listing diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index f88010aa..30a0156f 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -11,6 +11,7 @@ local Theme = require(Plugin.App.Theme) local IconButton = require(Plugin.App.Components.IconButton) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local Tooltip = require(Plugin.App.Components.Tooltip) +local Listing = require(script.Listing) local e = Roact.createElement @@ -64,6 +65,42 @@ function PermissionsPage:render() return Theme.with(function(theme) theme = theme.Settings + local sources = {} + if next(self.props.headlessAPI._permissions) == nil then + sources.noSources = e("TextLabel", { + Text = "No third-party plugins have been granted permissions.", + Font = Enum.Font.Gotham, + TextSize = 18, + TextColor3 = theme.Setting.DescriptionColor, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = UDim2.new(1, 0, 0, 48), + LayoutOrder = 0, + + BackgroundTransparency = 1, + }) + else + for source in self.props.headlessAPI._permissions do + local meta = self.props.headlessAPI:_getMetaFromSource(source) + sources[source] = e(Listing, { + layoutOrder = string.byte(source), + transparency = self.props.transparency, + + name = meta.Name, + description = string.format( + "%s plugin%s", + meta.Type, + if meta.Creator then " by " .. meta.Creator else "" + ), + + onClick = function() + self.props.onEdit(source, meta, self.props.headlessAPI._permissions[source] or {}) + end, + }) + end + end + return e(ScrollingFrame, { size = UDim2.new(1, 0, 1, 0), contentSize = self.contentSize, @@ -84,6 +121,8 @@ function PermissionsPage:render() end, }), + Sources = Roact.createFragment(sources), + Padding = e("UIPadding", { PaddingLeft = UDim.new(0, 20), PaddingRight = UDim.new(0, 20), diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 15dd98ed..235e365d 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -519,11 +519,26 @@ function App:render() }), Permissions = createPageElement(AppStatus.Permissions, { + headlessAPI = self.headlessAPI, + onBack = function() self:setState({ appStatus = AppStatus.Settings, }) end, + + onEdit = function(source, meta, apiMap) + local apiList = {} + for api in apiMap do + table.insert(apiList, api) + end + self:requestPermission( + source, + meta.Name .. if meta.Creator then " by " .. meta.Creator else "", + apiList, + apiMap + ) + end, }), Error = createPageElement(AppStatus.Error, { diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 78ab84fe..a219ec78 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -26,6 +26,7 @@ local Assets = { Back = "rbxassetid://6017213752", Reset = "rbxassetid://10142422327", Expand = "rbxassetid://12045401097", + Settings = "rbxassetid://12046309515", }, Checkbox = { Active = "rbxassetid://6016251644", diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index cde2ad50..0268ab9e 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -95,6 +95,43 @@ function API.new(app) return "Command Bar" end + function Rojo:_getMetaFromSource(source) + local localPlugin = string.match(source, "user_(.+)") + if localPlugin then + return { + Type = "Local", + Name = localPlugin, + } + end + + local cloudId = string.match(source, "cloud_(%d+)") + if cloudId then + local info = cloudIdInfoCache[cloudId] + if info then + return { + Type = "Cloud", + Name = info.Name, + Creator = info.Creator.Name, + } + else + local success, newInfo = pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) + if success then + cloudIdInfoCache[cloudId] = newInfo + return { + Type = "Cloud", + Name = newInfo.Name, + Creator = newInfo.Creator.Name, + } + end + end + end + + return { + Type = "Studio", + Name = "Command Bar", + } + end + function Rojo:_getCallerType() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1]