add gs
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
local RobloxGui = game:GetService("CoreGui").RobloxGui
|
||||
local ShellModules = RobloxGui.Modules.Shell
|
||||
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
|
||||
local AccountManager = require(ShellModules.AccountManager)
|
||||
local AssetManager = require(ShellModules.AssetManager)
|
||||
local Errors = require(ShellModules.Errors)
|
||||
local ErrorOverlay = require(ShellModules.ErrorOverlay)
|
||||
local GlobalSettings = require(ShellModules.GlobalSettings)
|
||||
local LoadingWidget = require(ShellModules.LoadingWidget)
|
||||
local ScreenManager = require(ShellModules.ScreenManager)
|
||||
local SoundManager = require(ShellModules.SoundManager)
|
||||
local Strings = require(ShellModules.LocalizedStrings)
|
||||
local ThumbnailLoader = require(ShellModules.ThumbnailLoader)
|
||||
local UnlinkAccountOverlay = require(ShellModules.UnlinkAccountOverlay)
|
||||
local Utility = require(ShellModules.Utility)
|
||||
local XboxAppState = require(ShellModules.AppState)
|
||||
|
||||
local function createAccountLinkingView()
|
||||
local this = {}
|
||||
|
||||
local gamerTag = XboxAppState.store:getState().XboxUser.gamertag
|
||||
local robloxName = XboxAppState.store:getState().RobloxUser.robloxName
|
||||
local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid
|
||||
local linkedAsPhrase = string.format(Strings:LocalizedString("LinkedAsPhrase"), gamerTag, robloxName)
|
||||
|
||||
local dummySelection = Utility.Create"Frame" {
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
|
||||
local Container = Utility.Create"Frame" {
|
||||
Name = "Container",
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(0, 765, 0, 630),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Selectable = true,
|
||||
SelectionImageObject = dummySelection,
|
||||
}
|
||||
|
||||
Utility.Create"ImageLabel" {
|
||||
Name = "GamerPic",
|
||||
Position = UDim2.new(0, 40, 0, 25),
|
||||
Size = UDim2.new(0, 300, 0, 300),
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
Image = "rbxapp://xbox/localgamerpic",
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
Utility.Create"ImageLabel" {
|
||||
Name = "AccountLinkIcon",
|
||||
Position = UDim2.new(0, 354, 0, 166),
|
||||
Size = UDim2.new(0, 58, 0, 20),
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/Shell/Icons/AccountLinkIcon.png",
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
local ProfileImage = Utility.Create"ImageLabel" {
|
||||
Name = "ProfileImage",
|
||||
Position = UDim2.new(0, 425, 0, 25),
|
||||
Size = UDim2.new(0, 300, 0, 300),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = GlobalSettings.CharacterBackgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
if rbxuid then
|
||||
spawn(function()
|
||||
local thumbnailSize = ThumbnailLoader.AvatarSizes.Size352x352
|
||||
local thumbLoader = ThumbnailLoader:LoadAvatarThumbnailAsync(ProfileImage, rbxuid,
|
||||
Enum.ThumbnailType.AvatarThumbnail, Enum.ThumbnailSize.Size352x352, true)
|
||||
thumbLoader:LoadAsync()
|
||||
ProfileImage.ImageRectSize = Vector2.new(thumbnailSize.X, (1) * thumbnailSize.X)
|
||||
end)
|
||||
end
|
||||
|
||||
Utility.Create"TextLabel" {
|
||||
Name = "ProfileLabel",
|
||||
Position = UDim2.new(0, 575, 0, 335),
|
||||
Text = robloxName or "",
|
||||
TextXAlignment = "Center",
|
||||
TextYAlignment = "Top",
|
||||
BackgroundColor3 = Color3.new(1, 0, 0),
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.SubHeaderSize,
|
||||
BackgroundTransparency = 1,
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
Utility.Create"TextLabel" {
|
||||
Name = "GamerLabel",
|
||||
Text = gamerTag or "",
|
||||
TextXAlignment = "Center",
|
||||
TextYAlignment = "Top",
|
||||
Position = UDim2.new(0, 190, 0, 335),
|
||||
BackgroundColor3 = Color3.new(1,0,0),
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.SubHeaderSize,
|
||||
BackgroundTransparency = 1,
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
Utility.Create"TextLabel" {
|
||||
Name = "LinkedAsText",
|
||||
Position = UDim2.new(0, 40, 0, 395),
|
||||
Size = UDim2.new(0, 686, 0, 120),
|
||||
Text = linkedAsPhrase,
|
||||
TextXAlignment = "Center",
|
||||
TextYAlignment = "Top",
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
TextColor3 = GlobalSettings.GreyTextColor,
|
||||
FontSize = GlobalSettings.SubHeaderSize,
|
||||
TextWrapped = true,
|
||||
Parent = Container,
|
||||
}
|
||||
|
||||
local UnlinkButton = Utility.Create"ImageButton" {
|
||||
Name = "UnlinkButton",
|
||||
Position = UDim2.new(0, 220, 0, 520),
|
||||
Size = UDim2.new(0, 320, 0, 80),
|
||||
BackgroundTransparency = 1,
|
||||
ImageColor3 = GlobalSettings.GreySelectedButtonColor,
|
||||
Image = GlobalSettings.RoundCornerButtonImage,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)),
|
||||
ZIndex = 2,
|
||||
Parent = Container,
|
||||
SoundManager:CreateSound("MoveSelection"),
|
||||
AssetManager.CreateShadow(1),
|
||||
}
|
||||
|
||||
local DefaultButtonColor = GlobalSettings.GreyButtonColor
|
||||
local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor
|
||||
local DefaultButtonTextColor = GlobalSettings.WhiteTextColor
|
||||
local SelectedButtonTextColor = GlobalSettings.TextSelectedColor
|
||||
|
||||
local UnlinkText = Utility.Create"TextLabel" {
|
||||
Name = "UnlinkText",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextColor3 = Color3.new(0, 0, 0),
|
||||
Text = Strings:LocalizedString("UnlinkGamerTagWord"),
|
||||
ZIndex = 2,
|
||||
Parent = UnlinkButton,
|
||||
}
|
||||
Utility.ResizeButtonWithText(UnlinkButton, UnlinkText, GlobalSettings.TextHorizontalPadding)
|
||||
|
||||
Container.SelectionGained:connect(function()
|
||||
Utility.SetSelectedCoreObject(UnlinkButton)
|
||||
end)
|
||||
|
||||
UnlinkButton.SelectionGained:connect(function()
|
||||
UnlinkButton.ImageColor3 = SelectedButtonColor
|
||||
UnlinkText.TextColor3 = SelectedButtonTextColor
|
||||
end)
|
||||
UnlinkButton.SelectionLost:connect(function()
|
||||
UnlinkButton.ImageColor3 = DefaultButtonColor
|
||||
UnlinkText.TextColor3 = DefaultButtonTextColor
|
||||
end)
|
||||
|
||||
local ModalOverlay = Utility.Create"Frame" {
|
||||
Name = "ModalOverlay",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency,
|
||||
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 4,
|
||||
}
|
||||
|
||||
local isUnlinking = false
|
||||
local function unlinkAccountAsync()
|
||||
if isUnlinking then return end
|
||||
isUnlinking = true
|
||||
local unlinkResult = nil
|
||||
local loader = LoadingWidget(
|
||||
{ Parent = Container }, {
|
||||
function()
|
||||
unlinkResult = AccountManager:UnlinkAccountAsync()
|
||||
end
|
||||
})
|
||||
|
||||
-- set up full screen loader
|
||||
ModalOverlay.Parent = RobloxGui
|
||||
ContextActionService:BindCoreAction("BlockB", function() end, false, Enum.KeyCode.ButtonB)
|
||||
UnlinkButton.SelectionImageObject = dummySelection
|
||||
UnlinkButton.ImageColor3 = GlobalSettings.GreyButtonColor
|
||||
UnlinkText.TextColor3 = GlobalSettings.WhiteTextColor
|
||||
|
||||
-- call loader
|
||||
loader:AwaitFinished()
|
||||
|
||||
-- clean up
|
||||
-- NOTE: Unlink success will fire the ThirdPartyUserService ActiveUserSignedOut event.
|
||||
-- This event will fire and listeners will run before the loader is finished. The below
|
||||
-- code needs to run in case of errors, but on success will not interfere with the reauth
|
||||
-- logic in AppHome.lua
|
||||
loader:Cleanup()
|
||||
UnlinkButton.SelectionImageObject = nil
|
||||
UnlinkButton.ImageColor3 = GlobalSettings.GreySelectedButtonColor
|
||||
UnlinkText.TextColor3 = GlobalSettings.TextSelectedColor
|
||||
ContextActionService:UnbindCoreAction("BlockB")
|
||||
ModalOverlay.Parent = nil
|
||||
|
||||
if unlinkResult ~= AccountManager.AuthResults.Success then
|
||||
local err = unlinkResult and Errors.Authentication[unlinkResult] or Errors.Default
|
||||
ScreenManager:OpenScreen(ErrorOverlay(err), false)
|
||||
end
|
||||
isUnlinking = false
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
end
|
||||
|
||||
UnlinkButton.MouseButton1Click:connect(function()
|
||||
if isUnlinking then return end
|
||||
SoundManager:Play("ButtonPress")
|
||||
local confirmTitleAndMsg = { Title = Strings:LocalizedString("UnlinkTitle"),
|
||||
Msg = Strings:LocalizedString("UnlinkPhrase") }
|
||||
|
||||
ScreenManager:OpenScreen(UnlinkAccountOverlay(confirmTitleAndMsg, unlinkAccountAsync), false)
|
||||
end)
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:SetParent(newParent)
|
||||
Container.Parent = newParent
|
||||
end
|
||||
|
||||
function this:GetUnlinkButton()
|
||||
return UnlinkButton
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createAccountLinkingView
|
||||
@@ -0,0 +1,225 @@
|
||||
--[[
|
||||
// AccountManager.lua
|
||||
|
||||
// Handles all account related functions
|
||||
]]
|
||||
local IsNewUsernameCheckEnabled = settings():GetFFlag("XboxUseNewValidUsernameCheck2")
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
local AnalyticsService = game:GetService("AnalyticsService")
|
||||
|
||||
local Http = require(ShellModules:FindFirstChild('Http'))
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local AccountManager = {}
|
||||
|
||||
AccountManager.AuthResults = {
|
||||
Error = -1;
|
||||
Success = 0;
|
||||
InProgress = 1;
|
||||
AccountUnlinked = 2;
|
||||
MissingGamePad = 3;
|
||||
NoUserDetected = 4;
|
||||
HttpErrorDetected = 5;
|
||||
SignUpDisabled = 6;
|
||||
Flooded = 7;
|
||||
LeaseLocked = 8;
|
||||
AccountLinkingDisabled = 9;
|
||||
InvalidRobloxUser = 10;
|
||||
RobloxUserAlreadyLinked = 11;
|
||||
XboxUserAlreadyLinked = 12;
|
||||
IllgealChildAccountLinking = 13;
|
||||
InvalidPassword = 14;
|
||||
UsernamePasswordNotSet = 15;
|
||||
UsernameAlreadyTaken = 16;
|
||||
SiteMaintenance = 19,
|
||||
}
|
||||
|
||||
AccountManager.InvalidUsernameReasons = {
|
||||
Valid = "Valid";
|
||||
InvalidUsername = "Invalid Username";
|
||||
AlreadyTaken = "Already Taken";
|
||||
InvalidCharactersUsed = "Invalid Characters Used";
|
||||
UsernameCannotContainSpaces = "Username Cannot Contain Spaces";
|
||||
}
|
||||
|
||||
--[[ Authentication ]]--
|
||||
local function authenticateStudio()
|
||||
return AccountManager.AuthResults.Success
|
||||
end
|
||||
|
||||
--[[ Signup/Login ]]--
|
||||
function AccountManager:LoginAsync()
|
||||
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
|
||||
return authenticateStudio()
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
return PlatformService:BeginPlatformLogin()
|
||||
end)
|
||||
-- catch pcall failure, something went wrong with API call
|
||||
if not success then
|
||||
return self.AuthResults.Error
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function AccountManager:SignupAsync(username, password)
|
||||
local success, result = pcall(function()
|
||||
return PlatformService:BeginPlatformSignup(username, password)
|
||||
end)
|
||||
|
||||
-- catch pcall failure, something went wrong with the API call
|
||||
if not success then
|
||||
result = self.AuthResults.Error
|
||||
end
|
||||
|
||||
if result == self.AuthResults.Success then
|
||||
AnalyticsService:ReportCounter("Xbox_SignUp_New_Account_Success")
|
||||
AnalyticsService:ReportCounter("Xbox_SignUp_Success")
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[ Account Linking ]]--
|
||||
-- called at sign in
|
||||
function AccountManager:LinkAccountAsync(accountName, password)
|
||||
local success, result = pcall(function()
|
||||
-- PlatformService may not exist on studio platform
|
||||
return PlatformService:BeginAccountLink(accountName, password)
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog("AccountManager:LinkAccountAsync() failed because", result)
|
||||
result = AccountManager.AuthResults.Error
|
||||
end
|
||||
|
||||
if result == self.AuthResults.Success then
|
||||
AnalyticsService:ReportCounter("Xbox_SignUp_Account_Link_Success")
|
||||
AnalyticsService:ReportCounter("Xbox_SignUp_Success")
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- used when setting credentials for a generated account
|
||||
function AccountManager:SetRobloxCredentialsAsync(accountName, password)
|
||||
local success, result = pcall(function()
|
||||
-- PlatformService may not exist on studio platform
|
||||
return PlatformService:BeginSetRobloxCredentials(accountName, password)
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog("AccountManager:SetRobloxCredentialsAsync() failed because", result)
|
||||
result = AccountManager.AuthResults.Error
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- called when user has roblox credentials
|
||||
function AccountManager:UnlinkAccountAsync()
|
||||
local success, result = pcall(function()
|
||||
-- PlatformService may not exist on studio platform
|
||||
return PlatformService:BeginUnlinkAccount()
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog("AccountManager:UnlinkAccountAsync() failed because", result)
|
||||
result = AccountManager.AuthResults.Error
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Do NOT do this anywhere else. This is a special case for has-linked-account when we are under maintenance.
|
||||
-- has-linked-account is the first endpoint we hit when authenticating
|
||||
-- Puts a 15 sec throttle on request to has-linked-account during maintenance.
|
||||
local isThrottled = false
|
||||
local function setMaintenanceThrottle()
|
||||
isThrottled = true
|
||||
|
||||
delay(15, function()
|
||||
isThrottled = false
|
||||
end)
|
||||
end
|
||||
|
||||
function AccountManager:HasLinkedAccountAsync()
|
||||
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
|
||||
return AccountManager.AuthResults.Success
|
||||
end
|
||||
|
||||
if isThrottled == true then
|
||||
return AccountManager.AuthResults.SiteMaintenance
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
-- PlatformService may not exist on studio platform
|
||||
return PlatformService:BeginHasLinkedAccount()
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog("AccountManager:HasLinkedAccountAsync() failed because", result)
|
||||
result = AccountManager.AuthResults.Error
|
||||
end
|
||||
|
||||
if result == AccountManager.AuthResults.SiteMaintenance then
|
||||
setMaintenanceThrottle()
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function AccountManager:HasRobloxCredentialsAsync()
|
||||
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
|
||||
return AccountManager.AuthResults.Success
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
-- PlatformService may not exist on studio platform
|
||||
return PlatformService:BeginHasRobloxCredentials()
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog("AccountManager:HasRobloxCredentialsAsync() failed because", result)
|
||||
result = AccountManager.AuthResults.Error
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function AccountManager:IsValidUsernameAsync(username)
|
||||
local result = Http.IsValidUsername(username)
|
||||
if not result then
|
||||
-- return false
|
||||
return nil
|
||||
end
|
||||
|
||||
-- there are two endpoints being used based on flag, they return different casing, check for both
|
||||
-- old returns IsValid and ErrorMessage
|
||||
-- new returns isValid and errorMessage
|
||||
if IsNewUsernameCheckEnabled then
|
||||
return result["isValid"], result["errorMessage"]
|
||||
else
|
||||
return result["IsValid"], result["ErrorMessage"]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function AccountManager:IsValidPasswordAsync(username, password)
|
||||
local result = Http.IsValidPassword(username, password)
|
||||
if not result then
|
||||
-- return false
|
||||
return nil
|
||||
end
|
||||
|
||||
return result["IsValid"], result["ErrorMessage"]
|
||||
end
|
||||
|
||||
return AccountManager
|
||||
@@ -0,0 +1,84 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local AccountLinkingView = require(ShellModules:FindFirstChild('AccountLinkingView'))
|
||||
local GameplaySettingsView = require(ShellModules:FindFirstChild('GameplaySettingsView'))
|
||||
|
||||
local function createAccountScreen(errorCode)
|
||||
local this = BaseScreen()
|
||||
|
||||
this:SetTitle(Strings:LocalizedString("AccountSettingsTitle"))
|
||||
|
||||
local AccountLinkingViewContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "AccountLinkingViewContainer";
|
||||
Position = UDim2.new(0, 75, 0, 275);
|
||||
Size = UDim2.new(0, 765, 0, 630);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = this.Container
|
||||
}
|
||||
|
||||
local accountLinkingView = AccountLinkingView()
|
||||
accountLinkingView:SetParent(AccountLinkingViewContainer)
|
||||
|
||||
local ScreenDivide = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ScreenDivide";
|
||||
Size = UDim2.new(0, 2, 0, 615);
|
||||
Position = UDim2.new(0, 840, 0, 275);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.PageDivideColor;
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
local gameplaySettingsViewContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "GameplaySettingsViewContainer";
|
||||
Position = UDim2.new(0, 840, 0, 275);
|
||||
Size = UDim2.new(0, 765, 0, 630);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
local gameplaySettingsView = GameplaySettingsView(errorCode)
|
||||
gameplaySettingsView:SetParent(gameplaySettingsViewContainer)
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('UnlinkAccountScreenId')}
|
||||
end
|
||||
|
||||
-- Override
|
||||
function this:GetDefaultSelectionObject()
|
||||
return accountLinkingView:GetUnlinkButton()
|
||||
end
|
||||
|
||||
-- Override
|
||||
local baseFocus = this.Focus
|
||||
function this:Focus()
|
||||
baseFocus(self)
|
||||
accountLinkingView:Focus()
|
||||
gameplaySettingsView:Focus()
|
||||
end
|
||||
|
||||
-- Override
|
||||
local baseRemoveFocus = this.RemoveFocus
|
||||
function this:RemoveFocus()
|
||||
baseRemoveFocus(self)
|
||||
accountLinkingView:RemoveFocus()
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createAccountScreen
|
||||
@@ -0,0 +1,388 @@
|
||||
-- Written by Kip Turner, Copyright Roblox 2015
|
||||
|
||||
-- Achievement Manager
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
local ThirdPartyUserService = nil
|
||||
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local Http = require(ShellModules:FindFirstChild('Http'))
|
||||
local UserData = require(ShellModules:FindFirstChild('UserData'))
|
||||
local PlatformInterface = require(ShellModules:FindFirstChild('PlatformInterface'))
|
||||
local Alerts = require(ShellModules:FindFirstChild('Alerts'))
|
||||
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local SortsData = require(ShellModules:FindFirstChild('SortsData'))
|
||||
local XboxAppState = require(ShellModules:FindFirstChild('AppState'))
|
||||
|
||||
|
||||
--[[ ACHIEVEMENT NAMES --]]
|
||||
-- "Award10DayRoll"
|
||||
-- "Award20DayRoll"
|
||||
-- "Award3DayRoll"
|
||||
-- "AwardDeepDiver"
|
||||
-- "AwardFoursCompany"
|
||||
-- "AwardOneNameManyFaces"
|
||||
-- "AwardPollster"
|
||||
-- "AwardSampler"
|
||||
-- "AwardStrengthInNumbers"
|
||||
-- "AwardWorldTraveler"
|
||||
-- "AwardYouDidIt"
|
||||
-- "GameProgress"
|
||||
-- "MultiplayerRoundEnd"
|
||||
-- "MultiplayerRoundStart"
|
||||
-- "PlayerSessionEnd"
|
||||
-- "PlayerSessionPause"
|
||||
-- "PlayerSessionResume"
|
||||
-- "PlayerSessionStart"
|
||||
-- "Test_XPresses"
|
||||
--[[ END OF ACHIEVEMENT NAMES --]]
|
||||
|
||||
local VIEW_GAMETYPE_ENUM =
|
||||
{
|
||||
AppShell = 0;
|
||||
Game = 1;
|
||||
}
|
||||
|
||||
local GAMES_FOR_YOU_DID_IT = 1
|
||||
local GAMES_FOR_AWARD_SAMPLER = 5
|
||||
|
||||
local DAYS_FOR_3DAYROLL = 3
|
||||
local DAYS_FOR_10DAYROLL = 10
|
||||
local DAYS_FOR_20DAYROLL = 20
|
||||
|
||||
local GAMES_RATED_FOR_POLLSTER = 5
|
||||
|
||||
local PLAY_SECONDS_FOR_DEEP_DIVER = 60 * 60
|
||||
|
||||
local NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY = 3
|
||||
|
||||
|
||||
local SECONDS_BETWEEN_FOURS_COMPANY_CHECKS = 30
|
||||
|
||||
|
||||
local AchievementManager = {}
|
||||
|
||||
local CurrentView = VIEW_GAMETYPE_ENUM['AppShell']
|
||||
|
||||
local partyUpdateConn = nil
|
||||
|
||||
AchievementManager.AchivementId = {
|
||||
Scout = "2";
|
||||
Explorer = "3";
|
||||
Trailblazer = "4";
|
||||
Pollster = "5";
|
||||
Marathon = "6";
|
||||
OneNameManyFaces = "7";
|
||||
ThreeDayRoll = "8";
|
||||
TenDayRoll = "9";
|
||||
TwentyDayRoll = "10";
|
||||
StrengthInNumbers = "11";
|
||||
FoursCompany = "12";
|
||||
}
|
||||
-- Map ID to trigger name so we can do a look up in SessionAchievementState
|
||||
local AchievementIdToTrigger = {
|
||||
["2"] = "AwardYouDidIt";
|
||||
["3"] = "AwardSampler";
|
||||
["4"] = "AwardWorldTraveler";
|
||||
["5"] = "AwardPollster";
|
||||
["6"] = "AwardDeepDiver";
|
||||
["7"] = "AwardOneNameManyFaces";
|
||||
["8"] = "Award3DayRoll";
|
||||
["9"] = "Award10DayRoll";
|
||||
["10"] = "Award20DayRoll";
|
||||
["11"] = "AwardStrengthInNumbers";
|
||||
["12"] = "AwardFoursCompany";
|
||||
}
|
||||
|
||||
local SessionAchievementState = {}
|
||||
|
||||
local function GetTotalNumberOfGamesOnXbox()
|
||||
-- TODO: is there a programmatic way of figuring this out?
|
||||
return 15
|
||||
end
|
||||
|
||||
local function FilterInGameFriends(onlineFriends, playersInGame)
|
||||
local result = {}
|
||||
|
||||
if onlineFriends and playersInGame then
|
||||
-- Create reverse lookup for speed
|
||||
local playersInGameReverseLookup = {}
|
||||
for _, playerInGame in pairs(playersInGame) do
|
||||
-- TODO: Figure out what the actual lookup
|
||||
if playerInGame['robloxuid'] then
|
||||
playersInGameReverseLookup[playerInGame['robloxuid']] = true
|
||||
end
|
||||
end
|
||||
|
||||
for _, friend in pairs(onlineFriends) do
|
||||
if playersInGameReverseLookup[friend['robloxuid']] then
|
||||
table.insert(result, friend)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
local function OnPlayedGamesChanged()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["PlayedGamesChanged"])
|
||||
spawn(function()
|
||||
local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid
|
||||
if myUserId then
|
||||
local recentCollection
|
||||
-- TODO: is this the right way of getting num of played games?
|
||||
local recentlyPage1
|
||||
recentCollection = SortsData:GetUserRecent()
|
||||
recentlyPage1 = recentCollection and recentCollection:GetSortAsync(0, GetTotalNumberOfGamesOnXbox())
|
||||
local gamesPlayed = recentlyPage1 and #recentlyPage1 or 0
|
||||
|
||||
Utility.DebugLog("You have played:" , gamesPlayed , "games" )
|
||||
if gamesPlayed >= GAMES_FOR_YOU_DID_IT then
|
||||
AchievementManager:SendAchievementEventAsync("AwardYouDidIt")
|
||||
end
|
||||
|
||||
local hasExplorerAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.Explorer)
|
||||
if gamesPlayed >= GAMES_FOR_AWARD_SAMPLER then
|
||||
AchievementManager:SendAchievementEventAsync("AwardSampler")
|
||||
|
||||
-- if we didn't have it before then let's unlock UGC
|
||||
if not hasExplorerAchievement then
|
||||
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], true)
|
||||
else
|
||||
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false)
|
||||
end
|
||||
else
|
||||
--if the user has the ExplorerAchievement but hasn't played 5 games
|
||||
--(may happen when user links a new Roblox account to the Xbox account which already has the ExplorerAchievement)
|
||||
if hasExplorerAchievement then
|
||||
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false)
|
||||
end
|
||||
end
|
||||
if gamesPlayed >= GetTotalNumberOfGamesOnXbox() then
|
||||
AchievementManager:SendAchievementEventAsync("AwardWorldTraveler")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function OnJoinedGame()
|
||||
spawn(function()
|
||||
Utility.DebugLog("OnJoinGame: Fours Company check")
|
||||
|
||||
if PlatformService then
|
||||
local lastCheck = 0
|
||||
while CurrentView == VIEW_GAMETYPE_ENUM['Game'] do
|
||||
local now = tick()
|
||||
if now - lastCheck > SECONDS_BETWEEN_FOURS_COMPANY_CHECKS then
|
||||
|
||||
local friendsData = require(ShellModules:FindFirstChild('FriendsData'))
|
||||
local onlineFriends = friendsData.GetOnlineFriendsAsync()
|
||||
|
||||
-- TODO: add actually API
|
||||
local inGamePlayers = PlatformService:GetInGamePlayers()
|
||||
if inGamePlayers and onlineFriends then
|
||||
local inGameFriends = FilterInGameFriends(onlineFriends, inGamePlayers)
|
||||
|
||||
if #inGameFriends >= NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY then
|
||||
AchievementManager:SendAchievementEventAsync("AwardFoursCompany")
|
||||
return
|
||||
end
|
||||
|
||||
end
|
||||
lastCheck = now
|
||||
end
|
||||
wait(1)
|
||||
end
|
||||
end
|
||||
end)
|
||||
spawn(function()
|
||||
local startTime = tick()
|
||||
while tick() - startTime < PLAY_SECONDS_FOR_DEEP_DIVER do
|
||||
if CurrentView ~= VIEW_GAMETYPE_ENUM['Game'] then
|
||||
return
|
||||
end
|
||||
wait(1)
|
||||
end
|
||||
if CurrentView == VIEW_GAMETYPE_ENUM['Game'] then
|
||||
AchievementManager:SendAchievementEventAsync("AwardDeepDiver")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function CheckStrengthInNumbers()
|
||||
Utility.DebugLog("PartyTitlePresenceChanged: AwardStrengthInNumbers check")
|
||||
local partyMembers = PlatformInterface:GetPartyMembersAsync()
|
||||
if partyMembers then
|
||||
if PlatformInterface:IsInAParty(partyMembers) then
|
||||
AchievementManager:SendAchievementEventAsync("AwardStrengthInNumbers")
|
||||
if partyUpdateConn then
|
||||
partyUpdateConn:disconnect()
|
||||
partyUpdateConn = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AchievementManager:SendAchievementEventAsync(achievementName)
|
||||
-- Always set session state to awarded, we don't want to block session related achievement progress (UGC for example)
|
||||
-- see: https://forums.xboxlive.com/articles/56661/achievements-and-when-they-arent-unlocking-1.html
|
||||
-- there is an issue with xbox live granting achievements
|
||||
SessionAchievementState[achievementName] = true
|
||||
|
||||
Utility.DebugLog("Achievement Manager - Awarding achievement:" , achievementName)
|
||||
local achievementStatus = nil
|
||||
local success, msg = pcall(function()
|
||||
-- NOTE: Yielding function
|
||||
if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then
|
||||
achievementStatus = PlatformService:BeginAwardAchievement(achievementName)
|
||||
end
|
||||
end)
|
||||
if not success then
|
||||
-- NOTE: very likely this function ever throws an error but returns error codes
|
||||
Utility.DebugLog("Achievement Manager - Unable to award achievement:" , achievementName , "for reason:" , msg)
|
||||
end
|
||||
|
||||
Utility.DebugLog("Achievement Manager - Achievement:" , achievementName , "event status:" , achievementStatus)
|
||||
end
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "ShowUnlockedUGCOverlay",
|
||||
function(ShowAlert)
|
||||
if ShowAlert == true then
|
||||
ScreenManager:OpenScreen(ErrorOverlay(Alerts.UnlockedUGC), false)
|
||||
end
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AchievementManager",
|
||||
function()
|
||||
spawn(function()
|
||||
local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid
|
||||
local function stillLoggedIn()
|
||||
local newUserId = XboxAppState.store:getState().RobloxUser.rbxuid
|
||||
return newUserId ~= nil and myUserId == newUserId
|
||||
end
|
||||
|
||||
if myUserId ~= nil then
|
||||
local loggedInResult = Http.GetConsecutiveDaysLoggedInAsync()
|
||||
local daysLoggedIn = loggedInResult and loggedInResult['count']
|
||||
if daysLoggedIn then
|
||||
if daysLoggedIn >= DAYS_FOR_3DAYROLL and stillLoggedIn() then
|
||||
AchievementManager:SendAchievementEventAsync("Award3DayRoll")
|
||||
end
|
||||
if daysLoggedIn >= DAYS_FOR_10DAYROLL and stillLoggedIn() then
|
||||
AchievementManager:SendAchievementEventAsync("Award10DayRoll")
|
||||
end
|
||||
if daysLoggedIn >= DAYS_FOR_20DAYROLL and stillLoggedIn() then
|
||||
AchievementManager:SendAchievementEventAsync("Award20DayRoll")
|
||||
end
|
||||
end
|
||||
--Check if using new "StrengthInNumbers" achievement implementation
|
||||
if PlatformService then
|
||||
local hasStrengthInNumbersAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.StrengthInNumbers)
|
||||
if not hasStrengthInNumbersAchievement then
|
||||
--Haven't got "StrengthInNumbers" achievement
|
||||
partyUpdateConn = PlatformService.PartyTitlePresenceChanged:connect(CheckStrengthInNumbers)
|
||||
CheckStrengthInNumbers()
|
||||
end
|
||||
end
|
||||
OnPlayedGamesChanged()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "AchievementManager",
|
||||
function(assetId)
|
||||
AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces")
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "AchievementManager",
|
||||
function(assetId)
|
||||
AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces")
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["VotedOnPlace"], "AchievementManager",
|
||||
function()
|
||||
spawn(function()
|
||||
local voteCount = UserData:GetVoteCount()
|
||||
Utility.DebugLog("Vote Check: with vote count" , voteCount)
|
||||
if voteCount >= GAMES_RATED_FOR_POLLSTER then
|
||||
AchievementManager:SendAchievementEventAsync("AwardPollster")
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
if PlatformService then
|
||||
PlatformService.ViewChanged:connect(function(newView)
|
||||
Utility.DebugLog("ViewChanged:" , newView)
|
||||
CurrentView = newView
|
||||
if newView == VIEW_GAMETYPE_ENUM['AppShell'] then
|
||||
Utility.DebugLog("New view is appshell")
|
||||
OnPlayedGamesChanged()
|
||||
elseif newView == VIEW_GAMETYPE_ENUM['Game'] then
|
||||
Utility.DebugLog("New view is game")
|
||||
OnJoinedGame()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local UserChangedCount = 0
|
||||
if ThirdPartyUserService then
|
||||
ThirdPartyUserService.ActiveUserSignedOut:connect(function()
|
||||
UserChangedCount = UserChangedCount + 1
|
||||
SessionAchievementState = {}
|
||||
end)
|
||||
end
|
||||
|
||||
function AchievementManager:HasAchievementAsync(achievementId)
|
||||
local startCount = UserChangedCount
|
||||
if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then
|
||||
SessionAchievementState[AchievementIdToTrigger[achievementId]] = true
|
||||
return true
|
||||
end
|
||||
|
||||
if SessionAchievementState[AchievementIdToTrigger[achievementId]] == true then
|
||||
return true
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
return PlatformService:BeginHasAchievement(achievementId)
|
||||
end)
|
||||
if not success then
|
||||
return false
|
||||
end
|
||||
|
||||
--if the result is true, we use it to update SessionAchievementState
|
||||
if startCount == UserChangedCount then
|
||||
if result == true then
|
||||
SessionAchievementState[AchievementIdToTrigger[achievementId]] = result
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
If the result is false or the API fails, we need to use our session state as backup. There is an edge case where locally we
|
||||
grant the achievement, but xbox does not update/grant the achievement for some period of time (like xbox services being down).
|
||||
We want to avoid ever locking the user from UGC if they have in fact unlocked it.
|
||||
Two other cases remain. During the retro check, if xbox services are down, but we have in fact unlocked UGC because we've played
|
||||
5 games, we still unlock. The user will get the notification again however.
|
||||
If the user unlocks UGC then unlinks their account and creates a new Roblox account, and if xbox services are down, since we
|
||||
check games played, UGC will be locked for them. It will remain locked until they play 5 games on the new account, or until
|
||||
xbox services come back up and we can correctly get the state of the achievement. Dan says this is both of these are OK.
|
||||
]]
|
||||
return result or SessionAchievementState[AchievementIdToTrigger[achievementId]]
|
||||
end
|
||||
|
||||
function AchievementManager:AllGamesUnlocked()
|
||||
local achievementId = AchievementManager.AchivementId.Explorer
|
||||
return SessionAchievementState[AchievementIdToTrigger[achievementId]]
|
||||
end
|
||||
|
||||
return AchievementManager
|
||||
@@ -0,0 +1,14 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("AddError", function(error, timestamp)
|
||||
error = error or {}
|
||||
return {
|
||||
error = {
|
||||
Title = error.Title,
|
||||
Msg = error.Msg,
|
||||
Code = error.Code,
|
||||
timestamp = timestamp
|
||||
}
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,39 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.AddError)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.AddError)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table with an error with same Title, Msg, Code set as the passed in error and aslo has the error timestamp appended", function()
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local DefaultError = Errors.Default
|
||||
local action = require(script.Parent.AddError)(DefaultError, tick())
|
||||
local error = action.error
|
||||
expect(error).to.be.a("table")
|
||||
expect(error.Title).to.equal(DefaultError.Title)
|
||||
expect(error.Msg).to.equal(DefaultError.Msg)
|
||||
expect(error.Code).to.equal(DefaultError.Code)
|
||||
expect(error.timestamp).to.be.a("number")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.AddError)
|
||||
|
||||
expect(action.name).to.equal("AddError")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.AddError)()
|
||||
|
||||
expect(action.type).to.equal("AddError")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("DeleteError", function(error)
|
||||
error = error or {}
|
||||
return {
|
||||
error = {
|
||||
Title = error.Title,
|
||||
Msg = error.Msg,
|
||||
Code = error.Code
|
||||
}
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,38 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.DeleteError)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.DeleteError)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table with an error with same Title, Msg, Code set as the passed in error", function()
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local DefaultError = Errors.Default
|
||||
local action = require(script.Parent.DeleteError)(DefaultError)
|
||||
local error = action.error
|
||||
expect(error).to.be.a("table")
|
||||
expect(error.Title).to.equal(DefaultError.Title)
|
||||
expect(error.Msg).to.equal(DefaultError.Msg)
|
||||
expect(error.Code).to.equal(DefaultError.Code)
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.DeleteError)
|
||||
|
||||
expect(action.name).to.equal("DeleteError")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.DeleteError)()
|
||||
|
||||
expect(action.type).to.equal("DeleteError")
|
||||
end)
|
||||
end
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("FetchPrivilegeSettings", function()
|
||||
return {}
|
||||
end)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.FetchPrivilegeSettings)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.FetchPrivilegeSettings)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.FetchPrivilegeSettings)
|
||||
|
||||
expect(action.name).to.equal("FetchPrivilegeSettings")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.FetchPrivilegeSettings)()
|
||||
|
||||
expect(action.type).to.equal("FetchPrivilegeSettings")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("FetchUserThumbnail", function(thumbnailInfo)
|
||||
thumbnailInfo = thumbnailInfo or {}
|
||||
return {
|
||||
rbxuid = thumbnailInfo.rbxuid,
|
||||
thumbnailType = thumbnailInfo.thumbnailType,
|
||||
thumbnailSize = thumbnailInfo.thumbnailSize
|
||||
}
|
||||
end)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.FetchUserThumbnail)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.FetchUserThumbnail)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.FetchUserThumbnail)
|
||||
|
||||
expect(action.name).to.equal("FetchUserThumbnail")
|
||||
end)
|
||||
|
||||
it("should set the rbxuid, thumbnailType and thumbnailSize", function()
|
||||
local action = require(script.Parent.FetchUserThumbnail)(
|
||||
{
|
||||
rbxuid = 12345,
|
||||
thumbnailType = Enum.ThumbnailType.HeadShot,
|
||||
thumbnailSize = Enum.ThumbnailSize.Size180x180
|
||||
})
|
||||
|
||||
|
||||
expect(action.rbxuid).to.equal(12345)
|
||||
expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot)
|
||||
expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180)
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.FetchUserThumbnail)()
|
||||
|
||||
expect(action.type).to.equal("FetchUserThumbnail")
|
||||
end)
|
||||
end
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("GetCrossPlayEnabledFailed", function()
|
||||
return {}
|
||||
end)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.GetCrossPlayEnabledFailed)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.GetCrossPlayEnabledFailed)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.GetCrossPlayEnabledFailed)
|
||||
|
||||
expect(action.name).to.equal("GetCrossPlayEnabledFailed")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.GetCrossPlayEnabledFailed)()
|
||||
|
||||
expect(action.type).to.equal("GetCrossPlayEnabledFailed")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("InsertScreen", function(item)
|
||||
return {
|
||||
item = item,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,44 @@
|
||||
return function()
|
||||
describe("require", function()
|
||||
it("should create without errors", function()
|
||||
require(script.Parent.InsertScreen)
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.InsertScreen)
|
||||
|
||||
expect(action.name).to.equal("InsertScreen")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("call", function()
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.InsertScreen)
|
||||
|
||||
action = action({})
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.InsertScreen)
|
||||
|
||||
action = action({})
|
||||
expect(action.type).to.equal("InsertScreen")
|
||||
end)
|
||||
|
||||
it("should set the item", function()
|
||||
local action = require(script.Parent.InsertScreen)
|
||||
|
||||
local item = "foo"
|
||||
action = action(item)
|
||||
expect(action.item).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should set the type and name to be equal", function()
|
||||
local action = require(script.Parent.InsertScreen)
|
||||
|
||||
local actionItem = action({})
|
||||
expect(actionItem.type).to.equal(action.name)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("PostCrossPlayEnabledFailed", function()
|
||||
return {}
|
||||
end)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.PostCrossPlayEnabledFailed)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.PostCrossPlayEnabledFailed)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.PostCrossPlayEnabledFailed)
|
||||
|
||||
expect(action.name).to.equal("PostCrossPlayEnabledFailed")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.PostCrossPlayEnabledFailed)()
|
||||
|
||||
expect(action.type).to.equal("PostCrossPlayEnabledFailed")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("RemoveScreen", function(item)
|
||||
return {
|
||||
item = item,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,44 @@
|
||||
return function()
|
||||
describe("require", function()
|
||||
it("should create without errors", function()
|
||||
require(script.Parent.RemoveScreen)
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.RemoveScreen)
|
||||
|
||||
expect(action.name).to.equal("RemoveScreen")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("call", function()
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.RemoveScreen)
|
||||
|
||||
action = action({})
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.RemoveScreen)
|
||||
|
||||
action = action({})
|
||||
expect(action.type).to.equal("RemoveScreen")
|
||||
end)
|
||||
|
||||
it("should set the item", function()
|
||||
local action = require(script.Parent.RemoveScreen)
|
||||
|
||||
local item = "foo"
|
||||
action = action(item)
|
||||
expect(action.item).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should set the type and name to be equal", function()
|
||||
local action = require(script.Parent.RemoveScreen)
|
||||
|
||||
local actionItem = action({})
|
||||
expect(actionItem.type).to.equal(action.name)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("RequestCrossPlayEnabled", function()
|
||||
return {}
|
||||
end)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.RequestCrossPlayEnabled)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.RequestCrossPlayEnabled)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.RequestCrossPlayEnabled)
|
||||
|
||||
expect(action.name).to.equal("RequestCrossPlayEnabled")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.RequestCrossPlayEnabled)()
|
||||
|
||||
expect(action.type).to.equal("RequestCrossPlayEnabled")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("ResetUserThumbnails", function()
|
||||
return {}
|
||||
end)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.ResetUserThumbnails)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.ResetUserThumbnails)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.ResetUserThumbnails)
|
||||
|
||||
expect(action.name).to.equal("ResetUserThumbnails")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.ResetUserThumbnails)()
|
||||
|
||||
expect(action.type).to.equal("ResetUserThumbnails")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("SetCrossPlayEnabled", function(enabled, timestamp)
|
||||
return {
|
||||
enabled = enabled,
|
||||
timestamp = timestamp
|
||||
}
|
||||
end)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetCrossPlayEnabled)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.SetCrossPlayEnabled)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetCrossPlayEnabled)
|
||||
|
||||
expect(action.name).to.equal("SetCrossPlayEnabled")
|
||||
end)
|
||||
|
||||
it("should set the enabled value and timestamp", function()
|
||||
local action = require(script.Parent.SetCrossPlayEnabled)(true, 10)
|
||||
|
||||
expect(action.enabled).to.equal(true)
|
||||
expect(action.timestamp).to.equal(10)
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetCrossPlayEnabled)()
|
||||
|
||||
expect(action.type).to.equal("SetCrossPlayEnabled")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
--[[
|
||||
// friendsData is table
|
||||
// Table keys:
|
||||
// [index number] - table
|
||||
// xuid - number
|
||||
// robloxName - string
|
||||
// placeId - number
|
||||
// robloxStatus - string
|
||||
// robloxuid - number
|
||||
// lastLocation - string
|
||||
// gamertag - string
|
||||
// xboxStatus - string
|
||||
// friendsSource - string
|
||||
]]
|
||||
|
||||
return Action("SetFriendsData", function(friendsData)
|
||||
return {
|
||||
data = friendsData
|
||||
}
|
||||
end)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetFriendsData)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table without data when passed nil", function()
|
||||
local action = require(script.Parent.SetFriendsData)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
expect(action.data).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetFriendsData)
|
||||
|
||||
expect(action.name).to.equal("SetFriendsData")
|
||||
end)
|
||||
|
||||
it("should set the elements at the first depth", function()
|
||||
local action = require(script.Parent.SetFriendsData)( { {}, {} } )
|
||||
|
||||
expect(action.data[2]).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the elements at the second depth", function()
|
||||
local action = require(script.Parent.SetFriendsData)( { { a="A", b="B" } } )
|
||||
|
||||
expect(action.data[1]).to.be.a("table")
|
||||
expect(action.data[1].a).to.equal("A")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetFriendsData)()
|
||||
|
||||
expect(action.type).to.equal("SetFriendsData")
|
||||
end)
|
||||
end
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("SetPrivilegeSettings", function(privilegeSettings)
|
||||
privilegeSettings = privilegeSettings or {}
|
||||
return
|
||||
{
|
||||
Multiplayer = privilegeSettings.Multiplayer,
|
||||
SharedContent = privilegeSettings.SharedContent,
|
||||
timestamp = privilegeSettings.timestamp
|
||||
}
|
||||
end)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetPrivilegeSettings)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.SetPrivilegeSettings)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetPrivilegeSettings)
|
||||
|
||||
expect(action.name).to.equal("SetPrivilegeSettings")
|
||||
end)
|
||||
|
||||
it("should set the privilege settings and timestamp", function()
|
||||
local action = require(script.Parent.SetPrivilegeSettings)({Multiplayer = {}, SharedContent = {}, timestamp = 10})
|
||||
|
||||
expect(action.Multiplayer).to.be.a("table")
|
||||
expect(action.SharedContent).to.be.a("table")
|
||||
expect(action.timestamp).to.equal(10)
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetPrivilegeSettings)()
|
||||
|
||||
expect(action.type).to.equal("SetPrivilegeSettings")
|
||||
end)
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
--[[
|
||||
// friendsData is table
|
||||
// Table keys:
|
||||
// [index number] - table
|
||||
// xuid - number
|
||||
// robloxName - string
|
||||
// placeId - number
|
||||
// robloxStatus - string
|
||||
// robloxuid - number
|
||||
// lastLocation - string
|
||||
// gamertag - string
|
||||
// xboxStatus - string
|
||||
// friendsSource - string
|
||||
]]
|
||||
|
||||
return Action("SetRenderedFriendsData", function(friendsData)
|
||||
return {
|
||||
data = friendsData
|
||||
}
|
||||
end)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table without data when passed nil", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)()
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
expect(action.data).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)
|
||||
|
||||
expect(action.name).to.equal("SetRenderedFriendsData")
|
||||
end)
|
||||
|
||||
it("should set the elements at the first depth", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)( { {}, {} } )
|
||||
|
||||
expect(action.data[2]).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the elements at the second depth", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)( { { a="A", b="B" } } )
|
||||
|
||||
expect(action.data[1]).to.be.a("table")
|
||||
expect(action.data[1].a).to.equal("A")
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetRenderedFriendsData)()
|
||||
|
||||
expect(action.type).to.equal("SetRenderedFriendsData")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("SetRobloxUser", function(userInfo)
|
||||
userInfo = userInfo or {}
|
||||
return {
|
||||
robloxName = userInfo.robloxName,
|
||||
rbxuid = userInfo.rbxuid,
|
||||
under13 = userInfo.under13
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,44 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetRobloxUser)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetRobloxUser)
|
||||
|
||||
expect(action.name).to.equal("SetRobloxUser")
|
||||
end)
|
||||
|
||||
it("should set the robloxName, rbxuid, and under13 values", function()
|
||||
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
|
||||
|
||||
expect(action.robloxName).to.be.a("string")
|
||||
expect(action.robloxName).to.equal("TestRobloxName")
|
||||
expect(action.rbxuid).to.be.a("number")
|
||||
expect(action.rbxuid).to.equal(12345)
|
||||
expect(action.under13).to.be.a("boolean")
|
||||
expect(action.under13).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should set the robloxName, rbxuid, and under13 values to nil when passed an empty table", function()
|
||||
local action = require(script.Parent.SetRobloxUser)({})
|
||||
|
||||
expect(action.robloxName).to.equal(nil)
|
||||
expect(action.rbxuid).to.equal(nil)
|
||||
expect(action.under13).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
|
||||
|
||||
expect(action.type).to.equal("SetRobloxUser")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("SetUserThumbnail", function(thumbnailInfo)
|
||||
thumbnailInfo = thumbnailInfo or {}
|
||||
return {
|
||||
success = thumbnailInfo.success,
|
||||
rbxuid = thumbnailInfo.rbxuid,
|
||||
imageUrl = thumbnailInfo.imageUrl,
|
||||
thumbnailType = thumbnailInfo.thumbnailType,
|
||||
thumbnailSize = thumbnailInfo.thumbnailSize,
|
||||
isFinal = thumbnailInfo.isFinal,
|
||||
timestamp = thumbnailInfo.timestamp
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,10 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
|
||||
|
||||
return Action("SetXboxUser", function(userInfo)
|
||||
userInfo = userInfo or {}
|
||||
return {
|
||||
gamertag = userInfo.gamertag,
|
||||
xuid = userInfo.xuid
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,41 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetXboxUser)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetXboxUser)
|
||||
|
||||
expect(action.name).to.equal("SetXboxUser")
|
||||
end)
|
||||
|
||||
it("should set the gamertag and xuid values", function()
|
||||
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
|
||||
|
||||
expect(action.gamertag).to.be.a("string")
|
||||
expect(action.gamertag).to.equal("TestGamerTag")
|
||||
expect(action.xuid).to.be.a("number")
|
||||
expect(action.xuid).to.equal(12345)
|
||||
end)
|
||||
|
||||
it("should set gamertag and xuid to nil if passed an empty table", function()
|
||||
local action = require(script.Parent.SetXboxUser)({})
|
||||
|
||||
expect(action.gamertag).to.equal(nil)
|
||||
expect(action.xuid).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
|
||||
|
||||
expect(action.type).to.equal("SetXboxUser")
|
||||
end)
|
||||
end
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
return function()
|
||||
it("should return a table", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)(
|
||||
{
|
||||
success = true,
|
||||
rbxuid = 12345,
|
||||
imageUrl = "x",
|
||||
thumbnailType = Enum.ThumbnailType.HeadShot,
|
||||
thumbnailSize = Enum.ThumbnailSize.Size180x180,
|
||||
isFinal = true,
|
||||
timestamp = 10
|
||||
})
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)
|
||||
|
||||
expect(action.name).to.equal("SetUserThumbnail")
|
||||
end)
|
||||
|
||||
it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal values", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)(
|
||||
{
|
||||
rbxuid = 12345,
|
||||
thumbnailType = Enum.ThumbnailType.HeadShot,
|
||||
thumbnailSize = Enum.ThumbnailSize.Size180x180,
|
||||
success = true,
|
||||
imageUrl = "x",
|
||||
isFinal = true,
|
||||
timestamp = 10
|
||||
})
|
||||
|
||||
expect(action.success).to.equal(true)
|
||||
expect(action.rbxuid).to.equal(12345)
|
||||
expect(action.imageUrl).to.equal("x")
|
||||
expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot)
|
||||
expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180)
|
||||
expect(action.isFinal).to.equal(true)
|
||||
expect(action.timestamp).to.equal(10)
|
||||
end)
|
||||
|
||||
it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal to nil if passed an empty table", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)({})
|
||||
|
||||
expect(action.success).never.to.be.ok()
|
||||
expect(action.rbxuid).never.to.be.ok()
|
||||
expect(action.imageUrl).never.to.be.ok()
|
||||
expect(action.thumbnailType).never.to.be.ok()
|
||||
expect(action.thumbnailSize).never.to.be.ok()
|
||||
expect(action.isFinal).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should set the type", function()
|
||||
local action = require(script.Parent.SetUserThumbnail)({})
|
||||
expect(action.type).to.equal("SetUserThumbnail")
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
--[[
|
||||
// Alerts.lua
|
||||
|
||||
// Global alert codes, each alert has an unique Id
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
|
||||
local Alerts =
|
||||
{
|
||||
Default = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("DefaultAlertPhrase"), Id = 0 };
|
||||
|
||||
UnlockedUGC = { Title = Strings:LocalizedString("UnlockedUGCTitle"), Msg = Strings:LocalizedString("UnlockedUGCPhrase"), Id = 101};
|
||||
|
||||
LostConnection =
|
||||
{
|
||||
Controller = { Title = Strings:LocalizedString("ControllerLostConnectionTitle"), Msg = Strings:LocalizedString("ControllerLostConnectionPhrase"), Id = 201 };
|
||||
ActiveUser = { Title = Strings:LocalizedString("ActiveUserLostConnectionTitle"), Msg = Strings:LocalizedString("ActiveUserLostConnectionPhrase"), Id = 202 };
|
||||
};
|
||||
|
||||
PlayMyPlaceMoreGames = { Title = Strings:LocalizedString('PlayMyPlaceMoreGamesTitle'), Msg = Strings:LocalizedString('PlayMyPlaceMoreGamesPhrase'), Id = 301 };
|
||||
|
||||
CrossPlatformPlayWelcome = { Title = Strings:LocalizedString("CPPWelcomeTitle"), Msg = Strings:LocalizedString("CPPWelcomePhrase"), Id = 401};
|
||||
|
||||
Reauthentication =
|
||||
{
|
||||
-- index mapped to int error code from c++, you must index into this with a string
|
||||
[0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 };
|
||||
[1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 };
|
||||
[2] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1003 };
|
||||
[3] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1004 };
|
||||
[4] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1005 };
|
||||
[5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 };
|
||||
[6] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1007 };
|
||||
[7] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1008 };
|
||||
};
|
||||
|
||||
SignOut =
|
||||
{
|
||||
-- index mapped to int error code from c++, you must index into this with a string
|
||||
[0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 };
|
||||
[1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 };
|
||||
[2] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1003 };
|
||||
[3] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1004 };
|
||||
[4] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1005 };
|
||||
[5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 };
|
||||
[6] = nil;
|
||||
[7] = { Title = Strings:LocalizedString("ReauthMaintenanceTitle"), Msg = Strings:LocalizedString("ReauthMaintenancePhrase"), Id = 1007 };
|
||||
};
|
||||
|
||||
|
||||
PlatformLink = { Title = Strings:LocalizedString('PlatformLinkInfoTitle'), Msg = Strings:LocalizedString('PlatformLinkInfoMessage'), Id = 1201};
|
||||
}
|
||||
|
||||
return Alerts
|
||||
@@ -0,0 +1,173 @@
|
||||
--[[
|
||||
// Analytics.lua
|
||||
|
||||
// Fetches analytics data for console platform
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
--[[ Services ]]--
|
||||
local AnalyticsService = nil
|
||||
pcall(function() AnalyticsService = game:GetService('AnalyticsService') end)
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
|
||||
local Analytics = {}
|
||||
|
||||
--[[ Helper Functions ]]--
|
||||
local function setRBXEvent(eventName, additionalArgs)
|
||||
local target, eventContext = nil, nil
|
||||
local success, result = pcall(function()
|
||||
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
|
||||
target = "console"
|
||||
eventContext = "XboxOne"
|
||||
eventName = eventName or ""
|
||||
additionalArgs = additionalArgs or {}
|
||||
AnalyticsService:SetRBXEvent(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Utility.DebugLog("setRBXEvent() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName)
|
||||
end
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
local function setRBXEventStream(eventName, additionalArgs)
|
||||
local target, eventContext = nil, nil
|
||||
local success, result = pcall(function()
|
||||
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
|
||||
target = "console"
|
||||
eventContext = "XboxOne"
|
||||
eventName = eventName or ""
|
||||
additionalArgs = additionalArgs or {}
|
||||
AnalyticsService:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Utility.DebugLog("setRBXEventStream() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName)
|
||||
end
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
local function releaseRBXEventStream(eventName)
|
||||
local target = nil
|
||||
local success, result = pcall(function()
|
||||
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
|
||||
target = "console"
|
||||
AnalyticsService:ReleaseRBXEventStream(target)
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Utility.DebugLog("releaseRBXEventStream() failed because", result, "Input: target:", target)
|
||||
end
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
local function updateHeartbeatObject(additionalArgs)
|
||||
local success, result = pcall(function()
|
||||
AnalyticsService:UpdateHeartbeatObject(additionalArgs)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Utility.DebugLog("UpdateHeartbeatObject() failed because ", result, "Input: args:", additionalArgs)
|
||||
end
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
local function reportCounter(counterName, amount)
|
||||
local success, result = pcall(function()
|
||||
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
|
||||
counterName = counterName or ""
|
||||
counterName = "Xbox-"..tostring(counterName)
|
||||
amount = amount or 1
|
||||
AnalyticsService:ReportCounter(counterName, amount)
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Utility.DebugLog("reportCounter() failed because", result, "Input: counterName:", counterName, "amount:", amount)
|
||||
end
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
function Analytics.SetRBXEvent(eventName, additionalArgs)
|
||||
setRBXEvent(eventName, additionalArgs)
|
||||
end
|
||||
|
||||
-- Non-real time
|
||||
function Analytics.SetRBXEventStream(eventName, additionalArgs)
|
||||
setRBXEventStream(eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function Analytics.UpdateHeartbeatObject(additionalArgs)
|
||||
updateHeartbeatObject(additionalArgs)
|
||||
end
|
||||
|
||||
-- Real time
|
||||
function Analytics.ReportCounter(counterName, amount)
|
||||
reportCounter(counterName, amount)
|
||||
end
|
||||
|
||||
|
||||
local WidgetNames = {
|
||||
--Widget Name
|
||||
["WidgetId"] = "WidgetName";
|
||||
["AppHubId"] = "AppHub";
|
||||
["AvatarEditorScreenId"] = "AvatarEditorScreen";
|
||||
["AvatarPaneId"] = "AvatarPane";
|
||||
["AvatarTileId"] = "AvatarTile";
|
||||
["BadgeOverlayId"] = "BadgeOverlay";
|
||||
["BaseCarouselScreenId"] = "BaseCarouselScreen";
|
||||
["BaseOverlayId"] = "BaseOverlay";
|
||||
["BaseTileId"] = "BaseTile";
|
||||
["ConfirmPromptId"] = "ConfirmPrompt";
|
||||
["DisableCrossplayOverlayId"] = "DisableCrossplayOverlay";
|
||||
["EnableCrossplayOverlayId"] = "EnableCrossplayOverlay";
|
||||
["EngagementScreenId"] = "EngagementScreen";
|
||||
["ErrorOverlayId"] = "ErrorOverlay";
|
||||
["GameDetailId"] = "GameDetail";
|
||||
["GameGenreScreenId"] = "GameGenreScreen";
|
||||
["GamesPaneId"] = "GamesPane";
|
||||
["GameSearchScreenId"] = "GameSearchScreen";
|
||||
["HomePaneId"] = "HomePane";
|
||||
["ImageOverlayId"] = "ImageOverlay";
|
||||
["LinkAccountScreenId"] = "LinkAccountScreen"; --AKA "Sign In Screen"
|
||||
["NoActionOverlayId"] = "NoActionOverlay";
|
||||
["OutfitTileId"] = "OutfitTile";
|
||||
["OverscanScreenId"] = "OverscanScreen";
|
||||
["PurchasePackagePromptId"] = "PurchasePackagePrompt";
|
||||
["ReportOverlayId"] = "ReportOverlay";
|
||||
["RobuxBalanceOverlayId"] = "RobuxBalanceOverlay";
|
||||
["SetAccountCredentialsScreenId"] = "SetAccountCredentialsScreen"; --AKA "Sign Up Screen" and "Microwave Screen" (User sees it as two different screens)
|
||||
["SettingsScreenId"] = "SettingsScreen";
|
||||
["SideBarId"] = "SideBar";
|
||||
["SignInScreenId"] = "SignInScreen"; --AKA "Choice Screen"
|
||||
["SocialPaneId"] = "SocialPane";
|
||||
["StorePaneId"] = "StorePane";
|
||||
["TabDockId"] = "TabDock";
|
||||
["UnlinkAccountOverlayId"] = "UnlinkAccountOverlay";
|
||||
["UnlinkAccountScreenId"] = "UnlinkAccountScreen";
|
||||
}
|
||||
|
||||
function Analytics.WidgetNames(stringKey)
|
||||
local result = WidgetNames and WidgetNames[stringKey]
|
||||
if not result then
|
||||
Utility.DebugLog("Analytics.WidgetNames: Could not find widget name for:" , stringKey)
|
||||
result = stringKey
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return Analytics
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Creates UI for Action/Title safe container as per Microsofts reccomendations
|
||||
|
||||
local CoreGui = game:GetService('CoreGui')
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local ShellModules = script.Parent
|
||||
|
||||
local Utility = require(ShellModules.Utility)
|
||||
|
||||
local ACTION_SAFE_INSET = UDim2.new((128 / 1920) * 0.5, 0, (64 / 1080) * 0.5, 0)
|
||||
local TITLE_SAFE_INSET = UDim2.new((72 / 1792) * 0.5, 0, (16 / 1080) * 0.5, 0)
|
||||
|
||||
local AppContainer = {}
|
||||
|
||||
AppContainer.Root = Utility.Create'Frame'
|
||||
{
|
||||
Name = "AppContainerRoot";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = Color3.new(0, 0, 0);
|
||||
ClipsDescendants = true;
|
||||
Parent = RobloxGui;
|
||||
}
|
||||
|
||||
AppContainer.AspectRatioProtector = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'AspectRatioProtector';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
Position = UDim2.new(0,0,0,0);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = AppContainer.Root;
|
||||
}
|
||||
|
||||
AppContainer.ActionSafeContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ActionSafeContainer";
|
||||
Size = UDim2.new(1, 0, 1, 0) - (ACTION_SAFE_INSET + ACTION_SAFE_INSET);
|
||||
Position = UDim2.new(0,0,0,0) + ACTION_SAFE_INSET;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = AppContainer.AspectRatioProtector;
|
||||
}
|
||||
|
||||
AppContainer.TitleSafeContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "TitleSafeContainer";
|
||||
Size = UDim2.new(1, 0, 1, 0) - (TITLE_SAFE_INSET + TITLE_SAFE_INSET);
|
||||
Position = UDim2.new(0, 0, 0, 0) + TITLE_SAFE_INSET;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = AppContainer.ActionSafeContainer;
|
||||
}
|
||||
|
||||
local function OnAbsoluteSizeChanged()
|
||||
local newSize = Utility.CalculateFit(AppContainer.Root, Vector2.new(16,9))
|
||||
if newSize ~= AppContainer.AspectRatioProtector.Size then
|
||||
AppContainer.AspectRatioProtector.Size = newSize
|
||||
AppContainer.AspectRatioProtector.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
AppContainer.AspectRatioProtector.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
end
|
||||
end
|
||||
|
||||
AppContainer.Root:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
|
||||
OnAbsoluteSizeChanged()
|
||||
end)
|
||||
OnAbsoluteSizeChanged()
|
||||
|
||||
return AppContainer
|
||||
@@ -0,0 +1,198 @@
|
||||
-- Written by Kip Turner, Copyright Roblox 2015
|
||||
|
||||
-- App's Main
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = RobloxGui:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
-- TODO: Will use for re-auth when finished
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
local ThirdPartyUserService = nil
|
||||
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
-- Start up background scene before anything else
|
||||
require(ShellModules.BackgroundSceneManager)
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local AppHubModule = require(ShellModules:FindFirstChild('AppHub'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local GameGenreScreen = require(ShellModules:FindFirstChild('GameGenreScreen'))
|
||||
local EngagementScreenModule = require(ShellModules:FindFirstChild('EngagementScreen'))
|
||||
local BadgeScreenModule = require(ShellModules:FindFirstChild('BadgeScreen'))
|
||||
local AccountScreen = require(ShellModules:FindFirstChild('AccountScreen'))
|
||||
local UserData = require(ShellModules:FindFirstChild('UserData'))
|
||||
local ControllerStateManager = require(ShellModules:FindFirstChild('ControllerStateManager'))
|
||||
local Alerts = require(ShellModules:FindFirstChild('Alerts'))
|
||||
|
||||
local SiteInfoWidget = require(ShellModules:FindFirstChild('SiteInfoWidget'))
|
||||
|
||||
local GameDetailModule = require(ShellModules:FindFirstChild('GameDetailScreen'))
|
||||
|
||||
-- Initialize AppState and in turn initialize the Store
|
||||
local AppState = require(ShellModules.AppState)
|
||||
|
||||
local AppContainer = require(ShellModules.AppContainer)
|
||||
local TitleSafeContainer = AppContainer.TitleSafeContainer
|
||||
|
||||
local EngagementScreen = EngagementScreenModule()
|
||||
EngagementScreen:SetParent(TitleSafeContainer)
|
||||
|
||||
-- Site Info View
|
||||
SiteInfoWidget.new()
|
||||
|
||||
-- Initialzie Account Age View
|
||||
require(ShellModules.Components.AccountAgeStatus).new(AppState.store, TitleSafeContainer)
|
||||
|
||||
local function returnToEngagementScreen()
|
||||
if ScreenManager:ContainsScreen(EngagementScreen) then
|
||||
while ScreenManager:GetTopScreen() ~= EngagementScreen do
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
else
|
||||
while ScreenManager:GetTopScreen() do
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
ScreenManager:OpenScreen(EngagementScreen)
|
||||
end
|
||||
end
|
||||
|
||||
local AppHub = nil
|
||||
local function onAuthenticationSuccess(isNewLinkedAccount)
|
||||
-- Set UserData
|
||||
UserData:Initialize()
|
||||
|
||||
local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser)
|
||||
AppState.store:dispatch(SetRobloxUser( {
|
||||
robloxName = Players.LocalPlayer.Name,
|
||||
rbxuid = Players.LocalPlayer.UserId,
|
||||
under13 = Players.LocalPlayer:GetUnder13(),
|
||||
} ))
|
||||
|
||||
-- Unwind Screens if needed - this will be needed once we put in account linking
|
||||
returnToEngagementScreen()
|
||||
|
||||
AppHub = AppHubModule()
|
||||
AppHub:SetParent(TitleSafeContainer)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail",
|
||||
function(placeId)
|
||||
local gameDetail = GameDetailModule(placeId)
|
||||
gameDetail:SetParent(TitleSafeContainer);
|
||||
ScreenManager:OpenScreen(gameDetail);
|
||||
end);
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre",
|
||||
function(sortName, gameCollection)
|
||||
local gameGenre = GameGenreScreen(sortName, gameCollection)
|
||||
gameGenre:SetParent(TitleSafeContainer);
|
||||
ScreenManager:OpenScreen(gameGenre);
|
||||
end);
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges",
|
||||
function(badgeData, previousScreenName)
|
||||
local badgeScreen = BadgeScreenModule(badgeData, previousScreenName)
|
||||
badgeScreen:SetParent(TitleSafeContainer);
|
||||
ScreenManager:OpenScreen(badgeScreen);
|
||||
end)
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen",
|
||||
function(settingsScreen)
|
||||
settingsScreen:SetParent(TitleSafeContainer);
|
||||
ScreenManager:OpenScreen(settingsScreen);
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen",
|
||||
function(screen)
|
||||
screen:SetParent(AppContainer.Root);
|
||||
ScreenManager:OpenScreen(screen);
|
||||
end)
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen",
|
||||
function(errorCode)
|
||||
local accountScreen = AccountScreen(errorCode)
|
||||
accountScreen:SetParent(TitleSafeContainer);
|
||||
ScreenManager:OpenScreen(accountScreen);
|
||||
end)
|
||||
|
||||
Utility.DebugLog("User and Event initialization finished. Opening AppHub")
|
||||
ScreenManager:OpenScreen(AppHub);
|
||||
|
||||
if PlatformService and not PlatformService:SeenCPPWelcomeMsg() then
|
||||
ScreenManager:OpenScreen(ErrorOverlay(Alerts.CrossPlatformPlayWelcome), false)
|
||||
end
|
||||
|
||||
-- Comment out for now since this has never been called in current flow
|
||||
-- show info popup to users on newly linked accounts
|
||||
-- if isNewLinkedAccount == true then
|
||||
-- ScreenManager:OpenScreen(ErrorOverlay(Alerts.PlatformLink), false)
|
||||
-- end
|
||||
end
|
||||
|
||||
local function onReAuthentication(reauthenticationReason)
|
||||
Utility.DebugLog("Beging Reauth, cleaning things up")
|
||||
|
||||
local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser)
|
||||
AppState.store:dispatch(SetRobloxUser())
|
||||
|
||||
local SetXboxUser = require(ShellModules.Actions.SetXboxUser)
|
||||
AppState.store:dispatch(SetXboxUser())
|
||||
|
||||
-- unwind ScreenManager
|
||||
returnToEngagementScreen()
|
||||
|
||||
UserData:Reset()
|
||||
AppHub = nil
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail")
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre")
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges")
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen")
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen")
|
||||
EventHub:removeEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen")
|
||||
|
||||
Utility.DebugLog("Reauth complete. Return to engagement screen.")
|
||||
|
||||
-- show reason overlay
|
||||
local alert = Alerts.SignOut[reauthenticationReason] or Alerts.Default
|
||||
|
||||
if reauthenticationReason == 6 then
|
||||
alert = nil
|
||||
end
|
||||
|
||||
if alert then
|
||||
ScreenManager:OpenScreen(ErrorOverlay(alert), false)
|
||||
end
|
||||
end
|
||||
|
||||
local function onGameJoin(joinResult, placeId)
|
||||
-- 0 is success, anything else is an error
|
||||
local joinSuccess = joinResult == 0
|
||||
if not joinSuccess then
|
||||
local err = Errors.GameJoin[joinResult] or Errors.GameJoin.Default
|
||||
ScreenManager:OpenScreen(ErrorOverlay(err), false)
|
||||
end
|
||||
EventHub:dispatchEvent(EventHub.Notifications["GameJoin"], joinSuccess, placeId)
|
||||
end
|
||||
|
||||
if PlatformService then
|
||||
PlatformService.GameJoined:connect(onGameJoin)
|
||||
end
|
||||
|
||||
if ThirdPartyUserService then
|
||||
ThirdPartyUserService.ActiveUserSignedOut:connect(onReAuthentication)
|
||||
end
|
||||
|
||||
ControllerStateManager:Initialize()
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authUserSuccess", function(isNewLinkedAccount)
|
||||
Utility.DebugLog("User authenticated, initializing app shell")
|
||||
onAuthenticationSuccess(isNewLinkedAccount)
|
||||
end);
|
||||
ScreenManager:OpenScreen(EngagementScreen)
|
||||
|
||||
UserInputService.MouseIconEnabled = false
|
||||
|
||||
return {}
|
||||
@@ -0,0 +1,437 @@
|
||||
-- Written by Kip Turner, Copyright Roblox 2015
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local GuiService = game:GetService('GuiService')
|
||||
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local AppTabDockModule = require(ShellModules:FindFirstChild('TabDock'))
|
||||
local AppTabDockItemModule = require(ShellModules:FindFirstChild('TabDockItem'))
|
||||
local HomePaneModule = require(ShellModules:FindFirstChild('HomePane'))
|
||||
local GamesPaneModule = require(ShellModules:FindFirstChild('GamesPane'))
|
||||
local AvatarPaneModule = require(ShellModules:FindFirstChild('AvatarPane'))
|
||||
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local SocialPaneModule = require(ShellModules.Components.Social.SocialPane)
|
||||
local StorePaneModule = require(ShellModules:FindFirstChild('StorePane'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local SettingsScreen = require(ShellModules:FindFirstChild('SettingsScreen'))
|
||||
local AvatarEditorScreen = require(ShellModules:FindFirstChild('AvatarEditorScreen'))
|
||||
local GameSearchScreen = require(ShellModules:FindFirstChild('GameSearchScreen'))
|
||||
|
||||
local AchievementManager = require(ShellModules:FindFirstChild('AchievementManager'))
|
||||
local HintActionView = require(ShellModules:FindFirstChild('HintActionView'))
|
||||
|
||||
local function CreateAppHub()
|
||||
local this = {}
|
||||
|
||||
local AppTabDock = AppTabDockModule(
|
||||
UDim2.new(0,0,0.132,0),
|
||||
UDim2.new(0,0,0,0))
|
||||
local appHubCns = {}
|
||||
|
||||
local isShown = false
|
||||
|
||||
local lastSelectedContentPane = nil
|
||||
local lastParent = nil
|
||||
|
||||
local HubContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'HubContainer';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
local PaneContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'PaneContainer';
|
||||
Size = UDim2.new(1, 0, 0.786, 0);
|
||||
Position = UDim2.new(0,0,0.214,0);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = HubContainer;
|
||||
}
|
||||
|
||||
AppTabDock:SetParent(HubContainer)
|
||||
local HomeTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('HomeWord'),
|
||||
HomePaneModule(PaneContainer)))
|
||||
local AvatarTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('AvatarWord'),
|
||||
AvatarPaneModule(PaneContainer)))
|
||||
local GameTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('GamesWord'),
|
||||
GamesPaneModule(PaneContainer)))
|
||||
local SocialTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('FriendsWord'),
|
||||
SocialPaneModule(PaneContainer)))
|
||||
local StoreTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('CatalogWord'),
|
||||
StorePaneModule(PaneContainer)))
|
||||
|
||||
|
||||
Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'RobloxLogo';
|
||||
Size = UDim2.new(0, 232, 0, 56);
|
||||
Position = UDim2.new(0,0,0,0);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/ROBLOXLogoSmall@1080.png';
|
||||
Parent = HubContainer;
|
||||
}
|
||||
|
||||
local function SetSelectedTab(newTab)
|
||||
AppTabDock:SetSelectedTab(newTab)
|
||||
end
|
||||
|
||||
-- Hint Action View
|
||||
local hintActionViewX = HintActionView(HubContainer, "OpenHintAction")
|
||||
hintActionViewX:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png') -- always X button for tab views
|
||||
hintActionViewX:SetVisible(false)
|
||||
|
||||
local hintActionViewY = HintActionView(HubContainer, "OpenYHintAction", UDim2.new(0, 0, 1, -1))
|
||||
hintActionViewY:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/YButton.png')
|
||||
hintActionViewY:SetVisible(false)
|
||||
|
||||
-- Action Functions
|
||||
local seenXButtonPressed = false
|
||||
local seenYButtonPressed = false
|
||||
|
||||
local function onOpenSettings(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenXButtonPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
|
||||
local settingsScreen = SettingsScreen()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenSettingsScreen"], settingsScreen);
|
||||
end
|
||||
end
|
||||
|
||||
local function onOpenAvatarEditor(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenYButtonPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and seenYButtonPressed then
|
||||
local avatarEditorScreen = AvatarEditorScreen()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenAvatarEditorScreen"], avatarEditorScreen);
|
||||
seenYButtonPressed = false
|
||||
end
|
||||
end
|
||||
|
||||
local function onOpenPartyUI(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenXButtonPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
|
||||
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
|
||||
ScreenManager:OpenScreen(ErrorOverlay(Errors.Test.FeatureNotAvailableInStudio), false)
|
||||
else
|
||||
local success = pcall(function()
|
||||
-- PlatformService may not exist in studio
|
||||
return PlatformService:PopupPartyUI(inputObject.UserInputType)
|
||||
end)
|
||||
if not success then
|
||||
ScreenManager:OpenScreen(ErrorOverlay(Errors.PlatformError.PopupPartyUI), false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function onSearchGames(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenXButtonPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
|
||||
if PlatformService then
|
||||
PlatformService:ShowKeyboard(Strings:LocalizedString("SearchGamesPhrase"), "", "", Enum.XboxKeyBoardType.Default)
|
||||
end
|
||||
seenXButtonPressed = false
|
||||
end
|
||||
end
|
||||
|
||||
local function bindHintActionX(actionFunc, actionName)
|
||||
hintActionViewX:SetText(actionName)
|
||||
hintActionViewX:BindAction(actionFunc, Enum.KeyCode.ButtonX)
|
||||
hintActionViewX:SetVisible(true)
|
||||
end
|
||||
|
||||
local function setHintAction(selectedTab)
|
||||
hintActionViewX:UnbindAction()
|
||||
hintActionViewX:SetVisible(false)
|
||||
hintActionViewY:UnbindAction()
|
||||
hintActionViewY:SetVisible(false)
|
||||
|
||||
if selectedTab == HomeTab then
|
||||
bindHintActionX(onOpenSettings, Strings:LocalizedString("SettingsWord"))
|
||||
elseif selectedTab == GameTab then
|
||||
if AchievementManager:AllGamesUnlocked() then
|
||||
bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord"))
|
||||
end
|
||||
elseif selectedTab == SocialTab then
|
||||
bindHintActionX(onOpenPartyUI, Strings:LocalizedString("StartPartyPhrase"))
|
||||
elseif selectedTab == AvatarTab then
|
||||
hintActionViewY:SetText(Strings:LocalizedString("AvatarEditorWord"))
|
||||
hintActionViewY:BindAction(onOpenAvatarEditor, Enum.KeyCode.ButtonY)
|
||||
hintActionViewY:SetVisible(true)
|
||||
end
|
||||
|
||||
-- NOTE: Avatar Tab has its own HintActionView as it needs to change visibility based on whats selected
|
||||
end
|
||||
|
||||
function this:GetName()
|
||||
return lastSelectedContentPane and lastSelectedContentPane:GetName() or Strings:LocalizedString('HomeWord')
|
||||
end
|
||||
|
||||
--For analytics
|
||||
function this:GetAnalyticsInfo()
|
||||
local analyticsInfo = {}
|
||||
local WidgetId = Analytics.WidgetNames('WidgetId')
|
||||
local paneAnalyticsInfo = nil
|
||||
if lastSelectedContentPane and type(lastSelectedContentPane.GetAnalyticsInfo) == "function" then
|
||||
paneAnalyticsInfo = lastSelectedContentPane.GetAnalyticsInfo()
|
||||
end
|
||||
|
||||
--paneAnalyticsInfo should never be nil
|
||||
if type(paneAnalyticsInfo) == "table" and paneAnalyticsInfo[WidgetId] then
|
||||
analyticsInfo = paneAnalyticsInfo
|
||||
else
|
||||
analyticsInfo[WidgetId] = Analytics.WidgetNames('AppHubId')
|
||||
end
|
||||
return analyticsInfo
|
||||
end
|
||||
|
||||
function this:Show()
|
||||
isShown = true
|
||||
|
||||
HubContainer.Visible = true
|
||||
HubContainer.Parent = lastParent
|
||||
|
||||
EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch')
|
||||
EventHub:addEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch',
|
||||
function()
|
||||
if ScreenManager:ContainsScreen(this) then
|
||||
while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
if ScreenManager:GetTopScreen() == this then
|
||||
if AppTabDock:GetSelectedTab() ~= StoreTab then
|
||||
SetSelectedTab(StoreTab)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local openEquippedDebounce = false
|
||||
EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch')
|
||||
EventHub:addEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch',
|
||||
function()
|
||||
if openEquippedDebounce then return end
|
||||
openEquippedDebounce = true
|
||||
if ScreenManager:ContainsScreen(this) then
|
||||
while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
if ScreenManager:GetTopScreen() == this then
|
||||
if AppTabDock:GetSelectedTab() ~= AvatarTab then
|
||||
SetSelectedTab(AvatarTab)
|
||||
end
|
||||
end
|
||||
end
|
||||
openEquippedDebounce = false
|
||||
end)
|
||||
|
||||
if not AchievementManager:AllGamesUnlocked() then
|
||||
EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC')
|
||||
EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "AppHubUnlockedUGC", function()
|
||||
if isShown then
|
||||
local selectedTab = AppTabDock:GetSelectedTab()
|
||||
if selectedTab and selectedTab == GameTab then
|
||||
hintActionViewX:UnbindAction()
|
||||
hintActionViewX:SetVisible(false)
|
||||
bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord"))
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local currentlySelectedTab = AppTabDock:GetSelectedTab()
|
||||
AppTabDock:SetSelectedTab(currentlySelectedTab)
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:Show()
|
||||
end
|
||||
end
|
||||
|
||||
function this:Hide()
|
||||
isShown = false
|
||||
|
||||
if not ScreenManager:ContainsScreen(self) then
|
||||
EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch')
|
||||
EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch')
|
||||
end
|
||||
EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC')
|
||||
|
||||
HubContainer.Visible = false
|
||||
HubContainer.Parent = nil
|
||||
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
AppTabDock:ConnectEvents()
|
||||
|
||||
local function initTabDock()
|
||||
ContextActionService:BindCoreAction("CycleTabDock",
|
||||
function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
if not AppTabDock:IsFocused() then
|
||||
lastSelectedContentPane:RemoveFocus(true)
|
||||
AppTabDock:Focus()
|
||||
else
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then
|
||||
local prevTab = AppTabDock:GetPreviousTab()
|
||||
if prevTab then
|
||||
AppTabDock:SetSelectedTab(prevTab)
|
||||
end
|
||||
elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then
|
||||
local nextTab = AppTabDock:GetNextTab()
|
||||
if nextTab then
|
||||
AppTabDock:SetSelectedTab(nextTab)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
false,
|
||||
Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1)
|
||||
|
||||
local seenBButtonBegin = false
|
||||
ContextActionService:BindCoreAction("CloseAppHub",
|
||||
function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenBButtonBegin = true
|
||||
elseif inputState == Enum.UserInputState.End then
|
||||
if seenBButtonBegin then
|
||||
if not AppTabDock:IsFocused() then
|
||||
lastSelectedContentPane:RemoveFocus(true)
|
||||
AppTabDock:Focus()
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
false,
|
||||
Enum.KeyCode.ButtonB)
|
||||
|
||||
local function focusTab(tab)
|
||||
if tab then
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:Hide(true)
|
||||
lastSelectedContentPane:RemoveFocus(true)
|
||||
end
|
||||
local selectedContentPane = tab:GetContentItem()
|
||||
if selectedContentPane then
|
||||
selectedContentPane:Show(true)
|
||||
if not AppTabDock:IsFocused() then
|
||||
AppTabDock:Focus()
|
||||
end
|
||||
end
|
||||
lastSelectedContentPane = selectedContentPane
|
||||
|
||||
-- set X actionf
|
||||
setHintAction(tab)
|
||||
end
|
||||
end
|
||||
|
||||
local function onSelectedTabChanged(selectedTab)
|
||||
focusTab(selectedTab)
|
||||
end
|
||||
table.insert(appHubCns, AppTabDock.SelectedTabChanged:connect(onSelectedTabChanged))
|
||||
end
|
||||
|
||||
initTabDock()
|
||||
|
||||
local function onSelectedTabClicked(selectedTab)
|
||||
local selectedContentPane = selectedTab and selectedTab:GetContentItem()
|
||||
if selectedContentPane then
|
||||
selectedContentPane:Focus(AppTabDock)
|
||||
end
|
||||
end
|
||||
table.insert(appHubCns, AppTabDock.SelectedTabClicked:connect(onSelectedTabClicked))
|
||||
|
||||
local function onSelectionChanged(prop)
|
||||
if prop == "SelectedCoreObject" then
|
||||
if AppTabDock:IsFocused() then
|
||||
AppTabDock:Show()
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:RemoveFocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if prop == "SelectedObject" then
|
||||
local currentSelection = GuiService.SelectedCoreObject
|
||||
if currentSelection and lastSelectedContentPane then
|
||||
-- first condition checks if function exist
|
||||
if lastSelectedContentPane.IsFocused and not lastSelectedContentPane:IsFocused() and
|
||||
lastSelectedContentPane.IsAncestorOf then
|
||||
if lastSelectedContentPane:IsAncestorOf(currentSelection) then
|
||||
lastSelectedContentPane:Focus(AppTabDock)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(appHubCns, GuiService.Changed:connect(onSelectionChanged))
|
||||
|
||||
local function onKeyboardClosed(searchWord)
|
||||
searchWord = Utility.SpaceNormalizeString(searchWord)
|
||||
if #searchWord > 0 then
|
||||
local searchScreen = GameSearchScreen(searchWord)
|
||||
searchScreen:SetParent(HubContainer.Parent)
|
||||
ScreenManager:OpenScreen(searchScreen)
|
||||
end
|
||||
end
|
||||
if PlatformService then
|
||||
table.insert(appHubCns, PlatformService.KeyboardClosed:connect(onKeyboardClosed))
|
||||
end
|
||||
|
||||
if AppTabDock:GetSelectedTab() == nil then
|
||||
AppTabDock:SetSelectedTab(HomeTab)
|
||||
end
|
||||
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:Focus(AppTabDock)
|
||||
end
|
||||
|
||||
setHintAction(AppTabDock:GetSelectedTab())
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
AppTabDock:DisconnectEvents()
|
||||
|
||||
ContextActionService:UnbindCoreAction("CycleTabDock")
|
||||
ContextActionService:UnbindCoreAction("CloseAppHub")
|
||||
|
||||
if lastSelectedContentPane then
|
||||
lastSelectedContentPane:RemoveFocus()
|
||||
end
|
||||
|
||||
for k,v in pairs(appHubCns) do
|
||||
v:disconnect()
|
||||
appHubCns[k] = nil
|
||||
end
|
||||
|
||||
ContextActionService:UnbindCoreAction("OpenHintAction")
|
||||
ContextActionService:UnbindCoreAction("OpenYHintAction")
|
||||
end
|
||||
|
||||
function this:SetParent(newParent)
|
||||
lastParent = newParent
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return CreateAppHub
|
||||
@@ -0,0 +1,23 @@
|
||||
-- for now AppState will be singleton so we can more easily migrate to Rodux
|
||||
-- but if we also migrate to Roact, this will need to change
|
||||
|
||||
local ShellModules = script.Parent
|
||||
local Modules = ShellModules.Parent
|
||||
local Common = Modules.Common
|
||||
|
||||
local AppShellReducer = require(ShellModules.Reducers.AppShellReducer)
|
||||
local Store = require(Common.Rodux).Store
|
||||
|
||||
local AppState = {}
|
||||
|
||||
function AppState:Init()
|
||||
self.store = Store.new(AppShellReducer)
|
||||
end
|
||||
|
||||
function AppState:Destruct()
|
||||
self.store:destruct()
|
||||
end
|
||||
|
||||
AppState:Init()
|
||||
|
||||
return AppState
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Written by Kip Turner, Copyright Roblox 2015
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local RobloxGui = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = RobloxGui:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local AssetManager = {}
|
||||
|
||||
function AssetManager.CreateShadow(zIndex)
|
||||
return Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'Shadow';
|
||||
Image = 'rbxasset://textures/ui/Shell/Buttons/Generic9ScaleShadow.png';
|
||||
Size = UDim2.new(1,3,1,3);
|
||||
Position = UDim2.new(0,0,0,0);
|
||||
ScaleType = Enum.ScaleType.Slice;
|
||||
SliceCenter = Rect.new(10,10,28,28);
|
||||
BackgroundTransparency = 1;
|
||||
ZIndex = zIndex or 1;
|
||||
}
|
||||
end
|
||||
|
||||
return AssetManager
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
local CoreGui = game:GetService('CoreGui')
|
||||
local GuiRoot = CoreGui:FindFirstChild('RobloxGui')
|
||||
local Modules = GuiRoot:FindFirstChild('Modules')
|
||||
local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState)
|
||||
local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities)
|
||||
local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController)
|
||||
|
||||
local ShellModules = Modules:FindFirstChild('Shell')
|
||||
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
local CameraManager = require(ShellModules:FindFirstChild('CameraManager'))
|
||||
local CreateAvatarEditorView = require(ShellModules:FindFirstChild('AvatarEditorView'))
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local AppContainer = require(ShellModules.AppContainer)
|
||||
|
||||
local TitleSafeContainer = AppContainer.TitleSafeContainer
|
||||
|
||||
local ThirdPartyUserService = nil
|
||||
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
|
||||
|
||||
local AvatarEditorView = nil
|
||||
local AvatarEditorReconstruct = false
|
||||
|
||||
local function createAvatarEditorScreen()
|
||||
local this = BaseScreen()
|
||||
this.fixPosition = true
|
||||
local view = this.GetView()
|
||||
view.BackImage.ZIndex = 2
|
||||
view.BackText.ZIndex = 2
|
||||
view.TitleText.ZIndex = 2
|
||||
|
||||
local adjustPos = UDim2.new(0, TitleSafeContainer.AbsolutePosition.X, 0, TitleSafeContainer.AbsolutePosition.Y)
|
||||
local backImagePosition = view.BackImage.Position + adjustPos
|
||||
local backTextPosition = view.BackText.Position + adjustPos
|
||||
local titleTextPosition = view.TitleText.Position + adjustPos
|
||||
|
||||
view.BackImage.Position = backImagePosition
|
||||
view.BackText.Position = backTextPosition
|
||||
view.TitleText.Position = titleTextPosition
|
||||
|
||||
--Reconstruct the AvatarEditorView if user switched/assets purchased
|
||||
if AvatarEditorReconstruct and AvatarEditorView then
|
||||
AvatarEditorView:Destruct()
|
||||
AvatarEditorView = nil
|
||||
AvatarEditorReconstruct = false
|
||||
end
|
||||
AvatarEditorView = AvatarEditorView or CreateAvatarEditorView()
|
||||
|
||||
local storeChangedCn = nil
|
||||
local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
|
||||
local tweenPositionOffset = UDim2.new(0, -400, 0, 0)
|
||||
|
||||
local parentShow = this.Show
|
||||
function this:Show()
|
||||
parentShow(self)
|
||||
CameraManager:SwitchToAvatarEditor()
|
||||
AvatarEditorView:Show(this.Container)
|
||||
end
|
||||
|
||||
local parentHide = this.Hide
|
||||
function this:Hide()
|
||||
parentHide(self)
|
||||
AvatarEditorView:Hide()
|
||||
CameraManager:SwitchToFlyThrough()
|
||||
end
|
||||
|
||||
local parentFocus = this.Focus
|
||||
function this:Focus()
|
||||
parentFocus(self)
|
||||
storeChangedCn = AppState.Store.changed:connect(function(newState, oldState)
|
||||
if newState.FullView ~= oldState.FullView then
|
||||
if newState.FullView then
|
||||
TweenController(view.BackImage, tweenInfo, { Position = backImagePosition + tweenPositionOffset })
|
||||
TweenController(view.BackText, tweenInfo, { Position = backTextPosition + tweenPositionOffset })
|
||||
TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition + tweenPositionOffset })
|
||||
else
|
||||
TweenController(view.BackImage, tweenInfo, { Position = backImagePosition })
|
||||
TweenController(view.BackText, tweenInfo, { Position = backTextPosition })
|
||||
TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition })
|
||||
end
|
||||
end
|
||||
end)
|
||||
AvatarEditorView:Focus()
|
||||
end
|
||||
|
||||
local parentRemoveFocus = this.RemoveFocus
|
||||
function this:RemoveFocus()
|
||||
parentRemoveFocus(self)
|
||||
AvatarEditorView:RemoveFocus()
|
||||
Utilities.disconnectEvent(storeChangedCn)
|
||||
|
||||
--Don't save selectedObject, as we already saved it in AvatarEditorView
|
||||
self.SavedSelectedObject = nil
|
||||
end
|
||||
|
||||
local parentSetParent = this.SetParent
|
||||
function this:SetParent(newParent)
|
||||
parentSetParent(self, newParent)
|
||||
end
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarEditorScreenId')}
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
local function OnUserAccountChanged()
|
||||
AvatarEditorReconstruct = true
|
||||
EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen")
|
||||
EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen",
|
||||
function()
|
||||
AvatarEditorReconstruct = true
|
||||
end)
|
||||
end
|
||||
|
||||
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AvatarEditorScreen", OnUserAccountChanged)
|
||||
|
||||
local function OnUserSignOut()
|
||||
AvatarEditorReconstruct = true
|
||||
EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen")
|
||||
end
|
||||
|
||||
if ThirdPartyUserService then
|
||||
ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut)
|
||||
end
|
||||
|
||||
return createAvatarEditorScreen
|
||||
@@ -0,0 +1,516 @@
|
||||
-------------- CONSTANTS --------------
|
||||
local STICK_ROTATION_MULTIPLIER = 3
|
||||
local THUMBSTICK_DEADZONE = 0.2
|
||||
|
||||
-------------- SERVICES --------------
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local ReplicatedStorage = game:GetService('ReplicatedStorage')
|
||||
local CoreGui = game:GetService('CoreGui')
|
||||
|
||||
------------ MODULES -------------------
|
||||
local GuiRoot = CoreGui:FindFirstChild('RobloxGui')
|
||||
local Modules = GuiRoot:FindFirstChild('Modules')
|
||||
|
||||
local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState)
|
||||
local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel)
|
||||
local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory)
|
||||
local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType)
|
||||
local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView)
|
||||
|
||||
local CreateCharacterManager = require(Modules.LuaApp.Legacy.AvatarEditor.CharacterManager)
|
||||
local CreatePageManager = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerConsole)
|
||||
local CreateCategoryMenu = require(Modules.LuaApp.Legacy.AvatarEditor.CategoryMenuConsole)
|
||||
local CreateTabList = require(Modules.LuaApp.Legacy.AvatarEditor.TabListConsole)
|
||||
local ConsoleButtonIndicators = require(Modules.LuaApp.Legacy.AvatarEditor.ConsoleButtonIndicators)
|
||||
local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole)
|
||||
local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags)
|
||||
local XboxAppState = require(Modules.Shell.AppState)
|
||||
|
||||
----------- UTILITIES --------------
|
||||
local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities)
|
||||
local TableUtilities = require(Modules.LuaApp.TableUtilities)
|
||||
|
||||
------------ SHELL MODULES -------------------
|
||||
local ShellModules = script.Parent
|
||||
local CameraManager = require(ShellModules:FindFirstChild('CameraManager'))
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
|
||||
|
||||
-------------- FFLAGS --------------
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
|
||||
------------ VARIABLES -------------------
|
||||
local characterTemplates = {
|
||||
CharacterR6 = ReplicatedStorage:WaitForChild('CharacterR6');
|
||||
CharacterR15 = ReplicatedStorage:WaitForChild('CharacterR15');
|
||||
CharacterR15New = ReplicatedStorage:WaitForChild('CharacterR15New');
|
||||
}
|
||||
|
||||
|
||||
local cameraTweenerObject = {
|
||||
tweenCamera = function(newCFrame, newFOV)
|
||||
CameraManager:UpdateAvatarEditorCamera(newCFrame, newFOV)
|
||||
end
|
||||
}
|
||||
|
||||
----------- CLASS DECLARATION --------------
|
||||
|
||||
local function createAvatarEditorView()
|
||||
AppState:Init()
|
||||
local this = {}
|
||||
local storeChangedCn = nil
|
||||
local scrollingFrameCn = nil
|
||||
local avatarEditorLoader = nil
|
||||
local savedSelectedGuiObject = nil
|
||||
local selectedGuiObjectEnterFullView = nil
|
||||
local toggleButtonDebounce = true
|
||||
local toggleViewDebounce = true
|
||||
local showCount = 0
|
||||
local inShow = false
|
||||
local inFocus = false
|
||||
local lastCharacterSaveTime = nil
|
||||
|
||||
local characterUpdated = false
|
||||
local characterEquipped = false
|
||||
local savingAvatarMutex = false
|
||||
local rotationInfo = {
|
||||
rotation = 0;
|
||||
lastRotation = 0;
|
||||
delta = 0;
|
||||
}
|
||||
|
||||
local Container = Utilities.create'ScrollingFrame'
|
||||
{
|
||||
Name = 'AvatarEditorContainer';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
CanvasSize = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
ScrollingEnabled = false;
|
||||
Selectable = false;
|
||||
Visible = true;
|
||||
BorderSizePixel = 0;
|
||||
ScrollBarThickness = 0;
|
||||
}
|
||||
|
||||
local BackgroundOverlay = Utilities.create'ImageLabel'
|
||||
{
|
||||
Name = 'BackgroundOverlay';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Visible = true;
|
||||
Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-background overlay merge.png';
|
||||
ZIndex = LayoutInfo.BackgroundLayer;
|
||||
Parent = Container;
|
||||
}
|
||||
|
||||
local frame = Utilities.create'Frame'
|
||||
{
|
||||
Position = UDim2.new(0, 0, 0, 270);
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
Name = "PageFrame";
|
||||
BackgroundTransparency = 1;
|
||||
ClipsDescendants = true;
|
||||
Visible = true;
|
||||
Selectable = false;
|
||||
ZIndex = LayoutInfo.BasicLayer;
|
||||
Parent = Container;
|
||||
}
|
||||
local scrollingFrame = Utilities.create'ScrollingFrame'
|
||||
{
|
||||
AnchorPoint = Vector2.new(1, 0);
|
||||
Position = UDim2.new(1, -99, 0, 0);
|
||||
Size = UDim2.new(0, 491, 1, -270);
|
||||
Name = "ScrollingFrame";
|
||||
BackgroundTransparency = 1;
|
||||
ClipsDescendants = true;
|
||||
Visible = true;
|
||||
ScrollingEnabled = false;
|
||||
ScrollBarThickness = 0;
|
||||
Selectable = false;
|
||||
ZIndex = LayoutInfo.BasicLayer;
|
||||
Parent = frame;
|
||||
}
|
||||
|
||||
ConsoleButtonIndicators.init(Container)
|
||||
local warningFrame = Utilities.create'Frame'
|
||||
{
|
||||
AnchorPoint = Vector2.new(0.5, 0);
|
||||
Position = UDim2.new(0.5, 0, 0, 0);
|
||||
Size = UDim2.new(0, LayoutInfo.WarningMaxLength, 1, 0);
|
||||
Name = "WarningWidget";
|
||||
BackgroundTransparency = 1;
|
||||
Visible = true;
|
||||
Selectable = false;
|
||||
ZIndex = LayoutInfo.IndicatorLayer;
|
||||
Parent = Container;
|
||||
}
|
||||
|
||||
local CameraController = require(Modules.LuaApp.Legacy.AvatarEditor.CameraController)(
|
||||
cameraTweenerObject, LayoutInfo.CameraCenterScreenPosition)
|
||||
|
||||
|
||||
local characterManager = CreateCharacterManager(
|
||||
{
|
||||
get = Utilities.httpGet;
|
||||
post = Utilities.httpPost;
|
||||
},
|
||||
characterTemplates,
|
||||
1
|
||||
)
|
||||
|
||||
local WarningWidget = require(Modules.LuaApp.Legacy.AvatarEditor.WarningWidget)(warningFrame, characterManager, true)
|
||||
|
||||
local PageManager = CreatePageManager(
|
||||
XboxAppState.store:getState().RobloxUser.rbxuid,
|
||||
scrollingFrame,
|
||||
characterManager
|
||||
)
|
||||
characterManager:setUpdateCameraCallback(CameraController.updateCamera)
|
||||
|
||||
local CategoryMenu = CreateCategoryMenu(Container)
|
||||
local TabList = CreateTabList(Container, PageManager)
|
||||
|
||||
local function initStickRotation()
|
||||
local gamepadInput = Vector2.new(0, 0)
|
||||
ContextActionService:UnbindCoreAction("StickRotation")
|
||||
ContextActionService:BindCoreAction("StickRotation", function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Change then
|
||||
gamepadInput = inputObject.Position or gamepadInput
|
||||
gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y)
|
||||
if math.abs(gamepadInput.X) > THUMBSTICK_DEADZONE then
|
||||
rotationInfo.delta = STICK_ROTATION_MULTIPLIER * gamepadInput.X
|
||||
else
|
||||
rotationInfo.delta = 0
|
||||
end
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.Thumbstick2)
|
||||
end
|
||||
|
||||
local function initKeyControls()
|
||||
toggleButtonDebounce = true
|
||||
toggleViewDebounce = true
|
||||
ContextActionService:UnbindCoreAction("KeyControls")
|
||||
ContextActionService:BindCoreAction("KeyControls", function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonSelect and not AppState.Store:getState().FullView then
|
||||
if toggleButtonDebounce then
|
||||
toggleButtonDebounce = false
|
||||
SoundManager:Play('ButtonPress')
|
||||
AppState.Store:dispatch(ToggleAvatarType())
|
||||
toggleButtonDebounce = true
|
||||
end
|
||||
end
|
||||
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonR3 then
|
||||
if toggleViewDebounce then
|
||||
toggleViewDebounce = false
|
||||
SoundManager:Play('ScreenChange')
|
||||
AppState.Store:dispatch(ToggleAvatarEditorFullView())
|
||||
toggleViewDebounce = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonSelect)
|
||||
end
|
||||
|
||||
local function initMenuActions()
|
||||
ContextActionService:UnbindCoreAction("AvatarEditorMenu")
|
||||
|
||||
ContextActionService:BindCoreAction("AvatarEditorMenu", function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
|
||||
if AppState.Store:getState().ConsoleMenuLevel > LayoutInfo.ConsoleMenuLevel.CategoryMenu then
|
||||
local currentMenuLevel = AppState.Store:getState().ConsoleMenuLevel
|
||||
SoundManager:Play('PopUp')
|
||||
AppState.Store:dispatch(SetConsoleMenuLevel(currentMenuLevel - 1))
|
||||
else
|
||||
-- Back to avatar page in AppShell
|
||||
AppState.Store:dispatch(ResetCategory())
|
||||
AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.None))
|
||||
return Enum.ContextActionResult.Pass
|
||||
end
|
||||
end
|
||||
elseif inputState == Enum.UserInputState.Begin then
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonL2 then
|
||||
if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then
|
||||
CategoryMenu:SelectPreviousPage()
|
||||
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then
|
||||
TabList:SelectPreviousPage()
|
||||
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then
|
||||
PageManager:SelectPreviousPage()
|
||||
end
|
||||
elseif inputObject.KeyCode == Enum.KeyCode.ButtonR2 then
|
||||
if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then
|
||||
CategoryMenu:SelectNextPage()
|
||||
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then
|
||||
TabList:SelectNextPage()
|
||||
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then
|
||||
PageManager:SelectNextPage()
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2)
|
||||
end
|
||||
|
||||
local function updateFullViewCoreAction(fullView)
|
||||
ContextActionService:UnbindCoreAction("FullView")
|
||||
if fullView == true then
|
||||
ContextActionService:BindCoreAction("FullView",
|
||||
function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
|
||||
SoundManager:Play('ScreenChange')
|
||||
AppState.Store:dispatch(ToggleAvatarEditorFullView())
|
||||
end
|
||||
return Enum.ContextActionResult.Sink
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2)
|
||||
|
||||
BackgroundOverlay.Visible = false
|
||||
elseif fullView == false then
|
||||
BackgroundOverlay.Visible = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function FocusAll()
|
||||
initStickRotation()
|
||||
initKeyControls()
|
||||
initMenuActions()
|
||||
CameraController:Focus()
|
||||
|
||||
CategoryMenu:Focus()
|
||||
TabList:Focus()
|
||||
PageManager:Focus()
|
||||
WarningWidget:Focus()
|
||||
|
||||
if not savedSelectedGuiObject and not AppState.Store:getState().FullView then
|
||||
AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.CategoryMenu))
|
||||
elseif savedSelectedGuiObject then
|
||||
Utilities.setSelectedCoreObject(savedSelectedGuiObject)
|
||||
end
|
||||
updateFullViewCoreAction(AppState.Store:getState().FullView)
|
||||
|
||||
storeChangedCn = AppState.Store.changed:connect(function(newState, oldState)
|
||||
if newState.FullView ~= oldState.FullView then
|
||||
updateFullViewCoreAction(newState.FullView)
|
||||
if newState.FullView == true then
|
||||
local selectedCoreObject = Utilities.getSelectedCoreObject()
|
||||
if selectedCoreObject then
|
||||
selectedGuiObjectEnterFullView = selectedCoreObject
|
||||
end
|
||||
Utilities.setSelectedCoreObject(nil)
|
||||
else
|
||||
if selectedGuiObjectEnterFullView then
|
||||
Utilities.setSelectedCoreObject(selectedGuiObjectEnterFullView)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not characterEquipped or not characterUpdated then
|
||||
local didUpdate = false
|
||||
local didEquip = false
|
||||
|
||||
if newState.Character.AvatarType ~= oldState.Character.AvatarType then
|
||||
didUpdate = true
|
||||
end
|
||||
|
||||
if newState.Character.Assets ~= oldState.Character.Assets then
|
||||
for assetType, assetList in pairs(oldState.Character.Assets) do
|
||||
if not newState.Character.Assets[assetType] and assetList then
|
||||
if next(assetList) ~= nil then
|
||||
didUpdate = true
|
||||
didEquip = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not didUpdate or not didEquip then
|
||||
for assetType, _ in pairs(newState.Character.Assets) do
|
||||
if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then
|
||||
local addTheseAssets = TableUtilities.ListDifference(
|
||||
newState.Character.Assets[assetType] or {},
|
||||
oldState.Character.Assets[assetType] or {})
|
||||
local removeTheseAssets =
|
||||
TableUtilities.ListDifference(oldState.Character.Assets[assetType] or {},
|
||||
newState.Character.Assets[assetType] or {})
|
||||
|
||||
if next(addTheseAssets) ~= nil or next(removeTheseAssets) ~= nil then
|
||||
didUpdate = true
|
||||
didEquip = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if newState.Character.BodyColors ~= oldState.Character.BodyColors then
|
||||
local differentBodyColors =
|
||||
TableUtilities.TableDifference(newState.Character.BodyColors, oldState.Character.BodyColors)
|
||||
if next(differentBodyColors) ~= nil then
|
||||
didUpdate = true
|
||||
end
|
||||
end
|
||||
|
||||
if newState.Character.Scales ~= oldState.Character.Scales then
|
||||
local differentScales =
|
||||
TableUtilities.TableDifference(newState.Character.Scales, oldState.Character.Scales)
|
||||
if next(differentScales) ~= nil then
|
||||
didUpdate = true
|
||||
end
|
||||
end
|
||||
|
||||
if not characterEquipped then
|
||||
characterEquipped = didEquip
|
||||
end
|
||||
|
||||
if not characterUpdated then
|
||||
characterUpdated = didUpdate
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
scrollingFrameCn = scrollingFrame.Changed:connect(
|
||||
function(prop)
|
||||
if prop == 'CanvasPosition' then
|
||||
PageManager:updateListContent(scrollingFrame.CanvasPosition.Y)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local function RemoveFocusAll()
|
||||
CameraController:RemoveFocus()
|
||||
PageManager:RemoveFocus()
|
||||
CategoryMenu:RemoveFocus()
|
||||
TabList:RemoveFocus()
|
||||
WarningWidget:RemoveFocus()
|
||||
Utilities.disconnectEvent(storeChangedCn)
|
||||
Utilities.disconnectEvent(scrollingFrameCn)
|
||||
ContextActionService:UnbindCoreAction("FullView")
|
||||
ContextActionService:UnbindCoreAction("AvatarEditorMenu")
|
||||
ContextActionService:UnbindCoreAction("StickRotation")
|
||||
ContextActionService:UnbindCoreAction("KeyControls")
|
||||
end
|
||||
|
||||
function this:Show(parent)
|
||||
local startCount = showCount
|
||||
inShow = true
|
||||
characterUpdated = false
|
||||
characterEquipped = false
|
||||
local prevAvatarEditorLoader = avatarEditorLoader
|
||||
|
||||
Utilities.fastSpawn(function()
|
||||
if startCount == showCount then
|
||||
avatarEditorLoader = LoadingWidget(
|
||||
{Parent = parent},
|
||||
{function()
|
||||
if prevAvatarEditorLoader then
|
||||
prevAvatarEditorLoader:AwaitFinished()
|
||||
prevAvatarEditorLoader:Cleanup()
|
||||
end
|
||||
if startCount == showCount then
|
||||
characterManager.initFromServer()
|
||||
characterManager.show()
|
||||
end
|
||||
end}
|
||||
)
|
||||
avatarEditorLoader:AwaitFinished()
|
||||
if startCount == showCount then
|
||||
avatarEditorLoader:Cleanup()
|
||||
avatarEditorLoader = nil
|
||||
end
|
||||
end
|
||||
|
||||
if startCount == showCount then
|
||||
Container.Parent = parent
|
||||
if inFocus then
|
||||
FocusAll()
|
||||
end
|
||||
|
||||
while startCount == showCount do
|
||||
local deltaT = Utilities.renderWait()
|
||||
if startCount == showCount then
|
||||
rotationInfo.rotation = rotationInfo.rotation + deltaT * rotationInfo.delta
|
||||
characterManager.setRotation(rotationInfo.rotation)
|
||||
if not lastCharacterSaveTime or tick() - lastCharacterSaveTime > 5 then
|
||||
characterManager.saveToServer(false)
|
||||
lastCharacterSaveTime = tick()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function this:Hide()
|
||||
inShow = false
|
||||
showCount = showCount + 1
|
||||
characterManager.hide()
|
||||
savedSelectedGuiObject = nil
|
||||
selectedGuiObjectEnterFullView = nil
|
||||
Container.Parent = nil
|
||||
|
||||
if not avatarEditorLoader then
|
||||
RemoveFocusAll()
|
||||
CategoryMenu:Hide()
|
||||
WarningWidget:Hide()
|
||||
local savedCharacterUpdated = characterUpdated
|
||||
local savedCharacterEquipped = characterEquipped
|
||||
spawn(function()
|
||||
while savingAvatarMutex do
|
||||
wait()
|
||||
end
|
||||
savingAvatarMutex = true
|
||||
characterManager.saveToServer(true)
|
||||
savingAvatarMutex = false
|
||||
|
||||
if savedCharacterUpdated then
|
||||
Utilities.fastSpawn(function()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["CharacterUpdated"])
|
||||
end)
|
||||
end
|
||||
|
||||
if savedCharacterEquipped then
|
||||
Utilities.fastSpawn(function()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["CharacterEquipped"], AppState.Store:getState().Character.Assets, savedCharacterUpdated)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--Rebind actions, make them in the right order
|
||||
function this:Focus()
|
||||
inFocus = true
|
||||
if inShow and not avatarEditorLoader then
|
||||
FocusAll()
|
||||
end
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
inFocus = false
|
||||
if inShow and not avatarEditorLoader then
|
||||
savedSelectedGuiObject = Utilities.getSelectedCoreObject()
|
||||
RemoveFocusAll()
|
||||
end
|
||||
end
|
||||
|
||||
function this:Destruct()
|
||||
this:RemoveFocus()
|
||||
this:Hide()
|
||||
Container:Destroy()
|
||||
characterManager.destroy()
|
||||
AppState:Destruct()
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createAvatarEditorView
|
||||
@@ -0,0 +1,706 @@
|
||||
-- Written by Kip Turner and Bo Zhang, Copyright Roblox 2017
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiService = game:GetService('GuiService')
|
||||
local RunService = game:GetService('RunService')
|
||||
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local PackageData = require(ShellModules:FindFirstChild('PackageData'))
|
||||
local ScrollingGridModule = require(ShellModules:FindFirstChild('ScrollingGrid'))
|
||||
local AvatarTile = require(ShellModules:FindFirstChild('AvatarTile'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
|
||||
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
|
||||
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
|
||||
local HintActionView = require(ShellModules:FindFirstChild('HintActionView'))
|
||||
local XboxAppState = require(ShellModules:FindFirstChild('AppState'))
|
||||
|
||||
local GLOW_BASE_RPM = 2
|
||||
local GLOW_TOP_RPM = -0.5
|
||||
local GLOW_TRANSPARENCY = 0.2
|
||||
local CATALOG_BELOW_POSITION = 320
|
||||
|
||||
|
||||
local function CreateAvatarPane(parent)
|
||||
local this = {}
|
||||
|
||||
local inFocus = false
|
||||
local isShown = false
|
||||
|
||||
local AvatarObjects = {}
|
||||
|
||||
local OnGuiServiceChangedConn = nil
|
||||
|
||||
local lastParent = parent
|
||||
|
||||
|
||||
local MainContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'AvatarPane';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
local MyAvatarContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'MyAvatarContainer';
|
||||
Size = UDim2.new(0.38,0,1,0);
|
||||
Position = UDim2.new(0,0,0,0);
|
||||
BackgroundTransparency = 1;
|
||||
ClipsDescendants = true;
|
||||
Parent = MainContainer;
|
||||
}
|
||||
local MyNameLabel = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'MyNameLabel';
|
||||
Text = '';
|
||||
Size = UDim2.new(1,0,0,25);
|
||||
Position = UDim2.new(0,12,0,0);
|
||||
TextXAlignment = 'Left';
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.SubHeaderSize;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = MyAvatarContainer;
|
||||
};
|
||||
local ProfileImageContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'ProfileImageContainer';
|
||||
Size = UDim2.new(0.68,0,0.9,-MyNameLabel.Size.Y.Offset);
|
||||
Position = UDim2.new(0.16, 0, 0.05, MyNameLabel.Size.Y.Offset);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = MyAvatarContainer;
|
||||
}
|
||||
local ProfileImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'ProfileImage';
|
||||
Size = UDim2.new(0,780,0,780);
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0);
|
||||
BackgroundTransparency = 1;
|
||||
ZIndex = 3;
|
||||
Parent = ProfileImageContainer;
|
||||
};
|
||||
local CrossfadeProfileImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'CrossfadeProfileImage';
|
||||
Size = UDim2.new(1,0,1,0);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
BackgroundTransparency = 1;
|
||||
ImageTransparency = 1;
|
||||
ZIndex = 3;
|
||||
Parent = ProfileImage;
|
||||
};
|
||||
local CharacterGlowBase = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'CharacterGlowBase';
|
||||
Size = UDim2.new(0,1015,0,1002);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowBase.png';
|
||||
ImageTransparency = GLOW_TRANSPARENCY;
|
||||
Parent = CrossfadeProfileImage;
|
||||
AnchorPoint = Vector2.new(0.5, 0.5);
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0);
|
||||
};
|
||||
local CharacterGlowTop = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'CharacterGlowTop';
|
||||
Size = UDim2.new(0,1026,0,1009);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowTop.png';
|
||||
ImageTransparency = GLOW_TRANSPARENCY;
|
||||
ZIndex = 2;
|
||||
Parent = CrossfadeProfileImage;
|
||||
AnchorPoint = Vector2.new(0.5, 0.5);
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0);
|
||||
};
|
||||
local function onSizeChanged()
|
||||
ProfileImage.Size = Utility.CalculateFill(ProfileImage, ThumbnailLoader.AvatarSizes.Size352x352)
|
||||
ProfileImage.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
ProfileImage.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
end
|
||||
|
||||
-- Hint Action View
|
||||
local hintActionView = HintActionView(nil, "AvatarPaneSelectAction")
|
||||
hintActionView:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png')
|
||||
hintActionView:SetText(Strings:LocalizedString('EquipWord'))
|
||||
hintActionView:SetTransparency(1)
|
||||
hintActionView:SetParent(lastParent.Parent) -- this assumes parent is HubContainer from AppHub.lua
|
||||
|
||||
local function UpdateEquipButton(IsOwned, IsWearing)
|
||||
hintActionView:SetVisibleWithTween( (IsOwned and not IsWearing) and 0 or 1)
|
||||
end
|
||||
|
||||
local SelectableAvatarsContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = 'SelectableAvatarsContainer';
|
||||
Size = UDim2.new(0.6,0,1,0);
|
||||
Position = UDim2.new(0.4,0,0,0);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = MainContainer;
|
||||
}
|
||||
|
||||
local NoCatalogStatusMessage = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'NoCatalogStatusMessage';
|
||||
Text = Strings:LocalizedString('DefaultErrorPhrase');
|
||||
Size = UDim2.new(0.9,0,1,-125);
|
||||
Position = UDim2.new(0.05, 0, 0, 0);
|
||||
TextColor3 = GlobalSettings.GreyTextColor;
|
||||
TextWrapped = true;
|
||||
TextTransparency = GlobalSettings.FriendStatusTextTransparency;
|
||||
Font = GlobalSettings.BoldFont;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
BackgroundTransparency = 1;
|
||||
Visible = false;
|
||||
Parent = SelectableAvatarsContainer;
|
||||
};
|
||||
|
||||
local MyCollectionTitle = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'MyCollectionTitle';
|
||||
Text = Strings:LocalizedString('AvatarOutfitsTitle');
|
||||
Size = UDim2.new(1,0,0,40);
|
||||
TextXAlignment = 'Left';
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.SubHeaderSize;
|
||||
BackgroundTransparency = 1;
|
||||
Visible = false;
|
||||
Parent = SelectableAvatarsContainer;
|
||||
};
|
||||
|
||||
local MyCollectionScroller = ScrollingGridModule()
|
||||
MyCollectionScroller:SetSize(UDim2.new(1,0,1,-MyCollectionTitle.Size.Y.Offset - 40))
|
||||
MyCollectionScroller:SetScrollDirection(MyCollectionScroller.Enum.ScrollDirection.Horizontal)
|
||||
MyCollectionScroller:SetCellSize(Vector2.new(220, 220))
|
||||
MyCollectionScroller:SetSpacing(Vector2.new(25,25))
|
||||
MyCollectionScroller:SetPosition(UDim2.new(0,0,0,MyCollectionTitle.Size.Y.Offset))
|
||||
MyCollectionScroller:SetRowColumnConstraint(1)
|
||||
-- MyCollectionScroller:SetParent(SelectableAvatarsContainer)
|
||||
|
||||
local CatalogTitle = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'CatalogTitle';
|
||||
Text = Strings:LocalizedString('AvatarCatalogTitle');
|
||||
Size = UDim2.new(1,0,0,40);
|
||||
Position = UDim2.new(0,0,0,0);
|
||||
TextXAlignment = 'Left';
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.SubHeaderSize;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = SelectableAvatarsContainer;
|
||||
};
|
||||
|
||||
local AvatarScroller = ScrollingGridModule()
|
||||
AvatarScroller:SetSize(UDim2.new(1,0,1,-CatalogTitle.Size.Y.Offset - 40))
|
||||
AvatarScroller:SetScrollDirection(AvatarScroller.Enum.ScrollDirection.Horizontal)
|
||||
AvatarScroller:SetCellSize(Vector2.new(220, 220))
|
||||
AvatarScroller:SetPosition(UDim2.new(0,0,0,40))
|
||||
AvatarScroller:SetSpacing(Vector2.new(25,25))
|
||||
AvatarScroller:SetRowColumnConstraint(2)
|
||||
AvatarScroller:SetParent(SelectableAvatarsContainer)
|
||||
|
||||
local function SortMyCollectionScroller()
|
||||
MyCollectionScroller:SortItems(
|
||||
function(a, b)
|
||||
local aObject = a and AvatarObjects[a] and AvatarObjects[a]:GetPackageInfo()
|
||||
local bObject = b and AvatarObjects[b] and AvatarObjects[b]:GetPackageInfo()
|
||||
local aIsEquipped = aObject and aObject:IsWearing()
|
||||
local bIsEquipped = bObject and bObject:IsWearing()
|
||||
local aName = aObject and aObject:GetName()
|
||||
local bName = bObject and bObject:GetName()
|
||||
|
||||
if aIsEquipped then return true elseif bIsEquipped then return false end
|
||||
if aName and bName then
|
||||
return aName < bName
|
||||
end
|
||||
return aObject ~= nil
|
||||
end)
|
||||
end
|
||||
|
||||
--if all avatars owned, MyCollection should be 2 rows and catalog should be hidden
|
||||
local function CheckAllAvatarsOwned()
|
||||
if #AvatarScroller.GridItems == 0 then
|
||||
MyCollectionScroller:SetRowColumnConstraint(2)
|
||||
CatalogTitle.Visible = false
|
||||
AvatarScroller.Visible = false
|
||||
AvatarScroller:SetParent(nil)
|
||||
else
|
||||
MyCollectionScroller:SetRowColumnConstraint(1)
|
||||
CatalogTitle.Visible = true
|
||||
AvatarScroller.Visible = true
|
||||
AvatarScroller:SetParent(SelectableAvatarsContainer)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnAddToMyCollectionScroller()
|
||||
CatalogTitle.Position = UDim2.new(0,0,0, CATALOG_BELOW_POSITION)
|
||||
AvatarScroller:SetPosition(UDim2.new(0,0,0,CATALOG_BELOW_POSITION + 40))
|
||||
AvatarScroller:SetRowColumnConstraint(1)
|
||||
MyCollectionTitle.Visible = true
|
||||
MyCollectionScroller:SetParent(SelectableAvatarsContainer)
|
||||
CheckAllAvatarsOwned()
|
||||
SortMyCollectionScroller()
|
||||
end
|
||||
|
||||
local LoaderSpinner = nil
|
||||
local GlobalFadeCount = 1
|
||||
local function CrossfadeAvatarImage(frontImage, fadeImage, imageSource, duration)
|
||||
if not imageSource then return end
|
||||
duration = duration or 0.75
|
||||
|
||||
GlobalFadeCount = GlobalFadeCount + 1
|
||||
local thisFadeCount = GlobalFadeCount
|
||||
local fadeoutDuration = duration
|
||||
|
||||
spawn(function()
|
||||
local dummyImage = nil
|
||||
if LoaderSpinner then
|
||||
LoaderSpinner:Cleanup()
|
||||
end
|
||||
|
||||
--If imageSource if a function, then it's maybe a async call to get new image and we show the loaderspinner.
|
||||
--Otherwise, if the imageSource is a valid new image, we just do the Crossfade (without the spinner)
|
||||
if type(imageSource) == 'function' then
|
||||
local newLoaderSpinner = LoadingWidget({Parent = frontImage, ZIndex = 3}, {
|
||||
function() dummyImage = imageSource() end
|
||||
})
|
||||
LoaderSpinner = newLoaderSpinner
|
||||
newLoaderSpinner:AwaitFinished()
|
||||
newLoaderSpinner:Cleanup()
|
||||
elseif type(imageSource) == 'table' and imageSource.Image then
|
||||
dummyImage = imageSource
|
||||
end
|
||||
|
||||
if thisFadeCount == GlobalFadeCount then
|
||||
--Avoid updating with same image
|
||||
if dummyImage and frontImage and dummyImage.Image ~= frontImage.Image then
|
||||
local noPreviousImage = (frontImage.Image == "" and fadeImage.Image == "")
|
||||
if noPreviousImage then
|
||||
fadeoutDuration = 0
|
||||
else
|
||||
fadeImage.Image = frontImage.Image
|
||||
end
|
||||
|
||||
|
||||
Utility.PropertyTweener(fadeImage, 'ImageTransparency', noPreviousImage and 0 or frontImage.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
|
||||
Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
|
||||
Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
|
||||
frontImage.ImageTransparency = 1
|
||||
|
||||
frontImage.Image = dummyImage.Image
|
||||
|
||||
Utility.PropertyTweener(frontImage, 'ImageTransparency', frontImage.ImageTransparency, 0, duration, Utility.EaseInOutQuad, true)
|
||||
Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true)
|
||||
Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function UpdateProfileImage(imageSource)
|
||||
MyNameLabel.Text = XboxAppState.store:getState().XboxUser.gamertag
|
||||
CrossfadeAvatarImage(ProfileImage, CrossfadeProfileImage, imageSource)
|
||||
end
|
||||
|
||||
local function onOwnershipChanged(tile, nowOwns)
|
||||
-- Move purchased packages into my MyCollection
|
||||
if nowOwns then
|
||||
local guiObject = tile and tile:GetGuiObject()
|
||||
if guiObject and AvatarScroller:ContainsItem(guiObject) then
|
||||
AvatarScroller:RemoveItem(guiObject)
|
||||
MyCollectionScroller:AddItem(guiObject)
|
||||
OnAddToMyCollectionScroller()
|
||||
if inFocus then
|
||||
GuiService.SelectedCoreObject = guiObject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ownershipChangedCns = {}
|
||||
|
||||
local function listenToOwnershipChanged(tile)
|
||||
local packageInfo = tile and tile:GetPackageInfo()
|
||||
if packageInfo and not packageInfo:IsOwned() and (packageInfo.OwnershipChanged ~= nil) then
|
||||
if not ownershipChangedCns[tile] then
|
||||
ownershipChangedCns[tile] = packageInfo.OwnershipChanged:connect(function(nowOwns)
|
||||
onOwnershipChanged(tile, nowOwns)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function removeListenToOwnershipChanged(tile)
|
||||
if ownershipChangedCns[tile] then
|
||||
ownershipChangedCns[tile]:disconnect()
|
||||
ownershipChangedCns[tile] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function onEquipChanged()
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
|
||||
if AvatarObjects[selectedObject] then
|
||||
local packageInfo = AvatarObjects[selectedObject]:GetPackageInfo()
|
||||
if packageInfo then
|
||||
UpdateEquipButton(packageInfo:IsOwned(), packageInfo:IsWearing())
|
||||
end
|
||||
else
|
||||
-- user can select avatar tab button
|
||||
UpdateEquipButton(false, false)
|
||||
end
|
||||
SortMyCollectionScroller()
|
||||
end
|
||||
|
||||
local lastSelectedObject = GuiService.SelectedCoreObject
|
||||
local function OnSelectedCoreObjectChanged()
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
|
||||
if AvatarObjects[lastSelectedObject] then
|
||||
AvatarObjects[lastSelectedObject]:RemoveFocus()
|
||||
end
|
||||
if AvatarObjects[selectedObject] then
|
||||
AvatarObjects[selectedObject]:Focus()
|
||||
lastSelectedObject = selectedObject
|
||||
end
|
||||
onEquipChanged()
|
||||
end
|
||||
|
||||
|
||||
function this:GetDefaultSelectableObject()
|
||||
if MyCollectionScroller:GetFirstVisibleItem() then
|
||||
return MyCollectionScroller:GetFirstVisibleItem()
|
||||
end
|
||||
if AvatarScroller:GetFirstVisibleItem() then
|
||||
return AvatarScroller:GetFirstVisibleItem()
|
||||
end
|
||||
|
||||
--In case GetFirstVisibleItem breaks
|
||||
if MyCollectionScroller.GridItems[1] then
|
||||
return MyCollectionScroller.GridItems[1]
|
||||
end
|
||||
if AvatarScroller.GridItems[1] then
|
||||
return AvatarScroller.GridItems[1]
|
||||
end
|
||||
end
|
||||
|
||||
local packagesData = nil
|
||||
local packagesDataConns = {}
|
||||
local AvatarWebDataInitializeLoader = nil
|
||||
local function removeListenToPackagesDataUpdate()
|
||||
for _, conn in pairs(packagesDataConns) do
|
||||
conn:disconnect()
|
||||
end
|
||||
packagesDataConns = {}
|
||||
end
|
||||
|
||||
local function listenToPackagesDataUpdate()
|
||||
if packagesData and packagesDataConns then
|
||||
table.insert(packagesDataConns, packagesData.OnDifferentWearing:connect(function(assetId) onEquipChanged() end))
|
||||
table.insert(packagesDataConns, packagesData.OnDifferentOwned:connect(function(assetId, owned) onEquipChanged() end))
|
||||
table.insert(packagesDataConns, packagesData.OnProfileImageUpdateEnd:connect(function(newProfileImage)
|
||||
UpdateProfileImage(newProfileImage)
|
||||
end))
|
||||
table.insert(packagesDataConns, packagesData.OnProfileImageUpdateBegin:connect(function()
|
||||
UpdateProfileImage(function()
|
||||
return packagesData.OnProfileImageUpdateEnd.wait()
|
||||
end)
|
||||
end))
|
||||
end
|
||||
end
|
||||
|
||||
local function LoadAvatarWebData()
|
||||
local function clearCatalogPackages()
|
||||
AvatarScroller:RemoveAllItems()
|
||||
MyCollectionScroller:RemoveAllItems()
|
||||
for k,_ in pairs(AvatarObjects) do
|
||||
AvatarObjects[k] = nil
|
||||
end
|
||||
AvatarObjects = {}
|
||||
end
|
||||
|
||||
local function loadCatalogPackages(packages)
|
||||
clearCatalogPackages()
|
||||
if packages then
|
||||
for _, packageInfo in pairs(packages) do
|
||||
local avatarItemContainer = AvatarTile(packageInfo)
|
||||
|
||||
AvatarObjects[avatarItemContainer:GetGuiObject()] = avatarItemContainer
|
||||
|
||||
if packageInfo:IsOwned() then
|
||||
MyCollectionScroller:AddItem(avatarItemContainer:GetGuiObject())
|
||||
OnAddToMyCollectionScroller()
|
||||
else
|
||||
AvatarScroller:AddItem(avatarItemContainer:GetGuiObject())
|
||||
listenToOwnershipChanged(avatarItemContainer)
|
||||
end
|
||||
|
||||
if isShown then
|
||||
avatarItemContainer:Show()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--Wait until initialize is done
|
||||
if AvatarWebDataInitializeLoader then return end
|
||||
|
||||
if PackageData:HasCachedData() then --Sync process to get and update packagesData, no spinner
|
||||
local prevVersion = packagesData and packagesData.Version or nil
|
||||
packagesData = PackageData:GetCachedData()
|
||||
--If init or packagesData got updated
|
||||
if not prevVersion or prevVersion ~= packagesData.Version then
|
||||
SelectableAvatarsContainer.Visible = false
|
||||
UpdateProfileImage(packagesData.ProfileImage.Data)
|
||||
loadCatalogPackages(packagesData.Packages.Data)
|
||||
SelectableAvatarsContainer.Visible = true
|
||||
removeListenToPackagesDataUpdate()
|
||||
listenToPackagesDataUpdate()
|
||||
end
|
||||
else --Async process to get and update packagesData, show spinner
|
||||
SelectableAvatarsContainer.Visible = false
|
||||
local containerSize = SelectableAvatarsContainer.Size
|
||||
AvatarWebDataInitializeLoader = LoadingWidget(
|
||||
{Parent = MainContainer, Position = SelectableAvatarsContainer.Position + UDim2.new(containerSize.X.Scale / 2, containerSize.X.Offset / 2, containerSize.Y.Scale / 2, containerSize.Y.Offset / 2)},
|
||||
{function()
|
||||
packagesData = PackageData:GetCachedData()
|
||||
if packagesData and packagesData.Packages then
|
||||
loadCatalogPackages(packagesData.Packages.Data)
|
||||
end
|
||||
end}
|
||||
)
|
||||
spawn(function()
|
||||
NoCatalogStatusMessage.Visible = false
|
||||
|
||||
UpdateProfileImage(function()
|
||||
if AvatarWebDataInitializeLoader then
|
||||
AvatarWebDataInitializeLoader:AwaitFinished()
|
||||
end
|
||||
if packagesData and packagesData.ProfileImage then
|
||||
return packagesData.ProfileImage.Data
|
||||
end
|
||||
end)
|
||||
|
||||
AvatarWebDataInitializeLoader:AwaitFinished()
|
||||
AvatarWebDataInitializeLoader:Cleanup()
|
||||
AvatarWebDataInitializeLoader = nil
|
||||
SelectableAvatarsContainer.Visible = true
|
||||
removeListenToPackagesDataUpdate()
|
||||
|
||||
if not packagesData then
|
||||
NoCatalogStatusMessage.Visible = true
|
||||
end
|
||||
|
||||
--Hook connections if the pane is still visible after loading
|
||||
if isShown then
|
||||
listenToPackagesDataUpdate()
|
||||
end
|
||||
|
||||
if inFocus and isShown and GuiService.SelectedCoreObject == nil then
|
||||
GuiService.SelectedCoreObject = this:GetDefaultSelectableObject()
|
||||
end
|
||||
|
||||
if this.TransitionTweens == nil or #this.TransitionTweens == 0 then
|
||||
this.TransitionTweens = ScreenManager:FadeInSitu(SelectableAvatarsContainer)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
LoadAvatarWebData()
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetName()
|
||||
return Strings:LocalizedString('AvatarWord')
|
||||
end
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarPaneId')}
|
||||
end
|
||||
|
||||
function this:IsFocused()
|
||||
return inFocus
|
||||
end
|
||||
|
||||
local debounceSelect = false
|
||||
function this:OnSelectAction()
|
||||
if debounceSelect then return end
|
||||
debounceSelect = true
|
||||
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
if selectedObject and AvatarObjects[selectedObject] then
|
||||
if AvatarObjects[selectedObject]:Select() then
|
||||
SoundManager:Play('ButtonPress')
|
||||
end
|
||||
end
|
||||
|
||||
debounceSelect = false
|
||||
end
|
||||
|
||||
function this:OpenEquippedPackage()
|
||||
if isShown and inFocus then
|
||||
for _, avatarItemContainer in pairs(AvatarObjects) do
|
||||
local packageInfo = avatarItemContainer:GetPackageInfo()
|
||||
if packageInfo then
|
||||
if packageInfo:IsWearing() then
|
||||
avatarItemContainer:OnClick()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
--[[ End Public API ]]--
|
||||
|
||||
local profileImageChangeCn = nil
|
||||
function this:Show()
|
||||
isShown = true
|
||||
|
||||
Utility.DisconnectEvent(profileImageChangeCn)
|
||||
profileImageChangeCn = ProfileImageContainer:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
|
||||
onSizeChanged()
|
||||
end)
|
||||
onSizeChanged()
|
||||
|
||||
local lastUpdate = tick()
|
||||
RunService:BindToRenderStep("UpdateAvatarGlow", Enum.RenderPriority.Camera.Value,
|
||||
function()
|
||||
local now = tick()
|
||||
local delta = now - lastUpdate
|
||||
|
||||
CharacterGlowBase.Rotation = CharacterGlowBase.Rotation + delta * GLOW_BASE_RPM * 6 -- 6 = 360 / 60
|
||||
CharacterGlowTop.Rotation = CharacterGlowTop.Rotation + delta * GLOW_TOP_RPM * 6
|
||||
|
||||
lastUpdate = now
|
||||
end)
|
||||
|
||||
if packagesData then
|
||||
listenToPackagesDataUpdate()
|
||||
UpdateProfileImage(packagesData.ProfileImage.Data)
|
||||
end
|
||||
LoadAvatarWebData()
|
||||
|
||||
for _, avatarItemContainer in pairs(AvatarObjects) do
|
||||
avatarItemContainer:Show()
|
||||
listenToOwnershipChanged(avatarItemContainer)
|
||||
local packageInfo = avatarItemContainer and avatarItemContainer:GetPackageInfo()
|
||||
if packageInfo then
|
||||
onOwnershipChanged(avatarItemContainer, packageInfo:IsOwned())
|
||||
end
|
||||
end
|
||||
|
||||
local seenXButtonPressed = false
|
||||
local seenXSelectedObject = nil
|
||||
local function onSelectAvatar(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenXButtonPressed = true
|
||||
seenXSelectedObject = GuiService.SelectedCoreObject
|
||||
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
|
||||
if seenXSelectedObject and seenXSelectedObject == GuiService.SelectedCoreObject then
|
||||
self:OnSelectAction()
|
||||
end
|
||||
seenXButtonPressed = false
|
||||
seenXSelectedObject = nil
|
||||
end
|
||||
end
|
||||
|
||||
hintActionView:BindAction(onSelectAvatar, Enum.KeyCode.ButtonX)
|
||||
|
||||
self.TransitionTweens = ScreenManager:DefaultFadeIn(MainContainer)
|
||||
|
||||
SortMyCollectionScroller()
|
||||
|
||||
MainContainer.Parent = lastParent
|
||||
MainContainer.Visible = true
|
||||
ScreenManager:PlayDefaultOpenSound()
|
||||
end
|
||||
|
||||
|
||||
function this:Hide()
|
||||
isShown = false
|
||||
MainContainer.Visible = false
|
||||
removeListenToPackagesDataUpdate()
|
||||
|
||||
profileImageChangeCn = Utility.DisconnectEvent(profileImageChangeCn)
|
||||
|
||||
RunService:UnbindFromRenderStep("UpdateAvatarGlow")
|
||||
hintActionView:UnbindAction()
|
||||
hintActionView:SetTransparency(1)
|
||||
|
||||
for _, avatarItemContainer in pairs(AvatarObjects) do
|
||||
avatarItemContainer:Hide()
|
||||
removeListenToOwnershipChanged(avatarItemContainer)
|
||||
end
|
||||
|
||||
ScreenManager:DefaultCancelFade(self.TransitionTweens)
|
||||
self.TransitionTweens = nil
|
||||
-- Clean out saved selected object so when we tab back
|
||||
-- we will start with the default selection
|
||||
lastSelectedObject = nil
|
||||
self.SavedSelectObject = nil
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
inFocus = true
|
||||
|
||||
Utility.DisconnectEvent(OnGuiServiceChangedConn)
|
||||
OnGuiServiceChangedConn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
|
||||
OnSelectedCoreObjectChanged()
|
||||
end)
|
||||
|
||||
if self.SavedSelectObject and self.SavedSelectObject:IsDescendantOf(MainContainer) then
|
||||
GuiService.SelectedCoreObject = self.SavedSelectObject
|
||||
else
|
||||
local defaultSelection = self:GetDefaultSelectableObject()
|
||||
if defaultSelection then
|
||||
GuiService.SelectedCoreObject = defaultSelection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
inFocus = false
|
||||
--Remove Focus on AvatarObjects
|
||||
if AvatarObjects and AvatarObjects[lastSelectedObject] then
|
||||
AvatarObjects[lastSelectedObject]:RemoveFocus()
|
||||
end
|
||||
OnGuiServiceChangedConn = Utility.DisconnectEvent(OnGuiServiceChangedConn)
|
||||
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
if isShown then
|
||||
if selectedObject and selectedObject:IsDescendantOf(MainContainer) then
|
||||
self.SavedSelectObject = GuiService.SelectedCoreObject
|
||||
GuiService.SelectedCoreObject = nil
|
||||
else
|
||||
self.SavedSelectObject = lastSelectedObject
|
||||
end
|
||||
end
|
||||
OnSelectedCoreObjectChanged()
|
||||
end
|
||||
|
||||
function this:SetPosition(newPosition)
|
||||
MainContainer.Position = newPosition
|
||||
end
|
||||
|
||||
function this:SetParent(newParent)
|
||||
lastParent = newParent
|
||||
MainContainer.Parent = newParent
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return CreateAvatarPane
|
||||
@@ -0,0 +1,254 @@
|
||||
--[[
|
||||
// AvatarTile.lua
|
||||
|
||||
// Created by Kip Turner
|
||||
// Copyright Roblox 2015
|
||||
]]
|
||||
|
||||
local TextService = game:GetService('TextService')
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local Http = require(ShellModules:FindFirstChild('Http'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay'))
|
||||
local PurchasePackagePrompt = require(ShellModules:FindFirstChild('PurchasePackagePrompt'))
|
||||
|
||||
local BaseTile = require(ShellModules:FindFirstChild('BaseTile'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
|
||||
local function createAvatarInfoContainer(packageInfo)
|
||||
local this = BaseTile()
|
||||
local focused = false
|
||||
|
||||
local packageName = packageInfo:GetFullName()
|
||||
|
||||
local function wearPackageAsync()
|
||||
if packageInfo:IsOwned() and not packageInfo:IsWearing() then
|
||||
|
||||
local result = packageInfo:WearAsync()
|
||||
|
||||
if result and result['success'] == true then
|
||||
this:UpdateEquipButton()
|
||||
else
|
||||
local err = Errors.PackageEquip['Default']
|
||||
ScreenManager:OpenScreen(ErrorOverlayModule(err), false)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local function buyPackageAsync()
|
||||
local newPurchasePrompt = PurchasePackagePrompt(packageInfo)
|
||||
ScreenManager:OpenScreen(newPurchasePrompt, false)
|
||||
newPurchasePrompt:FadeInBackground()
|
||||
spawn(function()
|
||||
local didPurchase = newPurchasePrompt:ResultAsync()
|
||||
if didPurchase then
|
||||
SoundManager:Play('PurchaseSuccess')
|
||||
wearPackageAsync()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local PriceText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'PriceText';
|
||||
Text = '';
|
||||
Size = UDim2.new(1, 0, 0, 36);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
TextColor3 = GlobalSettings.BlackTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.PriceLabelColor;
|
||||
ZIndex = 2;
|
||||
Visible = false;
|
||||
Parent = this.AvatarItemContainer;
|
||||
};
|
||||
|
||||
local PromoText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = 'PromoText';
|
||||
Text = '';
|
||||
Size = UDim2.new(1, 0, 0, 36);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.PromoLabelColor;
|
||||
ZIndex = 2;
|
||||
Visible = false;
|
||||
Parent = this.AvatarItemContainer;
|
||||
};
|
||||
|
||||
function this:UpdatePriceText()
|
||||
local newText = ""
|
||||
local price = packageInfo:GetRobuxPrice()
|
||||
if price == 0 then
|
||||
newText = Strings:LocalizedString('FreeWord')
|
||||
elseif price then
|
||||
newText = "R$ " .. Utility.FormatNumberString(price)
|
||||
end
|
||||
|
||||
PriceText.Text = newText
|
||||
local priceTextSize = TextService:GetTextSize(PriceText.Text, Utility.ConvertFontSizeEnumToInt(PriceText.FontSize), PriceText.Font, Vector2.new())
|
||||
PriceText.Size = UDim2.new(0, priceTextSize.X + 28, 0, 36)
|
||||
PriceText.AnchorPoint = Vector2.new(1, 0)
|
||||
PriceText.Position = UDim2.new(1, -6, 0, 6)
|
||||
PriceText.Visible = price ~= nil and not packageInfo:IsOwned()
|
||||
end
|
||||
|
||||
function this:UpdatePromoText()
|
||||
if not packageInfo:IsOwned() then
|
||||
local saleIdStr = Utility.GetFastVariable("XboxSaleAvatarPackageIds")
|
||||
if type(saleIdStr) == "string" and saleIdStr ~= '' then
|
||||
for saleId in string.gmatch(saleIdStr, '([^;]+)') do
|
||||
if tonumber(saleId) ~= nil and tonumber(saleId) == tonumber(packageInfo:GetAssetId()) then --If the package is on sale
|
||||
PromoText.Text = "SALE!"
|
||||
local promoTextSize = TextService:GetTextSize(PromoText.Text, Utility.ConvertFontSizeEnumToInt(PromoText.FontSize), PromoText.Font, Vector2.new())
|
||||
PromoText.Size = UDim2.new(0, promoTextSize.X + 28, 0, 36)
|
||||
PromoText.AnchorPoint = Vector2.new(1, 0)
|
||||
PromoText.Position = UDim2.new(1, -6, 0, 42)
|
||||
PromoText.Visible = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
PromoText.Visible = false
|
||||
end
|
||||
end
|
||||
|
||||
if packageInfo:GetAssetId() then
|
||||
this:SetImage(Http.GetThumbnailUrlForAsset(packageInfo:GetAssetId()))
|
||||
else
|
||||
--TODO: show a no package image?
|
||||
end
|
||||
|
||||
|
||||
this:ColorizeImage(packageInfo:IsOwned() and 1 or 0, 0)
|
||||
this:SetPopupText(packageInfo:GetName())
|
||||
|
||||
function this:GetAssetId()
|
||||
return packageInfo:GetAssetId()
|
||||
end
|
||||
|
||||
function this:GetPackageInfo()
|
||||
return packageInfo
|
||||
end
|
||||
|
||||
function this:UpdateOwnership()
|
||||
local ownsAsset = packageInfo:IsOwned()
|
||||
self:SetActive(ownsAsset)
|
||||
self:ColorizeImage(ownsAsset and 1 or 0, 0)
|
||||
self:UpdateEquipButton()
|
||||
self:UpdatePriceText()
|
||||
--Added for promo label
|
||||
self:UpdatePromoText()
|
||||
end
|
||||
|
||||
function this:UpdateEquipButton()
|
||||
self.EquippedCheckmark.Visible = packageInfo:IsWearing()
|
||||
end
|
||||
|
||||
local selectDebounce = false
|
||||
function this:Select()
|
||||
if selectDebounce then return false end
|
||||
local result = false
|
||||
if packageInfo:IsOwned() and not packageInfo:IsWearing() then
|
||||
selectDebounce = true
|
||||
spawn(function()
|
||||
wearPackageAsync()
|
||||
selectDebounce = false
|
||||
end)
|
||||
result = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function this:OnClick()
|
||||
buyPackageAsync()
|
||||
end
|
||||
|
||||
local isWearingConn = nil
|
||||
local ownershipChangedCn = nil
|
||||
local baseShow = this.Show
|
||||
function this:Show()
|
||||
baseShow(self)
|
||||
Utility.DisconnectEvent(isWearingConn)
|
||||
packageInfo.IsWearingChanged:connect(function()self:UpdateEquipButton() end)
|
||||
Utility.DisconnectEvent(ownershipChangedCn)
|
||||
ownershipChangedCn = packageInfo.OwnershipChanged:connect(function()
|
||||
self:UpdateOwnership()
|
||||
end)
|
||||
self:UpdateEquipButton()
|
||||
|
||||
self:UpdateOwnership()
|
||||
end
|
||||
|
||||
local baseHide = this.Hide
|
||||
function this:Hide()
|
||||
baseHide(self)
|
||||
isWearingConn = Utility.DisconnectEvent(isWearingConn)
|
||||
ownershipChangedCn = Utility.DisconnectEvent(ownershipChangedCn)
|
||||
end
|
||||
|
||||
local baseFocus = this.Focus
|
||||
local avatarItemClickConn = nil
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return
|
||||
{
|
||||
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarTileId');
|
||||
AssetId = packageInfo:GetAssetId();
|
||||
IsOwned = packageInfo:IsOwned();
|
||||
}
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
baseFocus(self)
|
||||
focused = true
|
||||
|
||||
Utility.DisconnectEvent(avatarItemClickConn)
|
||||
avatarItemClickConn = self.AvatarItemContainer.MouseButton1Click:connect(function()
|
||||
self:OnClick()
|
||||
end)
|
||||
|
||||
self:UpdateEquipButton()
|
||||
|
||||
spawn(function()
|
||||
wait(0.17)
|
||||
if focused then
|
||||
if not packageInfo:IsOwned() then
|
||||
self:ColorizeImage(1)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local baseRemoveFocus = this.RemoveFocus
|
||||
function this:RemoveFocus()
|
||||
baseRemoveFocus(self)
|
||||
focused = false
|
||||
avatarItemClickConn = Utility.DisconnectEvent(avatarItemClickConn)
|
||||
|
||||
-- Decolorize unowned packages
|
||||
if not packageInfo:IsOwned() then
|
||||
self:ColorizeImage(0)
|
||||
end
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createAvatarInfoContainer
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Starts and manages the background 3D scene
|
||||
local RobloxGui = game:GetService('CoreGui').RobloxGui
|
||||
local ShellModules = script.Parent
|
||||
|
||||
local ContentProvider = game:GetService('ContentProvider')
|
||||
local ThirdPartyUserService = nil
|
||||
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
|
||||
|
||||
local CameraManager = require(ShellModules.CameraManager)
|
||||
local EventHub = require(ShellModules.EventHub)
|
||||
local SoundManager = require(ShellModules.SoundManager)
|
||||
local Utility = require(ShellModules.Utility)
|
||||
|
||||
local SceneManager = {}
|
||||
|
||||
-- set up the image before 3D background is displayed
|
||||
local BackgroundImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = 'BackgroundImage';
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 0;
|
||||
BackgroundColor3 = Color3.new(29/255, 47/255, 61/255);
|
||||
BorderSizePixel = 0;
|
||||
Image = 'rbxasset://textures/ui/Shell/Background/Home_screen_01.png';
|
||||
Parent = RobloxGui;
|
||||
}
|
||||
|
||||
-- We may want to move this to rodux flow later
|
||||
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authSuccessCameraControl", function(isNewLinkedAccount)
|
||||
CameraManager:DisableCameraControl()
|
||||
end);
|
||||
|
||||
if ThirdPartyUserService then
|
||||
ThirdPartyUserService.ActiveUserSignedOut:connect(function()
|
||||
CameraManager:EnableCameraControl()
|
||||
end)
|
||||
end
|
||||
|
||||
-- start up background music
|
||||
local backgroundSound = SoundManager:Play('BackgroundLoop', 0.33, true)
|
||||
if backgroundSound then
|
||||
local bgmLoopConn = nil
|
||||
bgmLoopConn = backgroundSound.DidLoop:connect(function(soundId, loopCount)
|
||||
if loopCount > 0 then
|
||||
bgmLoopConn = Utility.DisconnectEvent(bgmLoopConn)
|
||||
if backgroundSound then
|
||||
SoundManager:TweenSound(backgroundSound, 0.1, 3)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Check for 3D Background being loaded, then start it up
|
||||
local CROSSFADE_DURATION = 1.5
|
||||
spawn(function()
|
||||
while ContentProvider.RequestQueueSize > 0 do
|
||||
wait(0.01)
|
||||
end
|
||||
|
||||
CameraManager:EnableCameraControl()
|
||||
spawn(function()
|
||||
CameraManager:CameraMoveToAsync()
|
||||
end)
|
||||
Utility.PropertyTweener(BackgroundImage, 'BackgroundTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true)
|
||||
Utility.PropertyTweener(BackgroundImage, 'ImageTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true, function()
|
||||
BackgroundImage:Destroy()
|
||||
end)
|
||||
end)
|
||||
|
||||
return SceneManager
|
||||
@@ -0,0 +1,135 @@
|
||||
--[[
|
||||
// BadgeOverlay.lua
|
||||
|
||||
// Displays information for a single badge
|
||||
// Used by GameDetail and BadgeScreen
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local createBadgeOverlay = function(badgeData)
|
||||
local this = BaseOverlay()
|
||||
|
||||
local hasBadge = badgeData["IsOwned"]
|
||||
this:SetImageBackgroundTransparency(0)
|
||||
this:SetImageBackgroundColor(hasBadge and GlobalSettings.BadgeOwnedColor or GlobalSettings.BadgeOverlayColor)
|
||||
|
||||
local badgeImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "BadgeImage";
|
||||
Size = UDim2.new(0, 394, 0, 394);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'http://www.roblox.com/Thumbs/Asset.ashx?width='..
|
||||
tostring(250)..'&height='..tostring(250)..'&assetId='..tostring(badgeData.AssetId);
|
||||
}
|
||||
badgeImage.Position = UDim2.new(0.5, -197, 0.5, -197)
|
||||
this:SetImage(badgeImage)
|
||||
|
||||
local titleText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "TitleText";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0, this.RightAlign, 0, 88);
|
||||
BackgroundTransparency = 1;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.HeaderSize;
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
Text = badgeData.Name;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
--[[ Has Badge ]]--
|
||||
local hasBadgeContainer = nil
|
||||
if hasBadge then
|
||||
hasBadgeContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "HasBadgeContainer";
|
||||
Position = UDim2.new(0, titleText.Position.X.Offset, 0, titleText.Position.Y.Offset + 34);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = this.Container;
|
||||
}
|
||||
local hasBadgeImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "HasBadgeImage";
|
||||
BackgroundTransparency = 1;
|
||||
Parent = hasBadgeContainer;
|
||||
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
|
||||
Size = UDim2.new(0,35,0,35);
|
||||
}
|
||||
hasBadgeContainer.Size = UDim2.new(0, 200, 0, hasBadgeImage.Size.Y.Offset)
|
||||
local hasBadgeText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "HasBadgeText";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0, hasBadgeImage.Size.X.Offset + 12, 0.5, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Font = GlobalSettings.ItalicFont;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
TextColor3 = GlobalSettings.GreenTextColor;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
Text = Strings:LocalizedString("HaveBadgeWord");
|
||||
Parent = hasBadgeContainer;
|
||||
}
|
||||
end
|
||||
|
||||
--[[ Description ]]--
|
||||
local descriptionYOffset = hasBadgeContainer and hasBadgeContainer.Position.Y.Offset + hasBadgeContainer.Size.Y.Offset + 10 or
|
||||
titleText.Position.Y.Offset + 40
|
||||
|
||||
local descriptionScrollingTextBox = ScrollingTextBox(UDim2.new(0, 762, 0, 304),
|
||||
UDim2.new(0, titleText.Position.X.Offset, 0, descriptionYOffset),
|
||||
this.Container)
|
||||
descriptionScrollingTextBox:SetText(badgeData.Description)
|
||||
descriptionScrollingTextBox:SetFontSize(GlobalSettings.TitleSize)
|
||||
local descriptionFrame = descriptionScrollingTextBox:GetContainer()
|
||||
|
||||
local okButton = Utility.Create'TextButton'
|
||||
{
|
||||
Name = "OkButton";
|
||||
Size = UDim2.new(0, 320, 0, 66);
|
||||
Position = UDim2.new(0, titleText.Position.X.Offset, 1, -66 - 55);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.BlueButtonColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.ButtonSize;
|
||||
TextColor3 = GlobalSettings.TextSelectedColor;
|
||||
Text = Strings:LocalizedString("OkWord");
|
||||
Parent = this.Container;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
}
|
||||
Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding)
|
||||
|
||||
--[[ Input Events ]]--
|
||||
okButton.MouseButton1Click:connect(function()
|
||||
this:Close()
|
||||
end)
|
||||
local baseFocus = this.Focus
|
||||
function this:Focus()
|
||||
baseFocus(this)
|
||||
Utility.SetSelectedCoreObject(okButton)
|
||||
end
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return
|
||||
{
|
||||
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BadgeOverlayId');
|
||||
AssetId = badgeData.AssetId;
|
||||
}
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBadgeOverlay
|
||||
@@ -0,0 +1,124 @@
|
||||
--[[
|
||||
// BadgeScreen.lua
|
||||
|
||||
// Displays a 2xN grid of badges for a game
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local ScrollingGrid = require(ShellModules:FindFirstChild('ScrollingGrid'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
|
||||
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
|
||||
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
|
||||
|
||||
local createBadgeScreen = function(badgeData)
|
||||
local this = BaseScreen()
|
||||
|
||||
local ROWS = 2
|
||||
-- columns is dynamic
|
||||
|
||||
local BadgeContainer = this.Container
|
||||
this:SetTitle(Strings:LocalizedString("GameBadgesTitle"))
|
||||
|
||||
local defaultSelection = nil
|
||||
|
||||
-- create grid
|
||||
local BadgeScrollGrid = ScrollingGrid()
|
||||
BadgeScrollGrid:SetPosition(UDim2.new(0, 0, 0.5 - (0.57 / 2), 0))
|
||||
BadgeScrollGrid:SetSize(UDim2.new(1, 0, 0, 570))
|
||||
BadgeScrollGrid:SetScrollDirection(BadgeScrollGrid.Enum.ScrollDirection.Horizontal)
|
||||
BadgeScrollGrid:SetParent(BadgeContainer)
|
||||
BadgeScrollGrid:SetClipping(false)
|
||||
BadgeScrollGrid:SetCellSize(Vector2.new(276, 276))
|
||||
BadgeScrollGrid:SetSpacing(Vector2.new(18, 18))
|
||||
|
||||
local checkmarkImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "CheckMarkImage";
|
||||
BackgroundTransparency = 1;
|
||||
|
||||
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
|
||||
Size = UDim2.new(0,35,0,35);
|
||||
|
||||
ZIndex = 2;
|
||||
}
|
||||
|
||||
local function connectImageInput(image, data)
|
||||
image.MouseButton1Click:connect(function()
|
||||
-- Do not play sound because we are opening a screen here
|
||||
ScreenManager:OpenScreen(BadgeOverlayModule(data), false)
|
||||
end)
|
||||
end
|
||||
|
||||
local baseItem = Utility.Create'TextButton'
|
||||
{
|
||||
Name = "BadgeImage";
|
||||
BorderSizePixel = 0;
|
||||
BackgroundTransparency = 0.2;
|
||||
BackgroundColor3 = Color3.new(64/255, 81/255, 93/255);
|
||||
Text = "";
|
||||
ZIndex = 2;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
AssetManager.CreateShadow(1);
|
||||
}
|
||||
local badgeIcon = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "Thumb";
|
||||
Size = UDim2.new(0, 228, 0, 228);
|
||||
Position = UDim2.new(0.5, -228/2, 0.5, -228/2);
|
||||
BackgroundTransparency = 1;
|
||||
Image = "";
|
||||
ZIndex = 2;
|
||||
Parent = baseItem;
|
||||
}
|
||||
|
||||
for i = 1, #badgeData do
|
||||
local data = badgeData[i]
|
||||
local item = baseItem:Clone()
|
||||
local thumb = item:FindFirstChild("Thumb")
|
||||
if thumb then
|
||||
local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId,
|
||||
ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon)
|
||||
spawn(function()
|
||||
thumbLoader:LoadAsync()
|
||||
end)
|
||||
end
|
||||
local hasBadge = data["IsOwned"]
|
||||
if hasBadge then
|
||||
item.BackgroundColor3 = GlobalSettings.BadgeOwnedColor;
|
||||
item.BackgroundTransparency = 0;
|
||||
--
|
||||
local check = checkmarkImage:Clone()
|
||||
check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8)
|
||||
check.Parent = item
|
||||
end
|
||||
item.Name = tostring(i)
|
||||
BadgeScrollGrid:AddItem(item)
|
||||
connectImageInput(item, data)
|
||||
PopupText(item, data["Name"])
|
||||
if not defaultSelection then
|
||||
defaultSelection = item
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
--Override
|
||||
function this:GetDefaultSelectionObject()
|
||||
return defaultSelection
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBadgeScreen
|
||||
@@ -0,0 +1,185 @@
|
||||
--[[
|
||||
// BadgeSort.lua
|
||||
// Creates a badge sort for a game
|
||||
|
||||
// Handles the following for badges
|
||||
// Displays 2x2 of badges on game details page
|
||||
// Displays individual information about each badge (overlay)
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay'))
|
||||
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
|
||||
|
||||
local CreateBadgeSort = function(placeName, size, position, parent)
|
||||
local this = {}
|
||||
|
||||
local badgeData = nil
|
||||
local margin = 14
|
||||
local imageSize = (size.Y.Offset - margin) / 2
|
||||
|
||||
local gridImages = {}
|
||||
|
||||
local GRID_SIZE = 4
|
||||
|
||||
--[[ Game Details Grid ]]--
|
||||
local container = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ImageContainer";
|
||||
Size = size;
|
||||
Position = position;
|
||||
BackgroundTransparency = 1;
|
||||
Parent = parent;
|
||||
}
|
||||
-- create 2x2 preview grid
|
||||
local index = 1
|
||||
for i = 1, GRID_SIZE/2 do
|
||||
for j = 1, GRID_SIZE/2 do
|
||||
local image = Utility.Create'TextButton'
|
||||
{
|
||||
Name = tostring(index);
|
||||
Size = UDim2.new(0, imageSize, 0, imageSize);
|
||||
Position = UDim2.new(0, (i - 1) * imageSize + (i - 1) * margin, 0, (j - 1) * imageSize + (j - 1) * margin);
|
||||
BackgroundTransparency = GlobalSettings.FriendStatusTextTransparency;
|
||||
BackgroundColor3 = GlobalSettings.BadgeFrameColor;
|
||||
BorderSizePixel = 0;
|
||||
ZIndex = 2;
|
||||
Text = "";
|
||||
ClipsDescendants = true;
|
||||
Parent = container;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
AssetManager.CreateShadow(1);
|
||||
}
|
||||
local thumb = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "Thumb";
|
||||
Size = UDim2.new(0, 228, 0, 228);
|
||||
Position = UDim2.new(0.5, -228/2, 0.5, -228/2);
|
||||
BackgroundTransparency = 1;
|
||||
Image = "";
|
||||
ZIndex = 2;
|
||||
Parent = image;
|
||||
}
|
||||
gridImages[index] = image
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- more button visible when #badges > 4
|
||||
local moreBadgesButton = Utility.Create'ImageButton'
|
||||
{
|
||||
Name = "MoreBadgesButton";
|
||||
BackgroundTransparency = 1;
|
||||
Visible = false;
|
||||
Parent = container;
|
||||
Image = "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png";
|
||||
Size = UDim2.new(0,108,0,50);
|
||||
ZIndex = 2;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
}
|
||||
moreBadgesButton.Position = UDim2.new(1, - moreBadgesButton.AbsoluteSize.x, 1, 12)
|
||||
|
||||
local function updateMoreButton(isSelected)
|
||||
moreBadgesButton.Image = isSelected and 'rbxasset://textures/ui/Shell/Buttons/MoreButtonSelected@1080.png'
|
||||
or 'rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png'
|
||||
end
|
||||
|
||||
moreBadgesButton.SelectionGained:connect(function()
|
||||
updateMoreButton(true)
|
||||
end)
|
||||
moreBadgesButton.SelectionLost:connect(function()
|
||||
updateMoreButton(false)
|
||||
end)
|
||||
|
||||
local checkmarkImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "CheckMarkImage";
|
||||
BackgroundTransparency = 1;
|
||||
|
||||
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
|
||||
Size = UDim2.new(0,35,0,35);
|
||||
|
||||
ZIndex = 2;
|
||||
}
|
||||
|
||||
local function setBadgeData()
|
||||
if not badgeData then
|
||||
Utility.DebugLog("BadgeSort: failed to set badge data because data is nil.")
|
||||
return
|
||||
end
|
||||
--
|
||||
for i = 1, #gridImages do
|
||||
if badgeData[i] then
|
||||
local data = badgeData[i]
|
||||
local thumb = gridImages[i]:FindFirstChild("Thumb")
|
||||
if thumb then
|
||||
local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId,
|
||||
ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon)
|
||||
spawn(function()
|
||||
thumbLoader:LoadAsync()
|
||||
end)
|
||||
end
|
||||
local hasBadge = data["IsOwned"]
|
||||
if hasBadge then
|
||||
gridImages[i].BackgroundColor3 = GlobalSettings.BadgeOwnedColor
|
||||
gridImages[i].BackgroundTransparency = 0
|
||||
--
|
||||
local check = checkmarkImage:Clone()
|
||||
check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8)
|
||||
check.Parent = gridImages[i]
|
||||
end
|
||||
--
|
||||
gridImages[i].MouseButton1Click:connect(function()
|
||||
ScreenManager:OpenScreen(BadgeOverlayModule(data), false)
|
||||
end)
|
||||
-- connect popup text
|
||||
PopupText(gridImages[i], data["Name"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Input Events ]]--
|
||||
moreBadgesButton.MouseButton1Click:connect(function()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenBadgeScreen"], badgeData, placeName)
|
||||
end)
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetContainer()
|
||||
return container
|
||||
end
|
||||
|
||||
function this:GetDefaultSelection()
|
||||
return gridImages and gridImages[1] or nil
|
||||
end
|
||||
|
||||
function this:Initialize(data)
|
||||
if not badgeData then
|
||||
badgeData = data
|
||||
setBadgeData()
|
||||
if #data > GRID_SIZE then
|
||||
moreBadgesButton.Visible = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function this:Destroy()
|
||||
container:Destroy()
|
||||
gridImages = nil
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return CreateBadgeSort
|
||||
@@ -0,0 +1,475 @@
|
||||
--[[
|
||||
// BaseCarouselScreen.lua
|
||||
|
||||
// Creates a base screen for a carousel view
|
||||
// To be used for game genre and search screens
|
||||
|
||||
// Creates a Play and Favorite Button, details view (votes, description), and
|
||||
// a carousel view
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local Errors = require(ShellModules:FindFirstChild('Errors'))
|
||||
local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay'))
|
||||
local GameData = require(ShellModules:FindFirstChild('GameData'))
|
||||
local GameJoinModule = require(ShellModules:FindFirstChild('GameJoin'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
|
||||
local CarouselView = require(ShellModules:FindFirstChild('CarouselView'))
|
||||
local CarouselController = require(ShellModules:FindFirstChild('CarouselController'))
|
||||
local VoteFrame = require(ShellModules:FindFirstChild('VoteFrame'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local FFlagXboxSponsoredGames = settings():GetFFlag("XboxSponsoredGames")
|
||||
|
||||
local function CreateBaseCarouselScreen()
|
||||
local this = BaseScreen()
|
||||
|
||||
local newGameSelectedCn = nil
|
||||
local dataModelViewChangedCn = nil
|
||||
|
||||
local canJoinGame = true
|
||||
local returnedFromGame = true
|
||||
|
||||
-- we need to move ZIndex up because of drop shadows
|
||||
local BASE_ZINDEX = 2
|
||||
|
||||
local playButtonColor = GlobalSettings.GreenButtonColor
|
||||
local playButtonSelectedColor = GlobalSettings.GreenSelectedButtonColor
|
||||
local favoriteButtonColor = GlobalSettings.GreyButtonColor
|
||||
local favoriteSelectedButtonColor = GlobalSettings.GreySelectedButtonColor
|
||||
local buttonTextColor = GlobalSettings.WhiteTextColor
|
||||
local buttonSelectedTextColor = GlobalSettings.TextSelectedColor
|
||||
|
||||
local viewContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ViewContainer";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = this.Container;
|
||||
}
|
||||
this.ViewContainer = viewContainer
|
||||
|
||||
local myCarouselView = CarouselView()
|
||||
myCarouselView:SetSize(UDim2.new(0, 1724, 0, 450))
|
||||
myCarouselView:SetPosition(UDim2.new(0, 0, 0, 240))
|
||||
myCarouselView:SetPadding(18)
|
||||
myCarouselView:SetItemSizePercentOfContainer(2/3)
|
||||
myCarouselView:SetParent(viewContainer)
|
||||
|
||||
local myCarouselController = CarouselController(myCarouselView)
|
||||
|
||||
local playButton = Utility.Create'ImageButton'
|
||||
{
|
||||
Name = "PlayButton";
|
||||
Size = UDim2.new(0, 228, 0, 72);
|
||||
Position = UDim2.new(0, 0, 1, -77);
|
||||
BackgroundTransparency = 1;
|
||||
ImageColor3 = playButtonColor;
|
||||
Image = GlobalSettings.RoundCornerButtonImage;
|
||||
ScaleType = Enum.ScaleType.Slice;
|
||||
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
|
||||
ZIndex = BASE_ZINDEX;
|
||||
Parent = viewContainer;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
AssetManager.CreateShadow(1);
|
||||
}
|
||||
local playText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "PlayText";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Text = Strings:LocalizedString("PlayWord");
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.ButtonSize;
|
||||
TextColor3 = buttonTextColor;
|
||||
ZIndex = BASE_ZINDEX;
|
||||
Parent = playButton;
|
||||
}
|
||||
Utility.ResizeButtonWithText(playButton, playText, GlobalSettings.TextHorizontalPadding)
|
||||
|
||||
local favoriteButton = Utility.Create'ImageButton'
|
||||
{
|
||||
Name = "FavoriteButton";
|
||||
Position = UDim2.new(0, playButton.Size.X.Offset + 10, 1, -77);
|
||||
Size = UDim2.new(0, 228, 0, 72);
|
||||
BackgroundTransparency = 1;
|
||||
ImageColor3 = favoriteButtonColor;
|
||||
Image = GlobalSettings.RoundCornerButtonImage;
|
||||
ScaleType = Enum.ScaleType.Slice;
|
||||
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
|
||||
ZIndex = BASE_ZINDEX;
|
||||
Parent = viewContainer;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
AssetManager.CreateShadow(1);
|
||||
}
|
||||
local favoriteText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "FavoriteText";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Text = Strings:LocalizedString("FavoriteWord");
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.ButtonSize;
|
||||
TextColor3 = buttonTextColor;
|
||||
ZIndex = 2;
|
||||
Parent = favoriteButton;
|
||||
}
|
||||
|
||||
local favoriteStarImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "FavoriteStarImage";
|
||||
Size = UDim2.new(0, 32, 0, 31);
|
||||
Position = UDim2.new(0, 16, 0.5, -31/2);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/FavoriteStar@1080.png';
|
||||
Visible = false;
|
||||
ZIndex = BASE_ZINDEX;
|
||||
Parent = favoriteButton;
|
||||
}
|
||||
|
||||
--Make it big enough to hold the star and text
|
||||
Utility.ResizeButtonWithDynamicText(favoriteButton, favoriteText, {Strings:LocalizedString("FavoritedWord")},
|
||||
GlobalSettings.TextHorizontalPadding + (favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12) / 2)
|
||||
|
||||
-- begin game details content
|
||||
local gameDetailsContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "GameDetailsContainer";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0, 18, 0, 732);
|
||||
BackgroundTransparency = 1;
|
||||
Parent = viewContainer;
|
||||
}
|
||||
local gameTitle = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "GameTitleLabel";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Text = "";
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
Font = GlobalSettings.LightFont;
|
||||
FontSize = GlobalSettings.HeaderSize;
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local voteFrame = VoteFrame(gameDetailsContainer, UDim2.new(0, 38, 0, 46))
|
||||
local voteFrameContainer = voteFrame:GetContainer()
|
||||
|
||||
local thumbsUpImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "ThumbsUpImage";
|
||||
Size = UDim2.new(0, 28, 0, 28);
|
||||
Position = UDim2.new(0, 0, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset - 28);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsUpIcon@1080.png';
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local thumbsDownImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "ThumbsDownImage";
|
||||
Size = UDim2.new(0, 28, 0, 28);
|
||||
Position = UDim2.new(0, voteFrameContainer.Position.X.Offset + voteFrameContainer.Size.X.Offset + 10, 0, voteFrameContainer.Position.Y.Offset);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsDownIcon@1080.png';
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local separatorDot = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "SeparatorDot";
|
||||
Size = UDim2.new(0, 10, 0, 10);
|
||||
Position = UDim2.new(0, thumbsDownImage.Position.X.Offset + thumbsDownImage.Size.X.Offset + 32, 0, voteFrameContainer.Position.Y.Offset + (voteFrameContainer.Size.Y.Offset/2) - (10/2));
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/SeparatorDot@1080.png';
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local creatorIcon = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "CreatorIcon";
|
||||
Size = UDim2.new(0, 24, 0, 24);
|
||||
Position = UDim2.new(0, separatorDot.Position.X.Offset + separatorDot.Size.X.Offset + 32, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 12);
|
||||
BackgroundTransparency = 1;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png';
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local creatorName = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "CreatorName";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0, creatorIcon.Position.X.Offset + creatorIcon.Size.X.Offset + 8, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 2);
|
||||
BackgroundTransparency = 1;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
TextColor3 = GlobalSettings.LightGreyTextColor;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
Text = "";
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
local descriptionText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "DescriptionText";
|
||||
Size = UDim2.new(0, 850, 0, 64);
|
||||
Position = UDim2.new(0, gameTitle.Position.X.Offset, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset + 20);
|
||||
BackgroundTransparency = 1;
|
||||
Text = "";
|
||||
TextColor3 = GlobalSettings.LightGreyTextColor;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
TextYAlignment = Enum.TextYAlignment.Top;
|
||||
Font = GlobalSettings.LightFont;
|
||||
TextWrapped = true;
|
||||
FontSize = GlobalSettings.DescriptionSize;
|
||||
Parent = gameDetailsContainer;
|
||||
}
|
||||
|
||||
local noResultsText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "noResultsText";
|
||||
Size = UDim2.new(0, 0, 0, 0);
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Text = Strings:LocalizedString("NoGamesPhrase");
|
||||
TextColor3 = GlobalSettings.LightGreyTextColor;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.MediumFontSize;
|
||||
Visible = false;
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
-- Selection overrides
|
||||
playButton.NextSelectionLeft = playButton
|
||||
favoriteButton.NextSelectionRight = favoriteButton
|
||||
|
||||
playButton.SelectionGained:connect(function()
|
||||
playButton.ImageColor3 = playButtonSelectedColor
|
||||
playText.TextColor3 = buttonSelectedTextColor
|
||||
end)
|
||||
playButton.SelectionLost:connect(function()
|
||||
playButton.ImageColor3 = playButtonColor
|
||||
playText.TextColor3 = buttonTextColor
|
||||
end)
|
||||
favoriteButton.SelectionGained:connect(function()
|
||||
favoriteButton.ImageColor3 = favoriteSelectedButtonColor
|
||||
favoriteText.TextColor3 = buttonSelectedTextColor
|
||||
end)
|
||||
favoriteButton.SelectionLost:connect(function()
|
||||
favoriteButton.ImageColor3 = favoriteButtonColor
|
||||
favoriteText.TextColor3 = buttonTextColor
|
||||
end)
|
||||
|
||||
local function setIsFavorited(isFavorited)
|
||||
if isFavorited == true then
|
||||
favoriteStarImage.Visible = true
|
||||
favoriteText.Position = UDim2.new(0, favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12, 0, 0)
|
||||
favoriteText.Text = Strings:LocalizedString("FavoritedWord")
|
||||
favoriteText.TextXAlignment = Enum.TextXAlignment.Left
|
||||
else
|
||||
favoriteStarImage.Visible = false
|
||||
favoriteText.Position = UDim2.new(0, 0, 0, 0)
|
||||
favoriteText.Text = Strings:LocalizedString("FavoriteWord")
|
||||
favoriteText.TextXAlignment = Enum.TextXAlignment.Center
|
||||
end
|
||||
end
|
||||
local function setVoteView(voteData)
|
||||
local upVotes = voteData and voteData.UpVotes or 0
|
||||
local downVotes = voteData and voteData.DownVotes or 0
|
||||
if upVotes == 0 and downVotes == 0 then
|
||||
voteFrame:SetPercentFilled(nil)
|
||||
else
|
||||
voteFrame:SetPercentFilled(upVotes / (upVotes + downVotes))
|
||||
end
|
||||
end
|
||||
|
||||
local GameSelectedConns = {}
|
||||
local onNewGameSelected = nil
|
||||
|
||||
local function ClearGameSelectedView()
|
||||
--Disconnect Events
|
||||
Utility.DisconnectEvents(GameSelectedConns)
|
||||
GameSelectedConns = {}
|
||||
gameTitle.Text = ""
|
||||
creatorName.Text = ""
|
||||
descriptionText.Text = ""
|
||||
setVoteView()
|
||||
setIsFavorited()
|
||||
end
|
||||
|
||||
onNewGameSelected = function(placeId)
|
||||
Utility.DisconnectEvents(GameSelectedConns)
|
||||
GameSelectedConns = {}
|
||||
|
||||
local data = GameData:GetGameData(placeId)
|
||||
if data then
|
||||
--Use signals to make sure that these fetched data corresponds to the game we focus on
|
||||
table.insert(GameSelectedConns, data.OnGetVoteDataEnd:
|
||||
connect(function(voteData) setVoteView(voteData)
|
||||
end))
|
||||
|
||||
table.insert(GameSelectedConns, data.OnGetGameDetailsEnd:
|
||||
connect(function(gameData)
|
||||
descriptionText.Text = gameData.Description or ""
|
||||
setIsFavorited(gameData.IsFavorited)
|
||||
end))
|
||||
gameTitle.Text = data.Name
|
||||
creatorName.Text = data.CreatorName
|
||||
descriptionText.Text = data.Description or ""
|
||||
setVoteView(data.VoteData)
|
||||
setIsFavorited(data.IsFavorited)
|
||||
spawn(function()
|
||||
if not data.VoteData then
|
||||
data:GetVoteDataAsync()
|
||||
end
|
||||
|
||||
if not data.Description or data.IsFavorited == nil then
|
||||
data:GetGameDetailsAsync()
|
||||
end
|
||||
end)
|
||||
else
|
||||
ClearGameSelectedView()
|
||||
end
|
||||
end
|
||||
|
||||
playButton.MouseButton1Click:connect(function()
|
||||
SoundManager:Play('ButtonPress')
|
||||
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
|
||||
local placeId
|
||||
if FFlagXboxSponsoredGames then
|
||||
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
|
||||
else
|
||||
placeId = currentFocusGameData and currentFocusGameData or nil
|
||||
end
|
||||
local data = GameData:GetGameData(placeId)
|
||||
if data then
|
||||
if canJoinGame and returnedFromGame then
|
||||
canJoinGame = false
|
||||
--Should never happen, as all game in carousel inits with CreatorUserId
|
||||
if not data.CreatorUserId then
|
||||
data:GetGameDetailsAsync()
|
||||
end
|
||||
GameJoinModule:StartGame(GameJoinModule.JoinType.Normal, data.PlaceId, data.CreatorUserId)
|
||||
canJoinGame = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
favoriteButton.MouseButton1Click:connect(function()
|
||||
SoundManager:Play('ButtonPress')
|
||||
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
|
||||
local placeId
|
||||
if FFlagXboxSponsoredGames then
|
||||
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
|
||||
else
|
||||
placeId = currentFocusGameData and currentFocusGameData or nil
|
||||
end
|
||||
local data = GameData:GetGameData(placeId)
|
||||
if data then
|
||||
local success, reason = data:PostFavoriteAsync()
|
||||
if success then
|
||||
setIsFavorited(data.IsFavorited)
|
||||
elseif reason then
|
||||
local err = Errors.Favorite[reason]
|
||||
ScreenManager:OpenScreen(ErrorOverlayModule(err), false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function this:LoadGameCollection(gameCollection)
|
||||
viewContainer.Visible = false
|
||||
noResultsText.Visible = false
|
||||
myCarouselView:SetParent(nil)
|
||||
|
||||
spawn(function()
|
||||
local loader = LoadingWidget(
|
||||
{ Parent = this.Container },
|
||||
{
|
||||
function()
|
||||
myCarouselController:InitializeAsync(gameCollection)
|
||||
end
|
||||
}
|
||||
)
|
||||
|
||||
loader:AwaitFinished()
|
||||
loader:Cleanup()
|
||||
loader = nil
|
||||
|
||||
myCarouselView:SetParent(viewContainer)
|
||||
if this:IsFocused() then
|
||||
if myCarouselController:HasResults() then
|
||||
viewContainer.Visible = true
|
||||
Utility.SetSelectedCoreObject(myCarouselView:GetFocusItem())
|
||||
else
|
||||
noResultsText.Visible = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Override Base Functions
|
||||
function this:GetDefaultSelectionObject()
|
||||
return myCarouselView:GetFocusItem()
|
||||
end
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return
|
||||
{
|
||||
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseCarouselScreenId');
|
||||
Title = this:GetTitle();
|
||||
}
|
||||
end
|
||||
|
||||
local baseFocus = this.Focus
|
||||
function this:Focus()
|
||||
baseFocus(self)
|
||||
myCarouselView:Focus()
|
||||
|
||||
if PlatformService then
|
||||
dataModelViewChangedCn = PlatformService.ViewChanged:connect(function(viewType)
|
||||
-- return from game debounce
|
||||
if viewType == 0 then
|
||||
returnedFromGame = false
|
||||
wait(1)
|
||||
returnedFromGame = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
newGameSelectedCn = myCarouselController.NewItemSelected:connect(onNewGameSelected)
|
||||
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
|
||||
local placeId
|
||||
if FFlagXboxSponsoredGames then
|
||||
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
|
||||
else
|
||||
placeId = currentFocusGameData and currentFocusGameData or nil
|
||||
end
|
||||
onNewGameSelected(placeId)
|
||||
myCarouselController:Connect()
|
||||
end
|
||||
|
||||
local baseRemoveFocus = this.RemoveFocus
|
||||
function this:RemoveFocus()
|
||||
baseRemoveFocus(self)
|
||||
dataModelViewChangedCn = Utility.DisconnectEvent(dataModelViewChangedCn)
|
||||
newGameSelectedCn = Utility.DisconnectEvent(newGameSelectedCn)
|
||||
myCarouselController:Disconnect()
|
||||
Utility.DisconnectEvents(GameSelectedConns)
|
||||
GameSelectedConns = {}
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return CreateBaseCarouselScreen
|
||||
@@ -0,0 +1,160 @@
|
||||
--[[
|
||||
// BaseOverlay.lua
|
||||
|
||||
// Implements a base overlay for overlay screens.
|
||||
// Any other overlay classes should require this module
|
||||
// first, then implement its own logic
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local GuiService = game:GetService('GuiService')
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local FADE_TIME = 0.25
|
||||
|
||||
local MOCKUP_WIDTH = 1920
|
||||
local MOCKUP_HEIGHT = 1080
|
||||
local CONTENT_WIDTH = 1920
|
||||
local CONTENT_HEIGHT = 690
|
||||
|
||||
local CONTENT_POSITION = Vector2.new(0, 225)
|
||||
|
||||
local createBaseOverlay = function()
|
||||
local this = {}
|
||||
|
||||
local OVERLAY_TRANSPARENCY = GlobalSettings.ModalBackgroundTransparency
|
||||
|
||||
this.RightAlign = 776
|
||||
|
||||
local modalOverlay = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ModalOverlay";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
BackgroundColor3 = Color3.new();
|
||||
BorderSizePixel = 0;
|
||||
}
|
||||
local container = Utility.Create'Frame'
|
||||
{
|
||||
Name = "Container";
|
||||
Size = UDim2.new(CONTENT_WIDTH/MOCKUP_WIDTH, 0, CONTENT_HEIGHT/MOCKUP_HEIGHT, 0);
|
||||
Position = UDim2.new(CONTENT_POSITION.x/MOCKUP_WIDTH, 0, CONTENT_POSITION.y/MOCKUP_HEIGHT, 0);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.OverlayColor;
|
||||
Parent = modalOverlay;
|
||||
}
|
||||
local imageContainer = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ImageContainer";
|
||||
Size = UDim2.new(0, 576, 0, 642);
|
||||
Position = UDim2.new(0, 100, 0.5, -321);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundTransparency = 1;
|
||||
BackgroundColor3 = Color3.new();
|
||||
ZIndex = 2;
|
||||
Parent = container;
|
||||
}
|
||||
local imageDropShadow = AssetManager.CreateShadow(1)
|
||||
imageDropShadow.ImageTransparency = 1
|
||||
imageDropShadow.Parent = imageContainer
|
||||
|
||||
this.Container = container
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseOverlayId')}
|
||||
end
|
||||
|
||||
function this:SetImageBackgroundTransparency(value)
|
||||
imageContainer.BackgroundTransparency = value
|
||||
-- if the image has any transparency we don't show drop shadow since it will clip
|
||||
if value > 0 then
|
||||
imageDropShadow.ImageTransparency = 1
|
||||
else
|
||||
imageDropShadow.ImageTransparency = 0
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetImageBackgroundColor(value)
|
||||
imageContainer.BackgroundColor3 = value
|
||||
end
|
||||
|
||||
function this:SetImage(guiImage)
|
||||
guiImage.AnchorPoint = Vector2.new(0.5, 0.5);
|
||||
guiImage.Position = UDim2.new(0.5, 0, 0.5, 0);
|
||||
guiImage.ZIndex = imageContainer.ZIndex
|
||||
guiImage.Parent = imageContainer
|
||||
end
|
||||
|
||||
function this:GetOverlaySound()
|
||||
return 'OverlayOpen'
|
||||
end
|
||||
|
||||
function this:GetPriority()
|
||||
return GlobalSettings.OverlayPriority
|
||||
end
|
||||
|
||||
function this:Show()
|
||||
modalOverlay.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority())
|
||||
local overlayTweenIn = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency",
|
||||
1, OVERLAY_TRANSPARENCY, FADE_TIME, Utility.EaseInOutQuad, nil)
|
||||
SoundManager:Play(self:GetOverlaySound())
|
||||
|
||||
-- Show the modalOverlay when we are shown
|
||||
modalOverlay.Visible = true
|
||||
end
|
||||
|
||||
function this:Hide()
|
||||
local overlayTweenOut = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency",
|
||||
OVERLAY_TRANSPARENCY, 1, FADE_TIME, Utility.EaseInOutQuad, true,
|
||||
function()
|
||||
modalOverlay:Destroy()
|
||||
end)
|
||||
container.Parent = nil
|
||||
container:Destroy()
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
ContextActionService:BindCoreAction("CloseOverlay",
|
||||
function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.ButtonB)
|
||||
GuiService:AddSelectionParent("Overlay", container)
|
||||
|
||||
-- Don't show overlays when not focused
|
||||
modalOverlay.Visible = true
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
ContextActionService:UnbindCoreAction("CloseOverlay")
|
||||
GuiService:RemoveSelectionGroup("Overlay")
|
||||
|
||||
-- Don't show overlays when not focused
|
||||
modalOverlay.Visible = false
|
||||
end
|
||||
|
||||
function this:Close()
|
||||
if ScreenManager:GetTopScreen() == self then
|
||||
SoundManager:Play('ButtonPress')
|
||||
ScreenManager:CloseCurrent()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBaseOverlay
|
||||
@@ -0,0 +1,187 @@
|
||||
--[[
|
||||
// BaseScreen.lua
|
||||
|
||||
// Creates a base screen with breadcrumbs and title. Do not use for a pane/tab
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local GuiService = game:GetService('GuiService')
|
||||
|
||||
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local function createBaseScreen(usingMVC)
|
||||
local this = {}
|
||||
|
||||
local inFocus = false
|
||||
local isShown = false
|
||||
local defaultSelectionObject = nil
|
||||
local lastParent = nil
|
||||
|
||||
local BackText = ""
|
||||
|
||||
this.view = nil
|
||||
this.fixPosition = false
|
||||
|
||||
function this:GetBackText()
|
||||
return BackText
|
||||
end
|
||||
|
||||
local function GetView()
|
||||
if not this.view then
|
||||
local Templates = ShellModules:FindFirstChild("Templates")
|
||||
local BaseScreen = require(Templates:FindFirstChild('BaseScreen'))
|
||||
this.view = BaseScreen(this)
|
||||
end
|
||||
return this.view
|
||||
end
|
||||
|
||||
if not usingMVC then
|
||||
this.Container = GetView().Container
|
||||
end
|
||||
|
||||
local function CloseScreen()
|
||||
if inFocus then
|
||||
ScreenManager:CloseCurrent()
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
|
||||
function this:GetView()
|
||||
return GetView()
|
||||
end
|
||||
function this:GetTitle()
|
||||
return GetView().TitleText.Text
|
||||
end
|
||||
function this:SetTitle(newTitle)
|
||||
GetView().TitleText.Text = newTitle
|
||||
end
|
||||
function this:SetTitleZIndex(newZIndex)
|
||||
-- this needs some explantion...
|
||||
-- we recently changed the way z ordering works. In the old system if you had a text label and a image at the
|
||||
-- same zindex the text would render on top of the image. This is no longer the case. So in some cases
|
||||
-- we need to render this text last, but as it's a base class, it becomes hard to reorder the child add order.
|
||||
-- Instead I'm opting to allow changing the zindex
|
||||
GetView().TitleText.ZIndex = newZIndex
|
||||
end
|
||||
function this:SetBackText(newText)
|
||||
BackText = newText
|
||||
GetView():SetBackText(BackText)
|
||||
end
|
||||
function this:GetDefaultSelectionObject()
|
||||
return defaultSelectionObject
|
||||
end
|
||||
function this:Destroy()
|
||||
GetView().Container:Destroy()
|
||||
end
|
||||
|
||||
--[[ Public API - Screen Management ]]--
|
||||
function this:SetPosition(newPosition)
|
||||
GetView().Container.Position = newPosition
|
||||
end
|
||||
function this:SetParent(newParent)
|
||||
lastParent = newParent
|
||||
end
|
||||
function this:GetName()
|
||||
return GetView().TitleText.Text
|
||||
end
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = GetView().TitleText.Text}
|
||||
end
|
||||
function this:IsFocused()
|
||||
return inFocus
|
||||
end
|
||||
|
||||
-- View stuff --
|
||||
|
||||
function this:ReloadView(newView)
|
||||
local currentView = this.view
|
||||
local currentViewIsShown = isShown
|
||||
local currentViewIsFocused = inFocus
|
||||
|
||||
if currentView then
|
||||
if currentViewIsFocused then
|
||||
currentView:RemoveFocus()
|
||||
end
|
||||
if currentViewIsShown then
|
||||
currentView:Hide()
|
||||
end
|
||||
end
|
||||
if newView then
|
||||
if currentViewIsShown then
|
||||
newView:Show()
|
||||
end
|
||||
if currentViewIsFocused then
|
||||
newView:Focus()
|
||||
end
|
||||
end
|
||||
this.view = newView
|
||||
end
|
||||
|
||||
function this:OnBackButtonClick()
|
||||
CloseScreen()
|
||||
end
|
||||
|
||||
----------------
|
||||
|
||||
function this:Show()
|
||||
isShown = true
|
||||
local prevScreen = ScreenManager:GetScreenBelow(self)
|
||||
if prevScreen and prevScreen.GetName then
|
||||
self:SetBackText(prevScreen:GetName())
|
||||
else
|
||||
self:SetBackText(Strings:LocalizedString("BackWord"))
|
||||
end
|
||||
|
||||
GetView().Container.Parent = lastParent
|
||||
ScreenManager:DefaultCancelFade(self.TransitionTweens)
|
||||
if self.fixPosition then
|
||||
self.TransitionTweens = ScreenManager:FadeInSitu(GetView().Container)
|
||||
else
|
||||
self.TransitionTweens = ScreenManager:DefaultFadeIn(GetView().Container)
|
||||
end
|
||||
ScreenManager:PlayDefaultOpenSound()
|
||||
end
|
||||
function this:Hide()
|
||||
isShown = false
|
||||
GetView().Container.Parent = nil
|
||||
ScreenManager:DefaultCancelFade(self.TransitionTweens)
|
||||
self.TransitionTweens = nil
|
||||
end
|
||||
function this:Focus()
|
||||
inFocus = true
|
||||
if self.SavedSelectedObject and self.SavedSelectedObject:IsDescendantOf(GetView().Container) then
|
||||
Utility.SetSelectedCoreObject(self.SavedSelectedObject)
|
||||
else
|
||||
Utility.SetSelectedCoreObject(self:GetDefaultSelectionObject())
|
||||
end
|
||||
|
||||
ContextActionService:BindCoreAction("ReturnFromScreen",
|
||||
function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
CloseScreen()
|
||||
end
|
||||
end,
|
||||
false, Enum.KeyCode.ButtonB)
|
||||
end
|
||||
function this:RemoveFocus()
|
||||
inFocus = false
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
if selectedObject and selectedObject:IsDescendantOf(GetView().Container) then
|
||||
self.SavedSelectedObject = selectedObject
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
end
|
||||
ContextActionService:UnbindCoreAction("ReturnFromScreen")
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBaseScreen
|
||||
@@ -0,0 +1,211 @@
|
||||
--[[
|
||||
// BaseSignInScreen.lua
|
||||
|
||||
// Creates a base screen to be used for account linking and sign in
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
|
||||
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
|
||||
local TextBox = require(ShellModules:FindFirstChild('TextBox'))
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox'))
|
||||
|
||||
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
||||
|
||||
local TERMS_OF_SERVICE_URI = "https://en.help.roblox.com/hc/en-us/articles/205358110"
|
||||
|
||||
local function createBaseAccountScreen()
|
||||
local this = BaseScreen()
|
||||
|
||||
local DefaultButtonColor = GlobalSettings.GreyButtonColor
|
||||
local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor
|
||||
local DefaultButtonTextColor = GlobalSettings.WhiteTextColor
|
||||
local SelectedButtonTextColor = GlobalSettings.TextSelectedColor
|
||||
|
||||
local ScreenDivide = Utility.Create'Frame'
|
||||
{
|
||||
Name = "ScreenDivide";
|
||||
Size = UDim2.new(0, 2, 0, 610);
|
||||
Position = UDim2.new(0, 822, 0.5, -305);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundColor3 = GlobalSettings.PageDivideColor;
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
local TermsOfServiceText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = "TermsOfServiceText";
|
||||
Size = UDim2.new(0, 740, 0, 128);
|
||||
Position = UDim2.new(0, 16, 0, 835);
|
||||
BackgroundTransparency = 1;
|
||||
Font = GlobalSettings.LightFont;
|
||||
FontSize = GlobalSettings.ButtonSize;
|
||||
TextColor3 = GlobalSettings.WhiteTextColor;
|
||||
TextXAlignment = Enum.TextXAlignment.Left;
|
||||
TextYAlignment = Enum.TextYAlignment.Top;
|
||||
TextWrapped = true;
|
||||
Text = Strings:LocalizedString("ToSInfoLinkPhrase");
|
||||
Parent = this.Container;
|
||||
}
|
||||
|
||||
local UsernameObject = TextBox(UDim2.new(0, 656, 0, 84))
|
||||
UsernameObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 334))
|
||||
UsernameObject:SetSpacing(Vector2.new(20, 0))
|
||||
UsernameObject:SetParent(this.Container)
|
||||
UsernameObject:SetClipsDescendants(true)
|
||||
local UsernameTextBox = UsernameObject:GetTextBox()
|
||||
local UsernameSelection = UsernameObject:GetContainer()
|
||||
this.UsernameObject = UsernameObject
|
||||
this.UsernameTextBox = UsernameTextBox
|
||||
this.UsernameSelection = UsernameSelection
|
||||
|
||||
local PasswordObject = TextBox(UDim2.new(0, 656, 0, 84))
|
||||
PasswordObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 484))
|
||||
PasswordObject:SetSpacing(Vector2.new(20, 0))
|
||||
PasswordObject:SetParent(this.Container)
|
||||
PasswordObject:SetClipsDescendants(true)
|
||||
local PasswordTextBox = PasswordObject:GetTextBox()
|
||||
local PasswordSelection = PasswordObject:GetContainer()
|
||||
this.PasswordObject = PasswordObject
|
||||
this.PasswordTextBox = PasswordTextBox
|
||||
this.PasswordSelection = PasswordSelection
|
||||
|
||||
local function CreateSlicedTextButton(name, text, position)
|
||||
name = name or ""
|
||||
|
||||
local newButton = Utility.Create'ImageButton'
|
||||
{
|
||||
Name = name .. "Button";
|
||||
Size = UDim2.new(0, 376, 0, 64);
|
||||
Position = position or UDim2.new();
|
||||
BackgroundTransparency = 1;
|
||||
ImageColor3 = DefaultButtonColor;
|
||||
Image = GlobalSettings.RoundCornerButtonImage;
|
||||
ScaleType = Enum.ScaleType.Slice;
|
||||
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
|
||||
ZIndex = 2;
|
||||
Parent = this.Container;
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
AssetManager.CreateShadow(1)
|
||||
}
|
||||
local newText = Utility.Create'TextLabel'
|
||||
{
|
||||
Name = name .. "Text";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
BackgroundTransparency = 1;
|
||||
Font = GlobalSettings.RegularFont;
|
||||
FontSize = GlobalSettings.ButtonSize;
|
||||
TextColor3 = DefaultButtonTextColor;
|
||||
Text = text;
|
||||
ZIndex = 2;
|
||||
Parent = newButton;
|
||||
}
|
||||
|
||||
newButton.SelectionGained:connect(function()
|
||||
newButton.ImageColor3 = SelectedButtonColor
|
||||
newText.TextColor3 = SelectedButtonTextColor
|
||||
end)
|
||||
newButton.SelectionLost:connect(function()
|
||||
newButton.ImageColor3 = DefaultButtonColor
|
||||
newText.TextColor3 = DefaultButtonTextColor
|
||||
end)
|
||||
|
||||
return newButton, newText
|
||||
end
|
||||
|
||||
local function TryLaunchUri(uri)
|
||||
local success, msg = pcall(function()
|
||||
assert(not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows, "Can't use in studio")
|
||||
PlatformService:LaunchPlatformUri(uri)
|
||||
end)
|
||||
if not success then
|
||||
Utility.DebugLog(string.format("PlatformService:LaunchPlatformUri failed to launch uri: %s, for reason: %s", uri, msg))
|
||||
end
|
||||
end
|
||||
|
||||
local SignInButton, SignInText = CreateSlicedTextButton("SignIn",
|
||||
Strings:LocalizedString("SignInPhrase"),
|
||||
UDim2.new(0, PasswordSelection.Position.X.Offset, 0,
|
||||
PasswordSelection.Position.Y.Offset + PasswordSelection.Size.Y.Offset + 66))
|
||||
Utility.ResizeButtonWithText(SignInButton, SignInText, GlobalSettings.TextHorizontalPadding)
|
||||
this.SignInButton = SignInButton
|
||||
|
||||
local tosButtonY = SignInButton.Position.Y.Offset -- + SignInButton.Size.Y.Offset + 30
|
||||
local ToSButton, ToSText = CreateSlicedTextButton("ToS",
|
||||
Strings:LocalizedString("ToSPhrase"),
|
||||
UDim2.new(0, SignInButton.Position.X.Offset + SignInButton.Size.X.Offset + 10,
|
||||
0, tosButtonY))
|
||||
ToSButton.Size = UDim2.new(0, 270, 0, ToSButton.Size.Y.Offset)
|
||||
Utility.ResizeButtonWithText(ToSButton, ToSText, GlobalSettings.TextHorizontalPadding)
|
||||
|
||||
local tosButtonLastPress = tick() - 1
|
||||
ToSButton.MouseButton1Click:connect(function()
|
||||
if tick() - tosButtonLastPress < 1 then return end
|
||||
tosButtonLastPress = tick()
|
||||
TryLaunchUri(TERMS_OF_SERVICE_URI)
|
||||
end)
|
||||
--[[
|
||||
local PrivacyButton, PrivacyText = CreateSlicedTextButton("Privacy",
|
||||
Strings:LocalizedString("PrivacyPhrase"),
|
||||
UDim2.new(0, ToSButton.Position.X.Offset + ToSButton.Size.X.Offset + 5,
|
||||
0, tosButtonY))
|
||||
PrivacyButton.MouseButton1Click:connect(function()
|
||||
TryLaunchUri("http://www.roblox.com/info/privacy")
|
||||
end)
|
||||
--]]
|
||||
|
||||
|
||||
-- Override selection - issue with selections remembering their last selection, so in some cases
|
||||
-- the password selection become unselectable. I've talk to Ben about this and we're going to fix it
|
||||
-- TODO: Remove this when selection memory is fixed
|
||||
UsernameSelection.NextSelectionDown = PasswordSelection
|
||||
SignInButton.NextSelectionUp = PasswordSelection
|
||||
|
||||
|
||||
local DescriptionText = ScrollingTextBox(UDim2.new(0, 740, 0, 460), UDim2.new(0, 16, 0, 334), this.Container)
|
||||
DescriptionText:SetFont(GlobalSettings.RegularFont)
|
||||
DescriptionText:SetFontSize(GlobalSettings.TitleSize)
|
||||
|
||||
DescriptionText.OnSelectableChanged:connect(function(value)
|
||||
local descriptionSelectionObject = DescriptionText:GetSelectableObject()
|
||||
if value == true then
|
||||
descriptionSelectionObject.NextSelectionRight = SignInButton
|
||||
descriptionSelectionObject.NextSelectionLeft = descriptionSelectionObject
|
||||
descriptionSelectionObject.NextSelectionUp = descriptionSelectionObject
|
||||
descriptionSelectionObject.NextSelectionDown = descriptionSelectionObject
|
||||
else
|
||||
descriptionSelectionObject.NextSelectionRight = nil
|
||||
descriptionSelectionObject.NextSelectionLeft = nil
|
||||
descriptionSelectionObject.NextSelectionUp = nil
|
||||
descriptionSelectionObject.NextSelectionDown = nil
|
||||
end
|
||||
end)
|
||||
|
||||
--[[ Public API ]]--
|
||||
--Override
|
||||
function this:GetDefaultSelectionObject()
|
||||
return UsernameSelection
|
||||
end
|
||||
|
||||
function this:SetDescriptionText(newText)
|
||||
DescriptionText:SetText(newText)
|
||||
end
|
||||
|
||||
function this:SetButtonText(newText)
|
||||
SignInText.Text = newText
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBaseAccountScreen
|
||||
@@ -0,0 +1,136 @@
|
||||
--[[
|
||||
// BaseTile.lua
|
||||
|
||||
// Created by Kip Turner
|
||||
// Copyright Roblox 2015
|
||||
]]
|
||||
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
|
||||
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
|
||||
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
|
||||
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
|
||||
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
|
||||
|
||||
local ACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(45/255, 96/255, 128/255)
|
||||
local INACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(39/255, 69/255, 82/255) --Color3.new(106/255, 120/255, 129/255)
|
||||
|
||||
local function createBaseTileContainer()
|
||||
local this = {}
|
||||
this.focused = false
|
||||
this.active = false
|
||||
|
||||
|
||||
local avatarItemContainer = Utility.Create'ImageButton'
|
||||
{
|
||||
Name = 'AvatarItemContainer';
|
||||
Size = UDim2.new(0,220,0,220);
|
||||
BorderSizePixel = 0;
|
||||
BackgroundTransparency = 0;
|
||||
BackgroundColor3 = this.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR;
|
||||
AutoButtonColor = false;
|
||||
ClipsDescendants = true;
|
||||
ZIndex = 2;
|
||||
AssetManager.CreateShadow(1);
|
||||
|
||||
SoundManager:CreateSound('MoveSelection');
|
||||
}
|
||||
|
||||
local myPopText = PopupText(avatarItemContainer, '')
|
||||
myPopText:SetZIndex(3)
|
||||
|
||||
|
||||
local avatarImage = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "AvatarImage";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
BackgroundTransparency = 1;
|
||||
ZIndex = 2;
|
||||
Parent = avatarItemContainer;
|
||||
}
|
||||
|
||||
local equippedCheckmark = Utility.Create'ImageLabel'
|
||||
{
|
||||
Name = "EquippedCheckmark";
|
||||
Size = UDim2.new(1, 0, 1, 0);
|
||||
Position = UDim2.new(0, 0, 0, 0);
|
||||
BackgroundTransparency = 1;
|
||||
ZIndex = 3;
|
||||
Visible = false;
|
||||
Image = 'rbxasset://textures/ui/Shell/Icons/EquippedOverlay.png';
|
||||
Parent = avatarItemContainer;
|
||||
}
|
||||
|
||||
this.AvatarItemContainer = avatarItemContainer
|
||||
this.AvatarImage = avatarImage
|
||||
this.EquippedCheckmark = equippedCheckmark
|
||||
|
||||
local function colorizeImage(newColor, duration)
|
||||
duration = duration or 0.2
|
||||
Utility.PropertyTweener(avatarImage, 'ImageColor3', avatarImage.ImageColor3.r, newColor, duration,
|
||||
function(...) local scalar = Utility.EaseOutQuad(...) return Color3.new(scalar, scalar, scalar) end, true)
|
||||
end
|
||||
|
||||
function this:UpdateEquipButton()
|
||||
end
|
||||
|
||||
function this:ColorizeImage(...)
|
||||
colorizeImage(...)
|
||||
end
|
||||
|
||||
function this:SetPopupText(newText)
|
||||
myPopText:SetText(newText)
|
||||
end
|
||||
|
||||
function this:SetImage(imgUrl)
|
||||
avatarImage.Image = imgUrl
|
||||
end
|
||||
|
||||
function this:GetGuiObject()
|
||||
return avatarItemContainer
|
||||
end
|
||||
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseTileId')}
|
||||
end
|
||||
|
||||
function this:GetPackageInfo()
|
||||
end
|
||||
|
||||
function this:OnClick()
|
||||
end
|
||||
|
||||
function this:SetActive(isActive)
|
||||
self.active = isActive
|
||||
avatarItemContainer.BackgroundColor3 = self.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR;
|
||||
end
|
||||
|
||||
function this:Select()
|
||||
end
|
||||
|
||||
function this:Show()
|
||||
end
|
||||
|
||||
function this:Hide()
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
self.focused = true
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
self.focused = false
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createBaseTileContainer
|
||||
@@ -0,0 +1,267 @@
|
||||
--[[
|
||||
// BatchThumbnailLoader.lua
|
||||
|
||||
// Creates a batch thumbnail loader object that handles the loading
|
||||
// of mutiple thumb nails.
|
||||
|
||||
// Thumbnails may not yet be generated, so this will retry and
|
||||
// assign the final thumbnail
|
||||
]]
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local ContentProvider = game:GetService('ContentProvider')
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Http = require(Modules.Shell.Http)
|
||||
local LoadingWidget = require(Modules.Shell.LoadingWidget)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
|
||||
local RETRIES = 6
|
||||
local FADE_IN_TIME = 0.25
|
||||
local TEMPLATE_DECAL = Instance.new("Decal")
|
||||
local BATCH_LIMIT = 30
|
||||
local function preloadThumbnailAsync(assetId)
|
||||
TEMPLATE_DECAL.Texture = assetId
|
||||
ContentProvider:PreloadAsync({ TEMPLATE_DECAL })
|
||||
end
|
||||
|
||||
local BatchThumbnailLoader = {}
|
||||
--[[ Sizes ]]--
|
||||
BatchThumbnailLoader.Sizes =
|
||||
{
|
||||
Small = Vector2.new(100, 100);
|
||||
Medium = Vector2.new(250, 250);
|
||||
Large = Vector2.new(576, 324);
|
||||
}
|
||||
|
||||
function BatchThumbnailLoader:Init()
|
||||
self._active = true
|
||||
self.isLoading = false
|
||||
self.isRequesting = false
|
||||
self.imageObjects = {}
|
||||
self.loadingProps = {}
|
||||
self.newRequests = nil
|
||||
self.requests = nil
|
||||
self.uriMap = {}
|
||||
self.loaders = {}
|
||||
self.activeTry = 0
|
||||
end
|
||||
|
||||
local function createThumbnailKey(token, size)
|
||||
return tostring(token)..tostring(size)
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:CreateThumbnail(imageObject, token, size, showSpinner, fadeImage, spinnerProperties)
|
||||
spinnerProperties = spinnerProperties or {}
|
||||
if showSpinner == nil then
|
||||
showSpinner = true
|
||||
end
|
||||
if fadeImage == nil then
|
||||
fadeImage = true
|
||||
end
|
||||
local requestLoad = false
|
||||
local thumbnailKey = createThumbnailKey(token, size)
|
||||
if not self._active then
|
||||
BatchThumbnailLoader:Init()
|
||||
end
|
||||
if self.isLoading == false then
|
||||
requestLoad = true
|
||||
if self.newRequests == nil then
|
||||
self.newRequests = {}
|
||||
end
|
||||
end
|
||||
if self.newRequests[size] == nil then
|
||||
self.newRequests[size] = {}
|
||||
end
|
||||
table.insert(self.newRequests[size], token)
|
||||
|
||||
if self.imageObjects[thumbnailKey] == nil then
|
||||
self.imageObjects[thumbnailKey] = {}
|
||||
end
|
||||
self.imageObjects[thumbnailKey][imageObject] = imageObject
|
||||
|
||||
--Create the loading spinner.
|
||||
self.loadingProps[imageObject] = {}
|
||||
self.loadingProps[imageObject].showSpinner = showSpinner
|
||||
self.loadingProps[imageObject].fadeImage = fadeImage
|
||||
self.loadingProps[imageObject].spinnerProperties = spinnerProperties
|
||||
|
||||
if self.uriMap[thumbnailKey] ~= nil then
|
||||
spawn(function()
|
||||
preloadThumbnailAsync(self.uriMap[thumbnailKey])
|
||||
BatchThumbnailLoader:_loadImage(imageObject, self.uriMap[thumbnailKey])
|
||||
end)
|
||||
return
|
||||
else
|
||||
BatchThumbnailLoader:_loadImage(imageObject, nil)
|
||||
end
|
||||
|
||||
if requestLoad then
|
||||
spawn(function()
|
||||
BatchThumbnailLoader:BatchLoadThumbnailsAsync()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:_loadImage(imageObject, imageUri)
|
||||
local loaded = imageUri ~= nil
|
||||
if imageObject == nil then
|
||||
return
|
||||
end
|
||||
imageObject.Image = ""
|
||||
if self.loadingProps[imageObject].fadeImage then
|
||||
local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 1, 0,
|
||||
Utility.EaseInOutQuad, true, nil)
|
||||
end
|
||||
if not loaded then
|
||||
if self.loadingProps[imageObject].showSpinner then
|
||||
self.loadingProps[imageObject].spinnerProperties['Parent'] = self.loadingProps[imageObject].spinnerProperties['Parent']
|
||||
or imageObject
|
||||
self.loaders[imageObject] = LoadingWidget(self.loadingProps[imageObject].spinnerProperties, nil, true)
|
||||
end
|
||||
else
|
||||
if self.loaders[imageObject] then
|
||||
self.loaders[imageObject]:Cleanup()
|
||||
self.loaders[imageObject] = nil
|
||||
end
|
||||
imageObject.Image = imageUri
|
||||
if self.loadingProps[imageObject].fadeImage then
|
||||
local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 0, FADE_IN_TIME,
|
||||
Utility.EaseInOutQuad, true, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:_loadThumbnails(tokens, size)
|
||||
local batchTokens;
|
||||
local reloadBatchList = {}
|
||||
local batchCount = 0
|
||||
while #tokens > 0 do
|
||||
batchCount = batchCount + 1
|
||||
batchTokens = {}
|
||||
if #tokens <= BATCH_LIMIT then
|
||||
batchTokens = tokens
|
||||
tokens = {}
|
||||
else
|
||||
for _=1, BATCH_LIMIT do
|
||||
local newToken = table.remove(tokens)
|
||||
table.insert(batchTokens, newToken)
|
||||
end
|
||||
end
|
||||
local result = Http.GetAssetThumbnailBatchAsync(batchTokens, size.x, size.y)
|
||||
if result then
|
||||
for i,v in ipairs(result) do
|
||||
local token = batchTokens[i]
|
||||
local isFinal = v["final"]
|
||||
if isFinal then
|
||||
local uri = v["url"]
|
||||
local thumbnailKey = createThumbnailKey(token, size)
|
||||
self.uriMap[thumbnailKey] = uri
|
||||
for _,imageObj in pairs(self.imageObjects[thumbnailKey]) do
|
||||
spawn(function()
|
||||
preloadThumbnailAsync(self.uriMap[thumbnailKey])
|
||||
BatchThumbnailLoader:_loadImage(imageObj, self.uriMap[thumbnailKey])
|
||||
end)
|
||||
end
|
||||
else
|
||||
table.insert(reloadBatchList, token)
|
||||
end
|
||||
end
|
||||
else
|
||||
for _,token in ipairs(batchTokens) do
|
||||
table.insert(reloadBatchList, token)
|
||||
end
|
||||
end
|
||||
end
|
||||
return reloadBatchList
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:_batchLoadThumbnails(batchList)
|
||||
if batchList == nil then
|
||||
return nil
|
||||
end
|
||||
local requireReload = false
|
||||
local newBatchList = {};
|
||||
for size, tokens in pairs(batchList) do
|
||||
local reloadBatchList = BatchThumbnailLoader:_loadThumbnails(tokens, size)
|
||||
if #reloadBatchList ~= 0 then
|
||||
if newBatchList[size] == nil then
|
||||
newBatchList[size] = {}
|
||||
end
|
||||
requireReload = true
|
||||
newBatchList[size] = reloadBatchList
|
||||
end
|
||||
end
|
||||
if not requireReload then
|
||||
return nil
|
||||
end
|
||||
return newBatchList
|
||||
end
|
||||
|
||||
-- marge new array into old array
|
||||
local function mergeArray(newArray, oldArray)
|
||||
local mergedArray = {}
|
||||
if oldArray ~= nil then
|
||||
for size, tokens in pairs(oldArray) do
|
||||
mergedArray[size] = {}
|
||||
for _,token in ipairs(tokens) do
|
||||
table.insert(mergedArray[size], token)
|
||||
end
|
||||
end
|
||||
end
|
||||
if newArray ~= nil then
|
||||
for size, tokens in pairs(newArray) do
|
||||
if mergedArray[size] == nil then
|
||||
mergedArray[size] = {}
|
||||
end
|
||||
for _,token in ipairs(tokens) do
|
||||
table.insert(mergedArray[size], token)
|
||||
end
|
||||
end
|
||||
end
|
||||
return mergedArray
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:_tryBatchLoadThumbnails(startTime)
|
||||
local activeTry = startTime
|
||||
local tryCount = 1
|
||||
--retries until the max number of attempts. Or if this is still the most recent batch.
|
||||
while tryCount <= RETRIES and activeTry == self.activeTry do
|
||||
self.isRequesting = true
|
||||
self.requests = BatchThumbnailLoader:_batchLoadThumbnails(self.requests)
|
||||
self.isRequesting = false
|
||||
if self.requests == nil then
|
||||
break
|
||||
end
|
||||
tryCount = tryCount + 1
|
||||
wait(tryCount ^ 2)
|
||||
end
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:BatchLoadThumbnailsAsync()
|
||||
repeat
|
||||
wait(0.1)
|
||||
until self.isRequesting == false and self.isLoading == false
|
||||
self.isLoading = true
|
||||
if self.newRequests == nil then
|
||||
-- No new requests.
|
||||
self.isLoading = false
|
||||
return
|
||||
end
|
||||
--Merge the new list of tokens to load.
|
||||
self.requests = mergeArray(self.newRequests, self.requests)
|
||||
self.newRequests = nil
|
||||
self.isLoading = false
|
||||
if self.requests ~= nil then
|
||||
self.activeTry = tick()
|
||||
BatchThumbnailLoader:_tryBatchLoadThumbnails(self.activeTry)
|
||||
end
|
||||
end
|
||||
|
||||
function BatchThumbnailLoader:SetTransparency(value, imageObject)
|
||||
if self.loaders[imageObject] ~= nil then
|
||||
self.loaders[imageObject]:SetTransparency(value)
|
||||
end
|
||||
end
|
||||
|
||||
return BatchThumbnailLoader
|
||||
@@ -0,0 +1,48 @@
|
||||
local function CreateCacheData(data, version, getRefreshIntervalFunc, refreshFunc)
|
||||
local this = {}
|
||||
this.Data = data
|
||||
this.Version = version
|
||||
this.GetRefreshIntervalFunc = getRefreshIntervalFunc
|
||||
this.RefreshFunc = refreshFunc
|
||||
local refreshDebounce = false
|
||||
|
||||
--Use new data to update, usually we do this at intervals in BG
|
||||
function this:Update(newCached)
|
||||
if newCached and newCached.Version then
|
||||
if self.Version <= newCached.Version then
|
||||
self.Version = newCached.Version
|
||||
self.Data = newCached.Data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--Refresh the data, usually we will call the Refresh when some data is required
|
||||
function this:Refresh(forceRefresh)
|
||||
if self.RefreshFunc and type(self.RefreshFunc) == 'function' then
|
||||
while refreshDebounce do
|
||||
wait()
|
||||
end
|
||||
|
||||
--Get RefreshInterval
|
||||
local RefreshInterval = nil
|
||||
if self.GetRefreshIntervalFunc and type(self.GetRefreshIntervalFunc) == 'function' then
|
||||
RefreshInterval = tonumber(self.GetRefreshIntervalFunc())
|
||||
end
|
||||
|
||||
if forceRefresh or not self.Version or not RefreshInterval or (tick() - self.Version >= RefreshInterval) then
|
||||
refreshDebounce = true
|
||||
--RefreshFunc will update data, and return whether the refresh was valid
|
||||
if self.RefreshFunc(self.Data) then
|
||||
self.Version = tick()
|
||||
else --this refresh fails, we needs to reset Version to ensure next RefreshFunc call
|
||||
self.Version = nil
|
||||
end
|
||||
refreshDebounce = false
|
||||
end
|
||||
end
|
||||
end
|
||||
return this
|
||||
end
|
||||
|
||||
|
||||
return CreateCacheData
|
||||
@@ -0,0 +1,404 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
local Lighting = game:GetService('Lighting')
|
||||
|
||||
local BackgroundTintColor = Color3.new(0.0784, 0.1686, 0.2353)
|
||||
|
||||
local ColorCorrection = Utility.Create'ColorCorrectionEffect'
|
||||
{
|
||||
Brightness = 0.3;
|
||||
Contrast = 0.5;
|
||||
Saturation = -1;
|
||||
TintColor = BackgroundTintColor;
|
||||
Enabled = true;
|
||||
Parent = Lighting;
|
||||
}
|
||||
|
||||
local Blur = Utility.Create'BlurEffect'
|
||||
{
|
||||
Size = 10;
|
||||
Enabled = true;
|
||||
Parent = Lighting;
|
||||
}
|
||||
|
||||
local Bloom = Utility.Create'BloomEffect'
|
||||
{
|
||||
Intensity = 0.05;
|
||||
Size = 24;
|
||||
Threshold = 0.95;
|
||||
Enabled = true;
|
||||
Parent = Lighting;
|
||||
}
|
||||
|
||||
local function GetCameraParts(model)
|
||||
local parts = {}
|
||||
for i, part in pairs(model:GetChildren()) do
|
||||
parts[tonumber(part.Name:sub(4, -1))] = part
|
||||
part.Transparency = 1
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
local myCameraSets = nil
|
||||
|
||||
local function GetCameraSets()
|
||||
if myCameraSets == nil then
|
||||
myCameraSets = {}
|
||||
for k, f in pairs(workspace:WaitForChild("Cameras"):GetChildren()) do
|
||||
myCameraSets[f.Name] = GetCameraParts(f)
|
||||
end
|
||||
end
|
||||
return myCameraSets
|
||||
end
|
||||
|
||||
|
||||
local function onCameraChanged()
|
||||
if workspace.CurrentCamera then
|
||||
workspace.CurrentCamera.CameraType = 'Scriptable'
|
||||
end
|
||||
end
|
||||
|
||||
workspace:GetPropertyChangedSignal('CurrentCamera'):connect(function()
|
||||
onCameraChanged()
|
||||
end)
|
||||
onCameraChanged()
|
||||
|
||||
|
||||
local cameraMoveCn = nil
|
||||
|
||||
local gamepadInput = Vector2.new(0, 0)
|
||||
|
||||
|
||||
|
||||
local CameraManager = {}
|
||||
|
||||
function CameraManager:EnableCameraControl()
|
||||
cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn)
|
||||
cameraMoveCn = UserInputService.InputChanged:connect(function(input)
|
||||
if input.KeyCode == Enum.KeyCode.Thumbstick2 then
|
||||
gamepadInput = input.Position or gamepadInput
|
||||
gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function CameraManager:DisableCameraControl()
|
||||
cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn)
|
||||
gamepadInput = Vector2.new(0, 0)
|
||||
end
|
||||
|
||||
|
||||
local getGamepadInputCFrame;
|
||||
do
|
||||
local gamepadInputLerping = Vector2.new(0, 0)
|
||||
local timestamp0 = tick()
|
||||
function getGamepadInputCFrame()
|
||||
local timestamp1 = tick()
|
||||
local deltaTime = timestamp1 - timestamp0
|
||||
timestamp0 = timestamp1
|
||||
local unit = 0.125 ^ deltaTime
|
||||
gamepadInputLerping = gamepadInputLerping * unit + gamepadInput * (1 - unit)
|
||||
return CFrame.new(
|
||||
gamepadInputLerping.X/8, gamepadInputLerping.Y/8, 0) * CFrame.Angles(0, -gamepadInputLerping.X / 12, 0) * CFrame.Angles(gamepadInputLerping.Y / 12, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local defaultCFrame = CFrame.new();
|
||||
|
||||
local function defaultGetFrameInfo()
|
||||
return {
|
||||
CFrame = defaultCFrame;
|
||||
Contrast = 0.5;
|
||||
Saturation = -1;
|
||||
TintColor = BackgroundTintColor;
|
||||
BlurSize = 10;
|
||||
}
|
||||
end
|
||||
|
||||
local getFrameInfo = defaultGetFrameInfo
|
||||
|
||||
|
||||
local function startEternalRenderStep()
|
||||
RunService:BindToRenderStep("CameraScriptCutsceneLerp", Enum.RenderPriority.Camera.Value,
|
||||
function()
|
||||
local info = getFrameInfo()
|
||||
workspace.CurrentCamera.CoordinateFrame = info.CFrame
|
||||
ColorCorrection.Contrast = info.Contrast
|
||||
ColorCorrection.Saturation = info.Saturation
|
||||
ColorCorrection.TintColor = info.TintColor
|
||||
Blur.Size = info.BlurSize
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
local function transitionToCameraAnimator(cameraAnimator, transitionDuration, targetBrightness, targetBlurEnabled, targetBloomEnabled)
|
||||
Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness,
|
||||
-1, transitionDuration, Utility.EaseInOutQuad, true,
|
||||
function()
|
||||
getFrameInfo = cameraAnimator:get_getFrameInfo()
|
||||
Blur.Enabled = targetBlurEnabled
|
||||
Bloom.Enabled = targetBloomEnabled
|
||||
Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness,
|
||||
targetBrightness, transitionDuration, Utility.EaseInOutQuad, true)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
local function CFrameBezierLerp(cframes, t)
|
||||
local cframes2 = {}
|
||||
for i = 1, #cframes - 1 do
|
||||
cframes2[i] = cframes[i]:lerp(cframes[i + 1], t)
|
||||
end
|
||||
if #cframes2 == 1 then
|
||||
return cframes2[1]
|
||||
end
|
||||
return CFrameBezierLerp(cframes2, t)
|
||||
end
|
||||
|
||||
|
||||
local function getCFrameList(cameras)
|
||||
local cframes = {}
|
||||
for i = 1, #cameras do
|
||||
cframes[i] = cameras[i].CFrame
|
||||
end
|
||||
return cframes
|
||||
end
|
||||
|
||||
|
||||
local function CameraSeriesAnimator(cameraSeries, length)
|
||||
local timestamp0 = 0
|
||||
local cameraIndex = 0
|
||||
local cframes = nil
|
||||
local isRunning = false
|
||||
local tweenTable = {}
|
||||
|
||||
local animationInfo = {
|
||||
Contrast = -1;
|
||||
BlurSize = 50;
|
||||
}
|
||||
|
||||
local function myGetFrameInfo()
|
||||
local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length)
|
||||
|
||||
return {
|
||||
CFrame = CFrameBezierLerp(cframes, t) * getGamepadInputCFrame();
|
||||
Contrast = animationInfo.Contrast;
|
||||
Saturation = -1;
|
||||
TintColor = BackgroundTintColor;
|
||||
BlurSize = animationInfo.BlurSize;
|
||||
}
|
||||
end
|
||||
|
||||
local function advance()
|
||||
if not isRunning then
|
||||
return
|
||||
end
|
||||
cameraIndex = cameraIndex + 1
|
||||
if cameraIndex > #cameraSeries then
|
||||
cameraIndex = 1
|
||||
end
|
||||
cameraSeries[cameraIndex].init()
|
||||
cframes = getCFrameList(cameraSeries[cameraIndex].cameras)
|
||||
timestamp0 = tick()
|
||||
end
|
||||
|
||||
runCameraSeries = function(transitionDuration)
|
||||
local timePassed = tick() - timestamp0
|
||||
for i = #tweenTable, 1, -1 do
|
||||
if tweenTable[i]:IsFinished() then
|
||||
table.remove(tweenTable, i)
|
||||
end
|
||||
end
|
||||
|
||||
if timePassed >= length then
|
||||
local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, transitionDuration, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, newTween)
|
||||
newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, transitionDuration, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, newTween)
|
||||
advance()
|
||||
elseif timePassed >= length - transitionDuration and #tweenTable == 0 then
|
||||
local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, -1, transitionDuration, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, newTween)
|
||||
newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 50, transitionDuration, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, newTween)
|
||||
end
|
||||
end
|
||||
|
||||
local function startCameraSeries()
|
||||
local tween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, 4, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, tween)
|
||||
tween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, 4, Utility.EaseInOutQuad, true)
|
||||
table.insert(tweenTable, tween)
|
||||
advance()
|
||||
end
|
||||
|
||||
local this = {}
|
||||
|
||||
function this:Start()
|
||||
isRunning = true
|
||||
startCameraSeries()
|
||||
RunService:BindToRenderStep("runCameraSeries", Enum.RenderPriority.Camera.Value + 1, function()
|
||||
runCameraSeries(1.7)
|
||||
end)
|
||||
end
|
||||
|
||||
function this:Stop()
|
||||
isRunning = false
|
||||
for i = #tweenTable, 1, -1 do
|
||||
local tween = table.remove(tweenTable, i)
|
||||
tween:Finish()
|
||||
end
|
||||
animationInfo = {
|
||||
Contrast = 0.5;
|
||||
BlurSize = 10;
|
||||
}
|
||||
RunService:UnbindFromRenderStep("runCameraSeries")
|
||||
end
|
||||
|
||||
function this:get_getFrameInfo()
|
||||
return myGetFrameInfo
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
|
||||
local function CameraZoomAnimator(cframes, length, pathFunc)
|
||||
local timestamp0 = 0
|
||||
|
||||
local this = {}
|
||||
|
||||
function this:Reset()
|
||||
timestamp0 = tick()
|
||||
end
|
||||
|
||||
function this:Update(inCframes, length)
|
||||
cframes = inCframes
|
||||
timestamp0 = tick()
|
||||
end
|
||||
|
||||
local function myGetFrameInfo()
|
||||
local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length)
|
||||
|
||||
return {
|
||||
CFrame = CFrameBezierLerp(cframes, pathFunc(t));
|
||||
Contrast = 0.35;
|
||||
Saturation = 0.175;
|
||||
TintColor = Color3.fromRGB(255, 255, 255);
|
||||
BlurSize = 1;
|
||||
}
|
||||
end
|
||||
|
||||
function this:get_getFrameInfo()
|
||||
return myGetFrameInfo
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
|
||||
pcall(function()
|
||||
local function recurse(model)
|
||||
local children = model:GetChildren()
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
if child:IsA("BasePart") then
|
||||
child.Locked = true
|
||||
child.Anchored = true
|
||||
child.CanCollide = false
|
||||
end
|
||||
recurse(child)
|
||||
end
|
||||
end
|
||||
recurse(workspace)
|
||||
end)
|
||||
|
||||
|
||||
local ZoneManager = require(script.Parent:WaitForChild("CameraManagerModules"):WaitForChild("ZoneManager"))
|
||||
|
||||
local avatarEditorCameraAnimator = nil
|
||||
local flythroughAnimator = nil
|
||||
|
||||
local avatarEditorZoomCFrames = {
|
||||
CFrame.new(
|
||||
10.2426682, 5.1197648, -30.9536419,
|
||||
-0.946675897, 0.123298854, -0.297661126,
|
||||
0.0000000, 0.92387563, 0.382692933,
|
||||
0.322187454, 0.36228618, -0.874610782),
|
||||
|
||||
CFrame.new(
|
||||
12.50625, 4.83650, -24.764325,
|
||||
-0.94241035, 0.0557777137, -0.329775006,
|
||||
0.0000000, 0.98599577, 0.166770056,
|
||||
0.334458828, 0.157165825, -0.92921263)
|
||||
}
|
||||
|
||||
function CameraManager:CameraMoveToAsync()
|
||||
local cameraSets = GetCameraSets()
|
||||
|
||||
startEternalRenderStep();
|
||||
|
||||
flythroughAnimator = CameraSeriesAnimator({
|
||||
{
|
||||
init = function() ZoneManager:SetZone("City") end;
|
||||
cameras = cameraSets["City"]
|
||||
},
|
||||
{
|
||||
init = function() ZoneManager:SetZone("Space") end;
|
||||
cameras = cameraSets["Space"]
|
||||
},
|
||||
{
|
||||
init = function() ZoneManager:SetZone("Volcano") end;
|
||||
cameras = cameraSets["Volcano"]
|
||||
},
|
||||
}, 60)
|
||||
|
||||
avatarEditorCameraAnimator = CameraZoomAnimator(avatarEditorZoomCFrames, 1.0, function(t)
|
||||
t = Utility.Clamp(0, 1, t)
|
||||
return t * (2 - t)
|
||||
end)
|
||||
|
||||
flythroughAnimator:Start()
|
||||
transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false)
|
||||
end
|
||||
|
||||
|
||||
function CameraManager:SwitchToFlyThrough()
|
||||
transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false)
|
||||
flythroughAnimator:Start()
|
||||
end
|
||||
|
||||
|
||||
function CameraManager:SwitchToAvatarEditor()
|
||||
flythroughAnimator:Stop()
|
||||
ZoneManager:SetZone("AvatarEditor")
|
||||
avatarEditorCameraAnimator:Reset()
|
||||
|
||||
transitionToCameraAnimator(avatarEditorCameraAnimator, 0.25, 0.02, false, true)
|
||||
end
|
||||
|
||||
local targetCFrame = avatarEditorZoomCFrames[2]
|
||||
|
||||
function CameraManager:UpdateAvatarEditorCamera(newCFrame)
|
||||
targetCFrame = newCFrame
|
||||
local frameInfo = avatarEditorCameraAnimator:get_getFrameInfo()()
|
||||
|
||||
avatarEditorCameraAnimator:Update(
|
||||
{
|
||||
frameInfo.CFrame,
|
||||
targetCFrame
|
||||
},
|
||||
0.5
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
return CameraManager
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
-- Clean up this file when remove FIntXboxAvatarEditorRolloutPercent2
|
||||
|
||||
-- Written by Tomarty, Copyright Roblox 2015
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
|
||||
|
||||
|
||||
local ZoneAnimator;
|
||||
|
||||
do
|
||||
|
||||
local animators = {}
|
||||
|
||||
local function GetAnimator(zone)
|
||||
if animators[zone] then
|
||||
return animators[zone]
|
||||
end
|
||||
local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone))
|
||||
if not moduleScript then
|
||||
return
|
||||
end
|
||||
local animator = require(moduleScript)
|
||||
animators[zone] = animator
|
||||
return animator
|
||||
end
|
||||
|
||||
|
||||
ZoneAnimator = {
|
||||
CurrentZone = nil;
|
||||
}
|
||||
|
||||
local connection;
|
||||
|
||||
function ZoneAnimator:SetZone(zone)
|
||||
if ZoneAnimator.CurrentZone == zone then
|
||||
return
|
||||
end
|
||||
ZoneAnimator.CurrentZone = zone
|
||||
if connection then
|
||||
connection:disconnect()
|
||||
connection = nil
|
||||
end
|
||||
local animator = GetAnimator(zone)
|
||||
if not animator then
|
||||
return
|
||||
end
|
||||
animator:SetEnabled(true)
|
||||
connection = {
|
||||
disconnect = function()
|
||||
animator:SetEnabled(false)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
local SkyboxManager;
|
||||
|
||||
do
|
||||
SkyboxManager = {}
|
||||
|
||||
local activeSkybox = nil
|
||||
function SkyboxManager:SetZone(id)
|
||||
if activeSkybox then
|
||||
activeSkybox.Parent = nil
|
||||
activeSkybox = nil
|
||||
end
|
||||
local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes")
|
||||
local skybox = Skyboxes:FindFirstChild(id or "default")
|
||||
if skybox then
|
||||
activeSkybox = skybox:Clone()
|
||||
local lighting = game:GetService("Lighting")
|
||||
skybox.Parent = lighting
|
||||
pcall(function()
|
||||
for k, v in pairs(skybox:GetChildren()) do
|
||||
lighting[v.Name] = v.Value
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
local ZoneManager = {
|
||||
Zone = nil;
|
||||
}
|
||||
|
||||
--local ZoneAnimator = require(script.ZoneAnimator)
|
||||
--local Skybox = require(script.Skybox)
|
||||
|
||||
local function setZoneInternal(zone)
|
||||
ZoneManager.Zone = zone
|
||||
SkyboxManager:SetZone(zone)
|
||||
ZoneAnimator:SetZone(zone)
|
||||
end
|
||||
|
||||
function ZoneManager:SetZone(zone)
|
||||
if zone == ZoneManager.Zone then
|
||||
return
|
||||
end
|
||||
if runService:IsRunning() then
|
||||
setZoneInternal(zone)
|
||||
else
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait() end
|
||||
setZoneInternal(zone)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return ZoneManager
|
||||
|
||||
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
|
||||
local Zones = workspace:FindFirstChild("Zones")
|
||||
local City = Zones and Zones:FindFirstChild("City")
|
||||
|
||||
local CFramer = {}; do
|
||||
function CFramer.GetAllParts(model, list)
|
||||
list = list or {}
|
||||
|
||||
if model:IsA("BasePart") then
|
||||
list[#list + 1] = model
|
||||
end
|
||||
|
||||
local children = model:GetChildren()
|
||||
for i = 1, #children do
|
||||
CFramer.GetAllParts(children[i], list)
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function CFramer.GetAllPartOffsets(parts, cframeBase)
|
||||
-- The offset is a part's cframe relative to cframeBase
|
||||
|
||||
local offsets = {}
|
||||
local cframeBase_inv = cframeBase:inverse()
|
||||
for i = 1, #parts do
|
||||
offsets[i] = cframeBase_inv * parts[i].CFrame
|
||||
end
|
||||
|
||||
return offsets
|
||||
end
|
||||
|
||||
function CFramer.newCFramer(parts, offsets)
|
||||
return function(cf)
|
||||
for i = 1, #parts do
|
||||
parts[i].CFrame = cf * offsets[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CFramer.newCFramerFromModel(model, cframeBase)
|
||||
local parts = CFramer.GetAllParts(model)
|
||||
local offsets = CFramer.GetAllPartOffsets(parts, cframeBase)
|
||||
return CFramer.newCFramer(parts, offsets)
|
||||
end
|
||||
end
|
||||
|
||||
local renderTrains;
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait(0.1) end
|
||||
|
||||
if not City then
|
||||
City = workspace:WaitForChild("Zones"):WaitForChild("City")
|
||||
end
|
||||
|
||||
local exampleTrain = City.Train:Clone()
|
||||
local function newTrain()
|
||||
local train = exampleTrain:Clone()
|
||||
train.Parent = workspace
|
||||
return CFramer.newCFramerFromModel(train.Parts, train.Track.CFrame)
|
||||
end
|
||||
|
||||
|
||||
local function newTrack(part, direction)
|
||||
if not direction then
|
||||
direction = math.random(0, 1) * 2 - 1
|
||||
end
|
||||
local length = part.Size.Y
|
||||
local cf = part.CFrame * CFrame.Angles(math.pi / 2 * (direction + 1), 0, 0)
|
||||
local cf0 = cf * CFrame.new(0, -length/2, 0)
|
||||
return function(train, time)
|
||||
local distance = time * 48
|
||||
if distance >= 0 and distance <= length then
|
||||
train(cf0 * CFrame.new(0, distance, 0))
|
||||
end
|
||||
end, part.Size.Y
|
||||
end
|
||||
|
||||
local train0 = newTrain()
|
||||
local train1 = newTrain()
|
||||
local train2 = newTrain()
|
||||
local train3 = newTrain()
|
||||
local train4 = newTrain()
|
||||
local train5 = newTrain()
|
||||
local train6 = newTrain()
|
||||
|
||||
local tracks = City.Tracks
|
||||
|
||||
local track0 = newTrack(tracks.TrackA1)--, 1)
|
||||
local track1 = newTrack(tracks.TrackA2)--, -1)
|
||||
local track2 = newTrack(tracks.TrackA3)--, 1)
|
||||
local track3 = newTrack(tracks.TrackB1)--, 1)
|
||||
local track4 = newTrack(tracks.TrackB2)--, -1)
|
||||
local track5 = newTrack(tracks.TrackC1)--, -1)
|
||||
local track6 = newTrack(tracks.TrackC2)--, 1)
|
||||
local track7 = newTrack(tracks.TrackC3)--, 1)
|
||||
|
||||
function renderTrains(timestamp)
|
||||
|
||||
track0(train0, timestamp % 15)
|
||||
track1(train1, (timestamp - 3) % 20)
|
||||
track2(train2, (timestamp - 5.5) % 18)
|
||||
|
||||
track3(train3, (timestamp - 5.5) % 25)
|
||||
|
||||
track6(train4, (timestamp - 2.5) % 30)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
local renderFlows;
|
||||
|
||||
spawn(function()
|
||||
|
||||
if not City then
|
||||
City = workspace:WaitForChild("Zones"):WaitForChild("City")
|
||||
end
|
||||
|
||||
|
||||
local function newFlow(model, flowLength)
|
||||
|
||||
local parts = {}
|
||||
local partParts = {}
|
||||
local partPartOffsets = {}
|
||||
local cframes = {}
|
||||
|
||||
for j, part in pairs(model:GetChildren()) do
|
||||
local i = tonumber(part.Name)
|
||||
parts[i] = part
|
||||
cframes[i] = part.CFrame
|
||||
local pParts = {}
|
||||
local pPartOffsets = {}
|
||||
for i, pPart in pairs(part:GetChildren()) do
|
||||
if pPart:IsA("BasePart") then
|
||||
pParts[#pParts + 1] = pPart
|
||||
pPartOffsets[#pPartOffsets + 1] = part.CFrame:inverse() * pPart.CFrame
|
||||
end
|
||||
end
|
||||
partParts[i] = pParts
|
||||
partPartOffsets[i] = pPartOffsets
|
||||
end
|
||||
|
||||
|
||||
local partCycle = 0
|
||||
local unitOffset = 0
|
||||
local timestamp0 = 0
|
||||
return function(timestamp1)
|
||||
if timestamp1 < timestamp0 then
|
||||
timestamp0 = timestamp1
|
||||
end
|
||||
local unit = (timestamp1 - timestamp0) / flowLength + unitOffset
|
||||
|
||||
if unit >= 1 then
|
||||
partCycle = (partCycle + math.floor(unit)) % #parts
|
||||
unit = unit % 1
|
||||
unitOffset = unit
|
||||
timestamp0 = timestamp1 -- - deltaTime * flowLength
|
||||
end
|
||||
|
||||
for x = 0, #parts - 2 do
|
||||
local i = (x - partCycle) % #cframes + 1
|
||||
local cf = cframes[x + 1]:lerp(cframes[x + 2], unit)
|
||||
parts[i].CFrame = cf
|
||||
local pParts = partParts[i]
|
||||
local pPartOffsets = partPartOffsets[i]
|
||||
for n = 1, #pParts do
|
||||
pParts[n].CFrame = cf * pPartOffsets[n]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local flow1 = newFlow(City.Pipe1.Flow, 0.5)
|
||||
local flow2 = newFlow(City.Pipe2.Flow, 0.65)
|
||||
|
||||
function renderFlows(t)
|
||||
flow1(t)
|
||||
flow2(t)
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
|
||||
|
||||
local connection;
|
||||
|
||||
local self = {}
|
||||
|
||||
function self:SetEnabled(enabled)
|
||||
if enabled and connection then
|
||||
return
|
||||
end
|
||||
if connection then
|
||||
connection:disconnect()
|
||||
connection = nil
|
||||
end
|
||||
if not enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local timestamp0 = tick()
|
||||
connection = game:GetService("RunService").RenderStepped:connect(function()
|
||||
local t = tick() - timestamp0
|
||||
if renderFlows then
|
||||
renderFlows(t)
|
||||
end
|
||||
if renderTrains then
|
||||
renderTrains(t)
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
return self
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
local CFrameOffsetter = {};
|
||||
|
||||
do
|
||||
function CFrameOffsetter.GetAllParts(model, list)
|
||||
list = list or {}
|
||||
|
||||
if model:IsA("BasePart") then
|
||||
list[#list + 1] = model
|
||||
end
|
||||
|
||||
local children = model:GetChildren()
|
||||
for i = 1, #children do
|
||||
CFrameOffsetter.GetAllParts(children[i], list)
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
function CFrameOffsetter.GetAllPartOffsets(parts, cframeBase)
|
||||
-- The offset is a part's cframe relative to cframeBase
|
||||
|
||||
local offsets = {}
|
||||
local cframeBase_inv = cframeBase:inverse()
|
||||
for i = 1, #parts do
|
||||
offsets[i] = cframeBase_inv * parts[i].CFrame
|
||||
end
|
||||
|
||||
return offsets
|
||||
end
|
||||
function CFrameOffsetter.newOffsetter(parts, offsets, cframeBase)
|
||||
return function(offset)
|
||||
local cf = cframeBase * offset
|
||||
for i = 1, #parts do
|
||||
parts[i].CFrame = cf * offsets[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
function CFrameOffsetter.newOffsetterFromModel(model, cframeBase)
|
||||
local parts = CFrameOffsetter.GetAllParts(model)
|
||||
local offsets = CFrameOffsetter.GetAllPartOffsets(parts, cframeBase)
|
||||
return CFrameOffsetter.newOffsetter(parts, offsets, cframeBase)
|
||||
end
|
||||
end
|
||||
|
||||
local newOffsetterFromModel = CFrameOffsetter.newOffsetterFromModel
|
||||
|
||||
local renderStation;
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait(0.1) end
|
||||
|
||||
local Space = workspace:WaitForChild("Zones"):WaitForChild("Space")
|
||||
local model = Space.Station
|
||||
local cframeBase = model.rings.center.CFrame
|
||||
|
||||
local ring1 = newOffsetterFromModel(model.rings.Ring1, cframeBase)
|
||||
local ring2 = newOffsetterFromModel(model.rings.Ring2, cframeBase)
|
||||
local ring3 = newOffsetterFromModel(model.rings.Ring3, cframeBase)
|
||||
local station = newOffsetterFromModel(model.station, cframeBase)
|
||||
|
||||
|
||||
function renderStation(t)
|
||||
ring1(
|
||||
CFrame.Angles(0, (t / 6 % (math.pi * 2)), 0)
|
||||
* CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 1, 0)
|
||||
)
|
||||
ring2(
|
||||
CFrame.Angles(0, -(t / 4 % (math.pi * 2)), 0)
|
||||
* CFrame.new(0, math.sin(2 + t / 2 % (math.pi * 2)) * 1, 0)
|
||||
)
|
||||
ring3(
|
||||
CFrame.Angles(0, (t / 3.5 % (math.pi * 2)), 0)
|
||||
* CFrame.new(0, math.sin(1 + t / 2 % (math.pi * 2)) * 1, 0)
|
||||
)
|
||||
|
||||
station(
|
||||
CFrame.Angles(0, (t / -32 % (math.pi * 2)), 0)
|
||||
* CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 3 - 2, 0)
|
||||
)
|
||||
end
|
||||
end)
|
||||
|
||||
local renderOrbit;
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait(0.1) end
|
||||
|
||||
local Space = workspace:WaitForChild("Zones"):WaitForChild("Space")
|
||||
|
||||
local model = Space.Orbit
|
||||
local cframeBase = model.center.CFrame
|
||||
|
||||
local cuteMoonThings = newOffsetterFromModel(model, cframeBase)
|
||||
|
||||
local ringParts = {Space.Disk, Space.Disk2, Space.Disk3}
|
||||
|
||||
local rings = {}
|
||||
for i = 1, #ringParts do
|
||||
rings[i] = newOffsetterFromModel(ringParts[i], ringParts[i].CFrame)
|
||||
end
|
||||
|
||||
function renderOrbit(t)
|
||||
cuteMoonThings(
|
||||
CFrame.Angles((t / 80 % (math.pi * 2)), (t / 60 % (math.pi * 2)), (t / 40 % (math.pi * 2)))
|
||||
)
|
||||
for i = 1, #rings do
|
||||
rings[i](
|
||||
CFrame.Angles(0, 1 / 4 + (t / 60 % (math.pi * 2)), 0)
|
||||
)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
|
||||
local connection;
|
||||
|
||||
local self = {}
|
||||
|
||||
function self:SetEnabled(enabled)
|
||||
if enabled and connection then
|
||||
return
|
||||
end
|
||||
if connection then
|
||||
connection:disconnect()
|
||||
connection = nil
|
||||
end
|
||||
if not enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local timestamp0 = tick()
|
||||
connection = game:GetService("RunService").RenderStepped:connect(function()
|
||||
local t = (tick() - timestamp0)
|
||||
if renderStation then
|
||||
renderStation(t)
|
||||
end
|
||||
if renderOrbit then
|
||||
renderOrbit(t)
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
return self
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
|
||||
local Volcano;
|
||||
local lightning;
|
||||
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait(0.1) end
|
||||
|
||||
Volcano = workspace:WaitForChild("Zones"):WaitForChild("Volcano")
|
||||
lightning = Volcano.Lightning:GetChildren()
|
||||
for i = 1, #lightning do
|
||||
lightning[i].Parent = nil
|
||||
end
|
||||
end)
|
||||
|
||||
local function playLighting(model)
|
||||
if not (model and Volcano) then
|
||||
return
|
||||
end
|
||||
model.Parent = Volcano.Lightning
|
||||
wait(0.125)
|
||||
model.Parent = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
local connection;
|
||||
local handle;
|
||||
|
||||
local self = {}
|
||||
|
||||
function self:SetEnabled(enabled)
|
||||
if enabled and connection then
|
||||
return
|
||||
end
|
||||
if connection then
|
||||
connection:disconnect()
|
||||
connection = nil
|
||||
end
|
||||
handle = nil
|
||||
if not enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local h = {} -- our handle
|
||||
handle = h -- Stops a previous loop
|
||||
coroutine.wrap(function()
|
||||
while handle == h do
|
||||
wait(math.random() * 16 + 1)
|
||||
|
||||
if lightning then
|
||||
playLighting(lightning[math.random(1, #lightning)])
|
||||
end
|
||||
|
||||
end
|
||||
end)()
|
||||
|
||||
--[[
|
||||
local timestamp0 = tick()
|
||||
connection = game:GetService("RunService").RenderStepped:connect(function()
|
||||
local t = (tick() - timestamp0)
|
||||
--renderStation(t)
|
||||
--renderOrbit(t)
|
||||
end)
|
||||
--]]
|
||||
|
||||
end
|
||||
|
||||
return self
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
local ZoneAnimator;
|
||||
|
||||
do
|
||||
local animators = {}
|
||||
|
||||
local function GetAnimator(zone)
|
||||
if animators[zone] then
|
||||
return animators[zone]
|
||||
end
|
||||
local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone))
|
||||
if not moduleScript then
|
||||
return
|
||||
end
|
||||
local animator = require(moduleScript)
|
||||
animators[zone] = animator
|
||||
return animator
|
||||
end
|
||||
|
||||
ZoneAnimator = {
|
||||
CurrentZone = nil;
|
||||
}
|
||||
|
||||
local connection;
|
||||
|
||||
function ZoneAnimator:SetZone(zone)
|
||||
if ZoneAnimator.CurrentZone == zone then
|
||||
return
|
||||
end
|
||||
ZoneAnimator.CurrentZone = zone
|
||||
if connection then
|
||||
connection:disconnect()
|
||||
connection = nil
|
||||
end
|
||||
local animator = GetAnimator(zone)
|
||||
if not animator then
|
||||
return
|
||||
end
|
||||
animator:SetEnabled(true)
|
||||
connection = {
|
||||
disconnect = function()
|
||||
animator:SetEnabled(false)
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local SkyboxManager;
|
||||
|
||||
do
|
||||
SkyboxManager = {}
|
||||
|
||||
local activeSkybox = nil
|
||||
function SkyboxManager:SetZone(id)
|
||||
if activeSkybox then
|
||||
activeSkybox.Parent = nil
|
||||
activeSkybox:Destroy()
|
||||
activeSkybox = nil
|
||||
end
|
||||
local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes")
|
||||
local skybox = Skyboxes:FindFirstChild(id or "default")
|
||||
if skybox then
|
||||
activeSkybox = skybox:Clone()
|
||||
local lighting = game:GetService("Lighting")
|
||||
activeSkybox.Parent = lighting
|
||||
pcall(function()
|
||||
for k, v in pairs(activeSkybox:GetChildren()) do
|
||||
lighting[v.Name] = v.Value
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ZoneManager = {
|
||||
Zone = nil;
|
||||
}
|
||||
|
||||
local function setZoneInternal(zone)
|
||||
ZoneManager.Zone = zone
|
||||
SkyboxManager:SetZone(zone)
|
||||
ZoneAnimator:SetZone(zone)
|
||||
end
|
||||
|
||||
function ZoneManager:SetZone(zone)
|
||||
if zone == ZoneManager.Zone then
|
||||
return
|
||||
end
|
||||
if runService:IsRunning() then
|
||||
setZoneInternal(zone)
|
||||
else
|
||||
spawn(function()
|
||||
while not runService:IsRunning() do wait() end
|
||||
setZoneInternal(zone)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return ZoneManager
|
||||
@@ -0,0 +1,710 @@
|
||||
--[[
|
||||
// CarouselController.lua
|
||||
|
||||
// Controls how the data is updated for a carousel view
|
||||
]]
|
||||
local RobloxGui = game:GetService("CoreGui").RobloxGui
|
||||
local Modules = RobloxGui.Modules
|
||||
|
||||
local ContextActionService = game:GetService('ContextActionService')
|
||||
local GuiService = game:GetService('GuiService')
|
||||
|
||||
local EventHub = require(Modules.Shell.EventHub)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local SoundManager = require(Modules.Shell.SoundManager)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local GameData = require(Modules.Shell.GameData)
|
||||
|
||||
local Http = require(Modules.Shell.Http)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
|
||||
local BatchThumbnailLoader = require(Modules.Shell.BatchThumbnailLoader)
|
||||
local ThumbnailLoader = require(Modules.Shell.ThumbnailLoader)
|
||||
|
||||
local FFlagXboxSponsoredGames = settings():GetFFlag("XboxSponsoredGames")
|
||||
|
||||
local function createCarouselController(view, disableBumperBinds, FrontPageIndex, EndPageIndex, AbsoluteDataIndex)
|
||||
disableBumperBinds = disableBumperBinds or false
|
||||
local this = {}
|
||||
|
||||
local PAGE_SIZE = 25 -- Can only be 10, 25, 50, 100
|
||||
local MAX_PAGES = 2
|
||||
|
||||
local sortCollection = nil
|
||||
local currentFocusData = nil
|
||||
local absoluteDataIndex = tonumber(AbsoluteDataIndex) or 1
|
||||
local guiServiceChangedCn = nil
|
||||
|
||||
local loadBuffer = 10
|
||||
|
||||
local pages = {}
|
||||
local pageViews = {}
|
||||
local isLoading = false
|
||||
local frontPageIndex = tonumber(FrontPageIndex) or 1
|
||||
local endPageIndex = tonumber(EndPageIndex) or 0
|
||||
|
||||
local isCuratedSort = false
|
||||
|
||||
local reachFront = true --For all sorts to mark if there's no previous page
|
||||
local reachEnd = false --For all sorts to mark if there's no next page
|
||||
|
||||
-- Events
|
||||
this.NewItemSelected = Utility.Signal()
|
||||
|
||||
local function getNewItem(itemData)
|
||||
local placeId
|
||||
if FFlagXboxSponsoredGames then
|
||||
placeId = itemData.placeId
|
||||
else
|
||||
placeId = itemData
|
||||
end
|
||||
local data = GameData:GetGameData(placeId)
|
||||
if data then
|
||||
local item = Utility.Create"ImageButton"
|
||||
{
|
||||
Name = "CarouselViewImage",
|
||||
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
SoundManager:CreateSound("MoveSelection"),
|
||||
}
|
||||
|
||||
local overrideSelection = view:GetSelectionImageObject()
|
||||
if overrideSelection then
|
||||
item.SelectionImageObject = overrideSelection
|
||||
end
|
||||
|
||||
local thumbnailLoader
|
||||
if data.ImageToken then
|
||||
thumbnailLoader = BatchThumbnailLoader
|
||||
BatchThumbnailLoader:CreateThumbnail(item, data.ImageToken, BatchThumbnailLoader.Sizes.Medium, true, false, nil)
|
||||
else
|
||||
thumbnailLoader = ThumbnailLoader:Create(item, data.IconId, ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon)
|
||||
spawn(function()
|
||||
thumbnailLoader:LoadAsync(true, false, nil)
|
||||
end)
|
||||
end
|
||||
|
||||
local itemInfo = {
|
||||
item = item,
|
||||
thumbnailLoader = thumbnailLoader,
|
||||
}
|
||||
|
||||
itemInfo.PlaceId = placeId
|
||||
|
||||
local transparencyTweens = {}
|
||||
|
||||
function itemInfo:AnimateTransparency(initial, final, duration)
|
||||
Utility.CancelTweens(transparencyTweens)
|
||||
|
||||
local finished = nil
|
||||
if initial < final then
|
||||
thumbnailLoader:SetTransparency(final, item)
|
||||
else
|
||||
finished = function()
|
||||
thumbnailLoader:SetTransparency(final, item)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(transparencyTweens,
|
||||
Utility.PropertyTweener(
|
||||
item,
|
||||
"BackgroundTransparency",
|
||||
initial,
|
||||
final,
|
||||
duration,
|
||||
Utility.EaseOutQuad,
|
||||
true))
|
||||
|
||||
table.insert(transparencyTweens,
|
||||
Utility.PropertyTweener(
|
||||
item,
|
||||
"ImageTransparency",
|
||||
initial,
|
||||
final,
|
||||
duration,
|
||||
Utility.EaseOutQuad,
|
||||
true,
|
||||
finished))
|
||||
end
|
||||
|
||||
return itemInfo
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Remove this when caching is finished. Doing it this way is going to leave the old
|
||||
-- data issues in place with the carousel
|
||||
local function setInternalData(page)
|
||||
if not page then
|
||||
return {}
|
||||
end
|
||||
|
||||
local newData = {}
|
||||
newData.PreviousUrl = page.PreviousUrl
|
||||
newData.NextUrl = page.NextUrl
|
||||
|
||||
local placeIds = page:GetPagePlaceIds()
|
||||
local names = page:GetPagePlaceNames()
|
||||
local voteData = page:GetPageVoteData()
|
||||
local iconIds = page:GetPageIconIds()
|
||||
local creatorNames = page:GetCreatorNames()
|
||||
local creatorUserIds = page:GetPageCreatorUserIds()
|
||||
|
||||
for i = 1, #page.Data do
|
||||
local gameEntry = {
|
||||
Name = names[i];
|
||||
PlaceId = placeIds[i];
|
||||
IconId = iconIds[i];
|
||||
VoteData = voteData[i];
|
||||
CreatorName = creatorNames[i];
|
||||
CreatorUserId = creatorUserIds[i];
|
||||
-- Description and IsFavorites needs to be queried for each game when needed
|
||||
Description = nil;
|
||||
IsFavorited = nil;
|
||||
GameData = nil;
|
||||
}
|
||||
table.insert(newData, gameEntry)
|
||||
end
|
||||
|
||||
return newData
|
||||
end
|
||||
|
||||
|
||||
local onViewFocusChanged;
|
||||
|
||||
local function createPageView(page)
|
||||
local pageView = {}
|
||||
local viewItems = {}
|
||||
|
||||
if FFlagXboxSponsoredGames then
|
||||
for i = 1, #page do
|
||||
local itemData = page[i]
|
||||
local placeId = itemData.placeId
|
||||
local viewItemInfo = getNewItem(itemData)
|
||||
if viewItemInfo then
|
||||
viewItemInfo.item.Activated:connect(function()
|
||||
if itemData.isSponsored then
|
||||
spawn(function()
|
||||
Http.PostSponsoredGameClickAsync(itemData.nativeAdData)
|
||||
end)
|
||||
end
|
||||
if placeId then
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId)
|
||||
end
|
||||
end)
|
||||
table.insert(viewItems, viewItemInfo)
|
||||
end
|
||||
end
|
||||
else
|
||||
for i = 1, #page do
|
||||
local itemData = page[i]
|
||||
local placeId = itemData
|
||||
local viewItemInfo = getNewItem(itemData)
|
||||
if viewItemInfo then
|
||||
viewItemInfo.item.Activated:connect(function()
|
||||
if placeId then
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId)
|
||||
end
|
||||
end)
|
||||
table.insert(viewItems, viewItemInfo)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function pageView:LockFirstItem()
|
||||
if viewItems and viewItems[1] and viewItems[1].item then
|
||||
viewItems[1].item.NextSelectionLeft = viewItems[1].item
|
||||
end
|
||||
end
|
||||
function pageView:UnlockFirstItem()
|
||||
if viewItems and viewItems[1] and viewItems[1].item then
|
||||
viewItems[1].item.NextSelectionLeft = nil
|
||||
end
|
||||
end
|
||||
function pageView:LockLastItem()
|
||||
if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then
|
||||
viewItems[#viewItems].item.NextSelectionRight = viewItems[#viewItems].item
|
||||
end
|
||||
end
|
||||
function pageView:UnlockLastItem()
|
||||
if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then
|
||||
viewItems[#viewItems].item.NextSelectionRight = nil
|
||||
end
|
||||
end
|
||||
function pageView:GetCount()
|
||||
return #viewItems
|
||||
end
|
||||
function pageView:GetItems()
|
||||
return viewItems
|
||||
end
|
||||
function pageView:Destroy()
|
||||
for i,itemInfo in pairs(viewItems) do
|
||||
viewItems[i] = nil
|
||||
itemInfo.item:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
return pageView
|
||||
end
|
||||
|
||||
local function getPagesRangeIndex()
|
||||
local lowIndex, highIndex
|
||||
for index, page in pairs(pages) do
|
||||
if not lowIndex or lowIndex > index then
|
||||
lowIndex = index
|
||||
end
|
||||
|
||||
if not highIndex or highIndex < index then
|
||||
highIndex = index
|
||||
end
|
||||
end
|
||||
|
||||
return lowIndex, highIndex
|
||||
end
|
||||
|
||||
-- For all sorts except curated sort
|
||||
local function getPageAsync(pageIndex)
|
||||
if pageIndex < 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
--Caching is done inside sortsdata
|
||||
local startIndex = (pageIndex - 1) * PAGE_SIZE
|
||||
local newPageData = sortCollection:GetSortAsync(startIndex, PAGE_SIZE)
|
||||
if not newPageData then
|
||||
return nil
|
||||
end
|
||||
if #newPageData > 0 then
|
||||
pages[pageIndex] = newPageData
|
||||
end
|
||||
return pages[pageIndex]
|
||||
end
|
||||
|
||||
-- For curated sort
|
||||
local function getNextPageAsync(pageIndex)
|
||||
--Caching is done inside sortsdata
|
||||
local newPageData = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE)
|
||||
|
||||
-- No data in current page
|
||||
if not newPageData or #newPageData == 0 then
|
||||
reachEnd = true
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Current page is last page
|
||||
if #newPageData < PAGE_SIZE then
|
||||
reachEnd = true
|
||||
end
|
||||
|
||||
pages[pageIndex] = newPageData
|
||||
return pages[pageIndex]
|
||||
end
|
||||
|
||||
-- For curated sort
|
||||
local function getPreviousPageAsync(pageIndex)
|
||||
-- pageIndex should be >= 0
|
||||
-- When pageIndex == 0, means hasPrevPage is true in page1. Then we should fetch page0, insert front, and move all page index +1
|
||||
if pageIndex < 0 then
|
||||
reachFront = true
|
||||
return nil
|
||||
end
|
||||
|
||||
--Caching is done inside sortsdata
|
||||
local newPageData, hasPrevPage = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE, true)
|
||||
|
||||
-- No data in current page
|
||||
if not newPageData or #newPageData == 0 then
|
||||
reachFront = true
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Current page is first page
|
||||
if not hasPrevPage or #newPageData < PAGE_SIZE then
|
||||
reachFront = true
|
||||
end
|
||||
|
||||
if pageIndex == 0 then
|
||||
--Move all pages to their next pages
|
||||
local lowIndex, highIndex = getPagesRangeIndex()
|
||||
for i = highIndex, 1, -1 do
|
||||
pages[i + 1] = pages[i]
|
||||
end
|
||||
pages[1] = newPageData
|
||||
else
|
||||
pages[pageIndex] = newPageData
|
||||
end
|
||||
|
||||
-- Increase 1 for all page indexes if we insert page at front
|
||||
if pageIndex == 0 then
|
||||
pageIndex = 1
|
||||
frontPageIndex = frontPageIndex + 1
|
||||
endPageIndex = endPageIndex + 1
|
||||
end
|
||||
|
||||
return pages[pageIndex]
|
||||
end
|
||||
|
||||
-- For curated sort. If getPreviousPageAsync() reach front and frontPageIndex > 1, reset page index
|
||||
local function resetPageIndex()
|
||||
local pageOffset = frontPageIndex - 1
|
||||
frontPageIndex = 1
|
||||
endPageIndex = endPageIndex - pageOffset
|
||||
-- Remove previous pageviews
|
||||
for i = 1, pageOffset do
|
||||
local frontPageView = pageViews[i]
|
||||
if frontPageView then
|
||||
pageViews[i] = nil
|
||||
view:RemoveAmountFromFront(frontPageView:GetCount())
|
||||
frontPageView:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
-- Move pages and pageViews to left
|
||||
for i = 1, endPageIndex do
|
||||
pages[i] = pages[i + pageOffset]
|
||||
pageViews[i] = pageViews[i + pageOffset]
|
||||
end
|
||||
for i = endPageIndex + 1, endPageIndex + pageOffset do
|
||||
pages[i] = nil
|
||||
pageViews[i] = nil
|
||||
end
|
||||
absoluteDataIndex = absoluteDataIndex - (pageOffset * PAGE_SIZE)
|
||||
end
|
||||
|
||||
local previousFocusItem = nil
|
||||
function onViewFocusChanged(newFocusItem)
|
||||
local offset = 0
|
||||
if previousFocusItem then
|
||||
offset = view:GetItemIndex(newFocusItem) - view:GetItemIndex(previousFocusItem)
|
||||
end
|
||||
|
||||
local visibleItemCount = view:GetVisibleCount()
|
||||
local itemCount = view:GetCount()
|
||||
|
||||
if offset > 0 then
|
||||
-- scrolled right
|
||||
--lastVisibleItemIndex = firstVisibleItemIndex + visibleItemCount - 1
|
||||
local firstVisibleItemIndex = view:GetFirstVisibleItemIndex()
|
||||
local lastVisibleItemIndex = view:GetLastVisibleItemIndex()
|
||||
|
||||
-- Utility.DebugLog("Scroll right", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "needItemCount:", lastVisibleItemIndex + loadBuffer - itemCount, "reachEnd", reachEnd)
|
||||
|
||||
--Scrolling forward: If the last visible item(right most one) is no more than loadBuffer's steps from the trail, we will truncate new ones at back
|
||||
if not isLoading and not reachEnd and lastVisibleItemIndex + loadBuffer >= itemCount then
|
||||
isLoading = true
|
||||
spawn(function()
|
||||
local page
|
||||
if isCuratedSort then
|
||||
page = getNextPageAsync(endPageIndex + 1)
|
||||
else
|
||||
page = getPageAsync(endPageIndex + 1)
|
||||
end
|
||||
if page then
|
||||
local newView = createPageView(page)
|
||||
endPageIndex = endPageIndex + 1
|
||||
view:InsertCollectionBack(newView:GetItems())
|
||||
pageViews[endPageIndex] = newView
|
||||
if pageViews[endPageIndex - 1] then
|
||||
pageViews[endPageIndex - 1]:UnlockLastItem()
|
||||
end
|
||||
newView:LockLastItem()
|
||||
|
||||
if view:GetCount() > PAGE_SIZE * MAX_PAGES then
|
||||
local frontPageView
|
||||
frontPageView = pageViews[frontPageIndex]
|
||||
--Don't erase the front page if the firstVisibleItem becomes too close to the leading edge
|
||||
if firstVisibleItemIndex - frontPageView:GetCount() - loadBuffer > 0 then
|
||||
pageViews[frontPageIndex] = nil
|
||||
frontPageIndex = frontPageIndex + 1
|
||||
view:RemoveAmountFromFront(frontPageView:GetCount())
|
||||
frontPageView:Destroy()
|
||||
if reachFront then
|
||||
reachFront = false
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
reachEnd = true
|
||||
end
|
||||
|
||||
isLoading = false
|
||||
end)
|
||||
end
|
||||
elseif offset < 0 then
|
||||
-- scrolled left
|
||||
--firstVisibleItemIndex = lastVisibleItemIndex - visibleItemCount + 1
|
||||
local firstVisibleItemIndex = view:GetFirstVisibleItemIndex()
|
||||
local lastVisibleItemIndex = view:GetLastVisibleItemIndex()
|
||||
|
||||
-- Utility.DebugLog("Scroll left", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "firstVisibleItemIndex", firstVisibleItemIndex, "reachFront", reachFront)
|
||||
|
||||
--Scrolling backward: If the first visible item(left most one) is no more than loadBuffer's steps from the head, we will truncate new ones at front
|
||||
if not isLoading and not reachFront and firstVisibleItemIndex - loadBuffer <= 0 then
|
||||
isLoading = true
|
||||
spawn(function()
|
||||
local page
|
||||
if isCuratedSort then
|
||||
page = getPreviousPageAsync(frontPageIndex - 1)
|
||||
else
|
||||
page = getPageAsync(frontPageIndex - 1)
|
||||
end
|
||||
if page then
|
||||
local newView = createPageView(page)
|
||||
frontPageIndex = frontPageIndex - 1
|
||||
view:InsertCollectionFront(newView:GetItems())
|
||||
pageViews[frontPageIndex] = newView
|
||||
if pageViews[frontPageIndex + 1] then
|
||||
pageViews[frontPageIndex + 1]:UnlockFirstItem()
|
||||
end
|
||||
newView:LockFirstItem()
|
||||
|
||||
if view:GetCount() > PAGE_SIZE * MAX_PAGES then
|
||||
local endPageView
|
||||
endPageView = pageViews[endPageIndex]
|
||||
--Don't erase the trail page if the lastVisibleItemIndex becomes too close to the trail
|
||||
if lastVisibleItemIndex + loadBuffer < itemCount - endPageView:GetCount() then
|
||||
pageViews[endPageIndex] = nil
|
||||
endPageIndex = endPageIndex - 1
|
||||
view:RemoveAmountFromBack(endPageView:GetCount())
|
||||
endPageView:Destroy()
|
||||
if reachEnd then
|
||||
reachEnd = false
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
reachFront = true
|
||||
end
|
||||
if isCuratedSort and reachFront and frontPageIndex > 1 then
|
||||
resetPageIndex()
|
||||
end
|
||||
isLoading = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
absoluteDataIndex = absoluteDataIndex + offset
|
||||
previousFocusItem = newFocusItem
|
||||
view:ChangeFocus(newFocusItem)
|
||||
|
||||
local pageNumber = math.ceil(absoluteDataIndex/PAGE_SIZE)
|
||||
local pageDataIndex = absoluteDataIndex - ((pageNumber - 1) * PAGE_SIZE)
|
||||
currentFocusData = pages[pageNumber][pageDataIndex]
|
||||
if FFlagXboxSponsoredGames then
|
||||
this.NewItemSelected:fire(currentFocusData.placeId, nil, currentFocusData.isSponsored)
|
||||
else
|
||||
this.NewItemSelected:fire(currentFocusData)
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetFrontGameData()
|
||||
if pages[frontPageIndex] then
|
||||
return pages[frontPageIndex][1]
|
||||
end
|
||||
end
|
||||
|
||||
function this:GetCurrentFocusGameData()
|
||||
return currentFocusData
|
||||
end
|
||||
|
||||
function this:SelectFront()
|
||||
local frontViewItem = view:GetFront()
|
||||
if frontViewItem then
|
||||
onViewFocusChanged(frontViewItem)
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetLoadBuffer(newValue)
|
||||
loadBuffer = newValue
|
||||
end
|
||||
|
||||
function this:InitializeAsync(gameCollection)
|
||||
view:RemoveAllItems()
|
||||
frontPageIndex = frontPageIndex or 1
|
||||
endPageIndex = endPageIndex or 0
|
||||
absoluteDataIndex = absoluteDataIndex or 1
|
||||
currentFocusData = nil
|
||||
pages = {}
|
||||
pageViews = {}
|
||||
sortCollection = gameCollection
|
||||
isCuratedSort = gameCollection and gameCollection.GameSetTargetId and gameCollection.GameSetTargetId ~= 0
|
||||
-- Utility.DebugLog("CarouselController:InitializeAsync", gameCollection.GameSetTargetId, isCuratedSort)
|
||||
|
||||
local loadPrevDataFail = true
|
||||
-- For refreshed sorts, currenlty only RecentlyPlayed and Favorite sorts
|
||||
if endPageIndex >= frontPageIndex and absoluteDataIndex > 0 then
|
||||
local pageCount = 0
|
||||
local maxDataIndex = (frontPageIndex - 1) * PAGE_SIZE
|
||||
for i = frontPageIndex, endPageIndex do
|
||||
local page = isCuratedSort and getNextPageAsync(i) or getPageAsync(i)
|
||||
if page then
|
||||
local newView = createPageView(page)
|
||||
view:InsertCollectionBack(newView:GetItems())
|
||||
pageViews[i] = newView
|
||||
maxDataIndex = maxDataIndex + newView:GetCount()
|
||||
pageCount = pageCount + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
endPageIndex = frontPageIndex + pageCount - 1
|
||||
|
||||
if pageCount > 0 then
|
||||
local viewItem
|
||||
--if requested index overflow, we select the trail
|
||||
if absoluteDataIndex > maxDataIndex then
|
||||
absoluteDataIndex = maxDataIndex
|
||||
viewItem = view:GetBack()
|
||||
else
|
||||
local viewIndex = absoluteDataIndex - (frontPageIndex - 1) * PAGE_SIZE
|
||||
viewItem = view:GetItemAt(viewIndex)
|
||||
end
|
||||
if viewItem then
|
||||
previousFocusItem = viewItem
|
||||
--Use ChangeFocus to focus on the prev index immediately
|
||||
view:ChangeFocus(viewItem, 0)
|
||||
loadPrevDataFail = false
|
||||
end
|
||||
end
|
||||
|
||||
if loadPrevDataFail then --Clear data again
|
||||
view:RemoveAllItems()
|
||||
frontPageIndex = 1
|
||||
endPageIndex = 0
|
||||
absoluteDataIndex = 1
|
||||
currentFocusData = nil
|
||||
pages = {}
|
||||
for i = 1, #pageViews do
|
||||
pageViews[i]:Destroy()
|
||||
pageViews[i] = nil
|
||||
end
|
||||
pageViews = {}
|
||||
end
|
||||
end
|
||||
|
||||
if loadPrevDataFail then
|
||||
for i = 1, MAX_PAGES do
|
||||
local page
|
||||
if isCuratedSort then
|
||||
page = getNextPageAsync(i)
|
||||
else
|
||||
page = getPageAsync(i)
|
||||
end
|
||||
if page then
|
||||
local newView = createPageView(page)
|
||||
endPageIndex = endPageIndex + 1
|
||||
view:InsertCollectionBack(newView:GetItems())
|
||||
pageViews[i] = newView
|
||||
else
|
||||
break
|
||||
end
|
||||
|
||||
end
|
||||
local frontViewItem = view:GetFront()
|
||||
if frontViewItem then
|
||||
absoluteDataIndex = 1
|
||||
previousFocusItem = frontViewItem
|
||||
view:SetFocus(frontViewItem)
|
||||
end
|
||||
end
|
||||
|
||||
if pageViews and endPageIndex > 0 then
|
||||
if pageViews[frontPageIndex] then
|
||||
pageViews[frontPageIndex]:LockFirstItem()
|
||||
end
|
||||
if pageViews[endPageIndex] then
|
||||
pageViews[endPageIndex]:LockLastItem()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function this:GetIndexData()
|
||||
return frontPageIndex, endPageIndex, absoluteDataIndex
|
||||
end
|
||||
|
||||
function this:Connect()
|
||||
guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn)
|
||||
guiServiceChangedCn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
|
||||
local newSelection = GuiService.SelectedCoreObject
|
||||
if newSelection and view:ContainsItem(newSelection) then
|
||||
onViewFocusChanged(newSelection)
|
||||
end
|
||||
end)
|
||||
|
||||
local function getItemNextShiftItem(direction)
|
||||
local currentFocusIndex = view:GetItemIndex(previousFocusItem)
|
||||
local shiftAmount = view:GetFullVisibleItemCount()
|
||||
local nextItem = view:GetItemAt(currentFocusIndex + shiftAmount * direction)
|
||||
if not nextItem then
|
||||
nextItem = direction == 1 and view:GetBack() or view:GetFront()
|
||||
end
|
||||
|
||||
return nextItem
|
||||
end
|
||||
|
||||
local function shiftRight()
|
||||
local nextItem = getItemNextShiftItem(1)
|
||||
if nextItem then
|
||||
GuiService.SelectedCoreObject = nextItem
|
||||
end
|
||||
end
|
||||
|
||||
local function shiftLeft()
|
||||
local nextItem = getItemNextShiftItem(-1)
|
||||
if nextItem then
|
||||
GuiService.SelectedCoreObject = nextItem
|
||||
end
|
||||
end
|
||||
|
||||
local seenRightBumper = false
|
||||
local function onBumperRight(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenRightBumper = true
|
||||
elseif seenRightBumper and inputState == Enum.UserInputState.End then
|
||||
local currentSelection = GuiService.SelectedCoreObject
|
||||
if currentSelection and view:ContainsItem(currentSelection) then
|
||||
shiftRight()
|
||||
end
|
||||
seenRightBumper = false
|
||||
end
|
||||
end
|
||||
|
||||
local seenLeftBumper = false
|
||||
local function onBumperLeft(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
seenLeftBumper = true
|
||||
elseif seenLeftBumper and inputState == Enum.UserInputState.End then
|
||||
local currentSelection = GuiService.SelectedCoreObject
|
||||
if currentSelection and view:ContainsItem(currentSelection) then
|
||||
shiftLeft()
|
||||
end
|
||||
seenRightBumper = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Bumper Binds
|
||||
if not disableBumperBinds then
|
||||
ContextActionService:UnbindCoreAction("BumperRight")
|
||||
ContextActionService:UnbindCoreAction("BumperLeft")
|
||||
ContextActionService:BindCoreAction("BumperRight", onBumperRight, false, Enum.KeyCode.ButtonR1)
|
||||
ContextActionService:BindCoreAction("BumperLeft", onBumperLeft, false, Enum.KeyCode.ButtonL1)
|
||||
end
|
||||
end
|
||||
|
||||
function this:Disconnect()
|
||||
guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn)
|
||||
if not disableBumperBinds then
|
||||
ContextActionService:UnbindCoreAction("BumperRight")
|
||||
ContextActionService:UnbindCoreAction("BumperLeft")
|
||||
end
|
||||
end
|
||||
|
||||
function this:HasResults()
|
||||
return endPageIndex - frontPageIndex >= 0
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createCarouselController
|
||||
@@ -0,0 +1,393 @@
|
||||
--[[
|
||||
// CarouselView.lua
|
||||
|
||||
// View for a carousel. Used for GameGenre screen
|
||||
// TODO: Support Vertical?
|
||||
//
|
||||
// Current this supports a focus that is aligned to the left (0, 0), in the future we
|
||||
// could do other alignments if we need them
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
||||
local Modules = GuiRoot:FindFirstChild("Modules")
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
|
||||
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
||||
local GameData = require(ShellModules:FindFirstChild('GameData'))
|
||||
|
||||
local function createCarouselView()
|
||||
local this = {}
|
||||
|
||||
local itemInfos = {}
|
||||
local padding = 0
|
||||
local itemSizePercentOfContainer = 1
|
||||
local focusItem = nil
|
||||
|
||||
local BASE_TWEEN_TIME = 0.2
|
||||
|
||||
local isViewFocused = false
|
||||
|
||||
local overrideSelectionImage = nil
|
||||
local incomingSelectableValue = true;
|
||||
local incomingImageColor = Color3.new(1,1,1);
|
||||
|
||||
local DISABLED_COLOR = Color3.new(0.4, 0.4, 0.4)
|
||||
local ENABLED_COLOR = Color3.new(1, 1, 1)
|
||||
|
||||
local container = Utility.Create'ScrollingFrame'
|
||||
{
|
||||
Name = "CarouselContainer";
|
||||
BackgroundTransparency = 1;
|
||||
ClipsDescendants = false;
|
||||
ScrollingEnabled = false;
|
||||
Selectable = false;
|
||||
ScrollBarThickness = 0;
|
||||
}
|
||||
|
||||
local function isVisible(item)
|
||||
return item.AbsolutePosition.x + item.AbsoluteSize.x >= 0 and
|
||||
item.AbsolutePosition.x < GuiRoot.AbsoluteSize.x
|
||||
end
|
||||
|
||||
local function getFocusSize()
|
||||
local scaling = itemSizePercentOfContainer
|
||||
|
||||
if isViewFocused then
|
||||
scaling = 1
|
||||
end
|
||||
|
||||
local size = container.Size.Y.Offset * scaling
|
||||
return UDim2.new(0, size, 0, size)
|
||||
end
|
||||
|
||||
local function getNonFocusSize()
|
||||
local size = container.Size.Y.Offset * itemSizePercentOfContainer
|
||||
return UDim2.new(0, size, 0, size)
|
||||
end
|
||||
|
||||
local function getItemSize(item)
|
||||
if item == focusItem then
|
||||
return getFocusSize()
|
||||
else
|
||||
return getNonFocusSize()
|
||||
end
|
||||
end
|
||||
|
||||
local function getItemLayoutPosition(index)
|
||||
local focusIndex = this:GetItemIndex(focusItem)
|
||||
local offsetFromFocus = index - focusIndex
|
||||
local x, y = 0, 0
|
||||
|
||||
if index > focusIndex then
|
||||
-- items to the right of focus need additional buffer due to focus size being larger
|
||||
x = getFocusSize().X.Offset + offsetFromFocus * padding + (offsetFromFocus - 1) * getNonFocusSize().X.Offset
|
||||
else
|
||||
x = offsetFromFocus * padding + offsetFromFocus * getNonFocusSize().X.Offset
|
||||
end
|
||||
|
||||
local size = (focusIndex == index) and getFocusSize() or getNonFocusSize()
|
||||
|
||||
y = (container.Size.Y.Offset - size.Y.Offset) / 2
|
||||
|
||||
return UDim2.new(0, x, 0, y)
|
||||
end
|
||||
|
||||
local function recalcLayout(duration)
|
||||
duration = duration or 0
|
||||
|
||||
for i = 1, #itemInfos do
|
||||
local item = itemInfos[i].item
|
||||
local size = getItemSize(item)
|
||||
local position = getItemLayoutPosition(i)
|
||||
|
||||
if item:IsDescendantOf(game) then
|
||||
item:TweenSizeAndPosition(size, position, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, duration, true)
|
||||
else
|
||||
item.Size = size
|
||||
item.Position = position
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:ChangeFocus(newFocus, tweenTime)
|
||||
-- We don't use SetFocus() as that function will do a recalc. We want to get the next position
|
||||
-- from current position, not recalcd postion for each item
|
||||
|
||||
if self:ContainsItem(newFocus) then
|
||||
focusItem = newFocus
|
||||
recalcLayout(tweenTime or BASE_TWEEN_TIME)
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetSize(newSize)
|
||||
if newSize ~= container.Size then
|
||||
container.Size = newSize
|
||||
container.CanvasSize = UDim2.new(0, container.Size.X.Offset * 2, 1, 0)
|
||||
recalcLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetPosition(newPosition)
|
||||
container.Position = newPosition
|
||||
end
|
||||
|
||||
function this:SetPadding(newPadding)
|
||||
if newPadding ~= padding then
|
||||
padding = newPadding
|
||||
recalcLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetItemSizePercentOfContainer(value)
|
||||
if value ~= itemSizePercentOfContainer then
|
||||
itemSizePercentOfContainer = value
|
||||
recalcLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetParent(newParent)
|
||||
container.Parent = newParent
|
||||
end
|
||||
|
||||
function this:SetFocus(newFocusItem)
|
||||
if self:ContainsItem(newFocusItem) and newFocusItem ~= focusItem then
|
||||
focusItem = newFocusItem
|
||||
recalcLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetClipsDescendants(value)
|
||||
container.ClipsDescendants = value
|
||||
end
|
||||
|
||||
local DEFAULT_FADE_DURATION = 0.2
|
||||
local targetTransparency = 0
|
||||
|
||||
function this:SetTransparency(value, duration, refresh)
|
||||
if not refresh and value == targetTransparency then return end
|
||||
|
||||
if duration then
|
||||
targetTransparency = Utility.Clamp(0, 1, targetTransparency)
|
||||
if not refresh and value == targetTransparency then return end
|
||||
else
|
||||
duration = DEFAULT_FADE_DURATION
|
||||
end
|
||||
|
||||
for _, itemInfo in pairs(itemInfos) do
|
||||
itemInfo:AnimateTransparency(targetTransparency, value, duration)
|
||||
end
|
||||
|
||||
targetTransparency = value
|
||||
end
|
||||
|
||||
function this:GetTransparency()
|
||||
return targetTransparency
|
||||
end
|
||||
|
||||
function this:SetSelectable(value)
|
||||
incomingImageColor = ENABLED_COLOR
|
||||
if not value then
|
||||
incomingImageColor = DISABLED_COLOR
|
||||
end
|
||||
|
||||
incomingSelectableValue = value
|
||||
|
||||
for _, itemInfo in pairs(itemInfos) do
|
||||
itemInfo.item.Selectable = incomingSelectableValue
|
||||
itemInfo.item.ImageColor3 = incomingImageColor
|
||||
end
|
||||
end
|
||||
|
||||
function this:GetAvailableItem()
|
||||
if focusItem and focusItem.Parent then
|
||||
return focusItem
|
||||
end
|
||||
return self:GetFront()
|
||||
end
|
||||
|
||||
function this:GetFocusItem()
|
||||
return focusItem
|
||||
end
|
||||
|
||||
function this:GetItemAt(index)
|
||||
return itemInfos[index] and itemInfos[index].item
|
||||
end
|
||||
|
||||
function this:GetFront()
|
||||
return itemInfos[1] and itemInfos[1].item
|
||||
end
|
||||
|
||||
function this:GetBack()
|
||||
return itemInfos[#itemInfos] and itemInfos[#itemInfos].item
|
||||
end
|
||||
|
||||
function this:GetItemIndex(item)
|
||||
for i = 1, #itemInfos do
|
||||
if itemInfos[i].item == item then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function this:GetCount()
|
||||
return #itemInfos
|
||||
end
|
||||
|
||||
function this:GetVisibleCount()
|
||||
local visibleItemCount = 0
|
||||
for i = 1, #itemInfos do
|
||||
if isVisible(itemInfos[i].item) then
|
||||
visibleItemCount = visibleItemCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
return visibleItemCount
|
||||
end
|
||||
|
||||
function this:GetFirstVisibleItemIndex()
|
||||
for i = 1, #itemInfos do
|
||||
if isVisible(itemInfos[i].item) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function this:GetLastVisibleItemIndex()
|
||||
for i = #itemInfos, 1, -1 do
|
||||
if isVisible(itemInfos[i].item) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function this:GetFullVisibleItemCount()
|
||||
local containerSizeX = container.AbsoluteSize.x
|
||||
-- remove focus from the size, and figure out how many other items can fit
|
||||
local fittingSize = containerSizeX - getFocusSize().X.Offset
|
||||
if fittingSize <= 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
local itemSize = getNonFocusSize().X.Offset + padding
|
||||
local count = math.floor(fittingSize/itemSize) + 1
|
||||
return count
|
||||
end
|
||||
|
||||
|
||||
function this:InsertCollectionFront(collection)
|
||||
for i = #collection, 1, -1 do
|
||||
local itemInfo = collection[i]
|
||||
local item = itemInfo.item
|
||||
item.Selectable = incomingSelectableValue
|
||||
item.ImageColor3 = incomingImageColor
|
||||
item.ImageTransparency = targetTransparency
|
||||
item.BackgroundTransparency = targetTransparency
|
||||
|
||||
-- set item position in front of front item
|
||||
item.Position = getItemLayoutPosition(0)
|
||||
item.Size = getItemSize(item)
|
||||
|
||||
table.insert(itemInfos, 1, itemInfo)
|
||||
item.Parent = container
|
||||
GameData:AddRelatedGuiObject(itemInfo.PlaceId, item)
|
||||
end
|
||||
end
|
||||
|
||||
function this:InsertCollectionBack(collection)
|
||||
for i = 1, #collection do
|
||||
local itemInfo = collection[i]
|
||||
local item = itemInfo.item
|
||||
item.Selectable = incomingSelectableValue
|
||||
item.ImageColor3 = incomingImageColor
|
||||
item.ImageTransparency = targetTransparency
|
||||
item.BackgroundTransparency = targetTransparency
|
||||
|
||||
-- set item position behind last item
|
||||
item.Position = getItemLayoutPosition(#itemInfos + 1)
|
||||
item.Size = getItemSize(item)
|
||||
|
||||
table.insert(itemInfos, itemInfo)
|
||||
item.Parent = container
|
||||
GameData:AddRelatedGuiObject(itemInfo.PlaceId, item)
|
||||
end
|
||||
end
|
||||
|
||||
function this:RemoveAmountFromFront(amount)
|
||||
for i = 1, amount do
|
||||
local item = table.remove(itemInfos, 1).item
|
||||
item.Parent = nil
|
||||
end
|
||||
recalcLayout()
|
||||
end
|
||||
|
||||
function this:RemoveAmountFromBack(amount)
|
||||
for i = 1, amount do
|
||||
local item = table.remove(itemInfos).item
|
||||
item.Parent = nil
|
||||
end
|
||||
recalcLayout()
|
||||
end
|
||||
|
||||
function this:RemoveItem(item)
|
||||
for i = 1, #itemInfos do
|
||||
if itemInfos[i].item == item then
|
||||
local removedItem = table.remove(itemInfos, i).item
|
||||
removedItem.Parent = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
recalcLayout()
|
||||
end
|
||||
|
||||
function this:RemoveAllItems()
|
||||
for i = #itemInfos, 1, -1 do
|
||||
local item = table.remove(itemInfos, #itemInfos).item
|
||||
item.Parent = nil
|
||||
end
|
||||
end
|
||||
|
||||
function this:ContainsItem(item)
|
||||
if not item then
|
||||
return false
|
||||
end
|
||||
return item.Parent == container
|
||||
end
|
||||
|
||||
local function onFocusChanged()
|
||||
this:ChangeFocus(focusItem)
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
if isViewFocused then return end
|
||||
|
||||
isViewFocused = true
|
||||
onFocusChanged()
|
||||
end
|
||||
|
||||
function this:GetContainer()
|
||||
return container
|
||||
end
|
||||
|
||||
|
||||
function this:RemoveFocus()
|
||||
if not isViewFocused then return end
|
||||
|
||||
isViewFocused = false
|
||||
onFocusChanged()
|
||||
end
|
||||
|
||||
function this:SetSelectionImageObject(guiObject)
|
||||
overrideSelectionImage = guiObject
|
||||
end
|
||||
|
||||
function this:GetSelectionImageObject()
|
||||
return overrideSelectionImage
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return createCarouselView
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
local TextService = game:GetService('TextService')
|
||||
local ShellModules = script.Parent.Parent
|
||||
|
||||
local GlobalSettings = require(ShellModules.GlobalSettings)
|
||||
local Strings = require(ShellModules.LocalizedStrings)
|
||||
local Utility = require(ShellModules.Utility)
|
||||
|
||||
local AccountAgeStatus = {}
|
||||
AccountAgeStatus.__index = AccountAgeStatus
|
||||
|
||||
function AccountAgeStatus.new(store, parent)
|
||||
local self = {}
|
||||
|
||||
self.StoreChangedCn = nil
|
||||
|
||||
self.rbx = Utility.Create'Frame' {
|
||||
Name = "AccountAgeContainer",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Parent = parent,
|
||||
|
||||
Utility.Create'TextLabel' {
|
||||
Name = "AccountAgeText",
|
||||
Size = UDim2.new(1, -12, 1, -12),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = Enum.FontSize.Size14,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
Text = "",
|
||||
ZIndex = 2,
|
||||
}
|
||||
}
|
||||
|
||||
self.StoreChangedCn = store.changed:connect(function(newState, oldState)
|
||||
if newState.RobloxUser.under13 ~= oldState.RobloxUser.under13 then
|
||||
local isUnder13 = newState.RobloxUser.under13
|
||||
-- clear out if under13 is nil
|
||||
if isUnder13 == nil then
|
||||
self.rbx.AccountAgeText.Text = ""
|
||||
return
|
||||
end
|
||||
|
||||
local newText = Strings:LocalizedString("AccountUnder13Phrase")
|
||||
if isUnder13 == false then
|
||||
newText = Strings:LocalizedString("AccountOver13Phrase")
|
||||
end
|
||||
|
||||
if newText == self.rbx.AccountAgeText.Text then
|
||||
return
|
||||
end
|
||||
|
||||
self.rbx.AccountAgeText.Text = newText
|
||||
-- update layout
|
||||
local textSize = TextService:GetTextSize(newText,
|
||||
Utility.ConvertFontSizeEnumToInt(self.rbx.AccountAgeText.FontSize), self.rbx.AccountAgeText.Font, Vector2.new(0, 0))
|
||||
self.rbx.Size = UDim2.new(0, textSize.x, 0, 50)
|
||||
self.rbx.Position = UDim2.new(1, -textSize.x, 0, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
setmetatable(self, AccountAgeStatus)
|
||||
return self
|
||||
end
|
||||
|
||||
function AccountAgeStatus:Destruct()
|
||||
self.rbx:Destroy()
|
||||
self.rbx = nil
|
||||
if self.StoreChangedCn then
|
||||
self.StoreChangedCn:disconnect()
|
||||
self.StoreChangedCn = nil
|
||||
end
|
||||
end
|
||||
|
||||
return AccountAgeStatus
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
return function()
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Store = require(Modules.Common.Rodux).Store
|
||||
local AccountAgeStatus = require(script.Parent.AccountAgeStatus)
|
||||
local SetRobloxUser = require(Modules.Shell.Actions.SetRobloxUser)
|
||||
|
||||
it("should construct and destroy the object", function()
|
||||
local reducer = require(Modules.Shell.Reducers.AppShellReducer)
|
||||
local store = Store.new(reducer, {})
|
||||
local object = AccountAgeStatus.new(store, nil)
|
||||
|
||||
expect(object).to.be.ok()
|
||||
expect(object).to.be.a("table")
|
||||
|
||||
object:Destruct()
|
||||
|
||||
expect(object.rbx).never.to.be.ok()
|
||||
expect(object.StoreChangedCn).never.to.be.ok()
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should update status on store changed", function()
|
||||
local reducer = require(Modules.Shell.Reducers.AppShellReducer)
|
||||
local store = Store.new(reducer, {})
|
||||
local object = AccountAgeStatus.new(store, nil)
|
||||
|
||||
expect(object.rbx.AccountAgeText.Text).to.equal("")
|
||||
|
||||
local userInfo = {
|
||||
under13 = true,
|
||||
}
|
||||
|
||||
store:dispatch(SetRobloxUser(userInfo))
|
||||
store:flush()
|
||||
|
||||
expect(object.rbx.AccountAgeText.Text).never.to.equal("")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
end
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
--[[
|
||||
A Roact alert overlay screen.
|
||||
Props:
|
||||
Key : Variant - The key of the overlay screen.
|
||||
InFocus : bool - Is the component in focus.
|
||||
Title : String - The title of the screen.
|
||||
Description : String - The description of the screen.
|
||||
ImageLabel : Roact.Component - the image to be displayed.
|
||||
ButtonTextYes : String - The text on the yes button.
|
||||
ButtonTextNo : String - The text on the no button.
|
||||
CallbackYes : function() - The callback function for the yes button.
|
||||
CallbackNo : function() - The callback function for the no button.
|
||||
CallbackBack : function() - The callback function for when back is pressed.
|
||||
DefaultButton : 0 or 1 - The default selection for the overlay screen.
|
||||
0 for Yes
|
||||
1 for No
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local Modules = RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local SoundManager = require(Modules.Shell.SoundManager)
|
||||
|
||||
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
|
||||
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
|
||||
local AlertOverlay = Roact.PureComponent:extend("AlertOverlay")
|
||||
|
||||
local TEXT_LEFT_EDGE = 776
|
||||
local CONFRIM_KEY = "ComfirmButtonKey"
|
||||
local CANCEL_KEY = "CancelButtonKey"
|
||||
function AlertOverlay:init()
|
||||
self.key = self.props.Key
|
||||
self.guiObjs = {}
|
||||
self.defaultItemKey = CANCEL_KEY
|
||||
if self.props.DefaultButton == 0 then
|
||||
self.defaultItemKey = CONFRIM_KEY
|
||||
end
|
||||
self.state =
|
||||
{
|
||||
currentItemKey = self.defaultItemKey
|
||||
}
|
||||
self.onSelectionGained = function(key)
|
||||
self:setState(
|
||||
{
|
||||
currentItemKey = key
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function AlertOverlay:willUpdate(nextProps, nextState)
|
||||
self.defaultItemKey = nil
|
||||
if self.props.InFocus == nextProps.InFocus then
|
||||
return
|
||||
end
|
||||
if nextProps.InFocus then
|
||||
self.defaultItemKey = nextState.currentItemKey
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function AlertOverlay:render()
|
||||
local confirmButton;
|
||||
if not self.props.CallbackYes and self.defaultItemKey == CONFRIM_KEY then
|
||||
self.defaultItemKey = CANCEL_KEY
|
||||
elseif not self.props.CallbackNo and self.defaultItemKey == CANCEL_KEY then
|
||||
self.defaultItemKey = CONFRIM_KEY
|
||||
end
|
||||
if self.props.CallbackYes then
|
||||
local confirmButtonProps =
|
||||
{
|
||||
Position = UDim2.new(0, TEXT_LEFT_EDGE, 1, -166);
|
||||
AnchorPoint = Vector2.new(0,0),
|
||||
Size = UDim2.new(0, 320, 0, 66);
|
||||
}
|
||||
local confirmButtonTextProps =
|
||||
{
|
||||
Text = self.props.ButtonTextYes,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
}
|
||||
confirmButton = Roact.createElement(RoundedButton,
|
||||
{
|
||||
Button = confirmButtonProps,
|
||||
Text = confirmButtonTextProps,
|
||||
Focused = self.props.InFocus and self.state.currentItemKey == CONFRIM_KEY,
|
||||
Selected = self.defaultItemKey == CONFRIM_KEY,
|
||||
OnSelectionGained = function()
|
||||
self.onSelectionGained(CONFRIM_KEY)
|
||||
end,
|
||||
OnActivated = function()
|
||||
SoundManager:Play('ButtonPress')
|
||||
self.props.CallbackYes()
|
||||
end
|
||||
})
|
||||
end
|
||||
local cancelButton;
|
||||
if self.props.CallbackNo then
|
||||
local cancelButtonProps =
|
||||
{
|
||||
Position = UDim2.new(0, 1106, 1 ,-166),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Size = UDim2.new(0, 320, 0, 66),
|
||||
}
|
||||
local cancelButtonTextProps =
|
||||
{
|
||||
Text = self.props.ButtonTextNo,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
}
|
||||
cancelButton = Roact.createElement(RoundedButton,
|
||||
{
|
||||
Button = cancelButtonProps,
|
||||
Text = cancelButtonTextProps,
|
||||
Focused = self.props.InFocus and self.state.currentItemKey == CANCEL_KEY,
|
||||
Selected = self.defaultItemKey == CANCEL_KEY,
|
||||
OnSelectionGained = function()
|
||||
self.onSelectionGained(CANCEL_KEY)
|
||||
end,
|
||||
OnActivated = function()
|
||||
SoundManager:Play('ButtonPress')
|
||||
self.props.CallbackNo()
|
||||
end
|
||||
})
|
||||
elseif self.defaultItemKey == CANCEL_KEY then
|
||||
self.defaultItemKey = CONFRIM_KEY
|
||||
end
|
||||
local titleText = Roact.createElement("TextLabel",
|
||||
{
|
||||
Size = UDim2.new(0, 0, 0, 0),
|
||||
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 136),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.HeaderSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Text = self.props.Title,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
})
|
||||
local descriptionText = Roact.createElement("TextLabel",
|
||||
{
|
||||
Size = UDim2.new(0, 762, 0, 304),
|
||||
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 200),
|
||||
BackgroundTransparency = 1,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
Font = GlobalSettings.LightFont,
|
||||
FontSize = GlobalSettings.TitleSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextWrapped = true,
|
||||
Text = self.props.Description,
|
||||
})
|
||||
local reportIcon = self.props.ImageLabel
|
||||
if reportIcon == nil then
|
||||
reportIcon = Roact.createElement("ImageLabel",
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Image = GlobalSettings.Images.LargeErrorIcon,
|
||||
Size = UDim2.new(0, 321, 0, 264),
|
||||
})
|
||||
end
|
||||
local imageContainer = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(0, 576, 0, 642),
|
||||
Position = UDim2.new(0, 100, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
reportImage = reportIcon
|
||||
})
|
||||
local container = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1, 0, 0, 640),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Position = UDim2.new(0, 0, 0.5, 0),
|
||||
BackgroundColor3 = GlobalSettings.Colors.OverlayColor;
|
||||
},{
|
||||
ImageContainer = imageContainer,
|
||||
TitleText = titleText,
|
||||
DescriptionText = descriptionText,
|
||||
CancelButton = cancelButton,
|
||||
ConfirmButton = confirmButton,
|
||||
})
|
||||
local modalOverlay = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Position = UDim2.new(0, 0, 0.5, 0),
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
BackgroundTransparency = 0.3,
|
||||
[Roact.Ref] = function(rbx)
|
||||
self.ref = rbx
|
||||
end,
|
||||
},{
|
||||
Container = container,
|
||||
})
|
||||
local redirectObj;
|
||||
if self.props.CallbackBack then
|
||||
redirectObj = Roact.createElement(RedirectComponent,
|
||||
{
|
||||
Key = self.key,
|
||||
InFocus = true,
|
||||
RedirectBack = function()
|
||||
self.props.CallbackBack()
|
||||
end,
|
||||
})
|
||||
end
|
||||
return Roact.createElement(Roact.Portal,
|
||||
{
|
||||
target = CoreGui
|
||||
},{
|
||||
[self.key] = Roact.createElement("ScreenGui",
|
||||
{
|
||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
DisplayOrder = 1
|
||||
},
|
||||
{
|
||||
ModalOverlay = modalOverlay,
|
||||
RedirectObj = redirectObj,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
function AlertOverlay:didMount()
|
||||
delay(0,function()
|
||||
Utility.AddSelectionParent(self.key, self.ref)
|
||||
end)
|
||||
SoundManager:Play("OverlayOpen")
|
||||
end
|
||||
|
||||
function AlertOverlay:willUnmount()
|
||||
Utility.RemoveSelectionGroup(self.key)
|
||||
end
|
||||
|
||||
return AlertOverlay
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
--[[
|
||||
A simple base screen for Roact components.
|
||||
|
||||
Props:
|
||||
BackPageTitle : string - The title of the parent page
|
||||
Content : Roact.Component - The content of the frame
|
||||
]]
|
||||
local RobloxGui = game:GetService("CoreGui").RobloxGui
|
||||
local Modules = RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent)
|
||||
|
||||
local BaseScreen = Roact.PureComponent:extend("BaseScreen")
|
||||
|
||||
local BACK_IMAGE = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png"
|
||||
local BACK_IMAGE_SIZE = 48
|
||||
|
||||
function BaseScreen:init()
|
||||
self.onBack = function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self._seenPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and self._seenPressed then
|
||||
self.props.onUnmount()
|
||||
end
|
||||
end
|
||||
self.onCreate = function(rbx)
|
||||
end
|
||||
end
|
||||
|
||||
function BaseScreen:render()
|
||||
local backPageTitle = self.props.BackPageTitle
|
||||
local onCreate = self.props.OnCreate or self.onCreate
|
||||
return Roact.createElement("Frame",
|
||||
{
|
||||
[Roact.Ref] = function(rbx)
|
||||
onCreate(rbx)
|
||||
end,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0.5,0,0.5,0),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
backImage = Roact.createElement("ImageButton",
|
||||
{
|
||||
Size = UDim2.new(0,BACK_IMAGE_SIZE,0,BACK_IMAGE_SIZE),
|
||||
BackgroundTransparency = 1,
|
||||
Image = BACK_IMAGE,
|
||||
Selectable = false,
|
||||
[Roact.Event.Activated] = self.onBack
|
||||
}),
|
||||
BackPageTitleLabel = Roact.createElement("TextLabel",
|
||||
{
|
||||
Size = UDim2.new(0, 0, 0, BACK_IMAGE_SIZE),
|
||||
Position = UDim2.new(0,BACK_IMAGE_SIZE+8,0,BACK_IMAGE_SIZE/2),
|
||||
AnchorPoint = Vector2.new(0,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Text = backPageTitle
|
||||
}),
|
||||
-- NOTE: This will need to be changed when the screen is actually connected to the tree.
|
||||
BackConnector = Roact.createElement(ContextActionEvent, {
|
||||
name = "GoBackTo" .. backPageTitle,
|
||||
callback = self.onBack,
|
||||
binds = { Enum.KeyCode.ButtonB },
|
||||
}),
|
||||
view = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1,0,1,-(BACK_IMAGE_SIZE+2)),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0,0,0,(BACK_IMAGE_SIZE+2)),
|
||||
},{self.props.Content})
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
|
||||
return BaseScreen
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
--[[
|
||||
Creates a Roact component provides a vertical list menu for multiple elements
|
||||
Props:
|
||||
Key : Variant - The key for this category menu. The key cannot be changed after init.
|
||||
InFocus : bool - Is the component in focus.
|
||||
Navigator : Navigator - The Navigator object.
|
||||
DefaultCategoryFocus : Variant - This is the key of the default focus button.
|
||||
DefaultCategoryKey : Variant - The default selection of the category menu.
|
||||
Categories : Categories - An object that provides data needed for the creating the categories
|
||||
The object must have an order mapping and a StringKeys mapping for localization.
|
||||
E.g.
|
||||
Categories[CategoryKey].Key = "Category"
|
||||
Categories[CategoryKey].Order = 1
|
||||
Categories[CategoryKey].StringKey = "CategoryStringKey"
|
||||
|
||||
OnSelectSection : function(Key : Variant) - Callback for when a section of the key is select.
|
||||
OnLeaveSection : function(Key : Variant) - Callback for when a section of the key is no longer selected.
|
||||
EnterSection : function() - Callback functin to enter the menu section.
|
||||
RedirectUp : function() - Callback functin when redirect up.
|
||||
RedirectDown : function() - Callback functin when redirect down.
|
||||
ActionPriority : int - The action priority.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local LocalizedStrings = require(Modules.Shell.LocalizedStrings)
|
||||
|
||||
local VerticalListView = require(Modules.Shell.Components.Common.VerticalListView)
|
||||
local SelectorButton = require(Modules.Shell.Components.Common.SelectorButton)
|
||||
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
|
||||
|
||||
local CategoryMenuView = Roact.PureComponent:extend("CategoryMenuView")
|
||||
|
||||
local BUTTON_WIDTH = 360
|
||||
local BUTTON_HEIGHT = 80
|
||||
|
||||
function CategoryMenuView:OnSelectSection(key)
|
||||
if self.state.currentSectionKey ~= key then
|
||||
self:setState(
|
||||
{
|
||||
currentSectionKey = key,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function CategoryMenuView:init()
|
||||
self.key = self.props.Key
|
||||
self.defaultCategoryKey = self.props.DefaultCategoryKey
|
||||
self.defaultCategoryFocus = self.defaultCategoryKey or self.props.DefaultCategoryFocus
|
||||
self.state =
|
||||
{
|
||||
currentSectionKey = self.props.DefaultCategoryFocus,
|
||||
}
|
||||
self.getCurrentPageIndex = function()
|
||||
return self.CurrentSection
|
||||
end
|
||||
end
|
||||
|
||||
function CategoryMenuView:willUpdate(nextProps, nextState)
|
||||
self.defaultCategoryKey = nil
|
||||
if self.props.InFocus == nextProps.InFocus then
|
||||
return
|
||||
end
|
||||
if nextProps.InFocus then
|
||||
self.defaultCategoryKey = nextState.currentSectionKey or self.props.DefaultCategoryKey
|
||||
end
|
||||
end
|
||||
|
||||
function CategoryMenuView:render()
|
||||
self.categories = self.props.Categories
|
||||
local buttons = {}
|
||||
for k in pairs(self.categories) do
|
||||
buttons[k] = Roact.createElement(SelectorButton,
|
||||
{
|
||||
Size = UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Text = LocalizedStrings:LocalizedString(self.categories[k].StringKey),
|
||||
Key = k,
|
||||
LayoutOrder = self.categories[k].Order,
|
||||
Focused = self.state.currentSectionKey == k,
|
||||
Selected = self.defaultCategoryKey == k,
|
||||
OnSelectionGained = function(key)
|
||||
self:OnSelectSection(key)
|
||||
if self.props.OnSelectSection then
|
||||
self.props.OnSelectSection(key)
|
||||
end
|
||||
end,
|
||||
OnSelectionLost = self.props.OnLeaveSection,
|
||||
OnActivated = self.props.EnterSection,
|
||||
})
|
||||
end
|
||||
|
||||
local navObj = Roact.createElement(RedirectComponent,
|
||||
{
|
||||
ActionPriority = self.props.ActionPriority,
|
||||
Key = self.key,
|
||||
InFocus = self.props.InFocus,
|
||||
RedirectRight = self.props.EnterSection,
|
||||
RedirectUp = self.props.RedirectUp,
|
||||
RedirectDown = self.props.RedirectDown,
|
||||
})
|
||||
|
||||
local buttonView = Roact.createElement(VerticalListView,
|
||||
{
|
||||
PaddingTop = UDim.new(0.005, 0),
|
||||
PaddingBottom = UDim.new(0.005, 0),
|
||||
Spacing = UDim.new(0.025, 0),
|
||||
ScrollBarThickness = 0,
|
||||
Items = buttons,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
ScrollingEnabled = false,
|
||||
})
|
||||
|
||||
return Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Selectable = false,
|
||||
[Roact.Ref] = function(rbx)
|
||||
self.ref = rbx
|
||||
end,
|
||||
},{
|
||||
NavObj = navObj,
|
||||
ButtonView = buttonView,
|
||||
})
|
||||
end
|
||||
|
||||
function CategoryMenuView:didMount()
|
||||
delay(0, function()
|
||||
if self.props.InFocus and self.ref then
|
||||
Utility.RemoveSelectionGroup(self.key)
|
||||
Utility.AddSelectionParent(self.key, self.ref)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function CategoryMenuView:didUpdate(previousProps, previousState)
|
||||
if self.props.InFocus == previousProps.InFocus then
|
||||
return
|
||||
end
|
||||
if self.props.InFocus and self.ref then
|
||||
Utility.RemoveSelectionGroup(self.key)
|
||||
Utility.AddSelectionParent(self.key, self.ref)
|
||||
else
|
||||
Utility.RemoveSelectionGroup(self.key)
|
||||
end
|
||||
end
|
||||
|
||||
function CategoryMenuView:willUnmount()
|
||||
Utility.RemoveSelectionGroup(self.key)
|
||||
end
|
||||
|
||||
return CategoryMenuView
|
||||
@@ -0,0 +1,33 @@
|
||||
--[[
|
||||
Creates a vertical divider
|
||||
Props:
|
||||
Position : UDim2 - The position of the divider.
|
||||
DividerWidth : UDim - The width of the divider.
|
||||
DividerLength : UDim - The length of the divider.
|
||||
Color : Color3 - The color of the divider. Default GlobalSettings.PageDivideColor
|
||||
FillDirection : FillDirection - The direction of the divider.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
return function(props)
|
||||
local color = props.Color or GlobalSettings.PageDivideColor
|
||||
local dividerLength = props.DividerLength
|
||||
local dividerWidth = props.DividerWidth or UDim.new(0,2)
|
||||
local position = props.Position
|
||||
local size
|
||||
if props.FillDirection == Enum.FillDirection.Horizontal then
|
||||
size = UDim2.new(dividerLength.Scale, dividerLength.Offset, dividerWidth.Scale, dividerWidth.Offset)
|
||||
else
|
||||
size = UDim2.new(dividerWidth.Scale, dividerWidth.Offset, dividerLength.Scale, dividerLength.Offset)
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame",{
|
||||
BackgroundColor3 = color,
|
||||
BorderSizePixel = 0,
|
||||
Size = size,
|
||||
Position = position,
|
||||
})
|
||||
end
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
--[[
|
||||
A component that is used for navigation and redirects.
|
||||
Note: The parent must be a scolling
|
||||
|
||||
Props:
|
||||
Key : String
|
||||
InFocus : bool
|
||||
Scale : Vector2
|
||||
RedirectBack : function()
|
||||
RedirectLeft : function(Ref<GuiObject>) - If nil,
|
||||
RedirectRight : function(Ref<GuiObject>)
|
||||
RedirectUp : function(Ref<GuiObject>)
|
||||
RedirectDown : function(Ref<GuiObject>)
|
||||
ActionPriority : The action priority
|
||||
]]
|
||||
local RobloxGui = game:GetService("CoreGui").RobloxGui
|
||||
local Modules = RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
|
||||
local RedirectComponent = Roact.PureComponent:extend("RedirectComponent")
|
||||
|
||||
local BACK_KEY = Enum.KeyCode.ButtonB
|
||||
|
||||
local DEFAULT_ACTION_PRIORITY = 2000
|
||||
|
||||
local function addBackButton(self)
|
||||
self.exitActionName = "ExitSection"..self.Key
|
||||
ContextActionService:UnbindCoreAction(self.exitActionName)
|
||||
if self.props.ActionPriority then
|
||||
ContextActionService:BindCoreActionAtPriority(self.exitActionName, self.exitAction, false, DEFAULT_ACTION_PRIORITY + self.props.ActionPriority, BACK_KEY)
|
||||
else
|
||||
ContextActionService:BindCoreAction(self.exitActionName, self.exitAction, false, BACK_KEY)
|
||||
end
|
||||
end
|
||||
|
||||
local function removeBackButton(self)
|
||||
if self.exitActionName ~= nil then
|
||||
ContextActionService:UnbindCoreAction(self.exitActionName)
|
||||
self.exitActionName = nil
|
||||
end
|
||||
end
|
||||
|
||||
function RedirectComponent:init()
|
||||
self.Key = self.props.Key or "RedirectComponent"
|
||||
self.exitActionName = nil
|
||||
self.backPressed = false
|
||||
self.exitAction = function(actionName, inputState, inputObject)
|
||||
if not self.props.InFocus then
|
||||
return Enum.ContextActionResult.Pass
|
||||
end
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self.backPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and self.backPressed then
|
||||
self.backPressed = false
|
||||
self.props.RedirectBack()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RedirectComponent:render()
|
||||
local props = self.props
|
||||
local scale = props.Scale or Vector2.new(1,1)
|
||||
if props.InFocus and props.RedirectBack then
|
||||
addBackButton(self)
|
||||
else
|
||||
removeBackButton(self)
|
||||
end
|
||||
local redirectLeftButton;
|
||||
if props.InFocus and props.RedirectLeft then
|
||||
redirectLeftButton = Roact.createElement('TextButton',
|
||||
{
|
||||
Position = UDim2.new(-scale.X/2, -1, 0.5, 0),
|
||||
Size = UDim2.new(0, 2, 1+scale.Y, 0),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
[Roact.Event.SelectionGained] = function(rbx)
|
||||
props.RedirectLeft(rbx)
|
||||
end,
|
||||
})
|
||||
end
|
||||
local redirectRightButton;
|
||||
if props.InFocus and props.RedirectRight then
|
||||
redirectRightButton = Roact.createElement('TextButton',
|
||||
{
|
||||
Position = UDim2.new(1+scale.X/2, 1, 0.5, 0),
|
||||
Size = UDim2.new(0, 2, 1+scale.Y, 0),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
[Roact.Event.SelectionGained] = function(rbx)
|
||||
props.RedirectRight(rbx)
|
||||
end,
|
||||
})
|
||||
end
|
||||
local redirectUpButton;
|
||||
if props.InFocus and props.RedirectUp then
|
||||
redirectUpButton = Roact.createElement('TextButton',
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, -scale.Y/2, -1),
|
||||
Size = UDim2.new(1+scale.X, 0, 0, 2),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
[Roact.Event.SelectionGained] = function(rbx)
|
||||
props.RedirectUp(rbx)
|
||||
end,
|
||||
})
|
||||
end
|
||||
local redirectDownButton;
|
||||
if props.InFocus and props.RedirectDown then
|
||||
redirectDownButton = Roact.createElement('TextButton',
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, 1+scale.Y/2, 1),
|
||||
Size = UDim2.new(1+scale.X, 0, 0, 2),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
[Roact.Event.SelectionGained] = function(rbx)
|
||||
props.RedirectDown(rbx)
|
||||
end,
|
||||
})
|
||||
end
|
||||
return Roact.createElement('ScrollingFrame',
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(0.5,0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Selectable = false,
|
||||
ScrollingEnabled = false,
|
||||
ScrollBarThickness = 0,
|
||||
CanvasSize = UDim2.new(0, 0, 1, 0),
|
||||
},{
|
||||
RedirectLeftButton = redirectLeftButton,
|
||||
RedirectRightButton = redirectRightButton,
|
||||
RedirectUpButton = redirectUpButton,
|
||||
RedirectDownButton = redirectDownButton,
|
||||
})
|
||||
end
|
||||
|
||||
function RedirectComponent:willUnmount()
|
||||
removeBackButton(self)
|
||||
end
|
||||
|
||||
return RedirectComponent
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
Creates a Roact component that is a rounded button
|
||||
Props:
|
||||
Button : dictionary - Config for the button.
|
||||
.Image : Content - The image of the button.
|
||||
.Size : UDim2 - Size of the button.
|
||||
.Position : UDim2 - Position of the button.
|
||||
.AnchorPoint : UDim2 - The anchor point of the button.
|
||||
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
|
||||
.LayoutOrder: int - Controls the sorting priority of this button.
|
||||
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
|
||||
|
||||
Text : dictionary - A map of props for the text
|
||||
.Text : string - The label of the button.
|
||||
.Size : UDim2 - Size of the button.
|
||||
.Position : UDim2 - Position of the button.
|
||||
.AnchorPoint : The anchor point of the button.
|
||||
.Font : Font - The font used to display the given text.
|
||||
.TextSize : float - The font size in pixels.
|
||||
.TextXAlignment : TextXAlignment - Sets where text is placed on the X axis within the TextLabel.
|
||||
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
|
||||
|
||||
|
||||
Focused : bool - Is the button in focus.
|
||||
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
|
||||
Selected : bool - Should the button be selected.
|
||||
OnSelectionGained : bool function() -
|
||||
Fired when the GuiObject is being focused on with the Gamepad selector.
|
||||
Return true if it should be focused. False otherwise.
|
||||
OnSelectionLost : bool function() -
|
||||
Fired when the Gamepad selector stops focusing on the GuiObject.
|
||||
Return false if it should be un-focused. True otherwise.
|
||||
OnActivated : function() - Fires when the button is activated.
|
||||
|
||||
HideSelectionImage : bool - Whether or not to hide the selection object
|
||||
|
||||
DefaultProps : dictionary a map for the default props of the button.
|
||||
.ImageColor3 : Color3
|
||||
.ImageTransparency : float
|
||||
.TextColor3 : Color3
|
||||
|
||||
FocusedProps : dictionary a map for the focused props of the button.
|
||||
.ImageColor3 : Color3
|
||||
.ImageTransparency : float
|
||||
.TextColor3 : Color3
|
||||
|
||||
DisabledProps : dictionary a map for the disabled props of the button.
|
||||
.ImageColor3 : Color3
|
||||
.ImageTransparency : float
|
||||
.TextColor3 : Color3
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Immutable = require(Modules.Common.Immutable)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
|
||||
local RoundedButton = Roact.PureComponent:extend("RoundedButton")
|
||||
|
||||
function RoundedButton:init()
|
||||
self.selectionImageObject = Utility.Create "ImageLabel"
|
||||
{
|
||||
Name = "SelectorImage",
|
||||
Image = GlobalSettings.Images.ButtonSelector,
|
||||
Position = UDim2.new(0, -7, 0, -7),
|
||||
Size = UDim2.new(1, 14, 1, 14),
|
||||
BackgroundTransparency = 1,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(31, 31, 63, 63),
|
||||
}
|
||||
|
||||
self.defaultProps =
|
||||
{
|
||||
ImageColor3 = GlobalSettings.Colors.WhiteButton,
|
||||
ImageTransparency = 0.8,
|
||||
TextColor3 = GlobalSettings.Colors.WhiteText,
|
||||
}
|
||||
self.focusedProps =
|
||||
{
|
||||
ImageColor3 = GlobalSettings.Colors.BlueButton,
|
||||
ImageTransparency = 0,
|
||||
TextColor3 = GlobalSettings.Colors.TextSelected,
|
||||
}
|
||||
self.disabledProps =
|
||||
{
|
||||
ImageColor3 = GlobalSettings.Colors.WhiteButton,
|
||||
ImageTransparency = 1,
|
||||
TextColor3 = GlobalSettings.Colors.WhiteText,
|
||||
}
|
||||
self.buttonImage = GlobalSettings.Images.ButtonDefault
|
||||
|
||||
--TODO: Change to new Ref API
|
||||
self.onCreate = function(rbx)
|
||||
self.ref = rbx
|
||||
end
|
||||
end
|
||||
|
||||
function RoundedButton:render()
|
||||
local button = self.props.Button or {}
|
||||
local text = self.props.Text or {}
|
||||
|
||||
local inputDefaultProps = self.props.DefaultProps or {}
|
||||
local defaultProps = {}
|
||||
for k in pairs(self.defaultProps) do
|
||||
defaultProps[k] = inputDefaultProps[k] or self.defaultProps[k]
|
||||
end
|
||||
|
||||
local inputFocusedProps = self.props.FocusedProps or {}
|
||||
local focusedProps = {}
|
||||
for k in pairs(self.focusedProps) do
|
||||
focusedProps[k] = inputFocusedProps[k] or self.focusedProps[k]
|
||||
end
|
||||
|
||||
local inputDisabledProps = self.props.DisabledProps or {}
|
||||
local disabledProps = {}
|
||||
for k in pairs(self.disabledProps) do
|
||||
disabledProps[k] = inputDisabledProps[k] or self.disabledProps[k]
|
||||
end
|
||||
|
||||
if self.props.HideSelectionImage then
|
||||
self.selectionImageObject.Visible = false
|
||||
else
|
||||
self.selectionImageObject.Visible = true
|
||||
end
|
||||
local selectable = true
|
||||
if button.Selectable == false then
|
||||
selectable = false
|
||||
end
|
||||
local currentProps = defaultProps
|
||||
if self.props.Disabled then
|
||||
currentProps = disabledProps
|
||||
selectable = button.Selectable or false
|
||||
elseif self.props.Focused then
|
||||
currentProps = focusedProps
|
||||
end
|
||||
local baseButtonProps =
|
||||
{
|
||||
Image = self.buttonImage,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Selectable = selectable,
|
||||
[Roact.Ref] = self.onCreate,
|
||||
[Roact.Event.SelectionGained] = self.props.OnSelectionGained,
|
||||
[Roact.Event.SelectionLost] = self.props.OnSelectionLost,
|
||||
[Roact.Event.Activated] = self.props.OnActivated,
|
||||
ImageColor3 = currentProps.ImageColor3,
|
||||
ImageTransparency = currentProps.ImageTransparency,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(8, 8, 9, 9),
|
||||
SelectionImageObject = self.selectionImageObject,
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
local buttonProps = Immutable.JoinDictionaries(baseButtonProps, button)
|
||||
|
||||
local baseTextProps =
|
||||
{
|
||||
Text = "",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Font = GlobalSettings.Fonts.Regular,
|
||||
TextSize = GlobalSettings.TextSizes.Button,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = currentProps.TextColor3,
|
||||
TextTransparency = currentProps.TextTransparency,
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
|
||||
local textProps = Immutable.JoinDictionaries(baseTextProps, text)
|
||||
|
||||
local textLabel = Roact.createElement("TextLabel", textProps)
|
||||
local moveSelection = Roact.createElement(SoundComponent,
|
||||
{
|
||||
SoundName = "MoveSelection",
|
||||
}
|
||||
)
|
||||
|
||||
local children = self.props[Roact.Children] or {}
|
||||
|
||||
return Roact.createElement("ImageButton",
|
||||
buttonProps,
|
||||
Immutable.JoinDictionaries(
|
||||
{
|
||||
Label = textLabel,
|
||||
MoveSelection = moveSelection,
|
||||
}, children)
|
||||
)
|
||||
end
|
||||
|
||||
function RoundedButton:didMount()
|
||||
delay(0, function()
|
||||
if self.props.Selected then
|
||||
Utility.SetSelectedCoreObject(self.ref)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function RoundedButton:didUpdate(previousProps, previousState)
|
||||
if not previousProps.Selected and self.props.Selected then
|
||||
Utility.SetSelectedCoreObject(self.ref)
|
||||
end
|
||||
end
|
||||
|
||||
return RoundedButton
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
--[[
|
||||
Creates a Roact component that is a category selector button
|
||||
Props:
|
||||
Key : Variant - The key for the selector button.
|
||||
Text : String - The label of the button.
|
||||
Size : UDim2 - The size of the this button.
|
||||
AnchorPoint : Vector2 - The anchor point of the button.
|
||||
Position : UDim2 - The position of the button.
|
||||
LayoutOrder : int - The layout order of the button.
|
||||
|
||||
Focused : bool - Is the button in focus.
|
||||
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
|
||||
Selected : bool - Should the button be selected.
|
||||
OnSelectionGained : function(key : Variant) - Fired when the GuiObject
|
||||
is being focused on with the Gamepad selector.
|
||||
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
|
||||
OnActivated : function(key : Variant) - Fires when the button is activated.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Immutable = require(Modules.Common.Immutable)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
|
||||
|
||||
local SelectorButton = Roact.PureComponent:extend("SelectorButton")
|
||||
|
||||
local SELECTOR_ICON_SIZE_X = 15
|
||||
local SELECTOR_ICON_SIZE_Y = 30
|
||||
|
||||
local SELECTOR_OFFSET_X = 14
|
||||
|
||||
function SelectorButton:init()
|
||||
self.key = self.props.Key
|
||||
self.onSelectionGained = function()
|
||||
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
|
||||
end
|
||||
self.onSelectionLost = function()
|
||||
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
|
||||
end
|
||||
self.onActivated = function()
|
||||
if self.props.OnActivated then self.props.OnActivated(self.key) end
|
||||
end
|
||||
end
|
||||
|
||||
function SelectorButton:render()
|
||||
local buttonProps = {}
|
||||
local textProps = {}
|
||||
local size = self.props.Size
|
||||
local position = self.props.Position
|
||||
local anchorPoint = self.props.AnchorPoint
|
||||
local focused = self.props.Focused
|
||||
local disabled = self.props.Disabled
|
||||
local selected = self.props.Selected
|
||||
self.layoutOrder = self.props.LayoutOrder
|
||||
|
||||
buttonProps.Size = UDim2.new(1, 0, 1, 0)
|
||||
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
|
||||
textProps.Text = self.props.Text
|
||||
textProps.Size = UDim2.new(1, 0, 1, 0)
|
||||
textProps.AnchorPoint = Vector2.new(0, 0.5)
|
||||
textProps.Position = UDim2.new(0, 24, 0.5, 0)
|
||||
|
||||
local button = Roact.createElement(RoundedButton,
|
||||
{
|
||||
Button = buttonProps,
|
||||
Text = textProps,
|
||||
Focused = focused,
|
||||
Disabled = disabled,
|
||||
Selected = selected,
|
||||
OnSelectionGained = self.onSelectionGained,
|
||||
OnSelectionLost = self.onSelectionLost,
|
||||
OnActivated = self.onActivated,
|
||||
})
|
||||
|
||||
local selector;
|
||||
if focused == true then
|
||||
selector = Roact.createElement("ImageLabel",
|
||||
{
|
||||
Size = UDim2.new(0, SELECTOR_ICON_SIZE_X, 0, SELECTOR_ICON_SIZE_Y),
|
||||
Image = GlobalSettings.Images.RightArrow,
|
||||
Position = UDim2.new(1, SELECTOR_OFFSET_X, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
end
|
||||
|
||||
local children = self.props[Roact.Children] or {}
|
||||
|
||||
return Roact.createElement("Frame",
|
||||
{
|
||||
Size = size,
|
||||
Position = position,
|
||||
AnchorPoint = anchorPoint,
|
||||
LayoutOrder = self.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
},Immutable.JoinDictionaries(
|
||||
{
|
||||
Button = button,
|
||||
Selector = selector,
|
||||
}, children)
|
||||
)
|
||||
end
|
||||
|
||||
return SelectorButton
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
--[[
|
||||
Creates a component with a shadow image
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
return function(props)
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Name = 'Shadow',
|
||||
Image = GlobalSettings.Images.Shadow,
|
||||
Size = UDim2.new(1,3,1,3),
|
||||
Position = UDim2.new(0,0,0,0),
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10,10,28,28),
|
||||
BackgroundTransparency = 1,
|
||||
ZIndex = props.ZIndex or 1,
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,260 @@
|
||||
--[[
|
||||
Creates a component for sidebar
|
||||
Props:
|
||||
buttons : array An array of the buttons to be added on the sidebar.
|
||||
key - string The text to be shown on the sidebar button.
|
||||
value - function() A callback which will be called when the button is activated.
|
||||
text: string - The text to be shown on the sidebar.
|
||||
inFocus: bool - The boolean which indicates whether the sidebar is open.
|
||||
selectIndex: int - The button index we try to select when the sidebar is open.
|
||||
paddingTop : UDim - The padding to apply on the top side relative to the sidebar's normal size.
|
||||
paddingBottom : UDim - The padding to apply on the bottom side relative to the sidebar's normal size.
|
||||
displayOrder: int - The order that the sidebar ScreenGui is drawn.
|
||||
onRemoveFocus : function() - Callback function when the remove focus from sidebar
|
||||
onClose: function() - Callback function when the sidebar is closed.
|
||||
actionPriority : int - The action priority on sidebar.
|
||||
]]
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local RoactMotion = require(Modules.LuaApp.RoactMotion)
|
||||
local SoundManager = require(Modules.Shell.SoundManager)
|
||||
local Components = Modules.Shell.Components
|
||||
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
|
||||
local ContextActionEvent = require(Components.ContextActionEvent)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local INSET_X = 65
|
||||
local BUTTON_SIZE_Y = 75
|
||||
local SIDEBAR_SELECTION_GROUP_NAME = "SideBar"
|
||||
|
||||
local SideBar = Roact.PureComponent:extend("SideBar")
|
||||
function SideBar:init()
|
||||
self.groupKey = SIDEBAR_SELECTION_GROUP_NAME
|
||||
self.buttonImage = GlobalSettings.Images.ButtonDefault
|
||||
self.selectionImageObject = Utility.Create "ImageLabel"({
|
||||
Name = "SelectorImage",
|
||||
BackgroundTransparency = 1,
|
||||
Visible = false
|
||||
})
|
||||
|
||||
self.defaultProps = {
|
||||
buttonColor3 = GlobalSettings.Colors.WhiteButton,
|
||||
buttonTransparency = 1,
|
||||
textColor3 = GlobalSettings.Colors.WhiteText,
|
||||
}
|
||||
|
||||
self.focusedProps = {
|
||||
buttonColor3 = GlobalSettings.Colors.BlueButton,
|
||||
buttonTransparency = 0,
|
||||
textColor3 = GlobalSettings.Colors.TextSelected,
|
||||
}
|
||||
end
|
||||
|
||||
function SideBar:render()
|
||||
local props = self.props
|
||||
|
||||
local onClose = function()
|
||||
self.buttons = {}
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
if props.onClose then
|
||||
props.onClose()
|
||||
end
|
||||
end
|
||||
|
||||
local contents = {
|
||||
--Make it inside the title safe container
|
||||
UIPadding = Roact.createElement("UIPadding", {
|
||||
PaddingTop = props.paddingTop or UDim.new(0, 156),
|
||||
PaddingBottom = props.paddingBottom or UDim.new(0, 39)
|
||||
})
|
||||
}
|
||||
|
||||
if props.buttons then
|
||||
local index = 0
|
||||
for _, buttonObj in ipairs(props.buttons) do
|
||||
index = index + 1
|
||||
local btIndex = index
|
||||
local focused = self.state.selectedIndex and self.state.selectedIndex == btIndex
|
||||
local currProps = focused and self.focusedProps or self.defaultProps
|
||||
local btText = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(1, -INSET_X, 1, 0),
|
||||
Position = UDim2.new(0, INSET_X, 0, 0),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Text = buttonObj.text,
|
||||
TextSize = GlobalSettings.TextSizes.Medium,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = currProps.textColor3,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
local moveSelection = Roact.createElement(SoundComponent, {
|
||||
SoundName = "MoveSelection",
|
||||
})
|
||||
contents["Button"..btIndex] = Roact.createElement("ImageButton", {
|
||||
Image = self.buttonImage,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Size = UDim2.new(1, -1, 0, BUTTON_SIZE_Y),
|
||||
LayoutOrder = btIndex,
|
||||
ImageColor3 = currProps.buttonColor3,
|
||||
ImageTransparency = currProps.buttonTransparency,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(8, 8, 9, 9),
|
||||
SelectionImageObject = self.selectionImageObject,
|
||||
BackgroundTransparency = 1,
|
||||
[Roact.Event.SelectionGained] = function()
|
||||
self:setState({
|
||||
selectedIndex = btIndex
|
||||
})
|
||||
end,
|
||||
[Roact.Event.SelectionLost] = function()
|
||||
self:setState({
|
||||
selectedIndex = Roact.None
|
||||
})
|
||||
end,
|
||||
[Roact.Event.Activated] = function()
|
||||
SoundManager:Play("ButtonPress")
|
||||
onClose()
|
||||
buttonObj.callback()
|
||||
end,
|
||||
[Roact.Ref] = function(bt)
|
||||
self.buttons = self.buttons or {}
|
||||
self.buttons[btIndex] = bt
|
||||
end,
|
||||
}, {
|
||||
ButtonText = btText,
|
||||
MoveSelection = moveSelection,
|
||||
})
|
||||
end
|
||||
|
||||
if index > 0 then
|
||||
contents.UIListLayout = Roact.createElement("UIListLayout", {
|
||||
Padding = UDim.new(0, 0),
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
})
|
||||
end
|
||||
else
|
||||
contents.TextLabel = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(1, -INSET_X - 100, 1, 0),
|
||||
Position = UDim2.new(0, INSET_X, 0, 0),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
Text = props.text,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.DescriptionSize,
|
||||
TextWrapped = true,
|
||||
})
|
||||
end
|
||||
|
||||
local inFocus = props.inFocus
|
||||
local modalBackgroundTransparency = inFocus and GlobalSettings.ModalBackgroundTransparency or 1
|
||||
local containerPositionXScale = inFocus and 0.7 or 1
|
||||
if not inFocus then
|
||||
self.seenPressed = false
|
||||
end
|
||||
|
||||
return Roact.createElement(RoactMotion.SimpleMotion, {
|
||||
defaultStyle = {
|
||||
modalBackgroundTransparency = 1,
|
||||
containerPositionXScale = 1,
|
||||
},
|
||||
style = {
|
||||
modalBackgroundTransparency = RoactMotion.spring(modalBackgroundTransparency, 600, 60),
|
||||
containerPositionXScale = RoactMotion.spring(containerPositionXScale, 600, 60),
|
||||
},
|
||||
onRested = not inFocus and onClose,
|
||||
render = function(values)
|
||||
return Roact.createElement(Roact.Portal, { target = CoreGui }, {
|
||||
SideBarGui = Roact.createElement("ScreenGui", {
|
||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
DisplayOrder = props.displayOrder or 1
|
||||
}, {
|
||||
BackConnector = inFocus and Roact.createElement(ContextActionEvent, {
|
||||
name = "CloseSideBar",
|
||||
callback = function(actionName, inputState, inputObject)
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self.seenPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and self.seenPressed then
|
||||
self:setState({
|
||||
selectedIndex = Roact.None
|
||||
})
|
||||
if props.onRemoveFocus then
|
||||
props.onRemoveFocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
binds = { Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 },
|
||||
actionPriority = props.actionPriority
|
||||
}),
|
||||
ModalOverlay = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = values.modalBackgroundTransparency,
|
||||
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
SideBarContainer = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
Position = UDim2.new(values.containerPositionXScale, 0, 0, 0),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = GlobalSettings.OverlayColor,
|
||||
[Roact.Ref] = function(container)
|
||||
self.container = container
|
||||
end
|
||||
}, contents)
|
||||
})
|
||||
}),
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function SideBar:didMount()
|
||||
delay(0, function()
|
||||
if self.props.inFocus and self.container then
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
Utility.AddSelectionParent(self.groupKey, self.container)
|
||||
local trySelectIndex = self.props.selectIndex or 1
|
||||
if self.buttons and self.buttons[trySelectIndex] then
|
||||
if not self.state.selectedIndex then
|
||||
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
|
||||
end
|
||||
else
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function SideBar:didUpdate(previousProps, previousState)
|
||||
if self.props.inFocus == previousProps.inFocus then
|
||||
return
|
||||
end
|
||||
if self.props.inFocus and self.container then
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
Utility.AddSelectionParent(self.groupKey, self.container)
|
||||
local trySelectIndex = self.props.selectIndex or 1
|
||||
if self.buttons and self.buttons[trySelectIndex] then
|
||||
if not self.state.selectedIndex then
|
||||
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
|
||||
end
|
||||
else
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
end
|
||||
else
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
end
|
||||
end
|
||||
|
||||
function SideBar:willUnmount()
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
end
|
||||
|
||||
return SideBar
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
--[[
|
||||
A simple sound component
|
||||
|
||||
Props:
|
||||
SoundName : string - The name of the sound
|
||||
]]
|
||||
local RobloxGui = game:GetService("CoreGui").RobloxGui
|
||||
local Modules = RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
return function(props)
|
||||
local soundName = props.SoundName
|
||||
local soundsUrl = GlobalSettings.Sounds[soundName]
|
||||
return Roact.createElement('Sound',
|
||||
{
|
||||
SoundId = soundsUrl
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
--[[
|
||||
Creates a component as a spinner
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Components = Modules.Shell.Components
|
||||
local RenderStep = require(Components.RenderStep)
|
||||
local SPINNER_IMAGE = 'rbxasset://textures/ui/Shell/Icons/LoadingSpinner@1080.png'
|
||||
local Spinner = Roact.PureComponent:extend("Spinner")
|
||||
|
||||
function Spinner:init()
|
||||
self.state = {
|
||||
rotation = 0
|
||||
}
|
||||
|
||||
self.speed = 360
|
||||
|
||||
self.update = function(dt)
|
||||
self:setState({
|
||||
rotation = self.state.rotation + dt * self.speed
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function Spinner:render()
|
||||
local props = self.props
|
||||
local state = self.state
|
||||
local rotation = state.rotation
|
||||
self.speed = props.speed or 360
|
||||
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Rotation = rotation,
|
||||
BackgroundTransparency = 1,
|
||||
Size = props.Size or UDim2.new(0, 100, 0, 100),
|
||||
AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 0.5),
|
||||
Position = props.Position or UDim2.new(0.5, 0, 0.5, 0),
|
||||
Image = SPINNER_IMAGE,
|
||||
ZIndex = props.ZIndex or 10,
|
||||
ImageTransparency = props.ImageTransparency or 0,
|
||||
},{
|
||||
Render = Roact.createElement(RenderStep, {
|
||||
name = tick(),
|
||||
priority = Enum.RenderPriority.Input.Value,
|
||||
callback = self.update,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Spinner
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
--[[
|
||||
Creates a Roact component provides a left and a right view
|
||||
Props:
|
||||
Bias : number (0 - 1) - The bias for the size of left view vs the right view
|
||||
LeftView : Roact.Component - The left roact element that will be a child of the SplitView
|
||||
RightView : Roact.Component - The right roact element that will be a child of the SplitView
|
||||
Divider : Roact.Component - The divide roact element that will divide the two sides.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
|
||||
local SplitViewLR = Roact.Component:extend("SplitViewLR")
|
||||
|
||||
function SplitViewLR:init()
|
||||
end
|
||||
|
||||
function SplitViewLR:render()
|
||||
local props = self.props
|
||||
local bias = props.Bias
|
||||
self.leftView = props.LeftView
|
||||
self.rightView = props.RightView
|
||||
self.divider = props.Divider
|
||||
|
||||
return Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1,0,1,0),
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
LeftFrame = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(bias,0,1,0),
|
||||
Position = UDim2.new(0,0,0,0),
|
||||
BackgroundTransparency = 1,
|
||||
},{Left = self.leftView}),
|
||||
Divider = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(0,0,0,0),
|
||||
Position = UDim2.new(bias,0,0,0),
|
||||
BackgroundTransparency = 1,
|
||||
},{Divider = self.divider}),
|
||||
RightFrame = Roact.createElement("Frame",
|
||||
{
|
||||
Size = UDim2.new(1-bias,0,1,0),
|
||||
Position = UDim2.new(bias,0,0,0),
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
Right = self.rightView
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return SplitViewLR
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
--[[
|
||||
Create a Roact toggle button
|
||||
Props:
|
||||
Key : Variant - The key for the toggle button.
|
||||
Text : String - The text of the button.
|
||||
Size : UDim2 - The size of the this button.
|
||||
AnchorPoint : Vector2 - The anchor point of the button.
|
||||
Position : UDim2 - The position of the button.
|
||||
|
||||
Toggle : bool - Whether or not the button is toggled or not.
|
||||
Focused : bool - Is the button selected.
|
||||
Disabled : bool - Whether or not this button is disabled.
|
||||
Selected : bool - Should the button be selected.
|
||||
Selectable : bool - Whether or not the button is selectable.
|
||||
OnSelectionGained : function(key : Variant) - Fired when the GuiObject is being focused on with the Gamepad selector.
|
||||
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
|
||||
OnActivated : function() - Fires when the button is activated.
|
||||
]]
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Immutable = require(Modules.Common.Immutable)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
|
||||
|
||||
local ToggleButton = Roact.Component:extend("ToggleButton")
|
||||
|
||||
local TOGGLE_ICON_SIZE = 32
|
||||
|
||||
local TOGGLE_ICON_OFFSET_X = 20
|
||||
|
||||
function ToggleButton:init()
|
||||
self.key = self.props.Key
|
||||
self.state =
|
||||
{
|
||||
selected = false,
|
||||
active = false,
|
||||
}
|
||||
self.onSelectionGained = function()
|
||||
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
|
||||
end
|
||||
self.onSelectionLost = function()
|
||||
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
|
||||
end
|
||||
self.onActivated = function()
|
||||
if self.props.OnActivated then self.props.OnActivated() end
|
||||
end
|
||||
end
|
||||
|
||||
function ToggleButton:render()
|
||||
local buttonProps = {}
|
||||
local textProps = {}
|
||||
local size = self.props.Size
|
||||
local position = self.props.Position
|
||||
local anchorPoint = self.props.AnchorPoint
|
||||
local focused = self.props.Focused
|
||||
local disabled = self.props.Disabled
|
||||
local selected = self.props.Selected
|
||||
--Default is unknown since its not on or off
|
||||
local iconColor = GlobalSettings.Colors.StatusIconUnknown
|
||||
if self.props.Toggle == true then
|
||||
iconColor = GlobalSettings.Colors.StatusIconEnabled
|
||||
elseif self.props.Toggle == false then
|
||||
iconColor = GlobalSettings.Colors.StatusIconDisabled
|
||||
end
|
||||
|
||||
buttonProps.Size = UDim2.new(1, 0, 1, 0)
|
||||
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
buttonProps.Selectable = self.props.Selectable
|
||||
|
||||
textProps.Text = self.props.Text
|
||||
textProps.Size = UDim2.new(1, 0, 1, 0)
|
||||
textProps.AnchorPoint = Vector2.new(0, 0.5)
|
||||
textProps.Position = UDim2.new(0, TOGGLE_ICON_SIZE+2*TOGGLE_ICON_OFFSET_X, 0.5, 0)
|
||||
|
||||
local toggleImage = Roact.createElement("ImageLabel",
|
||||
{
|
||||
Size = UDim2.new(0, TOGGLE_ICON_SIZE, 0, TOGGLE_ICON_SIZE),
|
||||
Image = GlobalSettings.Images.EnabledStatusIcon,
|
||||
ImageColor3 = iconColor,
|
||||
Position = UDim2.new(0, TOGGLE_ICON_OFFSET_X, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
|
||||
local button = Roact.createElement(RoundedButton,
|
||||
{
|
||||
Button = buttonProps,
|
||||
Text = textProps,
|
||||
Focused = focused,
|
||||
Disabled = disabled,
|
||||
Selected = selected,
|
||||
OnSelectionGained = self.onSelectionGained,
|
||||
OnSelectionLost = self.onSelectionLost,
|
||||
OnActivated = self.onActivated,
|
||||
},{
|
||||
ToggleImage = toggleImage,
|
||||
})
|
||||
|
||||
local children = self.props[Roact.Children] or {}
|
||||
return Roact.createElement("Frame",
|
||||
{
|
||||
Size = size,
|
||||
Position = position,
|
||||
AnchorPoint = anchorPoint,
|
||||
BackgroundTransparency = 1,
|
||||
}, Immutable.JoinDictionaries(
|
||||
{
|
||||
Button = button,
|
||||
}, children))
|
||||
end
|
||||
|
||||
return ToggleButton
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
--[[
|
||||
Creates a Image Loader
|
||||
Props:
|
||||
rbxuid : int - Roblox user id.
|
||||
thumbnailType : Enum.ThumbnailType - Describes the type of user thumbnail that should be returned by GetUserThumbnailAsync.
|
||||
thumbnailSize : Enum.ThumbnailSize - Describes the resolution of a user thumbnail being returned by GetUserThumbnailAsync.
|
||||
Size : UDim2 - The thumbnail image size.
|
||||
Position : UDim2 - The thumbnail image position.
|
||||
BackgroundTransparency : float - Transparency of the thumbnail image background.
|
||||
BackgroundColor3 : Color3 - Color of the thumbnail image background.
|
||||
hasThumbnailData : bool - Whether we have the corresponding thumbnail data in store.
|
||||
imageUrl : string - The imageUrl for the thumbnail.
|
||||
isFetching : bool - Whether we are fetching the thumbnail.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Components = Modules.Shell.Components
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local ApiFetchUserThumbnail = require(Modules.Shell.Thunks.ApiFetchUserThumbnail)
|
||||
local Spinner = require(Components.Common.Spinner)
|
||||
local memoize = require(Modules.Common.memoize)
|
||||
local RETRIES = 6
|
||||
|
||||
local UserThumbnailLoader = Roact.PureComponent:extend("UserThumbnailLoader")
|
||||
|
||||
function UserThumbnailLoader:render()
|
||||
local props = self.props
|
||||
local rbxuid = props.rbxuid
|
||||
local children = {}
|
||||
|
||||
local imageUrl = ""
|
||||
if rbxuid and rbxuid > 0 then
|
||||
local hasThumbnailData = props.hasThumbnailData
|
||||
--TODO: Try refetch if last fetched failed after some interval
|
||||
if hasThumbnailData and props.imageUrl then
|
||||
imageUrl = props.imageUrl
|
||||
else
|
||||
props.fetchImage(rbxuid, props.thumbnailType, props.thumbnailSize)
|
||||
end
|
||||
|
||||
if props.showSpinner then
|
||||
children.Spinner = props.isFetching and Roact.createElement(Spinner)
|
||||
end
|
||||
else
|
||||
children.XboxDefaultProfileImage = Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Position = UDim2.new(0.25, 0, 0.25, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Image = GlobalSettings.Images.DefaultProfile,
|
||||
})
|
||||
end
|
||||
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Image = imageUrl,
|
||||
Size = props.size or UDim2.new(1, 0, 1, 0),
|
||||
Position = props.position or UDim2.new(0, 0, 0, 0),
|
||||
BackgroundTransparency = props.backgroundTransparency or 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = props.backgroundColor3 or GlobalSettings.Colors.CharacterBackground,
|
||||
}, children)
|
||||
end
|
||||
|
||||
local getThumbnailData = memoize(function(thumbnailData)
|
||||
return {
|
||||
hasThumbnailData = thumbnailData ~= nil,
|
||||
isFetching = thumbnailData and thumbnailData.isFetching,
|
||||
imageUrl = thumbnailData and thumbnailData.imageUrl,
|
||||
}
|
||||
end)
|
||||
|
||||
local function mapStateToProps(state, props)
|
||||
local rbxuid = props.rbxuid
|
||||
local thumbnailType = props.thumbnailType
|
||||
local thumbnailSize = props.thumbnailSize
|
||||
local thumbnailData;
|
||||
if rbxuid and rbxuid > 0 and thumbnailType and thumbnailSize then
|
||||
local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name }
|
||||
thumbnailData = state.UserThumbnails[thumbnailId]
|
||||
end
|
||||
return getThumbnailData(thumbnailData)
|
||||
end
|
||||
|
||||
local function mapDispatchToProps(dispatch)
|
||||
return {
|
||||
fetchImage = function(rbxuid, thumbnailType, thumbnailSize)
|
||||
return dispatch(ApiFetchUserThumbnail(rbxuid, thumbnailType, thumbnailSize, RETRIES))
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(UserThumbnailLoader)
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
--[[
|
||||
Allows the selector to navigate between different pages/levels.
|
||||
|
||||
How to use:
|
||||
The Navigator should be created at the root of the pages/levels.
|
||||
|
||||
- Call EnterRootSection(rootKey) to set the root page of the Navigator.
|
||||
|
||||
- Call Set(pageKey, defaultGuiObj) to set the default selection object for the given pageKey.
|
||||
|
||||
- Call Destruct() when done using the Navigator to clean up the event connections.
|
||||
|
||||
To Enter a page:
|
||||
1. Call SetNextPage(nextPageKey) to set the destination for the next page/level.
|
||||
2. Call EnterSection() to enter the next selection page/level.
|
||||
Set(pageKey, defaultGuiObj) needs to be called to set the default guiobject selection.
|
||||
Otherwise, the selection will fail and this function will return false.
|
||||
This function can be called anywhere to enter the next level.
|
||||
Eg. - Call by a GuiObject on selection.
|
||||
- Call by an event.
|
||||
- Call by an action with Rodux.
|
||||
|
||||
To Exit a page and return to the previous:
|
||||
1. Call ExitSection() to return to the previous page/level.
|
||||
This function can be called from anywhere.
|
||||
It will return false if it is unable to select the next that was set
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local ContextActionService = game:GetService('ContextActionService')
|
||||
local Nav = {}
|
||||
Nav.__index = Nav
|
||||
|
||||
local function AddEnterButtonHelper(self,keycode)
|
||||
local enterActionName = "EnterSection"..tostring(keycode)
|
||||
self._actions.EnterSection[keycode] = enterActionName
|
||||
ContextActionService:BindCoreAction(enterActionName,
|
||||
function(actionName, inputState, inputObject)
|
||||
return self:_enterAction(actionName, inputState, inputObject)
|
||||
end
|
||||
,false,keycode)
|
||||
end
|
||||
|
||||
local function AddExitButtonHelper(self,keycode)
|
||||
local exitActionName = "ExitSection"..tostring(keycode)
|
||||
self._actions.ExitSection[keycode] = exitActionName
|
||||
ContextActionService:BindCoreAction(exitActionName,
|
||||
function(actionName, inputState, inputObject)
|
||||
return self:_exitAction(actionName, inputState, inputObject)
|
||||
end
|
||||
,false,keycode)
|
||||
end
|
||||
|
||||
-- By default, the ButtonB binds to back
|
||||
-- Needs to be called before this object can be used.
|
||||
function Nav.new()
|
||||
local self = {}
|
||||
self._levels = {}
|
||||
self._guiobjs = {}
|
||||
self._nextPageKey = ""
|
||||
|
||||
self._actions = {}
|
||||
self._seenPressed = {}
|
||||
|
||||
self._actions.EnterSection = {}
|
||||
self._actions.ExitSection = {}
|
||||
|
||||
AddExitButtonHelper(self, Enum.KeyCode.ButtonB)
|
||||
setmetatable(self, Nav)
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets a custom key press to enter the next page.
|
||||
args:
|
||||
keycode : Enum.KeyCode - The key code of the button.
|
||||
]]
|
||||
function Nav:AddEnterButton(keycode)
|
||||
AddEnterButtonHelper(self,keycode)
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets a custom key press to exit to the previous page.
|
||||
args:
|
||||
keycode : Enum.KeyCode - The key code of the button.
|
||||
]]
|
||||
function Nav:AddExitButton(keycode)
|
||||
AddExitButtonHelper(self,keycode)
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets and enters the root page. Must be called before the navigator can be used.
|
||||
args:
|
||||
rootKey : Variant - The key for the page.
|
||||
]]
|
||||
function Nav:EnterRootSection(rootKey)
|
||||
if self._guiobjs[rootKey] == nil then
|
||||
return false
|
||||
end
|
||||
self._levels = {}
|
||||
--Stack push to the first element
|
||||
table.insert(self._levels,rootKey)
|
||||
Utility.SetSelectedCoreObject(self._guiobjs[rootKey])
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Enters the next page
|
||||
return:
|
||||
True : if it can enter the next page.
|
||||
False: otherwise.
|
||||
]]
|
||||
function Nav:EnterSection()
|
||||
local nextPageKey = self._nextPageKey
|
||||
if nextPageKey == nil or self._guiobjs[nextPageKey] == nil then
|
||||
return false
|
||||
end
|
||||
--Stack push
|
||||
table.insert(self._levels,nextPageKey)
|
||||
Utility.SetSelectedCoreObject(self._guiobjs[nextPageKey])
|
||||
self._nextPageKey = nil
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Exits to the previous page
|
||||
return:
|
||||
True : if it can enter the previous page
|
||||
False: otherwise.
|
||||
]]
|
||||
function Nav:ExitSection()
|
||||
--stack pop
|
||||
table.remove(self._levels)
|
||||
-- stack peak
|
||||
local key = self._levels[#self._levels]
|
||||
if self._guiobjs[key] == nil then
|
||||
return false
|
||||
end
|
||||
Utility.SetSelectedCoreObject(self._guiobjs[key])
|
||||
return true
|
||||
end
|
||||
|
||||
function Nav:_enterAction(actionName, inputState, inputObject)
|
||||
-- There is no next page to go to
|
||||
if self._nextPageKey == nil then
|
||||
return Enum.ContextActionResult.Pass
|
||||
end
|
||||
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self._seenPressed[inputObject.KeyCode] = true
|
||||
return Enum.ContextActionResult.Sink
|
||||
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
|
||||
self._seenPressed[inputObject.KeyCode] = false
|
||||
self:EnterSection()
|
||||
return Enum.ContextActionResult.Sink
|
||||
end
|
||||
end
|
||||
|
||||
function Nav:_exitAction(actionName, inputState, inputObject)
|
||||
--If we are current at the root page
|
||||
if(#self._levels <= 1) then
|
||||
-- There is nothing to go back to.
|
||||
return Enum.ContextActionResult.Pass
|
||||
end
|
||||
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self._seenPressed[inputObject.KeyCode] = true
|
||||
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
|
||||
self._seenPressed[inputObject.KeyCode] = false
|
||||
self:ExitSection()
|
||||
end
|
||||
return Enum.ContextActionResult.Sink
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets the default object to select.
|
||||
args:
|
||||
pageKey : Variant - The key for the page.
|
||||
defaultGuiObj : GuiObject - the default selected object for the page.
|
||||
]]
|
||||
function Nav:Set(pageKey, defaultGuiObj)
|
||||
self._guiobjs[pageKey] = defaultGuiObj
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets the key of the next page.
|
||||
args:
|
||||
nextPageKey : Variant - The key for the next page.
|
||||
]]
|
||||
function Nav:SetNextPage(nextPageKey)
|
||||
self._nextPageKey = nextPageKey
|
||||
end
|
||||
|
||||
-- Needs to be called to clean up event connection.
|
||||
function Nav:Destruct()
|
||||
for _,v in pairs(self._actions.EnterSection) do
|
||||
ContextActionService:UnbindCoreAction(v)
|
||||
end
|
||||
for _,v in pairs(self._actions.ExitSection) do
|
||||
ContextActionService:UnbindCoreAction(v)
|
||||
end
|
||||
end
|
||||
|
||||
return Nav
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
--[[
|
||||
Creates a Roact component provides a vertical list view for multiple elements
|
||||
Props:
|
||||
PaddingTop : UDim - The top padding for the list
|
||||
PaddingBottom : UDim - The bottom padding for the list
|
||||
Spacing : UDim - The spacing between each item of the list
|
||||
HorizontalAlignment : HorizontalAlignment - Determines how grid is placed within
|
||||
it's parent's container in the x direction.
|
||||
Can be Left, Center, or Right.
|
||||
ScrollingEnabled : bool - Determines whether or not scrolling is allowed on the frame.
|
||||
If false, no scroll bars will be rendered.
|
||||
ScrollBarThickness : number - How thick the scroll bar appears.
|
||||
This applies to both the horizontal and vertical scroll bars.
|
||||
If set to 0, no scroll bars are rendered.
|
||||
CanvasPosition : Vector2 - The location within the canvas, in pixels,
|
||||
that should be drawn at the top left of the scroll frame.
|
||||
CanvasSize : UDim2 - Determines the size of the area that is scrollable.
|
||||
The UDim2 is calculated using the parent gui's size,
|
||||
similar to the regular Size property on gui objects.
|
||||
Items: array<Roact.Component> - The items in the list view
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
|
||||
return function(props)
|
||||
local paddingTop = props.PaddingTop
|
||||
local paddingBottom = props.PaddingBottom
|
||||
local spacing = props.Spacing
|
||||
local horizontalAlignment = props.HorizontalAlignment
|
||||
local scrollBarThickness = props.ScrollBarThickness
|
||||
local scrollingEnabled = props.ScrollingEnabled
|
||||
local canvasPosition = props.CanvasPosition
|
||||
local canvasSize = props.CanvasSize
|
||||
local ListItems = {}
|
||||
ListItems["UIPadding"] = Roact.createElement("UIPadding",
|
||||
{
|
||||
PaddingTop = paddingTop,
|
||||
PaddingBottom = paddingBottom,
|
||||
})
|
||||
ListItems["UIListLayout"] = Roact.createElement("UIListLayout",
|
||||
{
|
||||
Padding = spacing,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = horizontalAlignment,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
CanvasPosition = canvasPosition,
|
||||
CanvasSize = canvasSize,
|
||||
})
|
||||
for k,v in pairs(props.Items) do
|
||||
ListItems[k] = v
|
||||
end
|
||||
|
||||
return Roact.createElement("ScrollingFrame",
|
||||
{
|
||||
Size = UDim2.new(1,0,1,0),
|
||||
ScrollBarThickness = scrollBarThickness,
|
||||
BackgroundTransparency = 1,
|
||||
Selectable = false,
|
||||
ScrollingEnabled = scrollingEnabled,
|
||||
},ListItems)
|
||||
end
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
--[[
|
||||
Creates a Roact component for inifinite scrolling
|
||||
Props:
|
||||
scrollingFrameProps : dictionary - props for the scrolling frame
|
||||
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
|
||||
.ClipsDescendants : bool - Determines whether Roblox will render any portions of its GUI descendants that are outside of its own borders.
|
||||
items : Array - An array of the input item data which is used to construct item component.
|
||||
itemSize : Vector2 - The size for each item in the scrolling frame.
|
||||
itemsPaddingOffset : int - The padding between each item.
|
||||
scrollingDirection : Enum.ScrollingDirection - The scrolling direction of the scrolling frame, can't be Enum.ScrollingDirection.XY.
|
||||
itemOffsetStart: int - The minimum distance of the selected guiobject to the window start border.
|
||||
itemOffsetEnd: int - The minimum distance of the selected guiobject to the window end border.
|
||||
customScrollDist: dictionary - Custom distances to trigger the scroll.
|
||||
generateKey : function() - Used to generate a name for the item.
|
||||
renderItem : function() -
|
||||
Input: item data, item index and an onSelectionGained callback
|
||||
Output: a Roact Component
|
||||
State:
|
||||
viewStart: int - The item start index.
|
||||
viewSize: int - The number of items can be put in the window.
|
||||
paddingStart: int - The padding to apply on the top / left side of the scrolling frame.
|
||||
]]
|
||||
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local WindowedScrollingFrame = Roact.PureComponent:extend("WindowedScrollingFrame")
|
||||
|
||||
|
||||
local function clipCanvasPosition(scrollingFrame, canvasPos)
|
||||
local canvasPosX = canvasPos.X
|
||||
local canvasPosY = canvasPos.Y
|
||||
canvasPosX = math.min(canvasPosX, scrollingFrame.CanvasSize.X.Offset - scrollingFrame.AbsoluteWindowSize.X)
|
||||
canvasPosX = math.max(0, canvasPosX)
|
||||
canvasPosY = math.min(canvasPosY, scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y)
|
||||
canvasPosY = math.max(0, canvasPosY)
|
||||
return Vector2.new(canvasPosX, canvasPosY)
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:init()
|
||||
self.state = {
|
||||
viewStart = 0,
|
||||
viewSize = 0,
|
||||
paddingStart = 0,
|
||||
}
|
||||
|
||||
self.scrollingFrameRef = function(rbx)
|
||||
self.scrollingFrame = rbx
|
||||
end
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:onSelectionChanged(selectedItem)
|
||||
if not self.scrollingFrame then
|
||||
return
|
||||
end
|
||||
if selectedItem == nil or selectedItem == self.savedSelectedObject or not selectedItem:IsDescendantOf(self.scrollingFrame) then
|
||||
return
|
||||
end
|
||||
self.savedSelectedObject = selectedItem
|
||||
|
||||
local scrollingFrame = self.scrollingFrame
|
||||
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
||||
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
|
||||
local canvasPosition = scrollingFrame.CanvasPosition
|
||||
|
||||
local axisKey = "X"
|
||||
if scrollingDirection == Enum.ScrollingDirection.Y then
|
||||
axisKey = "Y"
|
||||
end
|
||||
|
||||
-- If our scrolling frame has zero height / width, let's not bother trying to
|
||||
-- recompute our sizing
|
||||
if absoluteWindowSize[axisKey] == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local itemOffsetStart = self.props.itemOffsetStart or 0
|
||||
local itemOffsetEnd = self.props.itemOffsetEnd or 0
|
||||
local customScrollDist = self.props.customScrollDist or {}
|
||||
--If the selected guiobject is off-window, we move it back into the window instantly
|
||||
--Then make the motion
|
||||
local instantPos;
|
||||
local tweenTargetPos;
|
||||
if scrollingDirection == Enum.ScrollingDirection.Y then
|
||||
local topDistance = selectedItem.AbsolutePosition.Y - scrollingFrame.AbsolutePosition.Y
|
||||
local bottomDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).Y
|
||||
|
||||
local minDistTop = itemOffsetStart
|
||||
local minDistBottom = itemOffsetEnd
|
||||
|
||||
if topDistance < (customScrollDist.Top or minDistTop) then
|
||||
if topDistance < 0 then
|
||||
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y + topDistance)
|
||||
end
|
||||
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y - (minDistTop - topDistance))
|
||||
elseif bottomDistance < (customScrollDist.Bottom or minDistBottom) then
|
||||
if bottomDistance < 0 then
|
||||
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y - bottomDistance)
|
||||
end
|
||||
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y + minDistBottom - bottomDistance)
|
||||
end
|
||||
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
||||
local leftDistance = selectedItem.AbsolutePosition.X - scrollingFrame.AbsolutePosition.X
|
||||
local rightDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).X
|
||||
|
||||
local minDistLeft = itemOffsetStart
|
||||
local minDistRight = itemOffsetEnd
|
||||
|
||||
if leftDistance < (customScrollDist.Left or minDistLeft) then
|
||||
if leftDistance < 0 then
|
||||
instantPos = Vector2.new(canvasPosition.X + leftDistance, canvasPosition.Y)
|
||||
end
|
||||
tweenTargetPos = Vector2.new(canvasPosition.X - (minDistLeft - leftDistance), canvasPosition.Y)
|
||||
elseif rightDistance < (customScrollDist.Right or minDistRight) then
|
||||
if rightDistance < 0 then
|
||||
instantPos = Vector2.new(canvasPosition.X - rightDistance, canvasPosition.Y)
|
||||
end
|
||||
tweenTargetPos = Vector2.new(canvasPosition.X + minDistRight - rightDistance, canvasPosition.Y)
|
||||
end
|
||||
end
|
||||
if instantPos then
|
||||
instantPos = clipCanvasPosition(scrollingFrame, instantPos)
|
||||
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, instantPos, 0, Utility.EaseOutQuad, true, function()
|
||||
if tweenTargetPos then
|
||||
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
|
||||
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
if not instantPos and tweenTargetPos then
|
||||
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
|
||||
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", canvasPosition, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
|
||||
end
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:updateViewBounds()
|
||||
if not self.scrollingFrame then
|
||||
return
|
||||
end
|
||||
|
||||
local scrollingFrame = self.scrollingFrame
|
||||
local itemSize = self.props.itemSize
|
||||
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
|
||||
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
||||
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
|
||||
local canvasPosition = scrollingFrame.CanvasPosition
|
||||
|
||||
local axisKey = "X"
|
||||
if scrollingDirection == Enum.ScrollingDirection.Y then
|
||||
axisKey = "Y"
|
||||
end
|
||||
|
||||
-- If our scrolling frame has zero height / width, let's not bother trying to
|
||||
-- recompute our sizing
|
||||
if absoluteWindowSize[axisKey] == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
canvasPosition = clipCanvasPosition(scrollingFrame, canvasPosition)
|
||||
local itemTotalSize = (itemSize[axisKey] + itemsPaddingOffset)
|
||||
local viewSize = math.ceil(absoluteWindowSize[axisKey] / itemTotalSize) + 1
|
||||
local viewStart = math.floor(canvasPosition[axisKey] / itemTotalSize)
|
||||
local paddingStart = math.max(0, (viewStart - 1) * itemTotalSize)
|
||||
local shouldUpdate = viewSize ~= self.state.viewSize or viewStart ~= self.state.viewStart or paddingStart ~= self.state.paddingStart
|
||||
if shouldUpdate then
|
||||
self:setState({
|
||||
viewStart = viewStart,
|
||||
viewSize = viewSize,
|
||||
paddingStart = paddingStart,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:render()
|
||||
local items = self.props.items
|
||||
local generateKey = self.props.generateKey
|
||||
local renderItem = self.props.renderItem
|
||||
local itemSize = self.props.itemSize
|
||||
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
|
||||
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
||||
assert(scrollingDirection ~= Enum.ScrollingDirection.XY, "Can't set ScrollingDirection as XY.")
|
||||
|
||||
local children = {}
|
||||
|
||||
children.UIListLayout = Roact.createElement("UIListLayout", {
|
||||
Padding = UDim.new(0, itemsPaddingOffset),
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = scrollingDirection == Enum.ScrollingDirection.Y and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
|
||||
})
|
||||
|
||||
if scrollingDirection == Enum.ScrollingDirection.Y then
|
||||
children.UIPadding = Roact.createElement("UIPadding", {
|
||||
PaddingTop = UDim.new(0, self.state.paddingStart)
|
||||
})
|
||||
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
||||
children.UIPadding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, self.state.paddingStart)
|
||||
})
|
||||
end
|
||||
|
||||
local lowerBound = math.max(1, self.state.viewStart)
|
||||
local upperBound = math.min(#items, self.state.viewStart + self.state.viewSize)
|
||||
|
||||
for i = lowerBound, upperBound do
|
||||
local key = generateKey and generateKey(i) or i
|
||||
children[key] = renderItem(items[i], i)
|
||||
end
|
||||
|
||||
local scrollingFrameProps = self.props.scrollingFrameProps or {}
|
||||
local canvasSize = nil
|
||||
if scrollingDirection == Enum.ScrollingDirection.Y then
|
||||
canvasSize = UDim2.new(1, 0, 0, #items * itemSize.Y + (#items - 1) * itemsPaddingOffset)
|
||||
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
||||
canvasSize = UDim2.new(0, #items * itemSize.X + (#items - 1) * itemsPaddingOffset, 1, 0)
|
||||
end
|
||||
|
||||
return Roact.createElement("ScrollingFrame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
ScrollingEnabled = false, --Don't let the default select logic affect canvas position
|
||||
CanvasSize = canvasSize,
|
||||
Selectable = scrollingFrameProps.selectable or false,
|
||||
ScrollBarThickness = 0,
|
||||
ClipsDescendants = scrollingFrameProps.clipsDescendants,
|
||||
BackgroundTransparency = 1,
|
||||
ScrollingDirection = scrollingDirection,
|
||||
[Roact.Ref] = self.scrollingFrameRef,
|
||||
[Roact.Change.CanvasPosition] = function() self:updateViewBounds() end,
|
||||
[Roact.Change.AbsoluteSize] = function() self:updateViewBounds() end,
|
||||
}, children)
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:didMount()
|
||||
self:updateViewBounds()
|
||||
end
|
||||
|
||||
function WindowedScrollingFrame:didUpdate(prevProps, prevState)
|
||||
if not prevProps.inFocus and self.props.inFocus then
|
||||
self.conn = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):connect(function()
|
||||
self:onSelectionChanged(GuiService.SelectedCoreObject)
|
||||
end)
|
||||
elseif prevProps.inFocus and not self.props.inFocus then
|
||||
Utility.DisconnectEvent(self.conn)
|
||||
end
|
||||
|
||||
if self.props ~= prevProps then
|
||||
self:updateViewBounds()
|
||||
end
|
||||
end
|
||||
|
||||
return WindowedScrollingFrame
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
--[[
|
||||
A simple component that allows you to bind to ContextActionService at CoreScript level
|
||||
|
||||
Props
|
||||
name - the name of the binded action
|
||||
callback - the function that is invoked
|
||||
binds - the input that triggers the action
|
||||
this is a table - example { Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonX }
|
||||
actionPriority - the action priority
|
||||
Usage:
|
||||
ContextActionCn = Roact.createElement(ContextActionEvent, {
|
||||
name = "MyContextActionBind",
|
||||
callback = function() print("context event") end,
|
||||
binds = { Enum.KeyCode.Thumbstick2, Enum.KeyCode.ButtonB, Enum.KeyCode.A },
|
||||
actionPriority = 1,
|
||||
}),
|
||||
|
||||
Note: Cannot currently write a unit test for this component because it uses functions that
|
||||
are RobloxScript security. LuaCore team is looking into a solution for this
|
||||
]]
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local ContextActionEvent = Roact.Component:extend("ContextActionEvent")
|
||||
|
||||
local DEFAULT_ACTION_PRIORITY = 2000
|
||||
|
||||
function ContextActionEvent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
function ContextActionEvent:didMount()
|
||||
if self.props.actionPriority then
|
||||
ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds))
|
||||
else
|
||||
ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds))
|
||||
end
|
||||
end
|
||||
|
||||
function ContextActionEvent:didUpdate(oldProps)
|
||||
if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then
|
||||
ContextActionService:UnbindCoreAction(oldProps.name)
|
||||
if self.props.actionPriority then
|
||||
ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds))
|
||||
else
|
||||
ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ContextActionEvent:willUnmount()
|
||||
ContextActionService:UnbindCoreAction(self.props.name)
|
||||
end
|
||||
|
||||
return ContextActionEvent
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
--[[
|
||||
Creates a component with a button image and hint text
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
return function(props)
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(0, 65, 0, 65),
|
||||
Position = props.Position,
|
||||
BackgroundTransparency = 1,
|
||||
Image = props.Image,
|
||||
}, {
|
||||
Text = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
Position = UDim2.new(1, 5, 0, -3),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = props.Text,
|
||||
})
|
||||
})
|
||||
end
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
--[[
|
||||
Creates a component with a gamepad image with a resize hint for the overscan screen
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
|
||||
return function(props)
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(0, 599, 0, 404),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Controller@1080.png",
|
||||
}, {
|
||||
Line = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 240, 0, 1),
|
||||
Position = UDim2.new(0, 437, 0, 220),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
InputHint = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(0, 0, 0, 0),
|
||||
Position = UDim2.new(1, 3, 0, -1),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = Strings:LocalizedString("ResizeScreenInputHint"),
|
||||
})
|
||||
}),
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,230 @@
|
||||
local GameOptionsSettings = settings():FindFirstChild("Game Options")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService("PlatformService") end)
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
|
||||
local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent)
|
||||
local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection)
|
||||
local RenderStep = require(Modules.Shell.Components.RenderStep)
|
||||
|
||||
local MIN_EDGE_PERCENT = Vector2.new(0.85, 0.85)
|
||||
local START_EDGE_PERCENT = Vector2.new(0.9, 0.9)
|
||||
|
||||
local CONSOLE_RESOLUTION = Vector2.new(1920, 1080)
|
||||
local ZERO_VEC2 = Vector2.new(0,0)
|
||||
local MAX_STICK_ACCELERATION = 3
|
||||
local ACCELERATION_RATE = 1
|
||||
|
||||
local DPAD_STEP_AMOUNT = 2
|
||||
local DPAD_CODE_TO_EDGE_PUSH = {
|
||||
[Enum.KeyCode.DPadDown] = Vector2.new(0, DPAD_STEP_AMOUNT);
|
||||
[Enum.KeyCode.DPadUp] = Vector2.new(0, -DPAD_STEP_AMOUNT);
|
||||
[Enum.KeyCode.DPadLeft] = Vector2.new(-DPAD_STEP_AMOUNT, 0);
|
||||
[Enum.KeyCode.DPadRight] = Vector2.new(DPAD_STEP_AMOUNT, 0);
|
||||
}
|
||||
|
||||
local function EdgeImage(props)
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(0, 95, 0, 95),
|
||||
Position = props.Position,
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
BackgroundTransparency = 1,
|
||||
Rotation = props.Rotation,
|
||||
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenAdjustmentArrow.png",
|
||||
})
|
||||
end
|
||||
|
||||
local Edges = Roact.Component:extend("Edges")
|
||||
|
||||
function Edges:init()
|
||||
local function getCurrentEdgePercent(newEdgePercent)
|
||||
return Utility.ClampVector2(MIN_EDGE_PERCENT, Vector2.new(1, 1), newEdgePercent)
|
||||
end
|
||||
|
||||
local function getCurrentEdgeSize(edgePercent)
|
||||
local absoluteEdgeSize = edgePercent * CONSOLE_RESOLUTION
|
||||
local roundedAbsoluteEdgeSize = Vector2.new(Utility.Round(absoluteEdgeSize.X/2), Utility.Round(absoluteEdgeSize.Y/2)) * 2
|
||||
return Utility.ClampVector2(ZERO_VEC2, CONSOLE_RESOLUTION, roundedAbsoluteEdgeSize)
|
||||
end
|
||||
|
||||
self.onAdjustThumbstick = function(actionName, inputState, inputObject)
|
||||
self._stickPosition = Utility.GamepadLinearToCurve(Vector2.new(inputObject.Position.X, -inputObject.Position.Y), 0.2)
|
||||
end
|
||||
|
||||
self.onAdjustDPad = function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
local pushAmount = DPAD_CODE_TO_EDGE_PUSH[inputObject.KeyCode]
|
||||
if pushAmount then
|
||||
pushAmount = pushAmount / CONSOLE_RESOLUTION
|
||||
if Utility.IsFinite(pushAmount.X) and Utility.IsFinite(pushAmount.Y) then
|
||||
self._edgePercent = getCurrentEdgePercent(self._edgePercent + pushAmount)
|
||||
self:setState({
|
||||
currentSize = getCurrentEdgeSize(self._edgePercent)
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.onReset = function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.End then
|
||||
self._stickPosition = ZERO_VEC2
|
||||
self._edgePercent = getCurrentEdgePercent(START_EDGE_PERCENT)
|
||||
self._acceleration = 1
|
||||
self:setState({
|
||||
currentSize = getCurrentEdgeSize(self._edgePercent),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
self.onAccept = function(actionName, inputState, inputObject)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
self._seenAPressed = true
|
||||
elseif inputState == Enum.UserInputState.End and self._seenAPressed then
|
||||
local success, err = pcall(function()
|
||||
GameOptionsSettings.OverscanPX = math.min(1, self._edgePercent.X)
|
||||
GameOptionsSettings.OverscanPY = math.min(1, self._edgePercent.Y)
|
||||
end)
|
||||
if self.props.onSetEdges then
|
||||
self.props.onSetEdges()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.onRenderStep = function()
|
||||
local now = tick()
|
||||
if self._lastUpdate then
|
||||
if self._stickPosition ~= ZERO_VEC2 then
|
||||
local delta = now - self._lastUpdate
|
||||
local transformedStick = (self._stickPosition) * self._acceleration * delta * 0.05
|
||||
self._edgePercent = getCurrentEdgePercent(transformedStick + self._edgePercent)
|
||||
self._acceleration = math.min(self._acceleration + delta * ACCELERATION_RATE, MAX_STICK_ACCELERATION)
|
||||
|
||||
self:setState({
|
||||
currentSize = getCurrentEdgeSize(self._edgePercent)
|
||||
})
|
||||
else
|
||||
self._acceleration = 1
|
||||
end
|
||||
end
|
||||
self._lastUpdate = now
|
||||
end
|
||||
|
||||
self.onSuspended = function()
|
||||
pcall(function()
|
||||
GameOptionsSettings.OverscanPX = self._lastSavedOverscan.X
|
||||
GameOptionsSettings.OverscanPY = self._lastSavedOverscan.Y
|
||||
end)
|
||||
end
|
||||
|
||||
local overscansetting = getCurrentEdgePercent(START_EDGE_PERCENT)
|
||||
local startSize = getCurrentEdgeSize(overscansetting)
|
||||
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
|
||||
pcall(function()
|
||||
if GameOptionsSettings.OverscanPX > 0 and GameOptionsSettings.OverscanPY > 0 then
|
||||
overscansetting = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY)
|
||||
overscansetting = getCurrentEdgePercent(overscansetting)
|
||||
startSize = getCurrentEdgeSize(overscansetting)
|
||||
|
||||
-- set the overscan settings to max so the user can accurately estimate their TVs overscan
|
||||
-- save previous settings so we can save on suspend
|
||||
self._lastSavedOverscan = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY)
|
||||
GameOptionsSettings.OverscanPX = 1
|
||||
GameOptionsSettings.OverscanPY = 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
self._stickPosition = ZERO_VEC2
|
||||
self._edgePercent = overscansetting
|
||||
self._lastUpdate = nil
|
||||
self._acceleration = 1
|
||||
self._seenAPressed = false
|
||||
|
||||
self.state = {
|
||||
currentSize = startSize,
|
||||
}
|
||||
end
|
||||
|
||||
function Edges:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, self.state.currentSize.X, 0, self.state.currentSize.Y),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
SelectionImage = Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(1, 2, 1, 2),
|
||||
Position = UDim2.new(0, -1, 0, -1),
|
||||
BackgroundTransparency = 1,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(21, 21, 41, 41),
|
||||
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenRangeOverlay.png",
|
||||
}),
|
||||
|
||||
TopLeft = Roact.createElement(EdgeImage, {
|
||||
Rotation = 0,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
}),
|
||||
|
||||
TopRight = Roact.createElement(EdgeImage, {
|
||||
Rotation = 90,
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
}),
|
||||
|
||||
BottomRight = Roact.createElement(EdgeImage, {
|
||||
Rotation = 180,
|
||||
Position = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(1, 1),
|
||||
}),
|
||||
|
||||
BottomLeft = Roact.createElement(EdgeImage, {
|
||||
Rotation = 270,
|
||||
Position = UDim2.new(0, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
}),
|
||||
|
||||
Render = Roact.createElement(RenderStep, {
|
||||
name = "UpdateAdjustmentScreen",
|
||||
priority = Enum.RenderPriority.Input.Value,
|
||||
callback = self.onRenderStep,
|
||||
}),
|
||||
|
||||
AdjustConnectorThumbstick = Roact.createElement(ContextActionEvent, {
|
||||
name = "ThumbstickAdjustmentScreen",
|
||||
callback = self.onAdjustThumbstick,
|
||||
binds = { Enum.KeyCode.Thumbstick2 },
|
||||
}),
|
||||
|
||||
AdjustConnectorDPad = Roact.createElement(ContextActionEvent, {
|
||||
name = "DPadAdjustmentScreen",
|
||||
callback = self.onAdjustDPad,
|
||||
binds = { Enum.KeyCode.DPadDown, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight },
|
||||
}),
|
||||
|
||||
ResetConnector = Roact.createElement(ContextActionEvent, {
|
||||
name = "ResetAdjustmentScreen",
|
||||
callback = self.onReset,
|
||||
binds = { Enum.KeyCode.ButtonX },
|
||||
}),
|
||||
|
||||
AcceptConnector = Roact.createElement(ContextActionEvent, {
|
||||
name = "AcceptAdjustmentScreen",
|
||||
callback = self.onAccept,
|
||||
binds = { Enum.KeyCode.ButtonA },
|
||||
}),
|
||||
|
||||
SuspendedCn = PlatformService and Roact.createElement(ExternalEventConnection, {
|
||||
event = PlatformService.Suspended,
|
||||
callback = self.onSuspended,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Edges
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
local TextService = game:GetService('TextService')
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
|
||||
local ControllerHint = require(script.Parent.ControllerHint)
|
||||
local Edges = require(script.Parent.Edges)
|
||||
local ButtonHint = require(script.Parent.ButtonHint)
|
||||
|
||||
local Overscan = Roact.Component:extend("Overscan")
|
||||
|
||||
function Overscan:render()
|
||||
-- We should really have a better API to handle text fits
|
||||
local resetOffset = TextService:GetTextSize(
|
||||
Strings:LocalizedString('ResetWord'),
|
||||
Utility.ConvertFontSizeEnumToInt(GlobalSettings.ButtonSize),
|
||||
GlobalSettings.RegularFont,
|
||||
Vector2.new(0, 0)
|
||||
)
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BorderSizePixel = 1,
|
||||
BackgroundTransparency = self.props.BackgroundTransparency,
|
||||
BackgroundColor3 = Color3.new(3/255, 3/255, 3/255),
|
||||
}, {
|
||||
Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BackgroundColor3 = Color3.new(3/255, 3/255, 3/255),
|
||||
BorderSizePixel = 1,
|
||||
Visible = self.props.ImageVisible,
|
||||
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Background.png",
|
||||
}),
|
||||
|
||||
Title = Roact.createElement("TextLabel", {
|
||||
Position = UDim2.new(0, 230, 0, 205),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.LightFont,
|
||||
FontSize = GlobalSettings.HeaderSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = Strings:LocalizedString("ScreenSizeWord");
|
||||
}),
|
||||
|
||||
Prompt = Roact.createElement("TextLabel", {
|
||||
Position = UDim2.new(0, 230, 0, 243),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = Strings:LocalizedString("ResizeScreenPrompt"),
|
||||
}),
|
||||
|
||||
Controller = Roact.createElement(ControllerHint),
|
||||
|
||||
AcceptHint = Roact.createElement(ButtonHint, {
|
||||
Position = UDim2.new(0.5, 25, 0.75, 0),
|
||||
Image = "rbxasset://textures/ui/Shell/ButtonIcons/AButton.png",
|
||||
Text = Strings:LocalizedString('AcceptWord'),
|
||||
}),
|
||||
|
||||
ResetHint = Roact.createElement(ButtonHint, {
|
||||
Position = UDim2.new(0.5, -25 - 65 - resetOffset.x, 0.75, 0),
|
||||
Image = "rbxasset://textures/ui/Shell/ButtonIcons/XButton.png",
|
||||
Text = Strings:LocalizedString('ResetWord'),
|
||||
}),
|
||||
|
||||
EdgeSelector = Roact.createElement(Edges, {
|
||||
onSetEdges = self.props.onUnmount,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Overscan
|
||||
@@ -0,0 +1,43 @@
|
||||
--[[
|
||||
A simple component that allows you to bind to RenderStep
|
||||
|
||||
Props:
|
||||
name - the name of the bind
|
||||
priority - when during the render step to call the function
|
||||
callback - function that will be invoked on render step
|
||||
|
||||
Usage:
|
||||
|
||||
RenderCn = Roact.createElement(RenderStep, {
|
||||
name = "MyRenderStep",
|
||||
priority = Enum.RenderPriority.Input.Value,
|
||||
callback = function() print("stepping") end
|
||||
})
|
||||
]]
|
||||
local RunService = game:GetService("RunService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local RenderStep = Roact.Component:extend("RenderStep")
|
||||
|
||||
function RenderStep:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
function RenderStep:didMount()
|
||||
RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback)
|
||||
end
|
||||
|
||||
function RenderStep:didUpdate(oldProps)
|
||||
if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then
|
||||
RunService:UnbindFromRenderStep(oldProps.name)
|
||||
RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback)
|
||||
end
|
||||
end
|
||||
|
||||
function RenderStep:willUnmount()
|
||||
RunService:UnbindFromRenderStep(self.props.name)
|
||||
end
|
||||
|
||||
return RenderStep
|
||||
@@ -0,0 +1,14 @@
|
||||
return function()
|
||||
local Roact = require(game:GetService("CoreGui").RobloxGui.Modules.Common.Roact)
|
||||
local RenderStep = require(script.Parent.RenderStep)
|
||||
|
||||
it("should create and destroy", function()
|
||||
local element = Roact.createElement(RenderStep, {
|
||||
name = "myRenderStep",
|
||||
priority = Enum.RenderPriority.Input.Value,
|
||||
callback = function() print("hello render step") end,
|
||||
})
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
--[[
|
||||
This helper module will be a bridge to help migrate the console AppShell to using Roact.
|
||||
The idea is to add the interface of a "Screen" to the Roact component, so the ScreenManager
|
||||
can be happy and correctly manage screens that are not using Roact.
|
||||
|
||||
You will only need to wrap Roact components with this if they are the root of a route. Anything
|
||||
that is a child screen/compoent of this should use Roact Routing.
|
||||
|
||||
Usage:
|
||||
local MyScreen = require(ShellModules.Components.MyScreen)
|
||||
local myRoactScreen = RoactScreenManagerWrapper.new(MyScreen, GuiRoot, {
|
||||
backgroundTransparency = 0,
|
||||
name = "MyScreen",
|
||||
})
|
||||
|
||||
ScreenManager:OpenScreen(myRoactScreen)
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local ScreenManager = require(Modules.Shell.ScreenManager)
|
||||
local AppState = require(Modules.Shell.AppState)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
|
||||
local RoactScreenManagerWrapper = {}
|
||||
RoactScreenManagerWrapper.__index = RoactScreenManagerWrapper
|
||||
|
||||
function RoactScreenManagerWrapper.new(roactComponent, parent, props, closeCallback)
|
||||
local self = {}
|
||||
props = props or {}
|
||||
|
||||
-- this will be passed to top level Roact components in order to close the screen
|
||||
-- since we need some way to route back to the previous screen through the screen manager
|
||||
local function onUnmount()
|
||||
if self == ScreenManager:GetTopScreen() then
|
||||
ScreenManager:CloseCurrent()
|
||||
if closeCallback then
|
||||
closeCallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
props.onUnmount = onUnmount
|
||||
local element = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = AppState.store,
|
||||
}, {
|
||||
Roact.createElement(roactComponent, props)
|
||||
})
|
||||
self._instance = nil
|
||||
|
||||
function self:Show()
|
||||
self._instance = Roact.mount(element, parent, tostring(roactComponent))
|
||||
end
|
||||
|
||||
function self:Hide()
|
||||
self:Destruct()
|
||||
end
|
||||
|
||||
function self:Focus()
|
||||
-- do nothing
|
||||
end
|
||||
|
||||
function self:RemoveFocus()
|
||||
-- do nothing
|
||||
end
|
||||
|
||||
setmetatable(self, RoactScreenManagerWrapper)
|
||||
return self
|
||||
end
|
||||
|
||||
function RoactScreenManagerWrapper:Destruct()
|
||||
Roact.unmount(self._instance)
|
||||
end
|
||||
|
||||
return RoactScreenManagerWrapper
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local PlatformService = nil
|
||||
pcall(function() PlatformService = game:GetService("PlatformService") end)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local Components = Modules.Shell.Components
|
||||
local PresenceCard = require(Components.Social.PresenceCard)
|
||||
local WindowedScrollingFrame = require(Components.Common.WindowedScrollingFrame)
|
||||
local Immutable = require(Modules.Common.Immutable)
|
||||
local Spinner = require(Components.Common.Spinner)
|
||||
local SideBarComponent = require(Components.Common.SideBar)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local EventHub = require(Modules.Shell.EventHub)
|
||||
local GameJoinModule = require(Modules.Shell.GameJoin)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
local SoundManager = require(Modules.Shell.SoundManager)
|
||||
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
|
||||
local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader)
|
||||
|
||||
local SIDE_BAR_ITEMS = {
|
||||
JoinGame = Strings:LocalizedString("JoinGameWord"),
|
||||
ViewDetails = Strings:LocalizedString("ViewGameDetailsWord"),
|
||||
ViewProfile = Strings:LocalizedString("ViewGamerCardWord"),
|
||||
EmptyFriendSideBar = Strings:LocalizedString("EmptyFriendSideBarWord"),
|
||||
}
|
||||
|
||||
local FriendsScrollingView = Roact.PureComponent:extend("FriendsScrollingView")
|
||||
|
||||
function FriendsScrollingView:init()
|
||||
self.state = {
|
||||
sideBarInFocus = false,
|
||||
sideBarShow = false,
|
||||
currentSelectedIndex = 1,
|
||||
}
|
||||
|
||||
self.onSideBarClose = function()
|
||||
self:setState({
|
||||
sideBarInFocus = false,
|
||||
sideBarShow = false,
|
||||
})
|
||||
end
|
||||
self.onSideBarOpen = function(data)
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
self:setState({
|
||||
sideBarInFocus = true,
|
||||
sideBarShow = true,
|
||||
})
|
||||
end
|
||||
|
||||
self.groupKey = HttpService:GenerateGUID(false)
|
||||
end
|
||||
|
||||
function FriendsScrollingView:render()
|
||||
local props = self.props
|
||||
local friendsData = props.friendsData
|
||||
local initialized = props.initialized
|
||||
local actionPriority = self.props.actionPriority or 0
|
||||
|
||||
local hide = props.hide
|
||||
local inFocus = props.inFocus
|
||||
local sideBarInFocus = false
|
||||
local friendsScrollingFrameInFocus = false
|
||||
|
||||
local children = {}
|
||||
if not hide and inFocus then
|
||||
sideBarInFocus = self.state.sideBarShow and self.state.sideBarInFocus
|
||||
if not sideBarInFocus and #friendsData > 0 then
|
||||
friendsScrollingFrameInFocus = true
|
||||
end
|
||||
end
|
||||
|
||||
if initialized and friendsData then
|
||||
if #friendsData > 0 then
|
||||
local itemSize = Vector2.new(440, 120)
|
||||
local itemsPaddingOffset = 20
|
||||
local itemTotalSizeY = itemSize.Y + itemsPaddingOffset
|
||||
local itemsCount = math.floor(self.props.size.Y.Offset / itemTotalSizeY)
|
||||
assert(itemsCount ~= 0, "The scrolling window is too small to accommodate any presence card.")
|
||||
--We should have at least two items to ensure we can always select the top second item
|
||||
--while keep the top first item fully in view.
|
||||
--If the window is too small, we will always select the top first item.
|
||||
local itemOffsetStart = itemsCount > 2 and itemTotalSizeY or 0
|
||||
--This make sure when we scroll down the top card won't be clipped
|
||||
local itemOffsetEnd = self.props.size.Y.Offset - itemsCount * itemTotalSizeY + itemsPaddingOffset
|
||||
children.FriendsScrollingFrame = Roact.createElement(WindowedScrollingFrame, {
|
||||
items = friendsData,
|
||||
itemSize = itemSize,
|
||||
itemsPaddingOffset = itemsPaddingOffset,
|
||||
itemOffsetStart = itemOffsetStart,
|
||||
itemOffsetEnd = itemOffsetEnd,
|
||||
scrollingDirection = Enum.ScrollingDirection.Y,
|
||||
inFocus = friendsScrollingFrameInFocus,
|
||||
renderItem = function(data, index)
|
||||
local presenceCardProps = Immutable.JoinDictionaries(data, {
|
||||
layoutOrder = index,
|
||||
size = UDim2.new(0, itemSize.X, 0, itemSize.Y),
|
||||
focused = inFocus and self.state.currentSelectedIndex == index,
|
||||
selected = friendsScrollingFrameInFocus and self.state.currentSelectedIndex == index,
|
||||
})
|
||||
--Set up callbacks
|
||||
presenceCardProps.onActivated = function(bt)
|
||||
SoundManager:Play("SideMenuSlideIn")
|
||||
self.onSideBarOpen()
|
||||
end
|
||||
presenceCardProps.onSelectionGained = function()
|
||||
if self.state.currentSelectedIndex ~= index then
|
||||
self:setState({
|
||||
currentSelectedIndex = index,
|
||||
})
|
||||
end
|
||||
end
|
||||
return Roact.createElement(PresenceCard, presenceCardProps)
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
local data = friendsData[self.state.currentSelectedIndex]
|
||||
if inFocus and data and data.robloxuid then
|
||||
children.ProfileImage = Roact.createElement(UserThumbnailLoader, {
|
||||
rbxuid = data.robloxuid,
|
||||
thumbnailType = Enum.ThumbnailType.AvatarThumbnail,
|
||||
thumbnailSize = Enum.ThumbnailSize.Size352x352,
|
||||
position = UDim2.new(1, 101, 0, 0),
|
||||
size = UDim2.new(0, 680, 0, 680),
|
||||
backgroundTransparency = 1,
|
||||
showSpinner = true
|
||||
})
|
||||
end
|
||||
|
||||
if self.state.sideBarShow and data then
|
||||
local sideBarButtons = {}
|
||||
if data.robloxuid and data.robloxuid > 0 and data.robloxStatus == "InGame" then
|
||||
local placeId = data.placeId
|
||||
local lastLocation = data.lastLocation
|
||||
local robloxuid = data.robloxuid
|
||||
table.insert(sideBarButtons, {
|
||||
text = SIDE_BAR_ITEMS.JoinGame,
|
||||
callback = function()
|
||||
GameJoinModule:StartGame(GameJoinModule.JoinType.Follow, robloxuid)
|
||||
end
|
||||
})
|
||||
table.insert(sideBarButtons, {
|
||||
text = SIDE_BAR_ITEMS.ViewDetails,
|
||||
callback = function()
|
||||
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId, lastLocation, nil)
|
||||
end
|
||||
})
|
||||
end
|
||||
if data.xuid and #data.xuid > 0 and PlatformService then
|
||||
local xuid = data.xuid
|
||||
table.insert(sideBarButtons, {
|
||||
text = SIDE_BAR_ITEMS.ViewProfile,
|
||||
callback = function()
|
||||
-- NOTE: This will try to pop up the xbox system gamer card, failure will be handled by the xbox.
|
||||
pcall(function()
|
||||
PlatformService:PopupProfileUI(Enum.UserInputType.Gamepad1, xuid)
|
||||
end)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
local sideBarText = nil
|
||||
if #sideBarButtons == 0 then
|
||||
sideBarText = SIDE_BAR_ITEMS.EmptyFriendSideBar
|
||||
sideBarButtons = nil
|
||||
end
|
||||
children.SideBar = Roact.createElement(SideBarComponent, {
|
||||
actionPriority = actionPriority + 1,
|
||||
text = sideBarText,
|
||||
buttons = sideBarButtons,
|
||||
inFocus = sideBarInFocus,
|
||||
onClose = self.onSideBarClose,
|
||||
onRemoveFocus = function()
|
||||
self:setState({
|
||||
sideBarInFocus = false
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
else
|
||||
children.NoFriendsView = props.noFriendsView
|
||||
end
|
||||
else
|
||||
children.Spinner = Roact.createElement(Spinner)
|
||||
end
|
||||
|
||||
children.NavObj = Roact.createElement(RedirectComponent, {
|
||||
ActionPriority = actionPriority,
|
||||
Key = self.groupKey,
|
||||
InFocus = inFocus,
|
||||
RedirectBack = props.redirectBack,
|
||||
RedirectLeft = props.redirectLeft,
|
||||
RedirectRight = props.redirectRight,
|
||||
RedirectUp = props.redirectUp,
|
||||
RedirectDown = props.redirectDown,
|
||||
})
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
[Roact.Ref] = function(rbx)
|
||||
self.ref = rbx
|
||||
end,
|
||||
Visible = not hide,
|
||||
}, children)
|
||||
end
|
||||
|
||||
|
||||
function FriendsScrollingView:didMount()
|
||||
delay(0, function()
|
||||
if self.props.hide == false and self.props.inFocus then
|
||||
if self.ref ~= nil then
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
Utility.AddSelectionParent(self.groupKey, self.ref)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function FriendsScrollingView:didUpdate(previousProps, previousState)
|
||||
if self.props.hide or self.props.inFocus == previousProps.inFocus then
|
||||
return
|
||||
end
|
||||
if self.props.inFocus then
|
||||
if self.ref then
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
Utility.AddSelectionParent(self.groupKey, self.ref)
|
||||
end
|
||||
else
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
end
|
||||
end
|
||||
|
||||
function FriendsScrollingView:willUnmount()
|
||||
Utility.RemoveSelectionGroup(self.groupKey)
|
||||
end
|
||||
|
||||
return FriendsScrollingView
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local memoize = require(Modules.Common.memoize)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local CategoryMenuView = require(Modules.Shell.Components.Common.CategoryMenuView)
|
||||
local SplitViewLR = require(Modules.Shell.Components.Common.SplitViewLR)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local SoundManager = require(Modules.Shell.SoundManager)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
local FriendsScrollingView = require(Modules.Shell.Components.Social.FriendsScrollingView)
|
||||
local NoFriendsView = require(Modules.Shell.Components.Social.NoFriendsView)
|
||||
local PageKeys = require(Modules.Shell.PageKeys)
|
||||
|
||||
local FriendsView = Roact.PureComponent:extend("FriendsView")
|
||||
|
||||
local MenuKey = PageKeys.FriendCategories.Key
|
||||
local FriendCategoriesKeys = {
|
||||
OnlineFriends = PageKeys.FriendCategories.OnlineFriends.Key,
|
||||
AllFriends = PageKeys.FriendCategories.AllFriends.Key,
|
||||
}
|
||||
local FriendCategories = {}
|
||||
FriendCategories[FriendCategoriesKeys.OnlineFriends] = { Order = 1, StringKey = "OnlineWord" }
|
||||
FriendCategories[FriendCategoriesKeys.AllFriends] = { Order = 2, StringKey = "AllWord" }
|
||||
|
||||
local function onSelectSection(self, key)
|
||||
if self.state.currentPage ~= key then
|
||||
self:setState({
|
||||
currentPage = key,
|
||||
selectedPage = MenuKey,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function FriendsView:init()
|
||||
self.selectedPage = MenuKey
|
||||
self.onSelectionGained = function(key)
|
||||
onSelectSection(self,key)
|
||||
end
|
||||
|
||||
self.state = {
|
||||
currentPage = FriendCategoriesKeys.OnlineFriends,
|
||||
selectedPage = MenuKey,
|
||||
}
|
||||
|
||||
self.enterSection = function()
|
||||
SoundManager:Play("OverlayOpen")
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
self:setState({
|
||||
selectedPage = self.state.currentPage,
|
||||
})
|
||||
end
|
||||
self.exitSection = function()
|
||||
SoundManager:Play("PopUp")
|
||||
self:setState({
|
||||
selectedPage = MenuKey,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function FriendsView:render()
|
||||
local actionPriority = self.props.actionPriority or 0
|
||||
local currentPage = self.state.currentPage
|
||||
local friendsViewInFocus = self.props.inFocus
|
||||
local friendsViewHide = self.props.hide
|
||||
local friendCategoriesMenuInFocus = false
|
||||
local friendPagesInFocus = false
|
||||
if not friendsViewHide and friendsViewInFocus then
|
||||
if self.state.selectedPage == MenuKey then
|
||||
friendCategoriesMenuInFocus = true
|
||||
end
|
||||
if self.state.selectedPage ~= MenuKey then
|
||||
friendPagesInFocus = true
|
||||
end
|
||||
end
|
||||
|
||||
local enterSection = self.enterSection
|
||||
if currentPage == PageKeys.FriendCategories.OnlineFriends.Key and #self.props.onlineFriendsData == 0 then
|
||||
enterSection = nil
|
||||
elseif currentPage == PageKeys.FriendCategories.AllFriends.Key and #self.props.allFriendsData == 0 then
|
||||
enterSection = nil
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, -58, 1, 0),
|
||||
Position = UDim2.new(0, 58, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
Mainview = Roact.createElement(SplitViewLR, {
|
||||
Bias = 0.265,
|
||||
LeftView = Roact.createElement(CategoryMenuView, {
|
||||
Key = MenuKey,
|
||||
Categories = FriendCategories,
|
||||
InFocus = friendCategoriesMenuInFocus,
|
||||
DefaultCategoryFocus = FriendCategoriesKeys.OnlineFriends,
|
||||
OnSelectSection = self.onSelectionGained,
|
||||
EnterSection = enterSection,
|
||||
RedirectUp = self.props.redirectUp,
|
||||
ActionPriority = actionPriority,
|
||||
}),
|
||||
RightView = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
},{
|
||||
OnlineFriendsView = Roact.createElement(FriendsScrollingView, {
|
||||
friendsData = self.props.onlineFriendsData,
|
||||
initialized = self.props.initialized,
|
||||
hide = currentPage ~= PageKeys.FriendCategories.OnlineFriends.Key,
|
||||
inFocus = friendPagesInFocus,
|
||||
redirectLeft = self.exitSection,
|
||||
redirectBack = self.exitSection,
|
||||
redirectUp = self.props.redirectUp,
|
||||
redirectRight = self.props.redirectRight,
|
||||
size = UDim2.new(0, 440, 0, 770),
|
||||
noFriendsView = Roact.createElement(NoFriendsView, {
|
||||
text = Strings:LocalizedString("NoFriendsOnlinePhrase"),
|
||||
}),
|
||||
actionPriority = actionPriority + 1,
|
||||
}),
|
||||
AllFriendsView = Roact.createElement(FriendsScrollingView, {
|
||||
friendsData = self.props.allFriendsData,
|
||||
initialized = self.props.initialized,
|
||||
hide = currentPage ~= PageKeys.FriendCategories.AllFriends.Key,
|
||||
inFocus = friendPagesInFocus,
|
||||
redirectLeft = self.exitSection,
|
||||
redirectBack = self.exitSection,
|
||||
redirectUp = self.props.redirectUp,
|
||||
redirectRight = self.props.redirectRight,
|
||||
size = UDim2.new(0, 440, 0, 770),
|
||||
noFriendsView = Roact.createElement(NoFriendsView, {
|
||||
text = Strings:LocalizedString("PlayAndMakeFriendsPhrase"),
|
||||
}),
|
||||
actionPriority = actionPriority + 1,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local filterOnlineFriends = memoize(function(friendsData)
|
||||
local onlineFriendsData = {}
|
||||
for _, data in ipairs(friendsData) do
|
||||
if data.robloxStatus ~= "Offline" or data.xboxStatus == "Online" then
|
||||
table.insert(onlineFriendsData, data)
|
||||
end
|
||||
end
|
||||
return onlineFriendsData
|
||||
end)
|
||||
|
||||
local function mapStateToProps(state, props)
|
||||
local friendsData = state.RenderedFriends.data
|
||||
return {
|
||||
allFriendsData = friendsData,
|
||||
onlineFriendsData = filterOnlineFriends(friendsData),
|
||||
initialized = state.RenderedFriends.initialized
|
||||
}
|
||||
end
|
||||
|
||||
return RoactRodux.UNSTABLE_connect2(mapStateToProps)(FriendsView)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
--[[
|
||||
Creates a component for no friends
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
|
||||
return function(props)
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1
|
||||
}, {
|
||||
NoFriendsIcon = Roact.createElement("ImageLabel", {
|
||||
Size = UDim2.new(0, 296, 0, 259),
|
||||
Position = UDim2.new(0.5, -148, 0, 100),
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/Shell/Icons/FriendsIcon@1080.png",
|
||||
}),
|
||||
NoFriendsText = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(0, 440, 0, 72),
|
||||
Position = UDim2.new(0.5, -220, 0, 392),
|
||||
BackgroundTransparency = 1,
|
||||
Font = GlobalSettings.RegularFont,
|
||||
FontSize = GlobalSettings.ButtonSize,
|
||||
TextColor3 = GlobalSettings.WhiteTextColor,
|
||||
Text = props.text,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextWrapped = true
|
||||
}),
|
||||
})
|
||||
end
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
--[[
|
||||
Creates a PresenceCard component
|
||||
Props:
|
||||
gamertag: string - Xbox Gamertag.
|
||||
robloxName: string - Roblox name.
|
||||
robloxuid: int - Roblox user id.
|
||||
xuid: int - Xbox user id.
|
||||
robloxStatus: string - User's roblox xboxStatus.
|
||||
xboxStatus: string - User's xbox xboxStatus.
|
||||
lastLocation: string - User's last location info.
|
||||
layoutOrder: int - Controls the sorting priority of this button.
|
||||
size: UDim2 - The size of the presence card.
|
||||
onSelectionGained : function(guiObject : Ref<GuiObject>) -
|
||||
Fires when the GuiObject is being focused on with the Gamepad selector.
|
||||
onSelectionLost : function(guiObject : Ref<GuiObject>) -
|
||||
Fires when the Gamepad selector stops focusing on the GuiObject.
|
||||
onActivated : function(guiObject : Ref<GuiObject>) -
|
||||
Fires when the button is activated.
|
||||
]]
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local GlobalSettings = require(Modules.Shell.GlobalSettings)
|
||||
local Components = Modules.Shell.Components
|
||||
local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader)
|
||||
local Strings = require(Modules.Shell.LocalizedStrings)
|
||||
local Utility = require(Modules.Shell.Utility)
|
||||
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
|
||||
|
||||
local PresenceCard = Roact.PureComponent:extend("PresenceCard")
|
||||
|
||||
function PresenceCard:init()
|
||||
self.selectionImageObject = Utility.Create "ImageLabel"({
|
||||
Name = "SelectorImage",
|
||||
Image = GlobalSettings.Images.ButtonSelector,
|
||||
Position = UDim2.new(0, -7, 0, -7),
|
||||
Size = UDim2.new(1, 14, 1, 14),
|
||||
BackgroundTransparency = 1,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(31, 31, 63, 63),
|
||||
})
|
||||
self.onCreate = function(rbx)
|
||||
self.ref = rbx
|
||||
end
|
||||
self.defaultProps = {
|
||||
buttonColor3 = GlobalSettings.Colors.WhiteButton,
|
||||
buttonTransparency = 0.8,
|
||||
textColor3 = GlobalSettings.Colors.WhiteText,
|
||||
iconColor3 = GlobalSettings.Colors.WhiteText,
|
||||
}
|
||||
self.focusedProps = {
|
||||
buttonColor3 = GlobalSettings.Colors.BlueButton,
|
||||
buttonTransparency = 0,
|
||||
textColor3 = GlobalSettings.Colors.BlackText,
|
||||
iconColor3 = GlobalSettings.Colors.BlackText,
|
||||
}
|
||||
self.buttonImage = GlobalSettings.Images.ButtonDefault
|
||||
end
|
||||
|
||||
function PresenceCard:render()
|
||||
local props = self.props
|
||||
|
||||
local focused = self.props.focused
|
||||
local currProps = focused and self.focusedProps or self.defaultProps
|
||||
local gamertagText = props.gamertag or ""
|
||||
local robloxNameText = props.robloxName or ""
|
||||
local showGamertag = gamertagText ~= ""
|
||||
local showRobloxName = robloxNameText ~= ""
|
||||
local statusText = ""
|
||||
local statusImageColor3 = GlobalSettings.Colors.GreySelectedButton
|
||||
|
||||
local function setPresence(statusStr, statusColor)
|
||||
statusImageColor3 = statusColor
|
||||
if statusStr and statusStr ~= "" then
|
||||
statusText = statusStr
|
||||
end
|
||||
end
|
||||
if props.robloxStatus == "InGame" then
|
||||
setPresence(props.lastLocation, GlobalSettings.Colors.GreenText)
|
||||
elseif props.robloxStatus == "InStudio" then
|
||||
setPresence(props.lastLocation, GlobalSettings.Colors.OrangeText)
|
||||
elseif props.robloxStatus == "Online" then
|
||||
setPresence("Roblox", GlobalSettings.Colors.BlueText)
|
||||
else
|
||||
if props.xboxStatus and props.xboxStatus == "Online" then
|
||||
setPresence(Strings:LocalizedString("OnlineWord"), GlobalSettings.Colors.BlueText)
|
||||
else
|
||||
setPresence(Strings:LocalizedString("OfflineWord"), GlobalSettings.Colors.GreyText)
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement("ImageButton", {
|
||||
Image = self.buttonImage,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
LayoutOrder = props.layoutOrder,
|
||||
Size = props.size or UDim2.new(0, 440, 0, 120),
|
||||
ImageColor3 = currProps.buttonColor3,
|
||||
ImageTransparency = currProps.buttonTransparency,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(8, 8, 9, 9),
|
||||
SelectionImageObject = self.selectionImageObject,
|
||||
BackgroundTransparency = 1,
|
||||
[Roact.Event.SelectionGained] = props.onSelectionGained,
|
||||
[Roact.Event.SelectionLost] = props.onSelectionLost,
|
||||
[Roact.Event.Activated] = props.onActivated,
|
||||
[Roact.Ref] = self.onCreate,
|
||||
},{
|
||||
MoveSelection = Roact.createElement(SoundComponent, {
|
||||
SoundName = "MoveSelection",
|
||||
}),
|
||||
|
||||
AvatarImage = Roact.createElement(UserThumbnailLoader, {
|
||||
rbxuid = props.robloxuid,
|
||||
thumbnailType = Enum.ThumbnailType.HeadShot,
|
||||
thumbnailSize = Enum.ThumbnailSize.Size100x100,
|
||||
position = UDim2.new(0, 10, 0, 10),
|
||||
size = UDim2.new(0, 100, 0, 100),
|
||||
}),
|
||||
|
||||
ContentContainer = Roact.createElement("Frame",{
|
||||
Size = UDim2.new(1, -140, 1, 0),
|
||||
Position = UDim2.new(0, 126, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
}, {
|
||||
UIPadding = Roact.createElement("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 10),
|
||||
PaddingBottom = UDim.new(0, 10)
|
||||
}),
|
||||
|
||||
UIListLayout = Roact.createElement("UIListLayout", {
|
||||
Padding = UDim.new(0, 7),
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
GamertagContainer = showGamertag and Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
LayoutOrder = 1,
|
||||
BackgroundTransparency = 1
|
||||
},{
|
||||
GamertagLabel = Roact.createElement("TextLabel", {
|
||||
Text = gamertagText,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = currProps.textColor3,
|
||||
Font = GlobalSettings.Fonts.Regular,
|
||||
TextSize = 30,
|
||||
TextScaled = true,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
}),
|
||||
|
||||
RobloxNameContainer = showRobloxName and Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1
|
||||
},{
|
||||
RobloxIcon = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = GlobalSettings.Images.RobloxIcon,
|
||||
ImageColor3 = currProps.iconColor3,
|
||||
Position = UDim2.new(0, 0, 0, 1),
|
||||
Size = UDim2.new(0, 28, 0, 28),
|
||||
}),
|
||||
RobloxNameLabel = Roact.createElement("TextLabel", {
|
||||
Text = robloxNameText,
|
||||
Size = UDim2.new(1, -38, 1, 0),
|
||||
Position = UDim2.new(0, 38, 0, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = currProps.textColor3,
|
||||
Font = GlobalSettings.Fonts.Regular,
|
||||
TextSize = 30,
|
||||
TextScaled = true,
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
}),
|
||||
|
||||
StatusContainer = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 26),
|
||||
LayoutOrder = 3,
|
||||
BackgroundTransparency = 1
|
||||
},{
|
||||
PresenceStatusImage = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = GlobalSettings.Images.OnlineStatusIcon,
|
||||
Size = UDim2.new(0, 18, 0, 18),
|
||||
Position = UDim2.new(0, 5, 0, 4),
|
||||
ImageColor3 = statusImageColor3,
|
||||
}),
|
||||
PresenceLabel = Roact.createElement("TextLabel", {
|
||||
Text = statusText,
|
||||
Size = UDim2.new(1, -38, 1, 0),
|
||||
Position = UDim2.new(0, 33, 0, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = currProps.textColor3,
|
||||
Font = GlobalSettings.Fonts.Regular,
|
||||
TextSize = 26,
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
}),
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
function PresenceCard:didMount()
|
||||
delay(0, function()
|
||||
if self.props.selected then
|
||||
Utility.SetSelectedCoreObject(self.ref)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function PresenceCard:didUpdate(previousProps, previousState)
|
||||
if not previousProps.selected and self.props.selected then
|
||||
Utility.SetSelectedCoreObject(self.ref)
|
||||
end
|
||||
end
|
||||
|
||||
return PresenceCard
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local ShellModules = Modules:FindFirstChild("Shell")
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local Strings = require(ShellModules:FindFirstChild("LocalizedStrings"))
|
||||
local ScreenManager = require(ShellModules.ScreenManager)
|
||||
local AppState = require(ShellModules.AppState)
|
||||
local Analytics = require(ShellModules:FindFirstChild("Analytics"))
|
||||
local Utility = require(ShellModules:FindFirstChild("Utility"))
|
||||
local Components = ShellModules.Components
|
||||
local FriendsView = require(Components.Social.FriendsView)
|
||||
local FriendsData = require(ShellModules.FriendsData)
|
||||
|
||||
local function CreateSocialPane(parent)
|
||||
local this = {}
|
||||
local isPaneFocused = false
|
||||
local HubContainer = parent.Parent
|
||||
local UpSelector = HubContainer:FindFirstChild("TabContainer")
|
||||
|
||||
local noSelectionObject = Utility.Create"ImageLabel"({
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
|
||||
local SocialPaneContainer = Utility.Create"Frame"({
|
||||
Name = "SocialPane",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Visible = false,
|
||||
SelectionImageObject = noSelectionObject,
|
||||
Parent = parent,
|
||||
})
|
||||
|
||||
local FriendsContainer = Utility.Create"Frame"({
|
||||
Name = "FriendsContainer",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 23),
|
||||
BackgroundTransparency = 1,
|
||||
Parent = SocialPaneContainer,
|
||||
})
|
||||
|
||||
local friendsScrollerInstance;
|
||||
local function ReconcileFriendsScrollerInstance()
|
||||
if not friendsScrollerInstance then
|
||||
return
|
||||
end
|
||||
local friendsScroller = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = AppState.store,
|
||||
}, {
|
||||
FriendsView = Roact.createElement(FriendsView, {
|
||||
inFocus = isPaneFocused,
|
||||
hide = not SocialPaneContainer.Visible,
|
||||
redirectUp = function()
|
||||
Utility.SetSelectedCoreObject(UpSelector)
|
||||
end,
|
||||
})
|
||||
})
|
||||
friendsScrollerInstance = Roact.reconcile(friendsScrollerInstance, friendsScroller)
|
||||
end
|
||||
|
||||
function this:GetName()
|
||||
return Strings:LocalizedString("FriendsWord")
|
||||
end
|
||||
|
||||
function this:IsFocused()
|
||||
return isPaneFocused
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
function this:GetAnalyticsInfo()
|
||||
return {[Analytics.WidgetNames("WidgetId")] = Analytics.WidgetNames("SocialPaneId")}
|
||||
end
|
||||
|
||||
function this:Show(fromAppHub)
|
||||
SocialPaneContainer.Visible = true
|
||||
--Suspend Friends BG Update whenever we are on GamesPane
|
||||
FriendsData:SuspendUpdate()
|
||||
|
||||
--We rebuild Friends Scroller only if we navigate from other tabs
|
||||
if fromAppHub then
|
||||
if friendsScrollerInstance then
|
||||
Roact.unmount(friendsScrollerInstance)
|
||||
end
|
||||
local friendsScroller = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = AppState.store,
|
||||
}, {
|
||||
FriendsView = Roact.createElement(FriendsView, {
|
||||
hide = not SocialPaneContainer.Visible,
|
||||
inFocus = isPaneFocused,
|
||||
redirectUp = function()
|
||||
Utility.SetSelectedCoreObject(UpSelector)
|
||||
end,
|
||||
})
|
||||
})
|
||||
friendsScrollerInstance = Roact.mount(friendsScroller, FriendsContainer, "FriendsViewContainer")
|
||||
else
|
||||
ReconcileFriendsScrollerInstance()
|
||||
end
|
||||
ScreenManager:PlayDefaultOpenSound()
|
||||
end
|
||||
|
||||
function this:Hide(fromAppHub)
|
||||
SocialPaneContainer.Visible = false
|
||||
|
||||
--We destroy Friends Scroller if we navigate to other tabs
|
||||
if fromAppHub then
|
||||
if friendsScrollerInstance then
|
||||
Roact.unmount(friendsScrollerInstance)
|
||||
end
|
||||
friendsScrollerInstance = nil
|
||||
--We resume Friends Update only if we navigate to other tabs
|
||||
FriendsData:ResumeUpdate()
|
||||
else
|
||||
ReconcileFriendsScrollerInstance()
|
||||
end
|
||||
end
|
||||
|
||||
function this:Focus()
|
||||
isPaneFocused = true
|
||||
ReconcileFriendsScrollerInstance()
|
||||
end
|
||||
|
||||
function this:RemoveFocus()
|
||||
isPaneFocused = false
|
||||
ReconcileFriendsScrollerInstance()
|
||||
local selectedObject = GuiService.SelectedCoreObject
|
||||
if selectedObject and selectedObject:IsDescendantOf(SocialPaneContainer) then
|
||||
Utility.SetSelectedCoreObject(nil)
|
||||
end
|
||||
end
|
||||
|
||||
function this:SetPosition(newPosition)
|
||||
SocialPaneContainer.Position = newPosition
|
||||
end
|
||||
|
||||
function this:SetParent(newParent)
|
||||
SocialPaneContainer.Parent = newParent
|
||||
end
|
||||
|
||||
function this:IsAncestorOf(object)
|
||||
return SocialPaneContainer and SocialPaneContainer:IsAncestorOf(object)
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
return CreateSocialPane
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user