mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Improve UX/DX for requesting permissions
This commit is contained in:
parent
0de0610bda
commit
9444c84d73
6 changed files with 387 additions and 281 deletions
|
|
@ -1,137 +0,0 @@
|
||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
|
||||||
local Plugin = Rojo.Plugin
|
|
||||||
local Packages = Rojo.Packages
|
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
|
||||||
|
|
||||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
|
||||||
|
|
||||||
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 Toggle = Roact.Component:extend("Toggle")
|
|
||||||
|
|
||||||
function Toggle:init()
|
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
|
||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Toggle: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,
|
|
||||||
}, {
|
|
||||||
Input = e(Checkbox, {
|
|
||||||
active = self.props.active,
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(1, 0.5),
|
|
||||||
onClick = self.props.onClick,
|
|
||||||
}),
|
|
||||||
|
|
||||||
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 offset = self.props.options ~= nil and 120 or 40
|
|
||||||
local textBounds = getTextBounds(
|
|
||||||
self.props.description,
|
|
||||||
14,
|
|
||||||
Enum.Font.Gotham,
|
|
||||||
1.2,
|
|
||||||
Vector2.new(value.X - offset, math.huge)
|
|
||||||
)
|
|
||||||
return UDim2.new(1, -offset, 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 Toggle
|
|
||||||
|
|
@ -4,51 +4,139 @@ local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
local Toggle = require(Plugin.App.Components.Toggle)
|
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local DIVIDER_FADE_SIZE = 0.1
|
||||||
|
|
||||||
local PermissionPopup = Roact.Component:extend("PermissionPopup")
|
local PermissionPopup = Roact.Component:extend("PermissionPopup")
|
||||||
|
|
||||||
function PermissionPopup:init()
|
function PermissionPopup:init()
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
self.infoSize, self.setInfoSize = Roact.createBinding(Vector2.new(0, 0))
|
self.infoSize, self.setInfoSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
local response = {}
|
|
||||||
for _, api in self.props.apis do
|
|
||||||
response[api] = if self.props.initialState[api] == nil then true else self.props.initialState[api]
|
|
||||||
end
|
|
||||||
|
|
||||||
self:setState({
|
|
||||||
response = response,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function PermissionPopup:render()
|
function PermissionPopup:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Settings
|
theme = theme.Settings
|
||||||
|
|
||||||
local apiToggles = {}
|
local thumbnail = Assets.Images.ThirdPartyPlugin
|
||||||
|
local thumbnailId = string.match(self.props.source, "cloud_(%d+)")
|
||||||
|
if thumbnailId then
|
||||||
|
thumbnail = string.format("rbxthumb://type=Asset&id=%s&w=150&h=150", thumbnailId)
|
||||||
|
end
|
||||||
|
|
||||||
|
local apiRequests = {
|
||||||
|
Event = {},
|
||||||
|
Property = {},
|
||||||
|
Method = {},
|
||||||
|
}
|
||||||
for index, api in self.props.apis do
|
for index, api in self.props.apis do
|
||||||
apiToggles[api] = e(Toggle, {
|
local apiDesc = self.props.apiDescriptions[api]
|
||||||
active = self.state.response[api],
|
|
||||||
name = api,
|
apiRequests[apiDesc.Type][api] = e("Frame", {
|
||||||
description = self.props.apiDescriptions[api],
|
LayoutOrder = index,
|
||||||
transparency = self.props.transparency,
|
BackgroundTransparency = 1,
|
||||||
layoutOrder = index,
|
Size = UDim2.new(1, 0, 0, 17),
|
||||||
onClick = function()
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
self:setState(function(state)
|
}, {
|
||||||
state.response[api] = not state.response[api]
|
Divider = e("Frame", {
|
||||||
return state
|
BackgroundColor3 = theme.DividerColor,
|
||||||
end)
|
BackgroundTransparency = self.props.transparency,
|
||||||
end,
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 0, -2),
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Name = e("TextLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
|
Size = UDim2.new(0, 140, 0, 17),
|
||||||
|
TextWrapped = true,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Text = api,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 17,
|
||||||
|
TextColor3 = theme.Setting.NameColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}),
|
||||||
|
Desc = e("TextLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.new(0, 145, 0, 0),
|
||||||
|
Size = UDim2.new(1, -145, 0, 17),
|
||||||
|
TextWrapped = true,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Text = apiDesc.Description,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Add labels to explain the api types
|
||||||
|
if next(apiRequests.Event) then
|
||||||
|
apiRequests.Event["_apiTypeInfo"] = e("TextLabel", {
|
||||||
|
LayoutOrder = -1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 18),
|
||||||
|
Text = string.format("%s will be able to listen to these events:", self.props.name),
|
||||||
|
TextWrapped = true,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 17,
|
||||||
|
TextColor3 = theme.Setting.NameColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}, e("UIPadding", { PaddingBottom = UDim.new(0, 8) }))
|
||||||
|
end
|
||||||
|
if next(apiRequests.Property) then
|
||||||
|
apiRequests.Property["_apiTypeInfo"] = e("TextLabel", {
|
||||||
|
LayoutOrder = -1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 18),
|
||||||
|
Text = string.format("%s will be able to read these properties:", self.props.name),
|
||||||
|
TextWrapped = true,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 17,
|
||||||
|
TextColor3 = theme.Setting.NameColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}, e("UIPadding", { PaddingBottom = UDim.new(0, 8) }))
|
||||||
|
end
|
||||||
|
if next(apiRequests.Method) then
|
||||||
|
apiRequests.Method["_apiTypeInfo"] = e("TextLabel", {
|
||||||
|
LayoutOrder = -1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 18),
|
||||||
|
Text = string.format("%s will be able to call these methods:", self.props.name),
|
||||||
|
TextWrapped = true,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 17,
|
||||||
|
TextColor3 = theme.Setting.NameColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}, e("UIPadding", { PaddingBottom = UDim.new(0, 8) }))
|
||||||
|
end
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|
@ -56,8 +144,8 @@ function PermissionPopup:render()
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
FillDirection = Enum.FillDirection.Vertical,
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
Padding = UDim.new(0, 5),
|
Padding = UDim.new(0, 15),
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
|
|
@ -67,59 +155,171 @@ function PermissionPopup:render()
|
||||||
PaddingBottom = UDim.new(0, 15),
|
PaddingBottom = UDim.new(0, 15),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Icons = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 32),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
}),
|
||||||
|
|
||||||
|
ThirdPartyIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 32, 0, 32),
|
||||||
|
Image = thumbnail,
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
TransactIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 24, 0, 24),
|
||||||
|
Image = Assets.Images.Icons.Transact,
|
||||||
|
ImageColor3 = theme.Setting.DescriptionColor,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
RojoIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 32, 0, 32),
|
||||||
|
Image = Assets.Images.PluginButton,
|
||||||
|
LayoutOrder = 3,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
Info = e("TextLabel", {
|
Info = e("TextLabel", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, 0, 0, 0),
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
AutomaticSize = Enum.AutomaticSize.Y,
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
Text = string.format(
|
Text = string.format("%s is asking to use the Rojo API", self.props.name or "[Unknown]"),
|
||||||
"A third-party plugin, %s, is asking to use the following parts of the Rojo API. Please grant/deny access.",
|
Font = Enum.Font.GothamBold,
|
||||||
self.props.name or "[Unknown]"
|
|
||||||
),
|
|
||||||
Font = Enum.Font.GothamMedium,
|
|
||||||
TextSize = 17,
|
TextSize = 17,
|
||||||
TextColor3 = theme.Setting.NameColor,
|
TextColor3 = theme.Setting.NameColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 2,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||||
self.setInfoSize(rbx.AbsoluteSize)
|
self.setInfoSize(rbx.AbsoluteSize)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Submit = e(TextButton, {
|
Divider = e("Frame", {
|
||||||
text = "Submit",
|
LayoutOrder = 3,
|
||||||
style = "Solid",
|
BackgroundColor3 = theme.DividerColor,
|
||||||
transparency = self.props.transparency,
|
BackgroundTransparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
onClick = function()
|
Position = UDim2.new(0, 0, 0, -2),
|
||||||
self.props.responseEvent:Fire(self.state.response)
|
BorderSizePixel = 0,
|
||||||
end,
|
}, {
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ScrollingFrame = e(ScrollingFrame, {
|
ScrollingFrame = e(ScrollingFrame, {
|
||||||
size = self.infoSize:map(function(infoSize)
|
size = self.infoSize:map(function(infoSize)
|
||||||
return UDim2.new(1, 0, 1, -infoSize.Y - 44)
|
return UDim2.new(0.9, 0, 1, -infoSize.Y - 140)
|
||||||
end),
|
end),
|
||||||
layoutOrder = 2,
|
layoutOrder = 9,
|
||||||
contentSize = self.contentSize,
|
contentSize = self.contentSize,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}, {
|
}, {
|
||||||
APIToggles = Roact.createFragment(apiToggles),
|
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
|
||||||
PaddingRight = UDim.new(0, 15),
|
|
||||||
}),
|
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
FillDirection = Enum.FillDirection.Vertical,
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 18),
|
||||||
|
|
||||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||||
self.setContentSize(object.AbsoluteContentSize)
|
self.setContentSize(object.AbsoluteContentSize)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
PropertyRequests = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}, {
|
||||||
|
APIs = Roact.createFragment(apiRequests.Property),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
EventRequests = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}, {
|
||||||
|
APIs = Roact.createFragment(apiRequests.Event),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
MethodRequests = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
LayoutOrder = 3,
|
||||||
|
}, {
|
||||||
|
APIs = Roact.createFragment(apiRequests.Method),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Actions = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 34),
|
||||||
|
LayoutOrder = 10,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Deny = e(TextButton, {
|
||||||
|
text = "Deny",
|
||||||
|
style = "Bordered",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function()
|
||||||
|
self.props.responseEvent:Fire(false)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Allow = e(TextButton, {
|
||||||
|
text = "Allow",
|
||||||
|
style = "Solid",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function()
|
||||||
|
self.props.responseEvent:Fire(true)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -84,21 +84,6 @@ function PermissionsPage:render()
|
||||||
theme = theme.Settings
|
theme = theme.Settings
|
||||||
|
|
||||||
local sources = {}
|
local sources = {}
|
||||||
if next(self.state.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, permissions in self.state.permissions do
|
for source, permissions in self.state.permissions do
|
||||||
if next(permissions) == nil then
|
if next(permissions) == nil then
|
||||||
continue
|
continue
|
||||||
|
|
@ -126,6 +111,21 @@ function PermissionsPage:render()
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if next(sources) == 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,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,7 @@ function App:requestPermission(
|
||||||
source: string,
|
source: string,
|
||||||
name: string,
|
name: string,
|
||||||
apis: { string },
|
apis: { string },
|
||||||
initialState: { [string]: boolean? }
|
initialState: boolean?
|
||||||
): { [string]: boolean }
|
): { [string]: boolean }
|
||||||
local responseEvent = Instance.new("BindableEvent")
|
local responseEvent = Instance.new("BindableEvent")
|
||||||
|
|
||||||
|
|
@ -471,7 +471,7 @@ function App:requestPermission(
|
||||||
"Cancelling API permission request for '{}' because the third-party plugin has been removed.",
|
"Cancelling API permission request for '{}' because the third-party plugin has been removed.",
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
responseEvent:Fire(initialState)
|
responseEvent:Fire(initialState or false)
|
||||||
end)
|
end)
|
||||||
else nil
|
else nil
|
||||||
|
|
||||||
|
|
@ -480,7 +480,6 @@ function App:requestPermission(
|
||||||
name = name,
|
name = name,
|
||||||
content = e(PermissionPopup, {
|
content = e(PermissionPopup, {
|
||||||
responseEvent = responseEvent,
|
responseEvent = responseEvent,
|
||||||
initialState = initialState,
|
|
||||||
source = source,
|
source = source,
|
||||||
name = name,
|
name = name,
|
||||||
apis = apis,
|
apis = apis,
|
||||||
|
|
@ -488,7 +487,7 @@ function App:requestPermission(
|
||||||
transparency = Roact.createBinding(0),
|
transparency = Roact.createBinding(0),
|
||||||
}),
|
}),
|
||||||
onClose = function()
|
onClose = function()
|
||||||
responseEvent:Fire(initialState)
|
responseEvent:Fire(initialState or false)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
@ -833,7 +832,7 @@ function App:render()
|
||||||
initEnabled = true,
|
initEnabled = true,
|
||||||
overridePreviousState = true,
|
overridePreviousState = true,
|
||||||
floatingSize = Vector2.new(400, 300),
|
floatingSize = Vector2.new(400, 300),
|
||||||
minimumSize = Vector2.new(390, 240),
|
minimumSize = Vector2.new(390, 240) or popup.minimumSize,
|
||||||
|
|
||||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
|
@ -961,8 +960,12 @@ function App:render()
|
||||||
for api in apiMap do
|
for api in apiMap do
|
||||||
table.insert(apiList, api)
|
table.insert(apiList, api)
|
||||||
end
|
end
|
||||||
local response = self:requestPermission(plugin, source, name, apiList, apiMap)
|
local granted = self:requestPermission(plugin, source, name, apiList, true)
|
||||||
self.headlessAPI:_setPermissions(source, name, response)
|
if granted then
|
||||||
|
self.headlessAPI:_setPermissions(source, name, apiList)
|
||||||
|
else
|
||||||
|
self.headlessAPI:_removePermissions(source, name)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ local Assets = {
|
||||||
Reset = "rbxassetid://10142422327",
|
Reset = "rbxassetid://10142422327",
|
||||||
Expand = "rbxassetid://12045401097",
|
Expand = "rbxassetid://12045401097",
|
||||||
Settings = "rbxassetid://12046309515",
|
Settings = "rbxassetid://12046309515",
|
||||||
|
Transact = "rbxassetid://16350762910",
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
Add = "rbxassetid://10434145835",
|
Add = "rbxassetid://10434145835",
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,40 @@ function API.new(app)
|
||||||
Rojo._changedEvent = Instance.new("BindableEvent")
|
Rojo._changedEvent = Instance.new("BindableEvent")
|
||||||
Rojo._apiDescriptions = {}
|
Rojo._apiDescriptions = {}
|
||||||
|
|
||||||
Rojo._apiDescriptions.Changed = "An event that fires when a headless API property changes"
|
Rojo._apiDescriptions.Changed = {
|
||||||
|
Type = "Event",
|
||||||
|
Description = "An event that fires when a headless API property changes",
|
||||||
|
}
|
||||||
Rojo.Changed = Rojo._changedEvent.Event
|
Rojo.Changed = Rojo._changedEvent.Event
|
||||||
|
|
||||||
Rojo._apiDescriptions.Connected = "Whether or not the plugin is connected to a Rojo server"
|
Rojo._apiDescriptions.Connected = {
|
||||||
|
Type = "Property",
|
||||||
|
Description = "Whether or not the plugin is connected to a Rojo server",
|
||||||
|
}
|
||||||
Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false
|
Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false
|
||||||
|
|
||||||
Rojo._apiDescriptions.Address = "The address (host:port) that the plugin is connected to"
|
Rojo._apiDescriptions.Address = {
|
||||||
|
Type = "Property",
|
||||||
|
Description = "The address (host:port) that the plugin is connected to",
|
||||||
|
}
|
||||||
Rojo.Address = nil
|
Rojo.Address = nil
|
||||||
|
|
||||||
Rojo._apiDescriptions.ProjectName = "The name of the project that the plugin is connected to"
|
Rojo._apiDescriptions.ProjectName = {
|
||||||
|
Type = "Property",
|
||||||
|
Description = "The name of the project that the plugin is connected to",
|
||||||
|
}
|
||||||
Rojo.ProjectName = nil
|
Rojo.ProjectName = nil
|
||||||
|
|
||||||
Rojo._apiDescriptions.Version = "The version of the plugin"
|
Rojo._apiDescriptions.Version = {
|
||||||
|
Type = "Property",
|
||||||
|
Description = "The version of the plugin",
|
||||||
|
}
|
||||||
Rojo.Version = table.clone(Config.version)
|
Rojo.Version = table.clone(Config.version)
|
||||||
|
|
||||||
Rojo._apiDescriptions.ProtocolVersion = "The protocol version that the plugin is using"
|
Rojo._apiDescriptions.ProtocolVersion = {
|
||||||
|
Type = "Property",
|
||||||
|
Description = "The protocol version that the plugin is using",
|
||||||
|
}
|
||||||
Rojo.ProtocolVersion = Config.protocolVersion
|
Rojo.ProtocolVersion = Config.protocolVersion
|
||||||
|
|
||||||
function Rojo:_updateProperty(property: string, value: any?)
|
function Rojo:_updateProperty(property: string, value: any?)
|
||||||
|
|
@ -187,52 +205,49 @@ function API.new(app)
|
||||||
|
|
||||||
local source = Rojo:_getCallerSource()
|
local source = Rojo:_getCallerSource()
|
||||||
if Rojo._permissions[source] == nil then
|
if Rojo._permissions[source] == nil then
|
||||||
Rojo._permissions[source] = {}
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
return not not Rojo._permissions[source][key]
|
return not not Rojo._permissions[source][key]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Rojo:_setPermissions(source, name, permissions)
|
function Rojo:_setPermissions(source, name, permissions)
|
||||||
-- Ensure permissions exist for this source
|
if next(permissions) == nil then
|
||||||
if Rojo._permissions[source] == nil then
|
Rojo:_removePermissions(source, name)
|
||||||
Rojo._permissions[source] = {}
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Set permissions
|
-- Set permissions
|
||||||
for api, granted in permissions do
|
local sourcePermissions = {}
|
||||||
Log.warn(
|
for _, api in permissions do
|
||||||
string.format(
|
Log.info(string.format("Granting '%s' access to Rojo.%s", name, api))
|
||||||
"%s Rojo.%s for '%s'",
|
sourcePermissions[api] = true
|
||||||
granted and "Granting permission to" or "Denying permission to",
|
|
||||||
api,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Rojo._permissions[source][api] = granted
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Clear out source if no permissions are granted
|
-- Update stored permissions
|
||||||
local hasAnyPermissions = false
|
Rojo._permissions[source] = sourcePermissions
|
||||||
for _, granted in Rojo._permissions[source] do
|
Settings:set("apiPermissions", Rojo._permissions)
|
||||||
if granted then
|
|
||||||
hasAnyPermissions = true
|
-- Share changes
|
||||||
break
|
Rojo._permissionsChangedEvent:Fire(source, sourcePermissions)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
if not hasAnyPermissions then
|
function Rojo:_removePermissions(source, name)
|
||||||
Rojo._permissions[source] = nil
|
Rojo._permissions[source] = nil
|
||||||
end
|
Log.info(string.format("Denying access to Rojo APIs for '%s'", name))
|
||||||
|
|
||||||
-- Update stored permissions
|
-- Update stored permissions
|
||||||
Settings:set("apiPermissions", Rojo._permissions)
|
Settings:set("apiPermissions", Rojo._permissions)
|
||||||
|
|
||||||
-- Share changes
|
-- Share changes
|
||||||
Rojo._permissionsChangedEvent:Fire(source, Rojo._permissions[source])
|
Rojo._permissionsChangedEvent:Fire(source, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members"
|
Rojo._apiDescriptions.RequestAccess = {
|
||||||
function Rojo:RequestAccess(plugin: Plugin, apis: { string }): { [string]: boolean }
|
Type = "Method",
|
||||||
|
Description = "Used to gain access to Rojo API members",
|
||||||
|
}
|
||||||
|
function Rojo:RequestAccess(plugin: Plugin, apis: { string }): boolean
|
||||||
assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument")
|
assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument")
|
||||||
assert(
|
assert(
|
||||||
typeof(plugin) == "Instance" and plugin:IsA("Plugin"),
|
typeof(plugin) == "Instance" and plugin:IsA("Plugin"),
|
||||||
|
|
@ -244,7 +259,7 @@ function API.new(app)
|
||||||
|
|
||||||
if Rojo:_checkRateLimit("RequestAccess") then
|
if Rojo:_checkRateLimit("RequestAccess") then
|
||||||
-- Because this opens a popup, we dont want to let users get spammed by it
|
-- Because this opens a popup, we dont want to let users get spammed by it
|
||||||
return {}
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rojo._activePermissionRequests[source] then
|
if Rojo._activePermissionRequests[source] then
|
||||||
|
|
@ -256,10 +271,6 @@ function API.new(app)
|
||||||
end
|
end
|
||||||
Rojo._activePermissionRequests[source] = true
|
Rojo._activePermissionRequests[source] = true
|
||||||
|
|
||||||
if Rojo._permissions[source] == nil then
|
|
||||||
Rojo._permissions[source] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sanitize request
|
-- Sanitize request
|
||||||
local sanitizedApis = {}
|
local sanitizedApis = {}
|
||||||
for _, api in apis do
|
for _, api in apis do
|
||||||
|
|
@ -270,33 +281,40 @@ function API.new(app)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
assert(#sanitizedApis > 0, "Rojo:RequestAccess expects an array of valid API names")
|
assert(#sanitizedApis > 0, "Rojo:RequestAccess expects an array of valid API names")
|
||||||
|
table.sort(sanitizedApis)
|
||||||
|
|
||||||
local alreadyAllowed = true
|
local alreadyAllowed = true
|
||||||
|
if Rojo._permissions[source] == nil then
|
||||||
|
alreadyAllowed = false
|
||||||
|
else
|
||||||
for _, api in sanitizedApis do
|
for _, api in sanitizedApis do
|
||||||
if not Rojo._permissions[source][api] then
|
if not Rojo._permissions[source][api] then
|
||||||
alreadyAllowed = false
|
alreadyAllowed = false
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if alreadyAllowed then
|
if alreadyAllowed then
|
||||||
local response = {}
|
|
||||||
for _, api in sanitizedApis do
|
|
||||||
response[api] = true
|
|
||||||
end
|
|
||||||
Rojo._activePermissionRequests[source] = nil
|
Rojo._activePermissionRequests[source] = nil
|
||||||
return response
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local response = app:requestPermission(plugin, source, name, sanitizedApis, Rojo._permissions[source])
|
local granted = app:requestPermission(plugin, source, name, sanitizedApis, false)
|
||||||
|
if granted then
|
||||||
Rojo:_setPermissions(source, name, response)
|
Rojo:_setPermissions(source, name, sanitizedApis)
|
||||||
|
else
|
||||||
|
Rojo:_removePermissions(source, name)
|
||||||
|
end
|
||||||
|
|
||||||
Rojo._activePermissionRequests[source] = nil
|
Rojo._activePermissionRequests[source] = nil
|
||||||
return response
|
return granted
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.Test = "Prints the given arguments to the console"
|
Rojo._apiDescriptions.Test = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Prints the given arguments to the console",
|
||||||
|
}
|
||||||
function Rojo:Test(...)
|
function Rojo:Test(...)
|
||||||
local args = table.pack(...)
|
local args = table.pack(...)
|
||||||
for i = 1, args.n do
|
for i = 1, args.n do
|
||||||
|
|
@ -319,7 +337,10 @@ function API.new(app)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server"
|
Rojo._apiDescriptions.ConnectAsync = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Connects to a Rojo server",
|
||||||
|
}
|
||||||
function Rojo:ConnectAsync(host: string?, port: string?)
|
function Rojo:ConnectAsync(host: string?, port: string?)
|
||||||
assert(type(host) == "string" or host == nil, "Host must be type `string?`")
|
assert(type(host) == "string" or host == nil, "Host must be type `string?`")
|
||||||
assert(type(port) == "string" or port == nil, "Port must be type `string?`")
|
assert(type(port) == "string" or port == nil, "Port must be type `string?`")
|
||||||
|
|
@ -331,7 +352,10 @@ function API.new(app)
|
||||||
app:startSession(host, port)
|
app:startSession(host, port)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server"
|
Rojo._apiDescriptions.DisconnectAsync = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Disconnects from the Rojo server",
|
||||||
|
}
|
||||||
function Rojo:DisconnectAsync()
|
function Rojo:DisconnectAsync()
|
||||||
if Rojo:_checkRateLimit("DisconnectAsync") then
|
if Rojo:_checkRateLimit("DisconnectAsync") then
|
||||||
return
|
return
|
||||||
|
|
@ -340,14 +364,20 @@ function API.new(app)
|
||||||
app:endSession()
|
app:endSession()
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.GetSetting = "Gets a Rojo setting"
|
Rojo._apiDescriptions.GetSetting = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Gets a Rojo setting",
|
||||||
|
}
|
||||||
function Rojo:GetSetting(setting: string): any
|
function Rojo:GetSetting(setting: string): any
|
||||||
assert(type(setting) == "string", "Setting must be type `string`")
|
assert(type(setting) == "string", "Setting must be type `string`")
|
||||||
|
|
||||||
return Settings:get(setting)
|
return Settings:get(setting)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting"
|
Rojo._apiDescriptions.SetSetting = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Sets a Rojo setting",
|
||||||
|
}
|
||||||
function Rojo:SetSetting(setting: string, value: any)
|
function Rojo:SetSetting(setting: string, value: any)
|
||||||
assert(type(setting) == "string", "Setting must be type `string`")
|
assert(type(setting) == "string", "Setting must be type `string`")
|
||||||
assert(type(value) == type(Settings:get(setting)), "Value must be the same type as the setting")
|
assert(type(value) == type(Settings:get(setting)), "Value must be the same type as the setting")
|
||||||
|
|
@ -359,7 +389,10 @@ function API.new(app)
|
||||||
return Settings:set(setting, value)
|
return Settings:set(setting, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI"
|
Rojo._apiDescriptions.Notify = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Shows a notification in the Rojo UI",
|
||||||
|
}
|
||||||
function Rojo:Notify(
|
function Rojo:Notify(
|
||||||
msg: string,
|
msg: string,
|
||||||
timeout: number?,
|
timeout: number?,
|
||||||
|
|
@ -403,12 +436,18 @@ function API.new(app)
|
||||||
return app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions)
|
return app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions)
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to"
|
Rojo._apiDescriptions.GetHostAndPort = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Gets the host and port that Rojo is set to",
|
||||||
|
}
|
||||||
function Rojo:GetHostAndPort(): (string, string)
|
function Rojo:GetHostAndPort(): (string, string)
|
||||||
return app:getHostAndPort()
|
return app:getHostAndPort()
|
||||||
end
|
end
|
||||||
|
|
||||||
Rojo._apiDescriptions.CreateApiContext = "Creates a new API context"
|
Rojo._apiDescriptions.CreateApiContext = {
|
||||||
|
Type = "Method",
|
||||||
|
Description = "Creates a new API context",
|
||||||
|
}
|
||||||
function Rojo:CreateApiContext(baseUrl: string)
|
function Rojo:CreateApiContext(baseUrl: string)
|
||||||
assert(type(baseUrl) == "string", "Base URL must be type `string`")
|
assert(type(baseUrl) == "string", "Base URL must be type `string`")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue