├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── feature_request.yml ├── workflows │ └── test.yml └── pull_request_template.md ├── components ├── config │ ├── ConfigData.bs │ ├── ServerDiscoveryTask.xml │ ├── FormList.xml │ ├── ConfigData.xml │ ├── FormElement.xml │ ├── FormElement.bs │ └── JFServer.xml ├── data │ ├── PublicUserData.bs │ ├── OptionsButton.bs │ ├── AlbumData.bs │ ├── MusicArtistData.xml │ ├── MusicAlbumSongListData.xml │ ├── JFContentItem.bs │ ├── ServerData.bs │ ├── ImageData.bs │ ├── OptionsButton.xml │ ├── GetFiltersTask.xml │ ├── PhotoData.xml │ ├── TVEpisode.xml │ ├── AudioStreamData.xml │ ├── ServerData.xml │ ├── VideoData.xml │ ├── ColorOptionData.xml │ ├── PublicUserData.xml │ ├── AlbumData.xml │ ├── GetFiltersTask.bs │ ├── FolderData.xml │ ├── PersonData.xml │ ├── ChannelData.xml │ ├── GetPlaylistDataTask.bs │ ├── MusicAlbumData.xml │ ├── PlaylistData.bs │ ├── PlaylistItemData.bs │ ├── GetPlaylistDataTask.xml │ ├── TVSeasonData.bs │ ├── CollectionData.xml │ ├── SeriesData.xml │ ├── MusicAlbumData.bs │ ├── MusicSongData.bs │ ├── TVSeasonData.xml │ ├── PlaylistData.xml │ ├── ImageData.xml │ ├── OptionsData.xml │ ├── MusicSongData.xml │ ├── UserData.xml │ ├── RecordingData.bs │ ├── MovieData.xml │ ├── TVEpisodeData.bs │ ├── JFContentItem.xml │ ├── VideoData.bs │ ├── TVEpisodeData.xml │ ├── RecordingData.xml │ ├── ScheduleProgramData.xml │ ├── ExtrasData.xml │ ├── HomeData.xml │ ├── SearchData.xml │ ├── ChannelData.bs │ ├── PlaylistItemData.xml │ ├── SceneManager.xml │ ├── OptionsData.bs │ ├── TVEpisode.bs │ ├── MusicAlbumSongListData.bs │ ├── PhotoData.bs │ ├── PersonData.bs │ ├── MusicArtistData.bs │ ├── UserData.bs │ └── ScheduleProgramData.bs ├── options │ ├── OptionNode.bs │ ├── OptionNode.xml │ ├── OptionsSlider.xml │ └── OptionsSlider.bs ├── Buttons │ ├── ButtonData.bs │ ├── TextSizeTask.bs │ ├── TextSizeTask.xml │ ├── SlideOutButton.xml │ ├── ButtonData.xml │ ├── ExpandingLabel.xml │ └── JFButtons.xml ├── .DS_Store ├── labels │ ├── Text.xml │ ├── ScrollingText.xml │ ├── Text.bs │ └── ScrollingText.bs ├── Spinner.xml ├── settings │ ├── setting.xml │ ├── Slider.xml │ ├── ColorGrid.bs │ └── settings.xml ├── Spinner.bs ├── JFGroup.bs ├── JFButton.xml ├── WhatsNewDialog.xml ├── ButtonGroupHoriz.xml ├── JFScreen.xml ├── tvshows │ ├── TVSeasonRow.xml │ ├── TVExtrasTask.xml │ ├── TVEpisodeRow.xml │ ├── TVExtrasTask.bs │ ├── TVEpisodes.xml │ ├── TVListOptions.xml │ └── TVSeasonRow.bs ├── music │ ├── LoadScreenSaverTimeoutTask.xml │ ├── SimilarArtistGrid.xml │ ├── LoadScreenSaverTimeoutTask.bs │ ├── PlaylistItems.xml │ ├── AlbumTrackList.xml │ ├── AlbumGrid.xml │ ├── PlaylistItems.bs │ ├── SongItem.bs │ ├── SongItem.xml │ ├── AlbumTrackList.bs │ ├── SimilarArtistGrid.bs │ ├── Lyrics.xml │ ├── PlaylistItem.xml │ ├── AlbumGrid.bs │ ├── PlaylistView.xml │ └── AlbumView.xml ├── search │ ├── SearchTask.xml │ ├── SearchRow.xml │ ├── SearchTask.bs │ └── SearchResults.xml ├── PlaystateTask.xml ├── GetPlaybackInfoTask.xml ├── ItemGrid │ ├── FavoriteItemsTask.xml │ ├── AlphaItem.bs │ ├── AlphaItem.xml │ ├── FavoriteItemsTask.bs │ ├── ColorOption.bs │ ├── ColorOption.xml │ ├── MusicArtistGridItem.xml │ ├── GridItemSmall.xml │ ├── LoadVideoContentTask.xml │ ├── LoadItemsTask2.xml │ ├── GridItem.xml │ ├── GridItemMedium.xml │ └── AudioBookGridItem.xml ├── GetShuffleEpisodesTask.xml ├── login │ ├── UserRow.xml │ ├── UserItem.xml │ ├── UserRow.bs │ └── UserItem.bs ├── ButtonGroupVert.xml ├── StandardDialog.xml ├── photos │ ├── LoadPhotoTask.xml │ ├── LoadPhotoTask.bs │ └── PhotoDetails.xml ├── quickConnect │ ├── QuickConnect.xml │ ├── QuickConnectDialog.xml │ └── QuickConnect.bs ├── GetNextEpisodeTask.xml ├── home │ ├── HomeRow.xml │ ├── HomeRows.xml │ ├── LoadItemsTask.xml │ ├── Home.xml │ └── HomeItem.xml ├── subtitle │ ├── SubtitleData.xml │ ├── SubtitleItem.xml │ └── SubtitleItem.bs ├── movies │ ├── AudioTrackListData.xml │ ├── VideoTrackListData.xml │ ├── SubtitleTrackListData.xml │ ├── AudioTrackListItem.xml │ ├── VideoTrackListItem.xml │ ├── AudioTrackListItem.bs │ └── VideoTrackListItem.bs ├── liveTv │ ├── RecordProgramTask.xml │ ├── LoadSheduleTask.xml │ ├── LoadProgramDetailsTask.xml │ ├── LoadChannelsTask.xml │ ├── ChannelInfo.xml │ ├── schedule.xml │ ├── LoadSheduleTask.bs │ └── LoadProgramDetailsTask.bs ├── SearchBox.xml ├── keyboards │ └── IntegerKeyboard.xml ├── RadioDialog.xml ├── video │ ├── PreloadTrickplayImagesTask.xml │ └── PreloadTrickplayImagesTask.bs ├── RemoteSubtitleDialog.xml ├── GetShuffleEpisodesTask.bs ├── JFGroup.xml ├── Clock.xml ├── GetNextEpisodeTask.bs ├── section │ └── sectionScroller.xml ├── mediaPlayers │ └── AudioPlayer.xml ├── captionTask.xml ├── extras │ ├── ExtrasRowList.xml │ ├── ExtrasItem.xml │ └── ExtrasSlider.xml ├── tasks │ └── PostTask.xml ├── PlayedCheckmark.xml ├── JFButton.bs ├── ButtonGroupHoriz.bs ├── JFMessageDialog.xml ├── BaseScene.xml ├── JFScreen.bs ├── SettingDialog.xml ├── StandardButton.xml ├── ListPoster.xml ├── IconButton.xml ├── SearchBox.bs ├── TextButton.xml ├── MovieDetailButton.xml ├── Libraries │ ├── LiveTVLibraryView.xml │ └── AudioBookLibraryView.xml ├── StandardDialog.bs ├── ButtonGroupVert.bs ├── PlaystateTask.bs └── StandardButton.bs ├── source ├── enums │ ├── String.bs │ ├── MediaStreamType.bs │ ├── AnimationControl.bs │ ├── MenuType.bs │ ├── TaskControl.bs │ ├── SubtitleSelection.bs │ ├── TimerControl.bs │ ├── CaptionMoveDirection.bs │ ├── LiveTVScreen.bs │ ├── SearchButtonState.bs │ ├── SeriesStatus.bs │ ├── ViewLoadStatus.bs │ ├── PersonType.bs │ ├── AnimationState.bs │ ├── ImageLayout.bs │ ├── MediaSegmentAction.bs │ ├── VideoControl.bs │ ├── ResumePopupAction.bs │ ├── VideoType.bs │ ├── PosterLoadStatus.bs │ ├── SettingType.bs │ ├── MediaSegmentType.bs │ ├── PlaybackMethod.bs │ ├── MediaPlaybackState.bs │ ├── KeyCode.bs │ ├── CollectionType.bs │ ├── ImageType.bs │ ├── ColorPalette.bs │ └── ItemType.bs ├── constants │ └── HomeRowItemSizes.bs ├── static │ └── whatsNew │ │ └── 3.1.0.json └── utils │ └── parsedUrl.bs ├── bslint.json ├── images ├── logo.png ├── red.png ├── fresh.png ├── rotten.png ├── white.png ├── dialog.9.png ├── icons │ ├── cc.png │ ├── cd.png │ ├── album.png │ ├── check.png │ ├── heart.png │ ├── info.png │ ├── loop.png │ ├── next.png │ ├── pause.png │ ├── play.png │ ├── plus.png │ ├── star.png │ ├── stop.png │ ├── trash.png │ ├── cancel.png │ ├── cassette.png │ ├── circle.png │ ├── circle64.png │ ├── details.png │ ├── favorite.png │ ├── itemNext.png │ ├── mic_icon.png │ ├── options.png │ ├── previous.png │ ├── puzzle.png │ ├── settings.png │ ├── shuffle.png │ ├── subtitle.png │ ├── up_black.png │ ├── up_white.png │ ├── down_black.png │ ├── down_white.png │ ├── instantMix.png │ ├── musicNote.png │ ├── numberList.png │ ├── openCircle.png │ ├── playPause.png │ ├── trash-red.png │ ├── userGroup.png │ ├── videoInfo.png │ ├── Search-dark.png │ ├── addToMyList.png │ ├── check_black.png │ ├── check_white.png │ ├── dropdown-dark.png │ ├── itemPrevious.png │ ├── musicFolder.png │ ├── nextChapter.png │ ├── search-light.png │ ├── checkboxChecked.png │ ├── dropdown-light.png │ ├── previousChapter.png │ ├── checkboxUnchecked.png │ ├── favorite_selected.png │ ├── loopindicator-off.png │ ├── loopindicator-on.png │ ├── loopindicator1-on.png │ ├── shuffleIndicator-off.png │ └── shuffleIndicator-on.png ├── spinner.png ├── white.9.png ├── fhd_focus.9.png ├── hd_focus.9.png ├── postermask.png ├── transparent.png ├── background │ ├── bg1.jpg │ └── bg2.jpg ├── backgroundmask.png ├── logo-icon120.jpg ├── missingArtist.png ├── osdBackground.png ├── option-menu-bg.9.png ├── splash-screen_hd.png ├── splash-screen_sd.png ├── channel-poster_fhd.png ├── channel-poster_hd.png ├── channel-poster_sd.png ├── media_type_icons │ ├── tv.png │ ├── movie.png │ ├── photo.png │ ├── folder_white.png │ ├── music_white.png │ ├── photoFolder.png │ └── live_tv_white.png ├── splash-screen_fhd.png ├── channel-poster_hd_dev.png ├── channel-poster_sd_dev.png ├── sharp_star_white_18dp.png ├── channel-poster_fhd_dev.png └── baseline_person_white_48dp.png ├── screenshots ├── home.jpg ├── tvLibrary.jpg ├── movieDetails.jpg └── musicPlayback.jpg ├── .gitignore ├── renovate.json ├── dictionary.txt ├── PRIVACY.md ├── bsfmt.json ├── bsconfig.json ├── .vscode ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── bsconfig-tests.json ├── manifest └── package.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jellyfin/roku 2 | -------------------------------------------------------------------------------- /components/config/ConfigData.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | end sub 3 | -------------------------------------------------------------------------------- /components/data/PublicUserData.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | end sub 3 | -------------------------------------------------------------------------------- /components/options/OptionNode.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | end sub 3 | -------------------------------------------------------------------------------- /source/enums/String.bs: -------------------------------------------------------------------------------- 1 | enum String 2 | EMPTY = "" 3 | end enum 4 | -------------------------------------------------------------------------------- /bslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "unused-variable": "warn" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/red.png -------------------------------------------------------------------------------- /components/Buttons/ButtonData.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.iconSide = "left" 3 | end sub 4 | -------------------------------------------------------------------------------- /images/fresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/fresh.png -------------------------------------------------------------------------------- /images/rotten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/rotten.png -------------------------------------------------------------------------------- /images/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/white.png -------------------------------------------------------------------------------- /source/enums/MediaStreamType.bs: -------------------------------------------------------------------------------- 1 | enum MediaStreamType 2 | VIDEO = "video" 3 | end enum 4 | -------------------------------------------------------------------------------- /components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/components/.DS_Store -------------------------------------------------------------------------------- /images/dialog.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/dialog.9.png -------------------------------------------------------------------------------- /images/icons/cc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/cc.png -------------------------------------------------------------------------------- /images/icons/cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/cd.png -------------------------------------------------------------------------------- /images/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/spinner.png -------------------------------------------------------------------------------- /images/white.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/white.9.png -------------------------------------------------------------------------------- /screenshots/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/screenshots/home.jpg -------------------------------------------------------------------------------- /source/enums/AnimationControl.bs: -------------------------------------------------------------------------------- 1 | enum AnimationControl 2 | START = "start" 3 | end enum 4 | -------------------------------------------------------------------------------- /source/enums/MenuType.bs: -------------------------------------------------------------------------------- 1 | enum MenuType 2 | VIEW 3 | SORT 4 | FILTER 5 | end enum 6 | -------------------------------------------------------------------------------- /images/fhd_focus.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/fhd_focus.9.png -------------------------------------------------------------------------------- /images/hd_focus.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/hd_focus.9.png -------------------------------------------------------------------------------- /images/icons/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/album.png -------------------------------------------------------------------------------- /images/icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/check.png -------------------------------------------------------------------------------- /images/icons/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/heart.png -------------------------------------------------------------------------------- /images/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/info.png -------------------------------------------------------------------------------- /images/icons/loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/loop.png -------------------------------------------------------------------------------- /images/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/next.png -------------------------------------------------------------------------------- /images/icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/pause.png -------------------------------------------------------------------------------- /images/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/play.png -------------------------------------------------------------------------------- /images/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/plus.png -------------------------------------------------------------------------------- /images/icons/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/star.png -------------------------------------------------------------------------------- /images/icons/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/stop.png -------------------------------------------------------------------------------- /images/icons/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/trash.png -------------------------------------------------------------------------------- /images/postermask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/postermask.png -------------------------------------------------------------------------------- /images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/transparent.png -------------------------------------------------------------------------------- /source/enums/TaskControl.bs: -------------------------------------------------------------------------------- 1 | enum TaskControl 2 | RUN = "RUN" 3 | STOP = "STOP" 4 | end enum 5 | -------------------------------------------------------------------------------- /images/background/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/background/bg1.jpg -------------------------------------------------------------------------------- /images/background/bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/background/bg2.jpg -------------------------------------------------------------------------------- /images/backgroundmask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/backgroundmask.png -------------------------------------------------------------------------------- /images/icons/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/cancel.png -------------------------------------------------------------------------------- /images/icons/cassette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/cassette.png -------------------------------------------------------------------------------- /images/icons/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/circle.png -------------------------------------------------------------------------------- /images/icons/circle64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/circle64.png -------------------------------------------------------------------------------- /images/icons/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/details.png -------------------------------------------------------------------------------- /images/icons/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/favorite.png -------------------------------------------------------------------------------- /images/icons/itemNext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/itemNext.png -------------------------------------------------------------------------------- /images/icons/mic_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/mic_icon.png -------------------------------------------------------------------------------- /images/icons/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/options.png -------------------------------------------------------------------------------- /images/icons/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/previous.png -------------------------------------------------------------------------------- /images/icons/puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/puzzle.png -------------------------------------------------------------------------------- /images/icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/settings.png -------------------------------------------------------------------------------- /images/icons/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/shuffle.png -------------------------------------------------------------------------------- /images/icons/subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/subtitle.png -------------------------------------------------------------------------------- /images/icons/up_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/up_black.png -------------------------------------------------------------------------------- /images/icons/up_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/up_white.png -------------------------------------------------------------------------------- /images/logo-icon120.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/logo-icon120.jpg -------------------------------------------------------------------------------- /images/missingArtist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/missingArtist.png -------------------------------------------------------------------------------- /images/osdBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/osdBackground.png -------------------------------------------------------------------------------- /screenshots/tvLibrary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/screenshots/tvLibrary.jpg -------------------------------------------------------------------------------- /images/icons/down_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/down_black.png -------------------------------------------------------------------------------- /images/icons/down_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/down_white.png -------------------------------------------------------------------------------- /images/icons/instantMix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/instantMix.png -------------------------------------------------------------------------------- /images/icons/musicNote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/musicNote.png -------------------------------------------------------------------------------- /images/icons/numberList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/numberList.png -------------------------------------------------------------------------------- /images/icons/openCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/openCircle.png -------------------------------------------------------------------------------- /images/icons/playPause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/playPause.png -------------------------------------------------------------------------------- /images/icons/trash-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/trash-red.png -------------------------------------------------------------------------------- /images/icons/userGroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/userGroup.png -------------------------------------------------------------------------------- /images/icons/videoInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/videoInfo.png -------------------------------------------------------------------------------- /images/option-menu-bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/option-menu-bg.9.png -------------------------------------------------------------------------------- /images/splash-screen_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/splash-screen_hd.png -------------------------------------------------------------------------------- /images/splash-screen_sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/splash-screen_sd.png -------------------------------------------------------------------------------- /source/enums/SubtitleSelection.bs: -------------------------------------------------------------------------------- 1 | enum SubtitleSelection 2 | NOTSET = -2 3 | NONE = -1 4 | end enum 5 | -------------------------------------------------------------------------------- /source/enums/TimerControl.bs: -------------------------------------------------------------------------------- 1 | enum TimerControl 2 | START = "start" 3 | STOP = "stop" 4 | end enum 5 | -------------------------------------------------------------------------------- /images/channel-poster_fhd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_fhd.png -------------------------------------------------------------------------------- /images/channel-poster_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_hd.png -------------------------------------------------------------------------------- /images/channel-poster_sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_sd.png -------------------------------------------------------------------------------- /images/icons/Search-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/Search-dark.png -------------------------------------------------------------------------------- /images/icons/addToMyList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/addToMyList.png -------------------------------------------------------------------------------- /images/icons/check_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/check_black.png -------------------------------------------------------------------------------- /images/icons/check_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/check_white.png -------------------------------------------------------------------------------- /images/icons/dropdown-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/dropdown-dark.png -------------------------------------------------------------------------------- /images/icons/itemPrevious.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/itemPrevious.png -------------------------------------------------------------------------------- /images/icons/musicFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/musicFolder.png -------------------------------------------------------------------------------- /images/icons/nextChapter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/nextChapter.png -------------------------------------------------------------------------------- /images/icons/search-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/search-light.png -------------------------------------------------------------------------------- /images/media_type_icons/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/tv.png -------------------------------------------------------------------------------- /images/splash-screen_fhd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/splash-screen_fhd.png -------------------------------------------------------------------------------- /screenshots/movieDetails.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/screenshots/movieDetails.jpg -------------------------------------------------------------------------------- /screenshots/musicPlayback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/screenshots/musicPlayback.jpg -------------------------------------------------------------------------------- /source/enums/CaptionMoveDirection.bs: -------------------------------------------------------------------------------- 1 | enum CaptionMoveDirection 2 | NOTSET 3 | UP 4 | DOWN 5 | end enum 6 | -------------------------------------------------------------------------------- /source/enums/LiveTVScreen.bs: -------------------------------------------------------------------------------- 1 | enum LiveTVScreen 2 | CHANNELS = "channels" 3 | GUIDE = "guide" 4 | end enum 5 | -------------------------------------------------------------------------------- /images/channel-poster_hd_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_hd_dev.png -------------------------------------------------------------------------------- /images/channel-poster_sd_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_sd_dev.png -------------------------------------------------------------------------------- /images/icons/checkboxChecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/checkboxChecked.png -------------------------------------------------------------------------------- /images/icons/dropdown-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/dropdown-light.png -------------------------------------------------------------------------------- /images/icons/previousChapter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/previousChapter.png -------------------------------------------------------------------------------- /images/sharp_star_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/sharp_star_white_18dp.png -------------------------------------------------------------------------------- /source/enums/SearchButtonState.bs: -------------------------------------------------------------------------------- 1 | enum SearchButtonState 2 | ACTIVE 3 | INACTIVE 4 | DISABLED 5 | end enum 6 | -------------------------------------------------------------------------------- /source/enums/SeriesStatus.bs: -------------------------------------------------------------------------------- 1 | enum SeriesStatus 2 | CONTINUING = "continuing" 3 | ENDED = "ended" 4 | end enum 5 | -------------------------------------------------------------------------------- /images/channel-poster_fhd_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/channel-poster_fhd_dev.png -------------------------------------------------------------------------------- /images/icons/checkboxUnchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/checkboxUnchecked.png -------------------------------------------------------------------------------- /images/icons/favorite_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/favorite_selected.png -------------------------------------------------------------------------------- /images/icons/loopindicator-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/loopindicator-off.png -------------------------------------------------------------------------------- /images/icons/loopindicator-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/loopindicator-on.png -------------------------------------------------------------------------------- /images/icons/loopindicator1-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/loopindicator1-on.png -------------------------------------------------------------------------------- /images/media_type_icons/movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/movie.png -------------------------------------------------------------------------------- /images/media_type_icons/photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/photo.png -------------------------------------------------------------------------------- /source/enums/ViewLoadStatus.bs: -------------------------------------------------------------------------------- 1 | enum ViewLoadStatus 2 | INIT 3 | FIRSTLOAD 4 | RELOAD 5 | LOADED 6 | end enum 7 | -------------------------------------------------------------------------------- /components/labels/Text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/baseline_person_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/baseline_person_white_48dp.png -------------------------------------------------------------------------------- /images/icons/shuffleIndicator-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/shuffleIndicator-off.png -------------------------------------------------------------------------------- /images/icons/shuffleIndicator-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/icons/shuffleIndicator-on.png -------------------------------------------------------------------------------- /components/Spinner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/data/OptionsButton.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | 3 | sub init() 4 | end sub 5 | 6 | sub press() 7 | end sub 8 | -------------------------------------------------------------------------------- /images/media_type_icons/folder_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/folder_white.png -------------------------------------------------------------------------------- /images/media_type_icons/music_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/music_white.png -------------------------------------------------------------------------------- /images/media_type_icons/photoFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/photoFolder.png -------------------------------------------------------------------------------- /components/labels/ScrollingText.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /images/media_type_icons/live_tv_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/jellyfin-roku/HEAD/images/media_type_icons/live_tv_white.png -------------------------------------------------------------------------------- /source/enums/PersonType.bs: -------------------------------------------------------------------------------- 1 | enum PersonType 2 | DIRECTOR = "director" 3 | ACTOR = "actor" 4 | PERSON = "person" 5 | end enum 6 | -------------------------------------------------------------------------------- /source/enums/AnimationState.bs: -------------------------------------------------------------------------------- 1 | enum AnimationState 2 | PAUSED = "paused" 3 | RUNNING = "running" 4 | STOPPED = "stopped" 5 | end enum 6 | -------------------------------------------------------------------------------- /source/enums/ImageLayout.bs: -------------------------------------------------------------------------------- 1 | enum ImageLayout 2 | DEFAULT = "default" 3 | LANDSCAPE = "landscape" 4 | PORTRAIT = "portrait" 5 | end enum 6 | -------------------------------------------------------------------------------- /source/enums/MediaSegmentAction.bs: -------------------------------------------------------------------------------- 1 | enum MediaSegmentAction 2 | ASKTOSKIP = "asktoskip" 3 | SKIP = "skip" 4 | NONE = "none" 5 | end enum 6 | -------------------------------------------------------------------------------- /components/data/AlbumData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | datum = m.top.json 3 | 4 | m.top.id = datum.id 5 | m.top.title = datum.name 6 | end sub 7 | 8 | -------------------------------------------------------------------------------- /components/data/MusicArtistData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/enums/VideoControl.bs: -------------------------------------------------------------------------------- 1 | enum VideoControl 2 | STOP = "STOP" 3 | RESUME = "RESUME" 4 | PAUSE = "PAUSE" 5 | PLAY = "PLAY" 6 | end enum 7 | -------------------------------------------------------------------------------- /source/enums/ResumePopupAction.bs: -------------------------------------------------------------------------------- 1 | enum ResumePopupAction 2 | RESUME 3 | STARTOVER 4 | GOTOSERIES 5 | GOTOSEASON 6 | GOTOEPISODE 7 | end enum 8 | -------------------------------------------------------------------------------- /source/enums/VideoType.bs: -------------------------------------------------------------------------------- 1 | enum VideoType 2 | EPISODE = "episode" 3 | SERIES = "series" 4 | VIDEO = "video" 5 | VIDEOFILE = "videofile" 6 | end enum 7 | -------------------------------------------------------------------------------- /components/data/MusicAlbumSongListData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/enums/PosterLoadStatus.bs: -------------------------------------------------------------------------------- 1 | enum PosterLoadStatus 2 | NONE = "none" 3 | LOADING = "loading" 4 | READY = "ready" 5 | FAILED = "failed" 6 | end enum 7 | -------------------------------------------------------------------------------- /components/settings/setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /source/enums/SettingType.bs: -------------------------------------------------------------------------------- 1 | enum SettingType 2 | BOOL = "bool" 3 | COLORGRID = "colorgrid" 4 | integer = "integer" 5 | RADIO = "radio" 6 | SLIDER = "slider" 7 | end enum 8 | -------------------------------------------------------------------------------- /components/Spinner.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.poster.uri = "pkg:/images/spinner.png" 3 | m.top.control = "start" 4 | m.top.clockwise = true 5 | m.top.spinInterval = 1 6 | end sub 7 | -------------------------------------------------------------------------------- /components/JFGroup.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | end sub 3 | 4 | function onKeyEvent(key as string, press as boolean) as boolean 5 | if not press then return false 6 | 7 | return false 8 | end function 9 | -------------------------------------------------------------------------------- /components/data/JFContentItem.bs: -------------------------------------------------------------------------------- 1 | ' Called whenever m.top.json changes. 2 | ' It is expected that each node that extends JFContentItem will override this function 3 | sub setFields() 4 | 5 | end sub 6 | -------------------------------------------------------------------------------- /components/data/ServerData.bs: -------------------------------------------------------------------------------- 1 | ' Called whenever m.top.json changes. 2 | ' It is expected that each node that extends JFContentItem will override this function 3 | sub setFields() 4 | 5 | end sub 6 | -------------------------------------------------------------------------------- /components/JFButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/data/ImageData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | json = m.top.json 3 | m.top.imagetype = json.imagetype 4 | m.top.size = json.size 5 | m.top.height = json.height 6 | m.top.width = json.width 7 | end sub 8 | -------------------------------------------------------------------------------- /components/WhatsNewDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /source/enums/MediaSegmentType.bs: -------------------------------------------------------------------------------- 1 | enum MediaSegmentType 2 | UNKNOWN = "unknown" 3 | COMMERCIAL = "commercial" 4 | PREVIEW = "preview" 5 | RECAP = "recap" 6 | OUTRO = "outro" 7 | INTRO = "intro" 8 | end enum 9 | -------------------------------------------------------------------------------- /source/enums/PlaybackMethod.bs: -------------------------------------------------------------------------------- 1 | enum PlaybackMethod 2 | PLAYNORMALLY = "playNormally" 3 | FORCETRANSCODEALLOWREMUX = "forceTranscodeAllowRemux" 4 | FORCETRANSCODEDISABLEREMUX = "forceTranscodeDisableRemux" 5 | end enum 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | .env 3 | # BrightScript 4 | dist/apps 5 | out/ 6 | build/ 7 | roku_modules 8 | source/globals.brs 9 | jellyfin-roku.zip 10 | # NPM 11 | node_modules/ 12 | # Eclipse 13 | .buildpath 14 | .project 15 | .settings -------------------------------------------------------------------------------- /components/config/ServerDiscoveryTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /source/enums/MediaPlaybackState.bs: -------------------------------------------------------------------------------- 1 | enum MediaPlaybackState 2 | BUFFERING = "buffering" 3 | ERROR = "error" 4 | FINISHED = "finished" 5 | PAUSED = "paused" 6 | PLAYING = "playing" 7 | STOPPED = "stopped" 8 | end enum 9 | -------------------------------------------------------------------------------- /components/config/FormList.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/ButtonGroupHoriz.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/JFScreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/tvshows/TVSeasonRow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/music/LoadScreenSaverTimeoutTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/music/SimilarArtistGrid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/data/OptionsButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/search/SearchTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/constants/HomeRowItemSizes.bs: -------------------------------------------------------------------------------- 1 | ' @fileoverview Constants for rowItemSize on the home view 2 | 3 | namespace homeRowItemSizes 4 | const WIDE_POSTER = [464, 331] 5 | const MOVIE_POSTER = [180, 331] 6 | const MUSIC_ALBUM = [261, 331] 7 | end namespace 8 | -------------------------------------------------------------------------------- /components/PlaystateTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/tvshows/TVExtrasTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/music/LoadScreenSaverTimeoutTask.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.functionName = "getScreensaverTimeout" 3 | end sub 4 | 5 | sub getScreensaverTimeout() 6 | appinfo = CreateObject("roAppManager") 7 | m.top.content = appinfo.GetScreensaverTimeout() * 60 8 | end sub 9 | -------------------------------------------------------------------------------- /components/data/GetFiltersTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/data/PhotoData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/data/TVEpisode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/GetPlaybackInfoTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/ItemGrid/FavoriteItemsTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/GetShuffleEpisodesTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/data/AudioStreamData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/data/ServerData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/config/ConfigData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/data/VideoData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/login/UserRow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":disableDependencyDashboard"], 4 | "packageRules": [ 5 | { 6 | "matchUpdateTypes": ["minor", "patch"], 7 | "automerge": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /components/data/ColorOptionData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/ButtonGroupVert.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/music/PlaylistItems.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/StandardDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/data/PublicUserData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/options/OptionNode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/data/AlbumData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/music/AlbumTrackList.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/data/GetFiltersTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | import "pkg:/source/api/sdk.bs" 3 | 4 | sub init() 5 | m.top.functionName = "getFiltersTask" 6 | end sub 7 | 8 | sub getFiltersTask() 9 | m.filters = api.items.GetFilters(m.top.params) 10 | m.top.filters = m.filters 11 | end sub 12 | -------------------------------------------------------------------------------- /source/enums/KeyCode.bs: -------------------------------------------------------------------------------- 1 | enum KeyCode 2 | UP = "up" 3 | DOWN = "down" 4 | LEFT = "left" 5 | RIGHT = "right" 6 | OK = "OK" 7 | BACK = "back" 8 | OPTIONS = "options" 9 | FASTFORWRD = "fastforward" 10 | REWIND = "rewind" 11 | REPLAY = "replay" 12 | PLAY = "play" 13 | end enum 14 | -------------------------------------------------------------------------------- /components/music/AlbumGrid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/photos/LoadPhotoTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dictionary.txt: -------------------------------------------------------------------------------- 1 | Jellyfin 2 | VSCode 3 | BrighterScript 4 | BrightScript 5 | sideload 6 | Sideload 7 | Reddit 8 | DEVGUIDE 9 | ImageMagick 10 | ing 11 | hardcode 12 | Hardcoding 13 | pre-release 14 | breakpoint 15 | repo 16 | Repo 17 | dev 18 | Dev 19 | assignees 20 | HTTPS 21 | dropdown 22 | JSDoc 23 | JavaScript 24 | namespaces 25 | -------------------------------------------------------------------------------- /components/data/FolderData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/quickConnect/QuickConnect.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/GetNextEpisodeTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/data/PersonData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/home/HomeRow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/enums/CollectionType.bs: -------------------------------------------------------------------------------- 1 | enum CollectionType 2 | BOOKS = "books" 3 | BOXSET = "boxset" 4 | BOXSETS = "boxsets" 5 | HOMEVIDEOS = "homevideos" 6 | MOVIES = "movies" 7 | MUSIC = "music" 8 | MUSICVIDEOS = "musicvideos" 9 | MYLIST = "mylist" 10 | NEXTUP = "nextup" 11 | TVSHOWS = "tvshows" 12 | end enum 13 | -------------------------------------------------------------------------------- /components/data/ChannelData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/home/HomeRows.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/subtitle/SubtitleData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/tvshows/TVEpisodeRow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/movies/AudioTrackListData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/data/GetPlaylistDataTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/sdk.bs" 2 | import "pkg:/source/utils/config.bs" 3 | 4 | sub init() 5 | m.top.functionName = "getPlaylistData" 6 | end sub 7 | 8 | sub getPlaylistData() 9 | playlistData = api.playlists.GetUser(m.top.playlistID, m.global.session.user.id) 10 | m.top.playlistData = playlistData 11 | end sub 12 | -------------------------------------------------------------------------------- /components/data/MusicAlbumData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/liveTv/RecordProgramTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature 3 | labels: feature 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | # Do not submit feature requests here! It will get closed. Feature requests MUST be submitted at https://features.jellyfin.org/ 10 | -------------------------------------------------------------------------------- /components/SearchBox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/liveTv/LoadSheduleTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/ItemGrid/AlphaItem.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | import "pkg:/source/enums/ColorPalette.bs" 3 | 4 | sub init() 5 | m.letterText = m.top.findNode("letterText") 6 | m.letterText.color = ColorPalette.WHITE 7 | m.letterText.font.size = 25 8 | end sub 9 | 10 | sub showContent() 11 | m.letterText.text = m.top.itemContent.title 12 | end sub 13 | -------------------------------------------------------------------------------- /components/data/PlaylistData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | datum = m.top.json 3 | 4 | m.top.id = datum.id 5 | m.top.title = datum.name 6 | m.top.overview = datum.overview 7 | end sub 8 | 9 | sub setPoster() 10 | if m.top.image <> invalid 11 | m.top.posterURL = m.top.image.url 12 | else 13 | m.top.posterURL = "" 14 | end if 15 | end sub 16 | -------------------------------------------------------------------------------- /components/keyboards/IntegerKeyboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/data/PlaylistItemData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | datum = m.top.json 3 | 4 | m.top.id = datum.id 5 | m.top.title = datum.name 6 | m.top.overview = datum.overview 7 | end sub 8 | 9 | sub setPoster() 10 | if m.top.image <> invalid 11 | m.top.posterURL = m.top.image.url 12 | else 13 | m.top.posterURL = "" 14 | end if 15 | end sub 16 | -------------------------------------------------------------------------------- /components/movies/VideoTrackListData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/movies/SubtitleTrackListData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /source/enums/ImageType.bs: -------------------------------------------------------------------------------- 1 | enum ImageType 2 | PRIMARY = "Primary" 3 | ART = "Art" 4 | BACKDROP = "Backdrop" 5 | BANNER = "Banner" 6 | LOGO = "Logo" 7 | THUMB = "Thumb" 8 | DISC = "Disc" 9 | BOX = "Box" 10 | SCREENSHOT = "Screenshot" 11 | MENU = "Menu" 12 | CHAPTER = "Chapter" 13 | BOXREAR = "BoxRear" 14 | PROFILE = "Profile" 15 | end enum 16 | -------------------------------------------------------------------------------- /components/liveTv/LoadProgramDetailsTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/data/GetPlaylistDataTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/data/TVSeasonData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | datum = m.top.json 3 | 4 | m.top.id = datum.id 5 | m.top.title = datum.name 6 | m.top.overview = datum.overview 7 | 8 | setPoster() 9 | end sub 10 | 11 | sub setPoster() 12 | if m.top.image <> invalid 13 | m.top.posterURL = m.top.image.url 14 | else 15 | m.top.posterURL = "" 16 | end if 17 | 18 | end sub 19 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Jellyfin does not collect personal information outside of that needed to log in to the specified Jellyfin server. We simply pass this data along to the Jellyfin server. We do not collect, broadcast, nor send this data anywhere but to the Jellyfin server you specify. 4 | 5 | Read Jellyfin's Privacy Policy at 6 | -------------------------------------------------------------------------------- /components/data/CollectionData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/RadioDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/data/SeriesData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/video/PreloadTrickplayImagesTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /bsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentStyle": "spaces", 3 | "indentSpaceCount": 4, 4 | "keywordCase": "lower", 5 | "keywordCaseOverride": { 6 | "stop": "upper" 7 | }, 8 | "formatSingleLineCommentType": "singlequote", 9 | "sortImports": true, 10 | "files": [ 11 | "source/**/*.brs", 12 | "source/**/*.bs", 13 | "components/**/*.brs", 14 | "components/**/*.bs", 15 | "!**/roku_modules/**/*.*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /components/RemoteSubtitleDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/GetShuffleEpisodesTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | import "pkg:/source/api/sdk.bs" 3 | 4 | sub init() 5 | m.top.functionName = "getShuffleEpisodesTask" 6 | end sub 7 | 8 | sub getShuffleEpisodesTask() 9 | data = api.shows.GetEpisodes(m.top.showID, { 10 | UserId: m.global.session.user.id, 11 | SortBy: "Random", 12 | Limit: 200 13 | }) 14 | 15 | m.top.data = data 16 | end sub 17 | -------------------------------------------------------------------------------- /components/data/MusicAlbumData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | 3 | sub setFields() 4 | datum = m.top.json 5 | 6 | m.top.id = datum.id 7 | m.top.title = datum.name 8 | m.top.overview = datum.overview 9 | end sub 10 | 11 | sub setPoster() 12 | if isvalid(m.top.image) 13 | m.top.posterURL = m.top.image.url 14 | else 15 | m.top.posterURL = "pkg:/images/icons/album.png" 16 | end if 17 | end sub 18 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "manifest", 4 | "source/**/*.*", 5 | "components/**/*.*", 6 | "images/**/*.*", 7 | "locale/**/*.*", 8 | "settings/*.*" 9 | ], 10 | "plugins": ["@rokucommunity/bslint"], 11 | "diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"], 12 | "sourceMap": true, 13 | "autoImportComponentScript": true, 14 | "stagingDir": "build/staging", 15 | "retainStagingDir": true 16 | } 17 | -------------------------------------------------------------------------------- /components/search/SearchRow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/JFGroup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/Clock.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/data/MusicSongData.bs: -------------------------------------------------------------------------------- 1 | sub setFields() 2 | datum = m.top.json 3 | 4 | m.top.id = datum.id 5 | m.top.title = datum.name 6 | m.top.overview = datum.overview 7 | m.top.trackNumber = datum.IndexNumber 8 | m.top.length = datum.RunTimeTicks 9 | end sub 10 | 11 | sub setPoster() 12 | if m.top.image <> invalid 13 | m.top.posterURL = m.top.image.url 14 | else 15 | m.top.posterURL = "" 16 | end if 17 | end sub 18 | -------------------------------------------------------------------------------- /components/GetNextEpisodeTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | import "pkg:/source/api/sdk.bs" 3 | 4 | sub init() 5 | m.top.functionName = "getNextEpisodeTask" 6 | end sub 7 | 8 | sub getNextEpisodeTask() 9 | m.nextEpisodeData = api.shows.GetEpisodes(m.top.showID, { 10 | UserId: m.global.session.user.id, 11 | StartItemId: m.top.videoID, 12 | Limit: 2 13 | }) 14 | 15 | m.top.nextEpisodeData = m.nextEpisodeData 16 | end sub 17 | -------------------------------------------------------------------------------- /components/search/SearchTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Items.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | import "pkg:/source/api/Image.bs" 5 | import "pkg:/source/utils/deviceCapabilities.bs" 6 | 7 | sub init() 8 | m.top.functionName = "search" 9 | end sub 10 | 11 | sub search() 12 | if m.top.query <> invalid and m.top.query <> "" 13 | m.top.results = searchMedia(m.top.query) 14 | end if 15 | end sub 16 | -------------------------------------------------------------------------------- /components/data/TVSeasonData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/tvshows/TVExtrasTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Items.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | import "pkg:/source/api/Image.bs" 5 | import "pkg:/source/utils/deviceCapabilities.bs" 6 | 7 | sub init() 8 | m.top.functionName = "getExtras" 9 | end sub 10 | 11 | sub getExtras() 12 | if isValid(m.top.seasonID) and m.top.seasonID <> "" 13 | m.top.results = TVSeasonExtras(m.top.seasonID) 14 | end if 15 | end sub 16 | -------------------------------------------------------------------------------- /source/static/whatsNew/3.1.0.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Use 1st found episode image as season poster on TV series screen when no image available", 4 | "author": "1hitsong" 5 | }, 6 | { 7 | "description": "Use 1st found season image as series poster on TV series screen when no image available", 8 | "author": "1hitsong" 9 | }, 10 | { 11 | "description": "Allow translation of skip media segment buttons", 12 | "author": "1hitsong" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /components/ItemGrid/AlphaItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /components/Buttons/TextSizeTask.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.functionName = "getTextSize" 3 | end sub 4 | 5 | sub getTextSize() 6 | 7 | reg = CreateObject("roFontRegistry") 8 | font = reg.GetDefaultFont(m.top.fontsize, false, false) 9 | 10 | res = [] 11 | 12 | for each line in m.top.text 13 | res.push(font.GetOneLineWidth(line, m.top.maxWidth)) 14 | end for 15 | 16 | m.top.height = font.GetOneLineHeight() 17 | 18 | 19 | m.top.width = res 20 | 21 | end sub 22 | -------------------------------------------------------------------------------- /components/section/sectionScroller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/data/PlaylistData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/home/LoadItemsTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.ts": "xml" 4 | }, 5 | "[xml]": { 6 | "editor.defaultFormatter": "redhat.vscode-xml" 7 | }, 8 | "xml.format.maxLineWidth": 0, 9 | "editor.formatOnSave": true, 10 | "brightscript.output.hyperlinkFormat": "FilenameAndFunction", 11 | "brightscript.bsdk": "node_modules/brighterscript", 12 | "search.exclude": { 13 | "**/.git": true, 14 | "**/node_modules": true 15 | }, 16 | "brightscriptcomment.addExtraAtStartAndEnd": false 17 | } -------------------------------------------------------------------------------- /components/data/ImageData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/mediaPlayers/AudioPlayer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /components/Buttons/TextSizeTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/quickConnect/QuickConnectDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/config/FormElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/ItemGrid/FavoriteItemsTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/sdk.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub init() 6 | m.top.functionName = "setFavoriteStatus" 7 | end sub 8 | 9 | sub setFavoriteStatus() 10 | task = m.top.favTask 11 | 12 | if isStringEqual(task, "favorite") 13 | api.users.MarkFavorite(m.global.session.user.id, m.top.itemId) 14 | else 15 | api.users.UnmarkFavorite(m.global.session.user.id, m.top.itemId) 16 | end if 17 | end sub 18 | -------------------------------------------------------------------------------- /components/data/OptionsData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/data/MusicSongData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v6 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v6 18 | with: 19 | node-version: "24" 20 | cache: "npm" 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build jellyfin-roku client project 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /source/enums/ColorPalette.bs: -------------------------------------------------------------------------------- 1 | enum ColorPalette 2 | BLACK77 = "0x00000077" 3 | BLACK90 = "0x00000090" 4 | DARKGREY = "#101010" 5 | ELEMENTBACKGROUND = "#101420" 6 | HIGHLIGHT = "0xff6867FF" 7 | LIGHTBLUE = "#c8fafa" 8 | LIGHTGREY = "#aaaaaa" 9 | LIGHTHIGHLIGHT = "#ff9a99" 10 | MIDGREY = "#777777" 11 | OFFWHITE = "#cccccc" 12 | RED = "#FF0000" 13 | SMOKE = "#303030" 14 | TRANSPARENT = "0x00000000" 15 | TRIADBLUE = "#6867ff" 16 | TRIADGREEN = "#67ff68" 17 | VIEWBACKGROUND = "#000018" 18 | WHITE = "#ffffff" 19 | end enum 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "RokuCommunity.brightscript", 7 | "AliceBeckett.brightscriptcomment", 8 | "redhat.vscode-xml" 9 | ], 10 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 11 | "unwantedRecommendations": [] 12 | } 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Changes 7 | 8 | 9 | ## Issues 10 | 12 | -------------------------------------------------------------------------------- /components/captionTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /components/data/UserData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/settings/Slider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bsconfig-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "src": "source/**/!(Main.brs)", 5 | "dest": "source" 6 | }, 7 | { 8 | "src": "components/**/*", 9 | "dest": "components" 10 | }, 11 | { 12 | "src": "locale/**/*", 13 | "dest": "locale" 14 | }, 15 | { 16 | "src": "settings/**/*", 17 | "dest": "settings" 18 | }, 19 | "manifest" 20 | ], 21 | "diagnosticFilters": ["node_modules/**/*", "**/roku_modules/**/*"], 22 | "autoImportComponentScript": true, 23 | "stagingDir": "build", 24 | "retainStagingDir": true, 25 | "plugins": [], 26 | "sourceMap": true 27 | } 28 | -------------------------------------------------------------------------------- /components/data/RecordingData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ItemType.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub setFields() 5 | datum = m.top.json 6 | 7 | m.top.id = datum.id 8 | m.top.type = ItemType.RECORDING 9 | m.top.title = datum.name 10 | m.top.showID = datum.SeriesID 11 | m.top.seasonID = datum.SeasonID 12 | m.top.overview = datum.overview 13 | m.top.favorite = datum.UserData.isFavorite 14 | end sub 15 | 16 | sub setPoster() 17 | if isValid(m.top.image) 18 | m.top.posterURL = m.top.image.url 19 | else 20 | m.top.posterURL = "" 21 | end if 22 | end sub 23 | -------------------------------------------------------------------------------- /components/ItemGrid/ColorOption.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.backdrop = m.top.findNode("backdrop") 6 | m.checkmarkShadow = m.top.findNode("checkmarkShadow") 7 | m.checkmarkShadow.font.size = 42 8 | 9 | m.checkmark = m.top.findNode("checkmark") 10 | m.checkmark.font.size = 42 11 | end sub 12 | 13 | sub itemContentChanged() 14 | itemData = m.top.itemContent 15 | if not isValid(itemData) then return 16 | 17 | m.backdrop.color = itemData.colorCode 18 | m.checkmark.visible = itemData.isChecked 19 | m.checkmarkShadow.visible = itemData.isChecked 20 | end sub 21 | -------------------------------------------------------------------------------- /components/labels/Text.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/enums/String.bs" 3 | import "pkg:/source/utils/misc.bs" 4 | 5 | sub init() 6 | m.top.color = chainLookupReturn(m.global.session, "user.settings.colorText", ColorPalette.WHITE) 7 | setFont() 8 | 9 | m.top.observeFieldScoped("font", "onFontChanged") 10 | end sub 11 | 12 | sub setFont() 13 | if m.global.fallbackFont = string.EMPTY then return 14 | if not chainLookupReturn(m.global.session, "user.settings.useFallbackFont", false) then return 15 | 16 | m.top.font.uri = m.global.fallbackFont 17 | end sub 18 | 19 | sub onFontChanged() 20 | setFont() 21 | end sub 22 | -------------------------------------------------------------------------------- /components/music/PlaylistItems.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/KeyCode.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.top.setfocus(true) 6 | group = m.global.sceneManager.callFunc("getActiveScene") 7 | group.lastFocus = m.top 8 | end sub 9 | 10 | sub getData() 11 | m.top.content = m.top.PlaylistData 12 | m.top.doneLoading = true 13 | end sub 14 | 15 | function onKeyEvent(key as string, press as boolean) as boolean 16 | if not press then return false 17 | 18 | if isStringEqual(key, KeyCode.PLAY) 19 | m.top.itemSelected = m.top.itemFocused 20 | return true 21 | end if 22 | 23 | return false 24 | end function 25 | -------------------------------------------------------------------------------- /components/labels/ScrollingText.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/enums/String.bs" 3 | import "pkg:/source/utils/misc.bs" 4 | 5 | sub init() 6 | m.top.color = chainLookupReturn(m.global.session, "user.settings.colorText", ColorPalette.WHITE) 7 | setFont() 8 | 9 | m.top.observeFieldScoped("font", "onFontChanged") 10 | end sub 11 | 12 | sub setFont() 13 | if m.global.fallbackFont = string.EMPTY then return 14 | if not chainLookupReturn(m.global.session, "user.settings.useFallbackFont", false) then return 15 | 16 | m.top.font.uri = m.global.fallbackFont 17 | end sub 18 | 19 | sub onFontChanged() 20 | setFont() 21 | end sub 22 | -------------------------------------------------------------------------------- /components/liveTv/LoadChannelsTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/movies/AudioTrackListItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/movies/VideoTrackListItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/data/MovieData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/liveTv/ChannelInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /components/extras/ExtrasRowList.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /components/music/SongItem.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | 3 | sub init() 4 | m.itemText = m.top.findNode("itemText") 5 | m.trackNumber = m.top.findNode("trackNumber") 6 | m.tracklength = m.top.findNode("tracklength") 7 | end sub 8 | 9 | sub itemContentChanged() 10 | itemData = m.top.itemContent 11 | if itemData = invalid then return 12 | m.itemText.text = itemData.LookupCI("title") 13 | 14 | if itemData.LookupCI("trackNumber") <> 0 15 | m.trackNumber.text = itemData.LookupCI("trackNumber") 16 | end if 17 | 18 | if isValid(itemData.LookupCI("RunTimeTicks")) 19 | m.tracklength.text = ticksToHuman(itemData.LookupCI("RunTimeTicks")) 20 | end if 21 | end sub 22 | -------------------------------------------------------------------------------- /components/tasks/PostTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/data/TVEpisodeData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ItemType.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub setFields() 5 | if not isValid(m.top.json) then return 6 | datum = m.top.json 7 | 8 | m.top.id = datum.id 9 | m.top.type = ItemType.EPISODE 10 | m.top.title = datum.name 11 | m.top.showID = datum.SeriesID 12 | m.top.seasonID = datum.SeasonID 13 | m.top.overview = datum.overview 14 | m.top.favorite = datum.UserData.isFavorite 15 | m.top.watched = chainLookup(datum, "UserData.played") 16 | end sub 17 | 18 | sub setPoster() 19 | if isValid(m.top.image) 20 | m.top.posterURL = m.top.image.url 21 | else 22 | m.top.posterURL = "" 23 | end if 24 | end sub 25 | -------------------------------------------------------------------------------- /manifest: -------------------------------------------------------------------------------- 1 | ## Channel Details 2 | 3 | title=jellyfin 4 | major_version=3 5 | minor_version=1 6 | build_version=0 7 | 8 | ### Main Menu Icons / Channel Poster Artwork 9 | 10 | mm_icon_focus_fhd=pkg:/images/channel-poster_fhd_dev.png 11 | mm_icon_focus_hd=pkg:/images/channel-poster_hd_dev.png 12 | mm_icon_focus_sd=pkg:/images/channel-poster_sd_dev.png 13 | 14 | ### Splash Screen + Loading Screen Artwork 15 | 16 | splash_screen_fhd=pkg:/images/splash-screen_fhd.png 17 | splash_screen_hd=pkg:/images/splash-screen_hd.png 18 | splash_screen_sd=pkg:/images/splash-screen_sd.png 19 | 20 | splash_min_time=1000 21 | 22 | ui_resolutions=fhd 23 | 24 | confirm_partner_button=1 25 | 26 | supports_input_launch=1 27 | 28 | bs_const=printReg=false 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build-dev", 8 | "type": "shell", 9 | "command": "npm run build", 10 | "problemMatcher": [], 11 | "presentation": { 12 | "echo": true, 13 | "focus": false, 14 | "panel": "shared", 15 | "showReuseMessage": false, 16 | "clear": true 17 | }, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /components/PlayedCheckmark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/options/OptionsSlider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/data/JFContentItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/JFButton.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.observeFieldScoped("text", "onTextChanged") 3 | m.top.iconUri = "" 4 | m.top.focusedIconUri = "" 5 | m.top.showFocusFootprint = true 6 | m.top.minWidth = 0 7 | end sub 8 | 9 | ' 10 | ' Whenever the text changes, pad both sides with whitespace so we can center the button text 11 | ' 12 | sub onTextChanged() 13 | addSpaceAfter = true 14 | minChars = m.top.minChars 15 | if minChars = invalid then minChars = 50 16 | while m.top.text.Len() < minChars 17 | if addSpaceAfter 18 | m.top.text = m.top.text + Chr(160) 19 | else 20 | m.top.text = Chr(160) + m.top.text 21 | end if 22 | addSpaceAfter = addSpaceAfter = false 23 | end while 24 | end sub 25 | -------------------------------------------------------------------------------- /components/ButtonGroupHoriz.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.layoutDirection = "horiz" 3 | end sub 4 | 5 | function onKeyEvent(key as string, press as boolean) as boolean 6 | if not press then return false 7 | if key = "right" 8 | i = m.top.buttonFocused 9 | target = i + 1 10 | if target >= m.top.getChildCount() then return false 11 | m.top.focusButton = target 12 | return true 13 | else if key = "left" 14 | i = m.top.buttonFocused 15 | target = i - 1 16 | if target < 0 then return false 17 | m.top.focusButton = target 18 | return true 19 | else if key = "up" or key = "down" 20 | m.top.escape = key 21 | return true 22 | end if 23 | 24 | return false 25 | end function 26 | -------------------------------------------------------------------------------- /components/photos/LoadPhotoTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Image.bs" 2 | import "pkg:/source/utils/config.bs" 3 | import "pkg:/source/api/baserequest.bs" 4 | import "pkg:/source/utils/misc.bs" 5 | 6 | sub init() 7 | m.top.functionName = "loadItems" 8 | end sub 9 | 10 | sub loadItems() 11 | params = { 12 | maxHeight: 1080, 13 | maxWidth: 1920 14 | } 15 | 16 | if isValid(m.top.itemNodeContent) 17 | item = m.top.itemNodeContent 18 | m.top.results = ImageURL(item.Id, "Primary", params) 19 | else if isValid(m.top.itemArrayContent) 20 | item = m.top.itemArrayContent 21 | m.top.results = ImageURL(item.Id, "Primary", params) 22 | else 23 | m.top.results = invalid 24 | end if 25 | 26 | 27 | 28 | end sub 29 | -------------------------------------------------------------------------------- /source/enums/ItemType.bs: -------------------------------------------------------------------------------- 1 | enum ItemType 2 | AUDIO = "audio" 3 | AUDIOBOOK = "audiobook" 4 | BOOK = "book" 5 | BOXSET = "boxset" 6 | CHANNEL = "channel" 7 | COLLECTIONFOLDER = "collectionfolder" 8 | EPISODE = "episode" 9 | FOLDER = "folder" 10 | MOVIE = "movie" 11 | MUSICALBUM = "musicalbum" 12 | MUSICARTIST = "musicartist" 13 | MUSICVIDEO = "musicvideo" 14 | MYLIST = "mylist" 15 | PERSON = "person" 16 | PHOTO = "photo" 17 | PHOTOALBUM = "photoalbum" 18 | PLAYLIST = "playlist" 19 | PROGRAM = "program" 20 | RECORDING = "recording" 21 | SEASON = "season" 22 | SERIES = "series" 23 | STUDIO = "studio" 24 | TVCHANNEL = "tvchannel" 25 | USERVIEW = "userview" 26 | VIDEO = "video" 27 | end enum 28 | -------------------------------------------------------------------------------- /components/music/SongItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /components/music/AlbumTrackList.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/KeyCode.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | getData() 6 | m.top.setfocus(true) 7 | end sub 8 | 9 | function getData() 10 | if m.top.MusicArtistAlbumData = invalid 11 | data = CreateObject("roSGNode", "ContentNode") 12 | return data 13 | end if 14 | 15 | m.top.content = m.top.MusicArtistAlbumData 16 | 17 | m.top.doneLoading = true 18 | 19 | return data 20 | end function 21 | 22 | function onKeyEvent(key as string, press as boolean) as boolean 23 | if not press then return false 24 | 25 | if isStringEqual(key, KeyCode.PLAY) 26 | m.top.itemSelected = m.top.itemFocused 27 | return true 28 | end if 29 | 30 | return false 31 | end function 32 | -------------------------------------------------------------------------------- /components/quickConnect/QuickConnect.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/userauth.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub init() 6 | m.top.functionName = "monitorQuickConnect" 7 | end sub 8 | 9 | sub monitorQuickConnect() 10 | authenticated = checkQuickConnect(m.top.secret) 11 | 12 | if authenticated = true 13 | loggedIn = AuthenticateViaQuickConnect(m.top.secret) 14 | if loggedIn 15 | currentUser = AboutMe() 16 | session.user.Login(currentUser, m.top.saveCredentials) 17 | session.user.LoadUserPreferences() 18 | LoadUserAbilities() 19 | m.top.authenticated = 1 20 | return 21 | end if 22 | end if 23 | 24 | m.top.authenticated = -1 25 | end sub 26 | -------------------------------------------------------------------------------- /components/data/VideoData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/Image.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | 8 | m.top.id = json.id 9 | m.top.Title = json.name 10 | m.top.Description = json.overview 11 | m.top.favorite = json.UserData.isFavorite 12 | m.top.watched = chainLookup(json, "UserData.played") 13 | m.top.Type = "Video" 14 | 15 | setPoster() 16 | end sub 17 | 18 | sub setPoster() 19 | if m.top.image <> invalid 20 | m.top.posterURL = m.top.image.url 21 | else if m.top.json.ImageTags.Primary <> invalid 22 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary } 23 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 24 | end if 25 | end sub 26 | -------------------------------------------------------------------------------- /components/data/TVEpisodeData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/home/Home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/JFMessageDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/data/RecordingData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/BaseScene.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/data/ScheduleProgramData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/JFScreen.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | end sub 3 | ' Function called when the screen is displayed by the screen manager 4 | ' It is expected that screens override this function to handle focus 5 | ' managmenet and any other actions required on screen shown 6 | sub OnScreenShown() 7 | if m.top.lastFocus <> invalid 8 | m.top.lastFocus.setFocus(true) 9 | group = m.global.sceneManager.callFunc("getActiveScene") 10 | group.lastFocus = m.top.lastFocus 11 | else 12 | m.top.setFocus(true) 13 | group = m.global.sceneManager.callFunc("getActiveScene") 14 | group.lastFocus = m.top 15 | end if 16 | end sub 17 | 18 | ' Function called when the screen is hidden by the screen manager 19 | ' It is expected that screens override this function if required, 20 | ' to handle focus any actions required on the screen being hidden 21 | sub OnScreenHidden() 22 | end sub 23 | 24 | -------------------------------------------------------------------------------- /components/movies/AudioTrackListItem.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.title = m.top.findNode("title") 3 | m.description = m.top.findNode("description") 4 | m.selectedIcon = m.top.findNode("selectedIcon") 5 | end sub 6 | 7 | sub itemContentChanged() 8 | m.title.text = m.top.itemContent.title 9 | m.description.text = m.top.itemContent.description 10 | 11 | if m.top.itemContent.description = "" 12 | m.title.translation = [50, 20] 13 | end if 14 | 15 | if m.top.itemContent.selected 16 | m.selectedIcon.uri = m.global.constants.icons.check_white 17 | else 18 | m.selectedIcon.uri = "" 19 | end if 20 | 21 | end sub 22 | 23 | ' 24 | 'Scroll description if focused 25 | sub focusChanged() 26 | 27 | if m.top.itemHasFocus = true 28 | m.description.repeatCount = -1 29 | else 30 | m.description.repeatCount = 0 31 | end if 32 | 33 | end sub 34 | -------------------------------------------------------------------------------- /components/movies/VideoTrackListItem.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.title = m.top.findNode("title") 3 | m.description = m.top.findNode("description") 4 | m.selectedIcon = m.top.findNode("selectedIcon") 5 | end sub 6 | 7 | sub itemContentChanged() 8 | m.title.text = m.top.itemContent.title 9 | m.description.text = m.top.itemContent.description 10 | 11 | if m.top.itemContent.description = "" 12 | m.title.translation = [50, 20] 13 | end if 14 | 15 | if m.top.itemContent.selected 16 | m.selectedIcon.uri = m.global.constants.icons.check_white 17 | else 18 | m.selectedIcon.uri = "" 19 | end if 20 | 21 | end sub 22 | 23 | ' 24 | 'Scroll description if focused 25 | sub focusChanged() 26 | 27 | if m.top.itemHasFocus = true 28 | m.description.repeatCount = -1 29 | else 30 | m.description.repeatCount = 0 31 | end if 32 | 33 | end sub 34 | -------------------------------------------------------------------------------- /components/SettingDialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /components/StandardButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /components/login/UserItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/ListPoster.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/ItemGrid/ColorOption.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /components/data/ExtrasData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /components/IconButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/data/HomeData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/Buttons/SlideOutButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/data/SearchData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/subtitle/SubtitleItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/extras/ExtrasItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /components/ItemGrid/MusicArtistGridItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jellyfin", 3 | "type": "module", 4 | "version": "3.1.0", 5 | "description": "Official Roku app for Jellyfin media server", 6 | "dependencies": { 7 | "@rokucommunity/bslib": "0.1.1", 8 | "brighterscript-formatter": "1.7.19" 9 | }, 10 | "devDependencies": { 11 | "@rokucommunity/bslint": "0.8.38", 12 | "brighterscript": "0.70.3", 13 | "jshint": "2.13.6", 14 | "rimraf": "6.1.2", 15 | "roku-deploy": "3.16.1", 16 | "undent": "1.0.0" 17 | }, 18 | "scripts": { 19 | "build": "npx rimraf build/ out/ && npx bsc --project bsconfig.json" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/jellyfin/jellyfin-roku.git" 24 | }, 25 | "keywords": [ 26 | "jellyfin", 27 | "roku", 28 | "music", 29 | "movies" 30 | ], 31 | "author": "Jellyfin", 32 | "license": "GPL-2.0", 33 | "bugs": { 34 | "url": "https://github.com/jellyfin/jellyfin-roku" 35 | }, 36 | "homepage": "https://github.com/jellyfin/jellyfin-roku" 37 | } 38 | -------------------------------------------------------------------------------- /components/data/ChannelData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Image.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | m.top.id = json.id 8 | if isValid(json.number) 9 | m.top.title = `${tr("CH")} ${json.number} ${json.name}` 10 | else 11 | m.top.title = json.name 12 | end if 13 | m.top.live = true 14 | m.top.Type = "TvChannel" 15 | setPoster() 16 | end sub 17 | 18 | sub setPoster() 19 | if m.top.image <> invalid 20 | m.top.posterURL = m.top.image.url 21 | else if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Primary <> invalid 22 | imgParams = { "maxHeight": 60, "Tag": m.top.json.ImageTags.Primary } 23 | m.top.hdsmalliconurl = ImageURL(m.top.json.id, "Primary", imgParams) 24 | 25 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary } 26 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 27 | end if 28 | end sub 29 | -------------------------------------------------------------------------------- /components/Buttons/ButtonData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /components/data/PlaylistItemData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/config/FormElement.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/enums/String.bs" 3 | import "pkg:/source/utils/misc.bs" 4 | 5 | sub init() 6 | m.name = m.top.findNode("label") 7 | 8 | m.value = m.top.findNode("value") 9 | m.value.backgroundUri = "pkg:/images/transparent.png" 10 | m.value.width = 440 11 | 12 | m.name.color = ColorPalette.WHITE 13 | m.value.textColor = ColorPalette.DARKGREY 14 | m.value.hintTextColor = ColorPalette.MIDGREY 15 | 16 | m.name.width = 240 17 | m.name.height = 75 18 | 19 | m.name.vertAlign = "center" 20 | m.name.horizAlign = "center" 21 | 22 | m.value.hintText = tr("Enter a username") 23 | m.value.maxTextLength = 120 24 | end sub 25 | 26 | sub itemContentChanged() 27 | data = m.top.itemContent 28 | 29 | m.name.text = data.label 30 | if isStringEqual(data.type, "password") 31 | m.value.hintText = tr("Enter a password") 32 | m.value.secureMode = true 33 | end if 34 | 35 | m.value.text = data.value 36 | end sub 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "brightscript", 6 | "request": "launch", 7 | "name": "Jellyfin Debug", 8 | "rootDir": "${workspaceFolder}/build/staging", 9 | "preLaunchTask": "build-dev", 10 | "stopOnEntry": false, 11 | // To enable RALE: 12 | // set "brightscript.debug.raleTrackerTaskFileLocation": "/absolute/path/to/rale/TrackerTask.xml" in your vscode user settings 13 | // set the below field to true 14 | "injectRaleTrackerTask": false, 15 | "injectRdbOnDeviceComponent": true, 16 | //WARNING: don't edit this value. Instead, set "brightscript.debug.host": "YOUR_HOST_HERE" in your vscode user settings 17 | //"host": "${promptForHost}", 18 | //WARNING: don't edit this value. Instead, set "brightscript.debug.password": "YOUR_PASSWORD_HERE" in your vscode user settings 19 | //"password": "${promptForPassword}", 20 | }, 21 | ] 22 | } -------------------------------------------------------------------------------- /components/ItemGrid/GridItemSmall.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /components/video/PreloadTrickplayImagesTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/sdk.bs" 2 | import "pkg:/source/utils/config.bs" 3 | 4 | sub init() 5 | m.top.functionName = "preloadTrickplayImagesTask" 6 | end sub 7 | 8 | sub preloadTrickplayImagesTask() 9 | fs = CreateObject("roFileSystem") 10 | 11 | if m.top.method = "ADD" 12 | for tileIndex = 0 to m.top.numImagesToLoad 13 | updatedURL = `Videos/${m.top.videoID}/Trickplay/${m.top.trickplayWidth}/${tileIndex}.jpg?api_key=${get_user_setting("token")}` 14 | if not fs.Exists(`tmp:/${m.top.videoID}-${tileIndex}.jpg`) 15 | APIRequest(updatedURL).gettofile(`tmp:/${m.top.videoID}-${tileIndex}.jpg`) 16 | end if 17 | end for 18 | return 19 | end if 20 | 21 | if m.top.method = "REMOVE" 22 | for tileIndex = 0 to m.top.numImagesToLoad 23 | if fs.Exists(`tmp:/${m.top.videoID}-${tileIndex}.jpg`) 24 | fs.Delete(`tmp:/${m.top.videoID}-${tileIndex}.jpg`) 25 | end if 26 | end for 27 | return 28 | end if 29 | end sub 30 | -------------------------------------------------------------------------------- /components/data/SceneManager.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/options/OptionsSlider.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.visible = false 3 | 4 | panel = m.top.findNode("panel") 5 | panel.panelSize = "small" 6 | panel.leftPosition = 1250 7 | panel.focusable = true 8 | panel.hasNextPanel = false 9 | panel.leftOnly = true 10 | 11 | list = m.top.findNode("panelList") 12 | 13 | panel.list = list 14 | end sub 15 | 16 | sub setFields() 17 | options = m.top.options 18 | buttons = m.top.buttons 19 | row = m.top.findNode("fieldList") 20 | 21 | row.clear() 22 | row.appendChildren(options) 23 | row.appendChildren(buttons) 24 | end sub 25 | 26 | function onKeyEvent(key as string, press as boolean) as boolean 27 | if not press then return false 28 | 29 | if key = "options" or key = "back" 30 | m.top.visible = false 31 | m.top.closeSidePanel = true 32 | return true 33 | else if key = "OK" 34 | list = m.top.findNode("panelList") 35 | data = list.content.getChild(list.itemFocused) 36 | data.userMenuOptionSelected = true 37 | return true 38 | end if 39 | 40 | return false 41 | end function 42 | -------------------------------------------------------------------------------- /components/data/OptionsData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | 3 | sub init() 4 | m.top.value_index = 0 5 | end sub 6 | 7 | sub update_title() 8 | if m.top.choices.count() = 0 9 | m.top.title = m.top.base_title + ": " 10 | return 11 | end if 12 | 13 | for i = 0 to m.top.choices.count() - 1 14 | if m.top.choices[i].value = m.top.value 15 | m.top.value_index = i 16 | exit for 17 | end if 18 | end for 19 | m.top.title = m.top.base_title + ": " + m.top.choices[m.top.value_index].display 20 | end sub 21 | 22 | sub press() 23 | max_opt = m.top.choices.count() 24 | i = m.top.value_index + 1 25 | while i >= max_opt 26 | i = i - max_opt 27 | end while 28 | 29 | m.top.value_index = i 30 | m.top.value = m.top.choices[m.top.value_index].value 31 | 32 | if m.top.config_key = "" or m.top.config_key = invalid 33 | return 34 | end if 35 | if m.top.global_setting 36 | set_setting(m.top.config_key, m.top.value) 37 | else 38 | set_user_setting(m.top.config_key, m.top.value) 39 | end if 40 | end sub 41 | -------------------------------------------------------------------------------- /components/music/SimilarArtistGrid.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | 3 | sub init() 4 | end sub 5 | 6 | function onKeyEvent(key as string, press as boolean) as boolean 7 | if not press then return false 8 | 9 | if key = "up" 10 | if m.top.itemFocused <= 5 11 | m.top.escape = key 12 | return true 13 | end if 14 | else if key = "left" 15 | if m.top.itemFocused mod 6 = 0 16 | m.top.escape = key 17 | return true 18 | end if 19 | else if key = "right" 20 | if m.top.itemFocused + 1 mod 6 = 0 21 | m.top.escape = key 22 | return true 23 | end if 24 | else if key = "down" 25 | totalCount = 0 26 | if isValid(m.top.content) 27 | totalCount = m.top.content.getChildCount() 28 | end if 29 | 30 | totalRows = div_ceiling(totalCount, 6) 31 | currentRow = fix(m.top.itemFocused / 6) 32 | 33 | if currentRow = totalRows - 1 34 | m.top.escape = key 35 | return true 36 | end if 37 | end if 38 | 39 | return false 40 | end function 41 | -------------------------------------------------------------------------------- /components/subtitle/SubtitleItem.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | import "pkg:/source/enums/ColorPalette.bs" 3 | 4 | sub init() 5 | m.displayTitle = m.top.findNode("displayTitle") 6 | m.path = m.top.findNode("path") 7 | m.delete = m.top.findNode("delete") 8 | 9 | m.displayTitle.font.size = 30 10 | m.path.font.size = 28 11 | 12 | m.defaultTextColor = m.displayTitle.color 13 | end sub 14 | 15 | sub itemContentChanged() 16 | itemData = m.top.itemContent 17 | if itemData = invalid then return 18 | m.displayTitle.text = itemData.displayTitle 19 | m.path.text = itemData.path 20 | end sub 21 | 22 | sub focusChanged() 23 | color = m.defaultTextColor 24 | repeatcount = 0 25 | showDeleteIcon = false 26 | 27 | if m.top.itemHasFocus 28 | color = ColorPalette.WHITE 29 | repeatcount = -1 30 | showDeleteIcon = m.top.itemContent.canDelete 31 | end if 32 | 33 | m.displayTitle.color = color 34 | m.displayTitle.repeatCount = repeatcount 35 | m.delete.visible = showDeleteIcon 36 | 37 | m.path.color = color 38 | m.path.repeatCount = repeatcount 39 | end sub 40 | -------------------------------------------------------------------------------- /components/Buttons/ExpandingLabel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /components/SearchBox.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/Image.bs" 3 | import "pkg:/source/api/Items.bs" 4 | import "pkg:/source/utils/config.bs" 5 | import "pkg:/source/utils/deviceCapabilities.bs" 6 | 7 | sub init() 8 | m.top.layoutDirection = "vert" 9 | m.top.horizAlignment = "center" 10 | m.top.vertAlignment = "top" 11 | m.top.visible = false 12 | m.searchText = m.top.findNode("search_Key") 13 | m.searchText.textEditBox.hintText = tr("Search") 14 | m.searchText.keyGrid.keyDefinitionUri = "pkg:/components/data/CustomAddressKDF.json" 15 | m.searchText.textEditBox.voiceEnabled = true 16 | m.searchText.textEditBox.active = true 17 | m.searchText.ObserveField("text", "searchMedias") 18 | m.searchRow = m.top.findNode("searchRow") 19 | 20 | 'set lable text 21 | m.label = m.top.findNode("text") 22 | m.label.text = tr("Search now") 23 | 24 | end sub 25 | 26 | sub searchMedias() 27 | m.top.search_values = m.searchText.text 28 | if m.top.search_values.len() > 1 29 | m.searchText.textEditBox.leadingEllipsis = true 30 | else 31 | m.searchText.textEditBox.leadingEllipsis = false 32 | end if 33 | end sub 34 | -------------------------------------------------------------------------------- /components/data/TVEpisode.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/Image.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | 8 | m.top.id = json.id 9 | m.top.Description = json.overview 10 | m.top.favorite = json.UserData.isFavorite 11 | m.top.watched = chainLookup(json, "UserData.played") 12 | m.top.Type = json.Type 13 | 14 | setPoster() 15 | end sub 16 | 17 | sub setPoster() 18 | if m.top.image <> invalid 19 | m.top.posterURL = m.top.image.url 20 | else if m.top.json.ImageTags.Primary <> invalid 21 | imgParams = { "maxHeight": 440, "maxWidth": 295 } 22 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 23 | end if 24 | end sub 25 | 26 | sub setWatched(isWatched as boolean, unplayedItemCount = 0 as integer) 27 | if not isValid(m.top.json) then return 28 | 29 | json = m.top.json 30 | 31 | if isChainValid(json, "UserData.Played") 32 | json.UserData.AddReplace("Played", isWatched) 33 | json.UserData.AddReplace("PlaybackPositionTicks", 0) 34 | m.top.json = json 35 | end if 36 | 37 | m.top.watched = isWatched 38 | end sub 39 | -------------------------------------------------------------------------------- /components/tvshows/TVEpisodes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /components/liveTv/schedule.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /components/ItemGrid/LoadVideoContentTask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /components/music/Lyrics.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /components/ItemGrid/LoadItemsTask2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/music/PlaylistItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /components/config/JFServer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/TextButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /components/extras/ExtrasSlider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /source/utils/parsedUrl.bs: -------------------------------------------------------------------------------- 1 | ' 2 | ' Returns an object from a URL with its URI components as properties = { proto, host, port, path, query } 3 | ' If any of those URI components are missing, they will be set as empty strings = "" 4 | ' If the url param provided cannot be parsed, then invalid will be returned 5 | ' 6 | ' @param url a URL string which will be parsed into URI components 7 | function ParsedUrl(url as string) as object 8 | rgx = CreateObject("roRegex", "^(?:(.*):\/\/)?([^\/:?]+)(?::(\d+))?(\/[^\r\n?]+)?(?:\?(.*))?$", "") 9 | match = rgx.Match(url) 10 | if not isValid(match) then return invalid 11 | 12 | return { 13 | proto: isValid(match[1]) ? match[1] : string.EMPTY, 14 | host: isValid(match[2]) ? match[2] : string.EMPTY, 15 | port: isValid(match[3]) ? match[3] : string.EMPTY, 16 | path: isValid(match[4]) ? match[4].endswith("/") ? match[4].left(len(match[4]) - 1) : match[4] : string.EMPTY, 17 | query: isValid(match[5]) ? match[5] : string.EMPTY, 18 | toString: __ParsedUrl_ToString 19 | } 20 | end function 21 | 22 | ' Returns the parsed URL as a complete URL string 23 | function __ParsedUrl_ToString() as string 24 | return `${m.proto = "" ? "" : `${m.proto}://`}${m.host}${m.port = "" ? "" : `:${m.port}`}${m.path}${m.query = "" ? "" : `?${m.query}`}` 25 | end function 26 | -------------------------------------------------------------------------------- /components/Buttons/JFButtons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /components/ItemGrid/GridItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/liveTv/LoadSheduleTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/utils/config.bs" 3 | 4 | sub init() 5 | m.top.functionName = "loadSchedule" 6 | end sub 7 | 8 | sub loadSchedule() 9 | 10 | results = [] 11 | 12 | params = { 13 | UserId: m.global.session.user.id, 14 | SortBy: "startDate", 15 | EnableImages: false, 16 | EnableTotalRecordCount: false, 17 | EnableUserData: false, 18 | channelIds: m.top.channelIds, 19 | MaxStartDate: m.top.endTime, 20 | MinEndDate: m.top.startTime 21 | } 22 | 23 | url = "LiveTv/Programs" 24 | 25 | resp = APIRequest(url) 26 | data = postJson(resp, FormatJson(params)) 27 | 28 | if data = invalid 29 | m.top.schedule = results 30 | return 31 | end if 32 | 33 | results = [] 34 | 35 | for each item in data.Items 36 | program = createObject("roSGNode", "ScheduleProgramData") 37 | program.json = item 38 | ' Are we currently recording this program? 39 | if program.json.TimerId <> invalid and program.json.TimerId <> "" 40 | program.hdSmallIconUrl = "pkg:/images/red.png" 41 | else 42 | program.hdSmallIconUrl = invalid 43 | end if 44 | results.push(program) 45 | end for 46 | 47 | 48 | m.top.schedule = results 49 | 50 | end sub 51 | -------------------------------------------------------------------------------- /components/MovieDetailButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /components/data/MusicAlbumSongListData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Image.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | m.top.id = json.id 8 | m.top.favorite = json.UserData.isFavorite 9 | m.top.Type = "MusicAlbum" 10 | setPoster() 11 | end sub 12 | 13 | sub setPoster() 14 | if m.top.image <> invalid 15 | m.top.posterURL = m.top.image.url 16 | else 17 | 18 | if m.top.json.ImageTags.Primary <> invalid 19 | imgParams = { "maxHeight": 440, "maxWidth": 295 } 20 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 21 | else if m.top.json.BackdropImageTags[0] <> invalid 22 | imgParams = { "maxHeight": 440 } 23 | m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 24 | else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid 25 | imgParams = { "maxHeight": 440, "maxWidth": 295 } 26 | m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams) 27 | end if 28 | 29 | ' Add Backdrop Image 30 | if m.top.json.BackdropImageTags[0] <> invalid 31 | imgParams = { "maxHeight": 720, "maxWidth": 1280 } 32 | m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 33 | end if 34 | 35 | end if 36 | end sub 37 | -------------------------------------------------------------------------------- /components/home/HomeItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /components/liveTv/LoadProgramDetailsTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/utils/config.bs" 3 | 4 | sub init() 5 | m.top.functionName = "loadProgramDetails" 6 | 7 | end sub 8 | 9 | sub loadProgramDetails() 10 | 11 | channelIndex = m.top.ChannelIndex 12 | programIndex = m.top.ProgramIndex 13 | 14 | params = { 15 | UserId: m.global.session.user.id 16 | } 17 | 18 | url = Substitute("LiveTv/Programs/{0}", m.top.programId) 19 | 20 | resp = APIRequest(url, params) 21 | data = getJson(resp) 22 | 23 | if data = invalid 24 | m.top.programDetails = {} 25 | return 26 | end if 27 | 28 | program = createObject("roSGNode", "ScheduleProgramData") 29 | program.json = data 30 | program.channelIndex = channelIndex 31 | program.programIndex = programIndex 32 | program.fullyLoaded = true 33 | ' Are we currently recording this program? 34 | if program.json.TimerId <> invalid and program.json.TimerId <> "" 35 | ' This is needed here because the callee (onProgramDetailsLoaded) replaces the grid item with 36 | ' this newly created item from the server, without this, the red icon 37 | ' disappears when the user focuses on the program in question 38 | program.hdSmallIconUrl = "pkg:/images/red.png" 39 | else 40 | program.hdSmallIconUrl = invalid 41 | end if 42 | m.top.programDetails = program 43 | 44 | end sub 45 | -------------------------------------------------------------------------------- /components/Libraries/LiveTVLibraryView.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /components/music/AlbumGrid.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/misc.bs" 2 | 3 | sub init() 4 | getData() 5 | end sub 6 | 7 | sub getData() 8 | ' If we have no album data, return a blank node 9 | if not isValid(m.top.MusicArtistAlbumData) 10 | m.top.content = CreateObject("roSGNode", "ContentNode") 11 | return 12 | end if 13 | 14 | m.top.content = m.top.MusicArtistAlbumData 15 | end sub 16 | 17 | function onKeyEvent(key as string, press as boolean) as boolean 18 | if not press then return false 19 | 20 | if key = "up" 21 | if m.top.itemFocused <= 4 22 | m.top.escape = key 23 | return true 24 | end if 25 | else if key = "left" 26 | if m.top.itemFocused mod 5 = 0 27 | m.top.escape = key 28 | return true 29 | end if 30 | else if key = "right" 31 | if m.top.itemFocused + 1 mod 5 = 0 32 | m.top.escape = key 33 | return true 34 | end if 35 | else if key = "down" 36 | totalCount = 0 37 | if isValid(m.top.MusicArtistAlbumData) 38 | totalCount = m.top.MusicArtistAlbumData.getChildCount() 39 | end if 40 | totalRows = div_ceiling(totalCount, 5) 41 | currentRow = div_ceiling(m.top.itemFocused + 1, 5) 42 | 43 | if currentRow = totalRows 44 | m.top.escape = key 45 | return true 46 | end if 47 | end if 48 | 49 | return false 50 | end function 51 | -------------------------------------------------------------------------------- /components/search/SearchResults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /components/ItemGrid/GridItemMedium.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/tvshows/TVListOptions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /components/data/PhotoData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Image.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | 8 | m.top.id = json.id 9 | m.top.Title = json.name 10 | m.top.Type = "Photo" 11 | 12 | setPoster() 13 | end sub 14 | 15 | sub setPoster() 16 | if m.top.image <> invalid 17 | m.top.posterURL = m.top.image.url 18 | else 19 | 20 | if m.top.json.ImageTags.Primary <> invalid 21 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary } 22 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 23 | else if m.top.json.BackdropImageTags[0] <> invalid 24 | imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] } 25 | m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 26 | else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid 27 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag } 28 | m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams) 29 | end if 30 | 31 | ' Add Backdrop Image 32 | if m.top.json.BackdropImageTags[0] <> invalid 33 | imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] } 34 | m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 35 | end if 36 | 37 | end if 38 | end sub 39 | -------------------------------------------------------------------------------- /components/tvshows/TVSeasonRow.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.top.itemComponentName = "ListPoster" 6 | m.top.content = getData() 7 | 8 | m.top.rowFocusAnimationStyle = "fixedFocusWrap" 9 | m.top.focusBitmapBlendColor = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) 10 | 11 | m.top.showRowLabel = [false] 12 | m.top.showRowCounter = [true] 13 | m.top.rowLabelOffset = [0, 0] 14 | 15 | updateSize() 16 | 17 | m.top.setfocus(true) 18 | end sub 19 | 20 | sub updateSize() 21 | itemWidth = 200 22 | itemHeight = 320 ' width * 1.5 + text 23 | 24 | m.top.visible = true 25 | 26 | ' size of the whole row 27 | m.top.itemSize = [1680, (itemHeight + 40)] 28 | ' spacing between rows 29 | m.top.itemSpacing = [0, 0] 30 | 31 | ' size of the item in the row 32 | m.top.rowItemSize = [itemWidth, itemHeight] 33 | ' spacing between items in a row 34 | m.top.rowItemSpacing = [0, 0] 35 | end sub 36 | 37 | function getData() 38 | if m.top.TVSeasonData = invalid 39 | data = CreateObject("roSGNode", "ContentNode") 40 | return data 41 | end if 42 | 43 | seasonData = m.top.TVSeasonData 44 | data = CreateObject("roSGNode", "ContentNode") 45 | row = data.CreateChild("ContentNode") 46 | row.title = tr("Seasons") 47 | for each item in seasonData.items 48 | row.appendChild(item) 49 | end for 50 | m.top.content = data 51 | return data 52 | end function 53 | -------------------------------------------------------------------------------- /components/data/PersonData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/Image.bs" 2 | import "pkg:/source/api/baserequest.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | m.top.id = json.id 8 | m.top.favorite = json.UserData.isFavorite 9 | m.top.Type = "Person" 10 | setPoster() 11 | end sub 12 | 13 | sub setPoster() 14 | if m.top.image <> invalid 15 | m.top.posterURL = m.top.image.url 16 | else 17 | 18 | if m.top.json.ImageTags.Primary <> invalid 19 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary } 20 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 21 | else if m.top.json.BackdropImageTags[0] <> invalid 22 | imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] } 23 | m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 24 | else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid 25 | imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ParentThumbImageTag } 26 | m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams) 27 | end if 28 | 29 | ' Add Backdrop Image 30 | if m.top.json.BackdropImageTags[0] <> invalid 31 | imgParams = { "maxHeight": 720, "maxWidth": 1280, "Tag": m.top.json.BackdropImageTags[0] } 32 | m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 33 | end if 34 | 35 | end if 36 | end sub 37 | -------------------------------------------------------------------------------- /components/StandardDialog.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.content = m.top.findNode("content") 6 | m.top.observeField("contentData", "onContentDataChanged") 7 | 8 | m.top.id = "OKDialog" 9 | m.top.height = 900 10 | m.top.title = tr("What's New")+ "?" 11 | m.top.buttons = [tr("OK")] 12 | 13 | m.dialogStyles = { 14 | "default": { 15 | "fontSize": 27, 16 | "fontUri": "font:BoldSystemFontFile", 17 | "color": chainLookupReturn(m.global.session, "user.settings.colorDialogText", ColorPalette.WHITE) 18 | }, 19 | "b": { 20 | "fontSize": 27, 21 | "fontUri": "font:SystemFontFile", 22 | "color": chainLookupReturn(m.global.session, "user.settings.colorDialogBoldText", ColorPalette.TRIADGREEN) 23 | }, 24 | "header": { 25 | "fontSize": 35, 26 | "fontUri": "font:SystemFontFile", 27 | "color": chainLookupReturn(m.global.session, "user.settings.colorDialogText", ColorPalette.WHITE) 28 | }, 29 | "p": { 30 | "fontSize": 27, 31 | "fontUri": "font:SystemFontFile", 32 | "color": chainLookupReturn(m.global.session, "user.settings.colorDialogText", ColorPalette.WHITE) 33 | } 34 | } 35 | 36 | end sub 37 | 38 | sub onContentDataChanged() 39 | for each item in m.top.contentData.data 40 | textLine = m.content.CreateChild("StdDlgMultiStyleTextItem") 41 | textLine.drawingStyles = m.dialogStyles 42 | textLine.text = item 43 | end for 44 | end sub 45 | -------------------------------------------------------------------------------- /components/data/MusicArtistData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/Image.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | m.top.id = json.id 8 | if isChainValid(json, "UserData.isFavorite") 9 | m.top.favorite = json.UserData.isFavorite 10 | end if 11 | m.top.Type = "MusicArtist" 12 | setPoster() 13 | 14 | m.top.title = json.name 15 | end sub 16 | 17 | sub setPoster() 18 | if m.top.image <> invalid 19 | m.top.posterURL = m.top.image.url 20 | else 21 | 22 | if not isChainValid(m.top.json, "ImageTags") then return 23 | 24 | ' Add Artist Image 25 | if m.top.json.ImageTags.Primary <> invalid 26 | imgParams = { "maxHeight": 440, "maxWidth": 440 } 27 | m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams) 28 | else if m.top.json.BackdropImageTags[0] <> invalid 29 | imgParams = { "maxHeight": 440 } 30 | m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 31 | else if m.top.json.ParentThumbImageTag <> invalid and m.top.json.ParentThumbItemId <> invalid 32 | imgParams = { "maxHeight": 440, "maxWidth": 440 } 33 | m.top.posterURL = ImageURL(m.top.json.ParentThumbItemId, "Thumb", imgParams) 34 | end if 35 | 36 | ' Add Backdrop Image 37 | if m.top.json.BackdropImageTags[0] <> invalid 38 | imgParams = { "maxHeight": 720, "maxWidth": 1280 } 39 | m.top.backdropURL = ImageURL(m.top.json.id, "Backdrop", imgParams) 40 | end if 41 | 42 | end if 43 | end sub 44 | -------------------------------------------------------------------------------- /components/ButtonGroupVert.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.top.layoutDirection = "vert" 3 | m.top.observeField("focusedChild", "onFocusChanged") 4 | m.top.observeField("focusButton", "onFocusButtonChanged") 5 | end sub 6 | 7 | sub onFocusChanged() 8 | if m.top.hasFocus() 9 | m.top.getChild(0).setFocus(true) 10 | group = m.global.sceneManager.callFunc("getActiveScene") 11 | group.lastFocus = m.top.getChild(0) 12 | m.top.focusButton = 0 13 | end if 14 | end sub 15 | 16 | sub onFocusButtonChanged() 17 | m.top.getChild(m.top.focusButton).setFocus(true) 18 | 19 | group = m.global.sceneManager.callFunc("getActiveScene") 20 | group.lastFocus = m.top.getChild(m.top.focusButton) 21 | end sub 22 | 23 | function onKeyEvent(key as string, press as boolean) as boolean 24 | if key = "OK" 25 | m.top.selected = m.top.focusButton 26 | return true 27 | end if 28 | 29 | if not press then return false 30 | 31 | if key = "down" 32 | i = m.top.focusButton 33 | target = i + 1 34 | if target >= m.top.getChildCount() 35 | m.top.escape = key 36 | return false 37 | end if 38 | m.top.focusButton = target 39 | return true 40 | else if key = "up" 41 | i = m.top.focusButton 42 | target = i - 1 43 | if target < 0 44 | m.top.escape = key 45 | return false 46 | end if 47 | m.top.focusButton = target 48 | return true 49 | else if key = "left" or key = "right" 50 | m.top.escape = key 51 | return true 52 | end if 53 | 54 | return false 55 | end function 56 | -------------------------------------------------------------------------------- /components/data/UserData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/utils/config.bs" 2 | 3 | sub setDataFromJSON() 4 | json = m.top.json 5 | loadFromJSON(json) 6 | end sub 7 | 8 | sub loadFromJSON(json) 9 | m.top.id = json.User.id 10 | 11 | m.top.username = json.User.name 12 | m.top.token = json.AccessToken 13 | end sub 14 | 15 | sub loadFromRegistry(id as string) 16 | m.top.id = id 17 | 18 | m.top.username = get_user_setting("username") 19 | m.top.token = get_user_setting("token") 20 | end sub 21 | 22 | sub saveToRegistry() 23 | users = parseJson(get_setting("available_users", "[]")) 24 | this_user = invalid 25 | 26 | for each user in users 27 | if user.id = m.top.id then this_user = user 28 | end for 29 | 30 | if not isValid(this_user) 31 | users.push({ 32 | id: m.top.id, 33 | username: m.top.username, 34 | server: m.global.session.server.url 35 | }) 36 | set_setting("available_users", formatJson(users)) 37 | end if 38 | end sub 39 | 40 | sub removeFromRegistry() 41 | new_users = [] 42 | users = parseJson(get_setting("available_users", "[]")) 43 | for each user in users 44 | if m.top.id <> user.id then new_users.push(user) 45 | end for 46 | 47 | set_setting("available_users", formatJson(new_users)) 48 | end sub 49 | 50 | function getPreference(key as string) 51 | return get_user_setting("pref-" + key) 52 | end function 53 | 54 | function setPreference(key as string, value as string) 55 | return set_user_setting("pref-" + key, value) 56 | end function 57 | 58 | sub setServer(hostname as string) 59 | m.top.server = hostname 60 | end sub 61 | -------------------------------------------------------------------------------- /components/PlaystateTask.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/utils/config.bs" 3 | 4 | sub init() 5 | m.top.functionName = "PlaystateUpdate" 6 | end sub 7 | 8 | sub PlaystateUpdate() 9 | if m.top.status = "start" 10 | url = "Sessions/Playing" 11 | else if m.top.status = "stop" 12 | url = "Sessions/Playing/Stopped" 13 | else if m.top.status = "update" 14 | url = "Sessions/Playing/Progress" 15 | else 16 | ' Unknown State 17 | return 18 | end if 19 | params = PlaystateDefaults(m.top.params) 20 | resp = APIRequest(url) 21 | postJson(resp, params) 22 | end sub 23 | 24 | function PlaystateDefaults(params = {} as object) 25 | new_params = { 26 | '"CanSeek": false 27 | '"Item": "{}", ' TODO! 28 | '"NowPlayingQueue": "[]", ' TODO! 29 | '"PlaylistItemId": "", 30 | '"ItemId": id, 31 | '"SessionId": "", ' TODO! 32 | '"MediaSourceId": id, 33 | '"AudioStreamIndex": 1, 34 | '"SubtitleStreamIndex": 0, 35 | "IsPaused": false, 36 | '"IsMuted": false, 37 | "PositionTicks": 0 38 | '"PlaybackStartTimeTicks": 0, 39 | '"VolumeLevel": 100, 40 | '"Brightness": 100, 41 | '"AspectRatio": "16x9", 42 | '"PlayMethod": "DirectStream" 43 | '"LiveStreamId": "", 44 | '"PlaySessionId": "", 45 | '"RepeatMode": "RepeatNone" 46 | } 47 | 48 | paramsArray = params.items() 49 | for i = 0 to paramsArray.count() - 1 50 | item = paramsArray[i] 51 | new_params[item.key] = item.value 52 | end for 53 | return FormatJson(new_params) 54 | end function 55 | -------------------------------------------------------------------------------- /components/login/UserRow.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.top.focusBitmapBlendColor = chainLookupReturn(m.global.session, "user.settings.colorCursor", ColorPalette.HIGHLIGHT) 6 | m.top.itemComponentName = "UserItem" 7 | m.top.content = SetData() 8 | m.top.observeField("itemSelected", "SetUser") 9 | m.top.showRowLabel = [false] 10 | updateSize() 11 | m.top.setFocus(true) 12 | end sub 13 | 14 | sub updateSize() 15 | itemWidth = 300 16 | itemHeight = 410 17 | 18 | m.top.visible = true 19 | 20 | ' Size of the individual rows 21 | m.top.itemSize = [1660, itemHeight] 22 | ' Spacing between Rows 23 | m.top.itemSpacing = [0, 40] 24 | 25 | ' Size of items in the row 26 | m.top.rowItemSize = [itemWidth, itemHeight] 27 | ' Spacing between items in the row 28 | m.top.rowItemSpacing = [40, 0] 29 | end sub 30 | 31 | 32 | function setData() 33 | if m.top.itemContent = invalid 34 | data = CreateObject("roSGNode", "ContentNode") 35 | return data 36 | end if 37 | 38 | userData = m.top.itemContent 39 | data = CreateObject("roSGNode", "ContentNode") 40 | row = data.CreateChild("ContentNode") 41 | for each item in userData 42 | row.appendChild(item) 43 | end for 44 | m.top.content = data 45 | updateSize() 46 | return data 47 | end function 48 | 49 | sub setUser() 50 | m.top.userSelected = m.top.itemContent[m.top.rowItemFocused[1]].Name 51 | end sub 52 | 53 | function onKeyEvent(key as string, press as boolean) as boolean 54 | if not press then return false 55 | 56 | return false 57 | end function 58 | -------------------------------------------------------------------------------- /components/login/UserItem.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/ColorPalette.bs" 2 | import "pkg:/source/enums/PosterLoadStatus.bs" 3 | import "pkg:/source/utils/misc.bs" 4 | 5 | sub init() 6 | m.top.findNode("profileType").color = ColorPalette.LIGHTGREY 7 | 8 | m.profileImage = m.top.findNode("profileImage") 9 | m.profileImage.observeField("loadStatus", "onPosterLoadStatusChanged") 10 | end sub 11 | 12 | sub onPosterLoadStatusChanged() 13 | if m.profileImage.loadStatus <> PosterLoadStatus.LOADING 14 | m.profileImage.unobserveField("loadStatus") 15 | end if 16 | 17 | if isStringEqual(m.profileImage.loadStatus, PosterLoadStatus.FAILED) 18 | m.profileImage.uri = "pkg:/images/baseline_person_white_48dp.png" 19 | end if 20 | end sub 21 | 22 | sub onFocusChanged() 23 | itemData = m.top.itemContent 24 | if not isValid(itemData) then return 25 | 26 | m.top.findNode("forgetUserIcon").visible = m.top.itemHasFocus 27 | 28 | if m.top.itemHasFocus 29 | m.top.findNode("profileType").color = ColorPalette.WHITE 30 | else 31 | m.top.findNode("profileType").color = ColorPalette.LIGHTGREY 32 | end if 33 | end sub 34 | 35 | sub itemContentChanged() 36 | itemData = m.top.itemContent 37 | if not isValid(itemData) then return 38 | 39 | profileName = m.top.findNode("profileName") 40 | 41 | m.top.findNode("profileType").text = itemData.isPublic ? tr("Public Profile") : tr("Saved Profile") 42 | 43 | if itemData.imageURL = "" 44 | m.profileImage.uri = "pkg:/images/baseline_person_white_48dp.png" 45 | else 46 | m.profileImage.uri = itemData.imageURL 47 | end if 48 | 49 | profileName.text = itemData.name 50 | end sub 51 | -------------------------------------------------------------------------------- /components/photos/PhotoDetails.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /components/music/PlaylistView.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /components/ItemGrid/AudioBookGridItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /components/Libraries/AudioBookLibraryView.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /components/settings/ColorGrid.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/enums/String.bs" 2 | import "pkg:/source/utils/misc.bs" 3 | 4 | sub init() 5 | m.highlightedColor = m.top.findNode("highlightedColor") 6 | m.highlightedColor.font.size = 20 7 | 8 | m.selectedColor = m.top.findNode("selectedColor") 9 | m.selectedColor.font.size = 20 10 | 11 | m.top.observeField("itemFocused", "onItemFocused") 12 | m.top.observeField("itemSelected", "onItemSelected") 13 | end sub 14 | 15 | sub onItemFocused() 16 | if not isValid(m.top.itemFocused) 17 | setHighlightedColor(string.EMPTY) 18 | return 19 | end if 20 | 21 | focusedColor = m.top.content.getChild(m.top.itemFocused) 22 | 23 | if not isChainValid(focusedColor, "colorCode") 24 | setHighlightedColor(string.EMPTY) 25 | return 26 | end if 27 | 28 | setHighlightedColor(focusedColor.colorCode) 29 | end sub 30 | 31 | sub setHighlightedColor(colorCode as string) 32 | m.highlightedColor.text = `${tr("Highlighted Color")}: ${colorCode}` 33 | end sub 34 | 35 | sub onSettingChange() 36 | m.top.selectedColor = chainLookupReturn(m.global.session, `user.settings.${m.top.setting.settingName}`, m.top.setting.default) 37 | end sub 38 | 39 | sub onSelectedColorChange() 40 | selectedColor = m.top.selectedColor 41 | 42 | if not m.top.isInFocusChain() 43 | setHighlightedColor(string.EMPTY) 44 | end if 45 | 46 | m.selectedColor.text = `${tr("Selected Color")}: ${selectedColor}` 47 | 48 | for each color in m.top.content.getChildren(-1, 0) 49 | if color.isChecked then color.isChecked = false 50 | 51 | if isStringEqual(chainLookup(color, "colorCode"), selectedColor) 52 | color.isChecked = true 53 | end if 54 | end for 55 | end sub 56 | -------------------------------------------------------------------------------- /components/data/ScheduleProgramData.bs: -------------------------------------------------------------------------------- 1 | import "pkg:/source/api/baserequest.bs" 2 | import "pkg:/source/api/Image.bs" 3 | import "pkg:/source/utils/config.bs" 4 | 5 | sub setFields() 6 | json = m.top.json 7 | 8 | if isChainValid(json, "StartDate") 9 | startDate = createObject("roDateTime") 10 | startDate.FromISO8601String(json.StartDate) 11 | m.top.PlayStart = startDate.AsSeconds() 12 | end if 13 | 14 | if isChainValid(json, "EndDate") 15 | endDate = createObject("roDateTime") 16 | endDate.FromISO8601String(json.EndDate) 17 | m.top.PlayDuration = endDate.AsSeconds() - m.top.PlayStart 18 | end if 19 | 20 | m.top.Title = json.Name 21 | m.top.Id = json.Id 22 | m.top.Description = json.overview 23 | m.top.EpisodeTitle = json.EpisodeTitle 24 | m.top.isLive = json.isLive 25 | m.top.isRepeat = json.isRepeat 26 | m.top.startDate = json.startDate 27 | m.top.endDate = json.endDate 28 | m.top.channelId = json.channelId 29 | 30 | if json.IsSeries <> invalid and json.IsSeries = true 31 | if json.IndexNumber <> invalid 32 | m.top.episodeNumber = json.IndexNumber 33 | end if 34 | 35 | if json.ParentIndexNumber <> invalid 36 | m.top.seasonNumber = json.ParentIndexNumber 37 | end if 38 | end if 39 | 40 | setPoster() 41 | end sub 42 | 43 | sub setPoster() 44 | if m.top.image <> invalid 45 | m.top.posterURL = m.top.image.url 46 | else 47 | if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Thumb <> invalid 48 | imgParams = { "maxHeight": 500, "maxWidth": 500, "Tag": m.top.json.ImageTags.Thumb } 49 | m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams) 50 | end if 51 | end if 52 | end sub 53 | -------------------------------------------------------------------------------- /components/settings/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /components/music/AlbumView.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /components/StandardButton.bs: -------------------------------------------------------------------------------- 1 | sub init() 2 | m.buttonBackground = m.top.findNode("buttonBackground") 3 | m.buttonText = m.top.findNode("buttonText") 4 | 5 | m.top.observeField("background", "onBackgroundChanged") 6 | m.top.observeField("color", "onColorChanged") 7 | m.top.observeField("text", "onTextChanged") 8 | m.top.observeField("height", "onHeightChanged") 9 | m.top.observeField("width", "onWidthChanged") 10 | m.top.observeField("focus", "onFocusChanged") 11 | end sub 12 | 13 | sub onFocusChanged() 14 | if m.top.focus 15 | m.buttonBackground.blendColor = m.top.focusBackground 16 | m.buttonText.color = m.top.focusColor 17 | m.buttonText.font = "font:SmallBoldSystemFont" 18 | else 19 | m.buttonBackground.blendColor = m.top.background 20 | m.buttonText.color = m.top.color 21 | m.buttonText.font = "font:SmallSystemFont" 22 | end if 23 | end sub 24 | 25 | sub onBackgroundChanged() 26 | m.buttonBackground.blendColor = m.top.background 27 | m.top.unobserveField("background") 28 | end sub 29 | 30 | sub onColorChanged() 31 | m.buttonText.color = m.top.color 32 | m.top.unobserveField("color") 33 | end sub 34 | 35 | sub onTextChanged() 36 | m.buttonText.text = m.top.text 37 | end sub 38 | 39 | sub onHeightChanged() 40 | m.buttonBackground.height = m.top.height 41 | m.buttonText.height = m.top.height 42 | end sub 43 | 44 | sub onWidthChanged() 45 | m.buttonBackground.width = m.top.width 46 | m.buttonText.width = m.top.width 47 | end sub 48 | 49 | function onKeyEvent(key as string, press as boolean) as boolean 50 | if not press then return false 51 | 52 | if key = "right" and m.top.focus 53 | m.top.escape = "right" 54 | end if 55 | 56 | if key = "left" and m.top.focus 57 | m.top.escape = "left" 58 | end if 59 | 60 | return false 61 | end function 62 | --------------------------------------------------------------------------------