diff --git a/components/music/PlaylistItem.bs b/components/music/PlaylistItem.bs index 98add611..9ca36a4f 100644 --- a/components/music/PlaylistItem.bs +++ b/components/music/PlaylistItem.bs @@ -5,17 +5,27 @@ import "pkg:/source/utils/misc.bs" sub init() m.title = m.top.findNode("title") + if isValid(m.title) + m.title.font.size = 27 + end if + m.subtitle = m.top.findNode("subtitle") - m.subtitle.color = ColorPalette.LIGHTGREY - m.subtitle.font.size = 27 + if isValid(m.subtitle) + m.subtitle.font.size = 23 + end if end sub sub itemContentChanged() itemData = m.top.itemContent if not isValidAndNotEmpty(itemData) then return - m.title.text = itemData.LookupCI("title") - m.subtitle.text = getSubtitleText(itemData) + if isValid(m.title) + m.title.text = itemData.LookupCI("title") + end if + + if isValid(m.subtitle) + m.subtitle.text = getSubtitleText(itemData) + end if itemIconData = m.global.queueManager.callFunc("getItemTitleAndIcon", itemData) @@ -24,6 +34,17 @@ sub itemContentChanged() mediaTypeIcon.uri = itemIconData[0] end if + if isValid(itemData.LookupCI("posterUrl")) + poster = m.top.findNode("poster") + poster.uri = itemData.posterUrl + end if + + startingPoint = chainLookupReturn(itemData, "startingPoint", 0) + position = m.top.findNode("position") + if isValid(position) + position.text = ticksToHuman(startingPoint) + end if + if isValid(itemData.LookupCI("RunTimeTicks")) tracklength = m.top.findNode("tracklength") tracklength.text = ticksToHuman(itemData.LookupCI("RunTimeTicks")) @@ -31,7 +52,7 @@ sub itemContentChanged() ' Only apply the played checkmark to specific item types playlistItemType = chainLookupReturn(itemData, "type", string.EMPTY) - if inArray([ItemType.MOVIE, ItemType.MUSICVIDEO, ItemType.EPISODE, ItemType.RECORDING, ItemType.VIDEO, ItemType.PROGRAM], playlistItemType) + if inArray([ItemType.MOVIE, ItemType.MUSICVIDEO, ItemType.EPISODE, ItemType.RECORDING, ItemType.VIDEO, ItemType.PROGRAM, ItemType.AUDIO], playlistItemType) playedIndicator = m.top.findNode("playedIndicator") playedIndicator.data = { played: chainLookupReturn(itemData, "played", false) @@ -65,7 +86,7 @@ function getSubtitleText(itemData as object) as string if isValid(itemData.LookupCI("ProductionYear")) subtitleText += `${itemData.LookupCI("ProductionYear")}` if isValid(itemData.LookupCI("ProductionYear")) - subtitleText += ` - ` + subtitleText += ` • ` end if end if diff --git a/components/music/PlaylistItem.xml b/components/music/PlaylistItem.xml index 9ce2c0b4..f430dd0b 100644 --- a/components/music/PlaylistItem.xml +++ b/components/music/PlaylistItem.xml @@ -2,11 +2,14 @@ - + + + + - - + + @@ -14,6 +17,7 @@ + diff --git a/components/music/PlaylistItems.bs b/components/music/PlaylistItems.bs index 1fb34544..7928dcb8 100644 --- a/components/music/PlaylistItems.bs +++ b/components/music/PlaylistItems.bs @@ -2,7 +2,6 @@ import "pkg:/source/enums/KeyCode.bs" import "pkg:/source/utils/misc.bs" sub init() - m.top.setfocus(true) group = m.global.sceneManager.callFunc("getActiveScene") group.lastFocus = m.top end sub diff --git a/components/music/PlaylistView.bs b/components/music/PlaylistView.bs index 8d402a69..d0f5f193 100644 --- a/components/music/PlaylistView.bs +++ b/components/music/PlaylistView.bs @@ -1,24 +1,77 @@ +import "pkg:/source/enums/AnimationControl.bs" import "pkg:/source/enums/ColorPalette.bs" +import "pkg:/source/enums/KeyCode.bs" import "pkg:/source/enums/TaskControl.bs" +import "pkg:/source/enums/ViewLoadStatus.bs" import "pkg:/source/utils/misc.bs" sub init() + m.focuedItem = 0 + m.toplevel = m.top.findNode("toplevel") + m.revealEpisodesAnimation = m.top.findNode("revealEpisodesAnimation") + m.toplevelTranslation = m.top.findNode("toplevelTranslation") + m.tableHeadingTranslation = m.top.findNode("tableHeadingTranslation") + m.playlistTranslation = m.top.findNode("playlistTranslation") + m.songListRectTranslation = m.top.findNode("songListRectTranslation") + m.songListRectHeight = m.top.findNode("songListRectHeight") + m.songListRectWidth = m.top.findNode("songListRectWidth") + + m.defaultClippingRect = "[-10,-10,1650,545]" + + m.loadStatus = ViewLoadStatus.INIT m.top.optionsAvailable = false m.getPlaylistDataTask = createObject("roSGNode", "GetPlaylistDataTask") + setupButtons() + m.albumCover = m.top.findNode("albumCover") m.playlist = m.top.findNode("playlist") - m.infoGroup = m.top.FindNode("infoGroup") m.songListRect = m.top.FindNode("songListRect") m.songListRect.color = ColorPalette.ELEMENTBACKGROUND m.playlist.focusBitmapBlendColor = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) m.playlist.observeField("doneLoading", "onDoneLoading") +end sub + +' Setup playback buttons, default to Play button selected +sub setupButtons() + m.buttonGrp = m.top.findNode("buttons") + m.buttonCount = m.buttonGrp.getChildCount() + m.top.lastFocus = m.buttonGrp + + m.resume = m.top.findNode("resume") + if isValid(m.resume) + m.resume.focusBackground = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) + end if - m.dscr = m.top.findNode("overview") - m.dscr.ellipsisText = tr("... (Press * to read more)") + m.play = m.top.findNode("play") + m.play.focusBackground = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) + + shuffle = m.top.findNode("shuffle") + shuffle.focusBackground = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) + + m.previouslySelectedButtonIndex = -1 + + m.top.observeFieldScoped("selectedButtonIndex", "onButtonSelectedChange") + m.top.selectedButtonIndex = 0 +end sub + +' Event handler when user selected a different playback button +sub onButtonSelectedChange() + ' Change previously focused button back to default + if m.previouslySelectedButtonIndex > -1 + previousSelectedButton = m.buttonGrp.getChild(m.previouslySelectedButtonIndex) + if isValid(previousSelectedButton) + previousSelectedButton.focus = false + end if + end if + + ' Change selected button image to focus state + selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex) + selectedButton.focus = true + selectedButton.setfocus(true) end sub ' Set values for displayed values on screen @@ -33,6 +86,41 @@ sub pageContentChanged() title.text = item.title end if + runtime = m.top.findNode("runtime") + if isValid(runtime) + runtime.font.size = 23 + end if + + numberofsongs = m.top.findNode("numberofsongs") + if isValid(numberofsongs) + numberofsongs.font.size = 23 + end if + + trackHeading = m.top.findNode("trackHeading") + if isValid(trackHeading) + trackHeading.font.size = 23 + end if + + titleHeading = m.top.findNode("titleHeading") + if isValid(titleHeading) + titleHeading.font.size = 23 + end if + + playedHeading = m.top.findNode("playedHeading") + if isValid(playedHeading) + playedHeading.font.size = 23 + end if + + positionHeading = m.top.findNode("positionHeading") + if isValid(positionHeading) + positionHeading.font.size = 23 + end if + + lengthHeading = m.top.findNode("lengthHeading") + if isValid(lengthHeading) + lengthHeading.font.size = 23 + end if + setPosterImage(item.posterURL) setOnScreenTextValues(item.json) end sub @@ -44,25 +132,10 @@ sub setPosterImage(posterURL) end if end sub -' Adjust scene by removing overview node and showing more songs -sub adjustScreenForNoOverview() - m.infoGroup.removeChild(m.dscr) - m.songListRect.height = 800 - m.playlist.numRows = 7 -end sub - ' Populate on screen text variables sub setOnScreenTextValues(json) if not isValid(json) then return - if isValidAndNotEmpty(json.overview) - ' We have overview text - setFieldTextValue("overview", json.overview) - else - ' We don't have overview text - adjustScreenForNoOverview() - end if - setFieldTextValue("numberofsongs", `${json.ChildCount} items`) if isStringEqual(type(json.ProductionYear), "roInt") @@ -74,21 +147,155 @@ sub setOnScreenTextValues(json) end if end sub +sub removeResumeButton() + if not isValid(m.resume) then return + m.buttonGrp.removeChild(m.resume) + m.resume = invalid + + m.buttonCount = m.buttonGrp.getChildCount() + m.top.selectedButtonIndex = 0 + onButtonSelectedChange() +end sub + +sub addResumeButton() +end sub + +function shouldShowResume(playlistData) as boolean + if not isValidAndNotEmpty(playlistData) then return false + if playlistData.getChildCount() = 0 then return false + + for each playlistItem in playlistData.getChildren(-1, 0) + if not chainLookupReturn(playlistItem, "played", false) + return true + end if + if chainLookupReturn(playlistItem, "startingPoint", 0) > 0 + return true + end if + end for + + return false +end function + sub OnScreenShown() + if m.loadStatus <> ViewLoadStatus.INIT + m.playlist.doneLoading = false + m.focuedItem = m.playlist.itemFocused + m.top.refreshPlaylistData = not m.top.refreshPlaylistData + return + end if + if isValid(m.top.lastFocus) m.top.lastFocus.setFocus(true) group = m.global.sceneManager.callFunc("getActiveScene") group.lastFocus = m.top.lastFocus - else - m.playlist.setFocus(true) + return + end if + + if isValid(m.resume) + m.resume.setFocus(true) group = m.global.sceneManager.callFunc("getActiveScene") - group.lastFocus = m.playlist + group.lastFocus = m.resume + return end if + + m.playlist.setFocus(true) + group = m.global.sceneManager.callFunc("getActiveScene") + group.lastFocus = m.playlist end sub +function getFirstUnplayedItem(playlistData) as integer + if not isValidAndNotEmpty(playlistData) then return 0 + if playlistData.getChildCount() = 0 then return 0 + + firstUnplayedItemIndex = 0 + + for each playlistItem in playlistData.getChildren(-1, 0) + if not chainLookupReturn(playlistItem, "played", false) + return firstUnplayedItemIndex + end if + + firstUnplayedItemIndex++ + end for + + return 0 +end function + function onKeyEvent(key as string, press as boolean) as boolean + if isValid(m.buttonGrp) + if m.buttonGrp.isInFocusChain() + if isStringEqual(key, KeyCode.LEFT) + if m.buttonGrp.getChild(m.top.selectedButtonIndex).escape = "left" + if m.top.selectedButtonIndex > 0 + m.previouslySelectedButtonIndex = m.top.selectedButtonIndex + m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1 + return true + end if + + return false + end if + end if + + if isStringEqual(key, KeyCode.RIGHT) + if m.top.selectedButtonIndex < m.buttonCount - 1 + m.previouslySelectedButtonIndex = m.top.selectedButtonIndex + m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1 + return true + end if + + return false + end if + + if isStringEqual(key, KeyCode.DOWN) + if m.buttonGrp.getChild(m.top.selectedButtonIndex).escape = "down" + m.buttonGrp.getChild(m.top.selectedButtonIndex).escape = string.EMPTY + selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex) + selectedButton.focus = false + m.playlist.setFocus(true) + m.top.lastFocus = m.playlist + enterEpisodeListFullScreen() + return true + end if + end if + + if isStringEqual(key, KeyCode.OK) + selectedButton = m.buttonGrp.getChild(m.top.selectedButtonIndex) + if not isValid(selectedButton) then return false + + if isStringEqual(chainLookup(selectedButton, "id"), "resume") + m.top.playlistItemSelected = getFirstUnplayedItem(m.top.listData) + return true + end if + + if isStringEqual(chainLookup(selectedButton, "id"), "play") + firstItem = m.top.listData.getChild(0) + firstItem.startingPoint = 0 + + m.top.playlistItemSelected = 0 + return true + end if + + if isStringEqual(chainLookup(selectedButton, "id"), "shuffle") + m.global.queueManager.callFunc("set", m.top.listData.getChildren(-1, 0)) + m.global.queueManager.callFunc("setShuffle", true) + m.global.queueManager.callFunc("playQueue") + return true + end if + end if + + return false + end if + end if + if not press then return false + if m.playlist.hasFocus() + if isStringEqual(key, KeyCode.BACK) + exitEpisodeListFullScreen() + onButtonSelectedChange() + return true + end if + end if + if key = "options" selectedItem = m.playlist.content.getChild(m.playlist.itemFocused) if not isValid(selectedItem) then return true @@ -100,17 +307,90 @@ function onKeyEvent(key as string, press as boolean) as boolean return false end function -sub createFullDscrDlg() - if isAllValid([m.top.overhangTitle, m.dscr.text]) - m.global.sceneManager.callFunc("standardDialog", m.top.overhangTitle, { data: ["

" + m.dscr.text + "

"] }) - end if +sub enterEpisodeListFullScreen() + if m.toplevel.translation[1] <> 100 then return + + m.songListRect.color = ColorPalette.BLACKDD + m.tableHeadingTranslation.reverse = false + m.playlistTranslation.reverse = false + m.toplevelTranslation.reverse = false + m.songListRectTranslation.reverse = false + m.songListRectHeight.reverse = false + m.songListRectWidth.reverse = false + m.revealEpisodesAnimation.control = AnimationControl.START + m.playlist.clippingRect = "[-10,-10,1650,1000]" +end sub + +sub exitEpisodeListFullScreen() + m.songListRect.color = ColorPalette.BLACK77 + m.tableHeadingTranslation.reverse = true + m.playlistTranslation.reverse = true + m.toplevelTranslation.reverse = true + m.songListRectTranslation.reverse = true + m.songListRectHeight.reverse = true + m.songListRectWidth.reverse = true + m.revealEpisodesAnimation.control = AnimationControl.START + m.playlist.clippingRect = m.defaultClippingRect end sub sub onDoneLoading() - m.playlist.unobservefield("doneLoading") + if not m.playlist.doneLoading then return + + if isValidAndNotEmpty(m.top.listData) + if shouldShowResume(m.top.listData) + addResumeButton() + else + removeResumeButton() + end if + else + noPageContent() + end if + + if m.top.listData.getChildCount() = 0 + noPageContent() + end if + + if m.loadStatus = ViewLoadStatus.INIT + m.loadStatus = ViewLoadStatus.LOADED + end if + + if isValid(m.top.lastFocus) + m.top.lastFocus.setFocus(true) + group = m.global.sceneManager.callFunc("getActiveScene") + group.lastFocus = m.top.lastFocus + else + m.play.lastFocus.setFocus(true) + group = m.global.sceneManager.callFunc("getActiveScene") + group.lastFocus = m.play + end if + + if isValid(m.focuedItem) + m.playlist.jumpToItem = m.focuedItem + end if + stopLoadingSpinner() end sub +sub noPageContent() + if isValid(m.play) + m.buttonGrp.removeChild(m.play) + m.play = invalid + end if + + shuffle = m.top.findNode("shuffle") + if isValid(shuffle) + m.buttonGrp.removeChild(shuffle) + end if + + if isValid(m.buttonGrp) + m.top.removeChild(m.buttonGrp) + m.buttonGrp = invalid + end if + + m.buttonCount = 0 + m.top.setfocus(true) +end sub + sub confirmPlaylistAccess(playlistID as string, selectedItemID as string, selectedItemTitle as string) m.getPlaylistDataTask.observeFieldScoped("playlistData", "onPlaylistDataLoaded") m.getPlaylistDataTask.playlistID = playlistID diff --git a/components/music/PlaylistView.xml b/components/music/PlaylistView.xml index c574ece8..4161405f 100644 --- a/components/music/PlaylistView.xml +++ b/components/music/PlaylistView.xml @@ -1,39 +1,98 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Main.bs b/source/Main.bs index 3ace2f5a..2f3b83cf 100644 --- a/source/Main.bs +++ b/source/Main.bs @@ -124,6 +124,8 @@ sub Main (args as dynamic) as void onQuickPlayEvent(msg) else if isNodeEvent(msg, "refreshSeasonDetailsData") onRefreshSeasonDetailsDataEvent() + else if isNodeEvent(msg, "refreshPlaylistData") + onRefreshPlaylistDataEvent() else if isNodeEvent(msg, "refreshMovieDetailsData") onRefreshMovieDetailsDataEvent() else if isNodeEvent(msg, "selectedItem") diff --git a/source/MainEventHandlers.bs b/source/MainEventHandlers.bs index b261eb77..51d6e505 100644 --- a/source/MainEventHandlers.bs +++ b/source/MainEventHandlers.bs @@ -550,6 +550,12 @@ sub onLibrarySelection(selectedItem) m.global.sceneManager.callFunc("pushScene", group) end sub +sub onRefreshPlaylistDataEvent() + currentScene = m.global.sceneManager.callFunc("getActiveScene") + playlistID = chainLookup(currentScene, "pageContent.id") + currentScene.listData = PlaylistItemList(playlistID) +end sub + sub onRefreshSeasonDetailsDataEvent() startLoadingSpinner() diff --git a/source/ShowScenes.bs b/source/ShowScenes.bs index f310fa86..2d19e315 100644 --- a/source/ShowScenes.bs +++ b/source/ShowScenes.bs @@ -932,6 +932,8 @@ sub CreatePlaylistView(playlist as object) ' Watch for user clicking on an item group.observeField("playlistItemSelected", m.port) + + group.observeField("refreshPlaylistData", m.port) end sub function CreateSeasonDetailsGroup(series as object, season as object) as dynamic diff --git a/source/api/Items.bs b/source/api/Items.bs index 199cd875..1588654b 100644 --- a/source/api/Items.bs +++ b/source/api/Items.bs @@ -189,11 +189,19 @@ function ItemMetaData(id as string) tmp.image = PosterImage(data.id, imgParams) tmp.json = data return tmp - else if data.type = "BoxSet" or data.type = "Playlist" + else if data.type = "BoxSet" tmp = CreateObject("roSGNode", "CollectionData") tmp.image = PosterImage(data.id, imgParams) tmp.json = data return tmp + else if data.type = "Playlist" + tmp = CreateObject("roSGNode", "CollectionData") + tmp.image = PosterImage(data.id, { + maxHeight: 300, + maxWidth: 300 + }) + tmp.json = data + return tmp else if data.type = "Season" tmp = CreateObject("roSGNode", "TVSeasonData") tmp.image = PosterImage(data.id) @@ -409,6 +417,26 @@ function GetSongsByArtist(id as string, params = {} as object) return row end function +function getPosterURLFromTag(item, params) as string + if isValidAndNotEmpty(chainLookup(item, "ImageTags.primary")) + if not isValidAndNotEmpty(params.maxHeight) + params.append({ "maxHeight": "70" }) + end if + + if not isValidAndNotEmpty(params.maxWidth) + params.append({ "maxWidth": "70" }) + end if + + if not isValidAndNotEmpty(params.tag) + params.append({ "tag": chainLookup(item, "ImageTags.primary") }) + end if + + return ImageURL(item.LookupCI("id"), ImageType.PRIMARY, params) + end if + + return string.EMPTY +end function + ' Get Items that are under the provided item function PlaylistItemList(id as string) url = Substitute("Playlists/{0}/Items", id) @@ -426,6 +454,8 @@ function PlaylistItemList(id as string) for each item in data.Items tmp = CreateObject("roSGNode", "PlaylistItemData") + + tmp.posterUrl = getPosterURLFromTag(item, {}) tmp.id = item.LookupCI("id") tmp.title = item.LookupCI("name") tmp.artists = item.LookupCI("artists") @@ -440,6 +470,7 @@ function PlaylistItemList(id as string) tmp.IndexNumberEnd = item.LookupCI("IndexNumberEnd") tmp.ParentIndexNumber = item.LookupCI("ParentIndexNumber") tmp.played = chainLookupReturn(item, "UserData.played", false) + tmp.startingPoint = chainLookupReturn(item, "UserData.PlaybackPositionTicks", 0) results.push(tmp) end for diff --git a/source/static/whatsNew/3.1.8.json b/source/static/whatsNew/3.1.8.json index e845340d..c399958b 100644 --- a/source/static/whatsNew/3.1.8.json +++ b/source/static/whatsNew/3.1.8.json @@ -22,5 +22,9 @@ { "description": "Prevent subtitles from getting too close to the top or bottom of the screen", "author": "gabeluci" + }, + { + "description": "Redesign playlist screen", + "author": "1hitsong" } -] \ No newline at end of file +]