Clients/Client2018/content/internal/AppShell/Modules/Shell/CarouselController.lua

672 lines
19 KiB
Lua

--[[
// CarouselController.lua
// Controls how the data is updated for a carousel view
]]
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 EventHub = require(ShellModules:FindFirstChild('EventHub'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local GameData = require(ShellModules:FindFirstChild('GameData'))
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 sortData = 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(placeId)
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 = ThumbnailLoader:Create(item, data.IconId, ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon)
spawn(function()
thumbnailLoader:LoadAsync(true, false, nil)
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)
else
finished = function()
thumbnailLoader:SetTransparency(final)
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 = {}
for i = 1, #page do
local itemData = page[i]
local viewItemInfo = getNewItem(itemData)
if viewItemInfo then
viewItemInfo.item.MouseButton1Click:connect(function()
if itemData then
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], itemData)
end
end)
table.insert(viewItems, viewItemInfo)
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]
this.NewItemSelected:fire(currentFocusData)
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