diff --git a/lib/home/models/db/home_layout.dart b/lib/home/models/db/home_layout.dart index ad0a9015..c3bfea16 100644 --- a/lib/home/models/db/home_layout.dart +++ b/lib/home/models/db/home_layout.dart @@ -85,10 +85,7 @@ enum HomeDataSource { bool isPermitted(BuildContext context, bool isLoggedIn) { return switch (this) { - (HomeDataSource.subscription || - HomeDataSource.playlist || - HomeDataSource.history) => - isLoggedIn, + (HomeDataSource.playlist || HomeDataSource.history) => isLoggedIn, (HomeDataSource.searchHistory) => context.read().state.useSearchHistory, (_) => true diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 84313bf4..b75fc6e6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,1352 +1,1362 @@ { - "subscriptions": "Subscriptions", - "@subscriptions": { - "description": "User subscriptions" - }, - "playlists": "Playlists", - "@playlists": { - "description": "User playlists" - }, - "popular": "Popular", - "@popular": { - "description": "Popular videos title" - }, - "trending": "Trending", - "@trending": { - "description": "Trending videos title" - }, - "noVideoInPlayList": "No video in playlist", - "@noVideoInPlayList": { - "description": "When no videos in the playlist" - }, - "removeFromPlayList": "Remove from playlist", - "@removeFromPlayList": { - "description": "Menu item description to show remove a video from a playlist" - }, - "deletePlayListQ": "Delete Playlist?", - "@deletePlayListQ": { - "description": "Ask user to delete a playlist" - }, - "irreversibleAction": "This action is irreversible", - "@irreversibleAction": { - "description": "Tell the user that the action cannot be undone" - }, - "addPlayList": "Add Playlist", - "@addPlayList": { - "description": "Title for add playlist dialog" - }, - "playListName": "Playlist name", - "@playListName": { - "description": "Place holder for new playlist name text field" - }, - "playlistVisibility": "Visibility", - "@playlistVisibility": { - "description": "Dropdown label for playlist visibility" - }, - "publicPlaylist": "Public", - "@publicPlaylist": { - "description": "Public playlist" - }, - "privatePlaylist": "Private", - "@privatePlaylist": { - "description": "Private playlist" - }, - "cancel": "Cancel", - "@cancel": { - "description": "Cancel button label" - }, - "add": "Add", - "@add": { - "description": "Add button abel" - }, - "unlistedPlaylist": "Unlisted", - "@unlistedPlaylist": { - "description": "Unlisted playlist" - }, - "info": "Info", - "@info": { - "description": "Info label" - }, - "videos": "Videos", - "@videos": { - "description": "Videos label" - }, - "streams": "Streams", - "@streams": { - "description": "Streams label" - }, - "latestVideos": "Latest Videos", - "@latestVideos": { - "description": "Latest channel videos" - }, - "subscribed": "Subscribed", - "@subscribed": { - "description": "When the user is subscribed to a channel" - }, - "subscribe": "Subscribe", - "@subscribe": { - "description": "Label for user to subscribe to a channel" - }, - "nSubscribers": "{count, select, no{No subscribers} other{{count} subscribers}}", - "@nSubscribers": { - "description": "number of subscribers", - "placeholders": { - "count": { - "type": "String" - } - } - }, - "share": "Share", - "@share": { - "description": "asking user if to share" - }, - "shareYoutubeLink": "Share YouTube link", - "@shareYoutubeLink": { - "description": "asking user to share youtube link" - }, - "shareInvidiousLink": "Share Invidious link", - "@shareInvidiousLink": { - "description": "asking user to share invidious link" - }, - "redirectInvidiousLink": "Share Invidious Redirect link", - "@redirectInvidiousLink": { - "description": "asking user to share redirecting invidious link" - }, - "shareLinkWithTimestamp": "Add timestamp", - "@shareLinkWithTimestamp": { - "description": "asking user to share link along with timestamp" - }, - "ok": "OK", - "@ok": { - "description": "Ok" - }, - "noChannels": "No channels", - "@noChannels": { - "description": "when there are no channels to display" - }, - "noPlaylists": "No playlists", - "@noPlaylists": { - "description": "when there are no playlists to display" - }, - "channels": "Channels", - "@channels": { - "description": "Channels label" - }, - "couldntLoadVideo": "Could not load the video", - "@couldntLoadVideo": { - "description": "Message to display when a video can't be loaded" - }, - "comments": "Comments", - "@comments": { - "description": "Comments label" - }, - "recommended": "Recommended", - "@recommended": { - "description": "Recommended label" - }, - "couldntFetchVideos": "Could not fetch videos. Tap to try again.", - "@couldntFetchVideos": { - "description": "Can't load bunch of videos, asking user to try again" - }, - "wizardIntro": "Select a public server or add your own. (Can be changed later in the settings)", - "@wizardIntro": { - "description": "Welcome message on frst time use" - }, - "startUsingClipious": "Start using Clipious", - "@startUsingClipious": { - "description": "button label to start using the app" - }, - "videoAddedToPlaylist": "Video added to playlist", - "@videoAddedToPlaylist": { - "description": "Pop up message when a video was added to a playlist" - }, - "videoAddedToQueue": "Video added to queue", - "@videoAddedToQueue": { - "description": "Pop up message when a video was added at the end of the video queue" - }, - "errorAddingVideoToPlaylist": "Error while adding video to playlist", - "@errorAddingVideoToPlaylist": { - "description": "Error while adding video to playlist" - }, - "itemlistErrorGeneric": "Could not fetch data", - "@itemlistErrorGeneric": { - "description": "Error showing when the data can't be fetch" - }, - "itemListErrorInvalidScope": "You don''t have the permission to see this, if you logged in using the token method try to log out and in again", - "@itemListErrorInvalidScope": { - "description": "Error when the user doesn't have the proper scope to its current token" - }, - "selectPlaylist": "Select playlist", - "@selectPlaylist": { - "description": "Title when users wants to add a video to a playlist" - }, - "createNewPlaylist": "Create new playlist", - "@createNewPlaylist": { - "description": "Button label to create a new playlist when the user wants to add a video to a playlist" - }, - "nReplies": "{count, plural, =0{No replies} =1{1 reply} other{{count} replies}}", - "@nReplies": { - "description": "number of replies to a comment", - "placeholders": { - "count": { - "type": "num", - "format": "compact" - } - } - }, - "loadMore": "Load more", - "@loadMore": { - "description": "CTA to load more" - }, - "topSorting": "Top", - "@topSorting": { - "description": "Content sorting: top" - }, - "newSorting": "New", - "@newSorting": { - "description": "Content sorting: new" - }, - "streamIsLive": "Live", - "@streamIsLive": { - "description": "Label when a video is a live stream" - }, - "sponsorSkipped": "Sponsor skipped", - "@sponsorSkipped": { - "description": "When a sponsor segment is skipped thanks to sponsor block" - }, - "selectBrowsingCountry": "Select browsing country", - "@selectBrowsingCountry": { - "description": "Select country for trending content" - }, - "showOnStart": "Select what to show when the app starts", - "@showOnStart": { - "description": "Title of dialog asking which screen the users prefers to see" - }, - "settings": "Settings", - "@settings": { - "description": "Settings title" - }, - "browsing": "Browsing", - "@browsing": { - "description": "video browsing preferences" - }, - "country": "Country", - "@country": { - "description": "Country label" - }, - "whenAppStartsShow": "When the app starts, show…", - "@whenAppStartsShow": { - "description": "Setting title for selecting the screen to show on start" - }, - "servers": "Servers", - "@servers": { - "description": "Server management settings category" - }, - "manageServers": "Manage servers", - "@manageServers": { - "description": "Settings to manage servers" - }, - "currentServer": "Currently using {current}", - "@currentServer": { - "description": "Which server the user is currently using", - "placeholders": { - "current": { - "type": "String" - } - } - }, - "useSponsorBlock": "Use SponsorBlock", - "@useSponsorBlock": { - "description": "label for sponsorblock checkbox" - }, - "sponsorBlockDescription": "Skip sponsor segments submitted by the community", - "@sponsorBlockDescription": { - "description": "Sponsorblock setting description" - }, - "about": "About", - "@about": { - "description": "About" - }, - "name": "Name", - "@name": { - "description": "NAme label" - }, - "package": "Package", - "@package": { - "description": "package label" - }, - "version": "Version", - "@version": { - "description": "version label" - }, - "build": "Build", - "@build": { - "description": "build label" - }, - "addServer": "Add server", - "@addServer": { - "description": "Add server label" - }, - "useThisServer": "Use this server", - "@useThisServer": { - "description": "Use this server label" - }, - "logIn": "Log in", - "@logIn": { - "description": "CTA to log in to server" - }, - "delete": "Delete", - "@delete": { - "description": "Delete label" - }, - "invalidInvidiousServer": "Invalid Invidious server", - "@invalidInvidiousServer": { - "description": "Error when the user tries to add a server that is not a proper or reachable invidious server" - }, - "yourServers": "Your servers", - "@yourServers": { - "description": "Your servers label" - }, - "loggedIn": "Logged in", - "@loggedIn": { - "description": "Label to tell the user that he is logged in to the server" - }, - "notLoggedIn": "Not logged in", - "@notLoggedIn": { - "description": "Label when the user is not logged in to the server" - }, - "addServerHelpText": "Use the + button to add your own servers or tap on a public server and add it.", - "@addServerHelpText": { - "description": "label for when the user hasn't chosen a server yet" - }, - "publicServers": "Public servers", - "@publicServers": { - "description": "Public servers label" - }, - "loadingPublicServer": "Loading public servers", - "@loadingPublicServer": { - "description": "Message telling users the app is loading the list of public servers" - }, - "tapToAddServer": "Tap to add server to your list", - "@tapToAddServer": { - "description": "public server description" - }, - "publicServersError": "Could not fetch list of public servers. Tap to retry.", - "@publicServersError": { - "description": "Error message when trying to get public servers but it failed" - }, - "appearance": "Appearance", - "@appearance": { - "description": "Settings category title" - }, - "useDynamicTheme": "Dynamic colors", - "@useDynamicTheme": { - "description": "" - }, - "useDynamicThemeDescription": "Use Material You colors (only available on Android 12+)", - "@useDynamicThemeDescription": { - "description": "" - }, - "useDash": "Use DASH", - "@useDash": { - "description": "Label on video options if a user wants to switch to dash urls instead of the regular quality selection" - }, - "useDashDescription": "DASH adaptive streaming can sometimes be problematic, Youtube can throttle it.", - "@useDashDescription": { - "description": "Description for dash in the settings screen" - }, - "videoPlayer": "Video player", - "@videoPlayer": { - "description": "Title for video player related options" - }, - "videoListed": "Public", - "@videoListed": { - "description": "Status of a publicly available video" - }, - "videoUnlisted": "Unlisted", - "@videoUnlisted": { - "description": "Status of a video that is only accessible by link" - }, - "videoIsFamilyFriendly": "Family friendly", - "@videoIsFamilyFriendly": { - "description": "Displayed only when a video is family friendly" - }, - "tapToManage": "Tap to manage", - "@tapToManage": { - "description": "Text shown below a server in the 'Your servers' list" - }, - "authentication": "Authentication", - "@authentication": { - "description": "Label for server settings related to authentications" - }, - "tokenLogin": "Log in with token", - "@tokenLogin": { - "description": "Textto login to a server using the recommended way" - }, - "tokenLoginDescription": "Recommended way to log in", - "@tokenLoginDescription": { - "description": "Recommended way to log in" - }, - "cookieLogin": "Log in with cookie", - "@cookieLogin": { - "description": "Text to login to a server using the cookie jar method" - }, - "cookieLoginDescription": "Use this method if you face issues with the token authentication", - "@cookieLoginDescription": { - "description": "Cookie log in description" - }, - "logout": "Log out", - "@logout": { - "description": "CTA to logout of a server" - }, - "username": "Username", - "@username": { - "description": "Username label for login to a server" - }, - "password": "Password", - "@password": { - "description": "Password label for login to a server" - }, - "wrongUsernamePassword": "Wrong username or password", - "@wrongUsernamePassword": { - "description": "Error message when authentication fails" - }, - "error": "Error", - "@error": {}, - "malformedStatsEndpoint": "/api/v1/stats is not as expected", - "@malformedStatsEndpoint": { - "description": "Title for dialog when adding a server that isn't validated as it should" - }, - "malformedStatsEndpointDescription": "The server stats endpoint did not respond an expected payload, the key \"software.name\" should be equal to \"invidious\".\nResponse from the server:", - "@malformedStatsEndpointDescription": { - "description": "Description of the possible issue for an invalid stats endpoints" - }, - "serverIsNotReachable": "Server is not reachable", - "@serverIsNotReachable": { - "description": "Title for dialog when adding a server that is not reachable" - }, - "videoQueue": "Video queue", - "@videoQueue": { - "description": "Label for button to display the video queue" - }, - "addToQueueList": "Add to queue", - "@addToQueueList": { - "description": "Label on button to add a video to the queue list" - }, - "addToPlaylist": "Add to playlist", - "@addToPlaylist": { - "description": "Label to add a video to a playlist" - }, - "playNext": "Play next", - "@playNext": { - "description": "Label to play the video after the current one." - }, - "playNextAddedToQueue": "Video will play next", - "@playNextAddedToQueue": { - "description": "Pop up message to confirm that the video has been properly set to play next" - }, - "addRecommendedToQueue": "Auto-play recommended next", - "@addRecommendedToQueue": { - "description": "Switch when playing a video to automatically add the recommended videos to the video queue" - }, - "sponsorBlockSettingsQuickDescription": "Select which type of segments to skip", - "@sponsorBlockSettingsQuickDescription": { - "description": "Small description of what the sponsor block settings do" - }, - "sponsorBlockCategorySponsor": "Sponsor", - "@sponsorBlockCategorySponsor": { - "description": "Sponsor block 'Sponsor' Category" - }, - "sponsorBlockCategorySponsorDescription": "Paid promotion, paid referrals and direct advertisements. Not for self-promotion or free shoutouts to causes/creators/websites/products they like.", - "@sponsorBlockCategorySponsorDescription": { - "description": "Sponsor block 'Sponsor' Category description" - }, - "sponsorBlockCategoryUnpaidSelfPromo": "Unpaid/Self Promotion", - "@sponsorBlockCategoryUnpaidSelfPromo": { - "description": "Sponsor block 'Unpaid/Self promotion' Category" - }, - "sponsorBlockCategoryUnpaidSelfPromoDescription": "Similar to \"sponsor\" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated ", - "@sponsorBlockCategoryUnpaidSelfPromoDescription": { - "description": "Sponsor block 'Unpaid/Self promotion' Category description" - }, - "sponsorBlockCategoryInteraction": "Interaction Reminder (Subscribe)", - "@sponsorBlockCategoryInteraction": { - "description": "Sponsor block 'Interaction' Category" - }, - "sponsorBlockCategoryInteractionDescription": "When there is a short reminder to like, subscribe or follow them in the middle of content. If it is long or about something specific, it should be under self promotion instead.", - "@sponsorBlockCategoryInteractionDescription": { - "description": "Sponsor block 'Interaction' Category description" - }, - "sponsorBlockCategoryIntro": "Intermission/Intro Animation", - "@sponsorBlockCategoryIntro": { - "description": "Sponsorblock 'Intro' Category" - }, - "sponsorBlockCategoryIntroDescription": "An interval without actual content. Could be a pause, static frame, repeating animation. This should not be used for transitions containing information.", - "@sponsorBlockCategoryIntroDescription": { - "description": "Sponsorblock 'Intro' Category description" - }, - "sponsorBlockCategoryOutro": "Endcards/Credits", - "@sponsorBlockCategoryOutro": { - "description": "Outro block 'Outro' Category" - }, - "sponsorBlockCategoryOutroDescription": "Credits or when the YouTube endcards appear. Not for conclusions with information.", - "@sponsorBlockCategoryOutroDescription": { - "description": "Outro block 'Outro' Category description" - }, - "sponsorBlockCategoryPreview": "Preview/Recap", - "@sponsorBlockCategoryPreview": { - "description": "Sponsorblock 'Preview' Category" - }, - "sponsorBlockCategoryPreviewDescription": "Collection of clips that show what is coming up in in this video or other videos in a series where all information is repeated later in the video.", - "@sponsorBlockCategoryPreviewDescription": { - "description": "Sponsorblock 'Preview' Category description" - }, - "sponsorBlockCategoryFiller": "Filler Tangent/Jokes", - "@sponsorBlockCategoryFiller": { - "description": "Sponsorblock 'Filler' Category" - }, - "sponsorBlockCategoryFillerDescription": "Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include segments providing context or background details. This is a very aggressive category meant for when you aren''t in the mood for \"fun\".", - "@sponsorBlockCategoryFillerDescription": { - "description": "Sponsorblock 'Filler' Category description" - }, - "sponsorBlockCategoryMusicOffTopic": "Music: Non-Music Section", - "@sponsorBlockCategoryMusicOffTopic": { - "description": "Sponsorblock 'MusicOffTopic' Category" - }, - "sponsorBlockCategoryMusicOffTopicDescription": "Only for use in music videos. This only should be used for sections of music videos that aren''t already covered by another category.", - "@sponsorBlockCategoryMusicOffTopicDescription": { - "description": "Only for use in music videos. This only should be used for sections of music videos that aren't already covered by another category." - }, - "useProxy": "Proxy videos", - "@useProxy": { - "description": "label for settings switch to proxy videos from server" - }, - "useProxyDescription": "By proxying video streams from the server, you can bypass regional blocks or ISP blocking YouTube", - "@useProxyDescription": { - "description": "Description for the use proxy settings" - }, - "pressDownToShowSettings": "Press down to show settings", - "@pressDownToShowSettings": { - "description": "Instruction on how to show video settings when playing a video on TV" - }, - "quality": "Quality", - "@quality": { - "description": "Name of TV ui video settings" - }, - "audio": "Audio", - "@audio": { - "description": "Name TV ui audio settings" - }, - "subtitles": "Subtitles", - "@subtitles": { - "description": "Name of TV ui subtitles settings" - }, - "playbackSpeed": "Playback speed", - "@playbackSpeed": { - "description": "Name of TV ui Playback speed" - }, - "blackBackground": "Black background", - "@blackBackground": { - "description": "Settings name for black background" - }, - "blackBackgroundDescription": "For dark theme on OLED screen", - "@blackBackgroundDescription": { - "description": "Description for dark background setting" - }, - "search": "Search", - "@search": { - "description": "search title" - }, - "subtitleFontSize": "Subtitles font size", - "@subtitleFontSize": { - "description": "Settings label for the size of the subtitles" - }, - "subtitleFontSizeDescription": "Change the size of the subtitles if it is too small or too big on your device", - "@subtitleFontSizeDescription": { - "description": "Settings description for subtitle size" - }, - "skipSslVerification": "Skip SSL certificate verification", - "@skipSslVerification": { - "description": "Setting label to skip ssl certification verification" - }, - "skipSslVerificationDescription": "For using a self-signed SSL certificate, or when having SSL related issues with your server.", - "@skipSslVerificationDescription": { - "description": "Setting description for the skip ssl certification verification" - }, - "themeBrightness": "Theme", - "@themeBrightness": { - "description": "Ask the user to user dark / light / system theme" - }, - "themeLight": "Light", - "@themeLight": { - "description": "Light theme" - }, - "themeDark": "Dark", - "@themeDark": { - "description": "Dark theme" - }, - "followSystem": "Follow system", - "@followSystem": { - "description": "Follow system label" - }, - "requiresRestart": "Requires app restart", - "@requiresRestart": { - "description": "Requires app restart label" - }, - "appLanguage": "App language", - "@appLanguage": { - "description": "Select app language" - }, - "nVideos": "{count, plural, =0{No videos} =1{1 video} other{{count} videos}}", - "@nVideos": { - "description": "One or more videos", - "placeholders": { - "count": { - "type": "num", - "format": "compact" - } - } - }, - "returnYoutubeUrlValidation": "Url must start with http:// or https://", - "@returnYoutubeUrlValidation": { - "description": "error message for invalid custom url for return to youtube" - }, - "returnYoutubeDislikeDescription": "Show estimated video dislikes using API provided by returnyoutubedislike.com", - "@returnYoutubeDislikeDescription": { - "description": "ReturnYoutubeDislike setting description" - }, - "rydCustomInstance": "Custom RYD instance url", - "@rydCustomInstance": { - "description": "title for setting to set a custom ryd instance" - }, - "rydCustomInstanceDescription": "Use a different RYD instance, leave empty to use the default", - "@rydCustomInstanceDescription": { - "description": "description for custom ryd instancr setting" - }, - "autoplayVideoOnLoad": "Automatically play video on load", - "@autoplayVideoOnLoad": { - "description": "Label for settings to enable autoplay when a video loads" - }, - "autoplayVideoOnLoadDescription": "Automatically start playing the video after it has loaded", - "@autoplayVideoOnLoadDescription": { - "description": "Description for the autoplay video on load setting" - }, - "searchHistory": "Search history", - "@searchHistory": { - "description": "Settings label for search history" - }, - "searchHistoryDescription": "Search history settings", - "@searchHistoryDescription": { - "description": "Description for search history settings" - }, - "enableSearchHistory": "Enable search history", - "@enableSearchHistory": { - "description": "Settings label for enabling search history" - }, - "searchHistoryLimit": "Search history limit", - "@searchHistoryLimit": { - "description": "Settings label for search history limit" - }, - "searchHistoryLimitDescription": "Set how many previous searches will show up in suggestions", - "@searchHistoryLimitDescription": { - "description": "Settings label for search history limit description" - }, - "shorts": "Shorts", - "@shorts": { - "description": "Youtube shorts" - }, - "searchUploadDate": "Upload date", - "@searchUploadDate": { - "description": "Filter search result by upload date" - }, - "searchUploadDateAny": "Any date", - "@searchUploadDateAny": { - "description": "Do not filter search result by upload date" - }, - "searchUploadDateHour": "Last Hour", - "@searchUploadDateHour": { - "description": "Search for uploaded in last hour" - }, - "searchUploadDateToday": "Today", - "@searchUploadDateToday": { - "description": "Search for uploaded today" - }, - "searchUploadDateWeek": "This week", - "@searchUploadDateWeek": { - "description": "Search for uploaded this week" - }, - "searchUploadDateMonth": "This month", - "@searchUploadDateMonth": { - "description": "Search for uploaded this month" - }, - "searchUploadDateYear": "This year", - "@searchUploadDateYear": { - "description": "Search for uploaded this year" - }, - "searchDuration": "Duration", - "@searchDuration": { - "description": "Filter search result by duration" - }, - "searchDurationAny": "Any duration", - "@searchDurationAny": { - "description": "Do not filter search result by duration" - }, - "searchDurationShort": "Short (<4 minutes)", - "@searchDurationShort": { - "description": "Search for short videos only" - }, - "searchDurationLong": "Long (>20 minutes)", - "@searchDurationLong": { - "description": "Search for long videos only" - }, - "searchDurationMedium": "Medium (4-20 minutes)", - "@searchDurationMedium": { - "description": "Search for medium videos only" - }, - "searchSortBy": "Sort by", - "@searchSortBy": { - "description": "Search sorting option" - }, - "searchSortRelevance": "Relevance", - "@searchSortRelevance": { - "description": "Sort search by relevance" - }, - "searchSortRating": "Rating", - "@searchSortRating": { - "description": "Sort search by rating" - }, - "searchSortUploadDate": "Upload Date", - "@searchSortUploadDate": { - "description": "Sort search by upload date" - }, - "searchSortViewCount": "View Count", - "@searchSortViewCount": { - "description": "Sort search by view count" - }, - "clearSearchHistory": "Clear search history", - "@clearSearchHistory": { - "description": "Settings label for clearing search history" - }, - "appLogs": "Application Logs", - "@appLogs": { - "description": "Title for settings that leads to application logs" - }, - "appLogsDescription": "Get logs of what is happening in the application, can be useful to report issues", - "@appLogsDescription": { - "description": "Description of the app log settings" - }, - "copyToClipBoard": "Copy to clipboard", - "@copyToClipBoard": { - "description": "Text to copy something to clipboard" - }, - "logsCopied": "Logs copied to clipboard", - "@logsCopied": { - "description": "Message to tell user that logs have been copied to the clipboard" - }, - "rememberSubtitleLanguage": "Remember subtitles language", - "@rememberSubtitleLanguage": { - "description": "Settings label for remembering subtitle language" - }, - "videoFilters": "Video filters", - "@videoFilters": { - "description": "Title for video filter settings" - }, - "nFilters": "{count, plural, =0{No videos} =1{1 filter} other{{count} filters}}", - "@nFilters": { - "description": "One or more video filters", - "placeholders": { - "count": { - "type": "num", - "format": "compact" - } - } - }, - "videoFiltersExplanation": "Hide or Obfuscate videos from all the video feeds in the application based on the filters defined below. This allow you for example to hide sports spoilers or hide shorts from a certain channel.", - "@videoFiltersExplanation": { - "description": "Description on how filter work" - }, - "videoFiltersSettingTileDescriptions": "Define rules to filter out videos", - "@videoFiltersSettingTileDescriptions": { - "description": "Description for the main settings page" - }, - "videoFilterAllChannels": "All channels", - "@videoFilterAllChannels": { - "description": "Title for the sections that applies to all channels" - }, - "addVideoFilter": "Create filter", - "@addVideoFilter": { - "description": "Title when creating a new filter" - }, - "editVideoFilter": "Edit filter", - "@editVideoFilter": { - "description": "Title when editting a filter" - }, - "videoFilterType": "Type", - "@videoFilterType": { - "description": "Label for filter type" - }, - "videoFilterOperation": "Operation", - "@videoFilterOperation": { - "description": "Label for filter operation" - }, - "videoFilterValue": "Value", - "@videoFilterValue": { - "description": "Label for filter value" - }, - "save": "Save", - "@save": { - "description": "Text for save action" - }, - "videoFilterEditDescription": "Select an optional channel, a filter type, operation and a value to filter OUT videos from lists. Example, type: video name, operation: contains, value: test will EXCLUDE all the videos with the word 'test' in their name.", - "@videoFilterEditDescription": { - "description": "Descriptive test for video filter set up" - }, - "optional": "optional", - "@optional": { - "description": "Optional label" - }, - "videoFilterHideLabel": "Hide", - "@videoFilterHideLabel": { - "description": "Label to hide videos" - }, - "videoFilterFilterLabel": "Obfuscate", - "@videoFilterFilterLabel": { - "description": "Label to filter videos" - }, - "videoFilterDescriptionString": "{hideOrFilter} videos where {type} {operation} ''{value}''.", - "@videoFilterDescriptionString": { - "description": "Human readable description of a video filter, in this case is it for string comparison, example: Hide videos where the name of the video does not contain the following string 'test' (Do not translate text between { })", - "placeholders": { - "hideOrFilter": { - "type": "String", - "example": "Hide" - }, - "type": { - "type": "String", - "example": "video title" - }, - "operation": { - "type": "String", - "example": "does not contain" - }, - "value": { - "type": "String", - "example": "some filter text" - } - } - }, - "videoFiltered": "Video filtered for the following reason(s):", - "@videoFiltered": { - "description": "Label shown on video list when it is filtered out" - }, - "videoFilterTapToReveal": "Tap to reveal", - "@videoFilterTapToReveal": { - "description": "Label to tell user to tap to show a filtered video" - }, - "videoFilterHide": "Hide filtered videos", - "@videoFilterHide": { - "description": "Label for settings to hide filtered videos" - }, - "videoFilterHideDescription": "By default filtered videos are not hidden but shown as obfuscated with the reason(s) why it has been filtered. This setting remove the filtered videos from lists.", - "@videoFilterHideDescription": { - "description": "" - }, - "videoFilterNoFilters": "No video filters, tap the '+' button below to start adding filters.", - "@videoFilterNoFilters": { - "description": "Label when there are no video filters" - }, - "videoFilterTypeVideoTitle": "Video title", - "@videoFilterTypeVideoTitle": { - "description": "Label for video filter video title" - }, - "videoFilterTypeChannelName": "Channel name", - "@videoFilterTypeChannelName": { - "description": "Label for video filter channel name" - }, - "videoFilterTypeVideoLength": "Video length (seconds)", - "@videoFilterTypeVideoLength": { - "description": "Label for video filter video length" - }, - "videoFilterOperationContains": "Contains", - "@videoFilterOperationContains": { - "description": "Label for video filter operation Contains" - }, - "videoFilterOperationNotContain": "Does not contain", - "@videoFilterOperationNotContain": { - "description": "Label for video filter operation Does not contain" - }, - "videoFilterOperationLowerThan": "Lower than", - "@videoFilterOperationLowerThan": { - "description": "Label for video filter operation Lower than" - }, - "videoFilterOperationHigherThan": "Higher than", - "@videoFilterOperationHigherThan": { - "description": "Label for video filter operation Higher than" - }, - "channel": "Channel", - "@channel": { - "description": "A single channel" - }, - "videoFilterHideAllFromChannel": "Filter all videos from channel", - "@videoFilterHideAllFromChannel": { - "description": "Label for video filter switch to allow to hide all videos from a channel" - }, - "videoFilterWholeChannel": "{hideOrFilter} all videos from channel", - "@videoFilterWholeChannel": { - "description": "Label for whole channel filtering", - "placeholders": { - "hideOrFilter": { - "type": "String", - "example": "Hide" - } - } - }, - "rememberSubtitleLanguageDescription": "Automatically set subtitles to last language selected, if available", - "@rememberSubtitleLanguageDescription": { - "description": "Settings description for remembering subtitle language" - }, - "lockFullScreenToLandscape": "Lock full screen orientation to video aspect ratio", - "@lockFullScreenToLandscape": { - "description": "Title to force full screen to landscape" - }, - "lockFullScreenToLandscapeDescription": "Locks the full screen orientation based on video format, landscape for wide video and portrait for portrait videos", - "@lockFullScreenToLandscapeDescription": { - "description": "Setting description for forcing video to landscape when in full screen" - }, - "fillFullscreen": "Maximize video to fit screen", - "@fillFullscreen": { - "description": "Title to maximize video to fit screen" - }, - "fillFullscreenDescription": "Adjusts the video to fill the entire screen in landscape mode", - "@fillFullscreenDescription": { - "description": "Setting description for filling video to screen in landscape" - }, - "rememberPlaybackSpeed": "Remember playback speed", - "@rememberPlaybackSpeed": { - "description": "Setting label for remembering playback speed" - }, - "rememberPlaybackSpeedDescription": "Automatically set playback speed to the last speed selected", - "@rememberPlaybackSpeedDescription": { - "description": "Settings description for remembering playback speed" - }, - "downloads": "Downloads", - "@downloads": { - "description": "Downloads" - }, - "download": "Download", - "@download": { - "description": "A single download or CTA for downloading a video" - }, - "videoAlreadyDownloaded": "Video already downloaded", - "@videoAlreadyDownloaded": { - "description": "Message when a user tries to download a video he already has" - }, - "noDownloadedVideos": "No downloaded videos, browse, long press on a video in a list or tap the download button on a video screen to download", - "@noDownloadedVideos": { - "description": "Message showing when the user goes to the download screen but there are no offline videos." - }, - "downloadsPlayAll": "Play all", - "@downloadsPlayAll": { - "description": "Button to play all downloaded videos" - }, - "videoDownloadStarted": "Video download started", - "@videoDownloadStarted": { - "description": "Message when a video starts being downloaded" - }, - "videoFailedDownloadRetry": "Download failed, tap to retry", - "@videoFailedDownloadRetry": { - "description": "Shown on download manager when a download fails and prompt the user to retry" - }, - "videoDownloadAudioOnly": "Audio only", - "@videoDownloadAudioOnly": { - "description": "Label for toggle to download audio only " - }, - "manageSubscriptions": "Manage Subscriptions", - "@manageSubscriptions": { - "description": "Title of manage subscriptions page" - }, - "noSubscriptions": "No subscriptions, browse videos and subscribe to any channel you like.", - "@noSubscriptions": { - "description": "Message when the user has no subs" - }, - "youCanSubscribeAgainLater": "You can subscribe to this channel again later", - "@youCanSubscribeAgainLater": { - "description": "Text for the unscubscribe confirmation dialog" - }, - "unSubscribeQuestion": "Unsubscribe ?", - "@unSubscribeQuestion": { - "description": "Title for dialog if a user wants to unsubscribe in the subscribtion management screen" - }, - "clearHistoryQuestion": "Clear history ?", - "@clearHistoryQuestion": {}, - "clearHistoryQuestionExplanation": "This will clear your viewing history of your account on the Invidious instance you use. This cannot be undone.", - "@clearHistoryQuestionExplanation": { - "description": "Message for dialog before clearing full viewing history" - }, - "noHistory": "No viewing history, watch some videos and it will appear here", - "@noHistory": { - "description": "Message when the user visits the history tab but it's empty" - }, - "homeLayoutEditor": "Edit home layout", - "@homeLayoutEditor": { - "description": "Title of layout editor screen" - }, - "layoutEditorAddVideoSource": "Add video source", - "@layoutEditorAddVideoSource": { - "description": "Label for button to allow user to add more video sources to the home screen" - }, - "layoutEditorExplanation": "You can decide what to display on your home screen, you can have up to 2 small view with horizontal scrolling and one big source.", - "@layoutEditorExplanation": { - "description": "text to explain the home layout editor" - }, - "home": "Home", - "@home": { - "description": "Label for Home browsing tab" - }, - "library": "Library", - "@library": { - "description": "Name for user library" - }, - "customizeAppLayout": "Customize app sections", - "@customizeAppLayout": { - "description": "Settings label for the settings to allow the user to set up the app sections themselves" - }, - "customizeAppLayoutExplanation": "Select which sections you want to appear in the main app navigation bar. Click on the home icon to select which screen shows when the application starts. You can reorder the sections by dragging them around.", - "@customizeAppLayoutExplanation": { - "description": "" - }, - "navigationBarStyle": "Navigation bar style", - "@navigationBarStyle": { - "description": "Label for settings on customizing navigation bar style" - }, - "navigationBarLabelAlwaysShowing": "Label always showing", - "@navigationBarLabelAlwaysShowing": { - "description": "Label always showing option for navigation bar" - }, - "navigationBarLabelShowOnSelect": "Label shown on selected item", - "@navigationBarLabelShowOnSelect": { - "description": "Label only showing when selected option for navigation bar" - }, - "navigationBarLabelNeverShow": "Never show label", - "@navigationBarLabelNeverShow": { - "description": "Never show label option for navigation bar" - }, - "distractionFreeMode": "Distraction free mode", - "@distractionFreeMode": { - "description": "title for distraction free mode settings" - }, - "distractionFreeModeDescription": "Disable video comments and recommendations", - "@distractionFreeModeDescription": { - "description": "Description for distraction free mode" - }, - "secondsShortForm": "secs", - "@secondsShortForm": { - "description": "Short form for the word seconds" - }, - "videoFilterApplyDateToFilter": "Filter videos on given times", - "@videoFilterApplyDateToFilter": { - "description": "Label for switch to allow user to customize video filter and set days of week and time to them" - }, - "videoFilterDayOfWeek": "Select days to apply filters", - "@videoFilterDayOfWeek": { - "description": "Title for day selection for the filter" - }, - "videoFilterDayOfWeekDescription": "You can selectively choose days of the week and time to which the filters apply to, for example, avoid sport events spoilers.", - "@videoFilterDayOfWeekDescription": { - "description": "" - }, - "videoFilterStartTime": "Start time", - "@videoFilterStartTime": { - "description": "Title for filter start time" - }, - "videoFilterEndTime": "End time", - "@videoFilterEndTime": { - "description": "Title for filter end time" - }, - "videoFilterAppliedOn": "Applied on {selectedDays}", - "@videoFilterAppliedOn": { - "description": "Readable text on when the filter should apply", - "placeholders": { - "selectedDays": { - "type": "String", - "example": "Monday, Wednesday, Friday" - } - } - }, - "from": "From", - "@from": { - "description": "From word (as in 'From xx To xx')" - }, - "to": "To", - "@to": { - "description": "To word as in 'From xx To xx')" - }, - "videoFilterTimeOfDayFromTo": "From {from} to {to}", - "@videoFilterTimeOfDayFromTo": { - "description": "Time of day range", - "placeholders": { - "from": { - "type": "String", - "example": "3:00 AM" - }, - "to": { - "type": "String", - "example": "5:00 PM" - } - } - }, - "notifications": "Notifications", - "@notifications": { - "description": "Notification settings title" - }, - "notificationsDescription": "Enable and review what you are notified about", - "@notificationsDescription": { - "description": "Setting description for notifications" - }, - "enableNotificationDescriptions": "Runs foreground service to check and notify you on the changes you are monitoring", - "@enableNotificationDescriptions": { - "description": "" - }, - "subscriptionNotification": "Subscription notifications", - "@subscriptionNotification": { - "description": "Title for subscriptions notifications" - }, - "subscriptionNotificationDescription": "Get notified of new videos from your subscription feed if you are logged in to your current instance", - "@subscriptionNotificationDescription": { - "description": "Description for subscription notifications" - }, - "subscriptionNotificationTitle": "New videos from your subscriptions", - "@subscriptionNotificationTitle": { - "description": "Title for the notification showing that there are new videos from the subscription feed" - }, - "subscriptionNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in your subscription feed", - "@subscriptionNotificationContent": { - "description": "Content for subscription notification", - "placeholders": { - "count": { - "type": "num", - "format": "compact" - } - } - }, - "askForDisableBatteryOptimizationTitle": "Disabling battery optimization required", - "@askForDisableBatteryOptimizationTitle": { - "description": "Title for the dialog asking the user to turn off disabling battery optimization when turning on notifications" - }, - "askForDisableBatteryOptimizationContent": "In order to send notification Clipious needs to run a background service. For it to run smoothly it is required that Clipious is given unrestricted battery usage, tapping ok will open the battery optimization settings.", - "@askForDisableBatteryOptimizationContent": { - "description": "Content for the dialog asking the user to turn off disabling battery optimization when turning on notifications" - }, - "askToEnableBackgroundServiceTitle": "Notifications turned off", - "@askToEnableBackgroundServiceTitle": { - "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" - }, - "askToEnableBackgroundServiceContent": "To get notifications, Clipious notifications need to be enabled, press OK to enable it.", - "@askToEnableBackgroundServiceContent": { - "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" - }, - "otherNotifications": "Other notifications sources (bell icons)", - "@otherNotifications": { - "description": "Title for settings section in the notification settings" - }, - "deleteChannelNotificationTitle": "Delete channel notification ?", - "@deleteChannelNotificationTitle": { - "description": "Title for dialog to confirm whether to delete channel notifications" - }, - "deleteChannelNotificationContent": "You won''t receive anymore notifications from this channel.", - "@deleteChannelNotificationContent": { - "description": "Title for dialog to confirm whether to delete channel notifications" - }, - "deletePlaylistNotificationTitle": "Delete playlist notification ?", - "@deletePlaylistNotificationTitle": { - "description": "Title for dialog to confirm whether to delete playlist notifications" - }, - "deletePlaylistNotificationContent": "You won''t receive anymore notifications from this playlist.", - "@deletePlaylistNotificationContent": { - "description": "Title for dialog to confirm whether to delete playlist notifications" - }, - "channelNotificationTitle": "New videos from {channel}", - "@channelNotificationTitle": { - "description": "Title for the channel notifications when there are new videos", - "placeholders": { - "channel": { - "type": "String", - "example": "MKBHD" - } - } - }, - "channelNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} from {channel}", - "@channelNotificationContent": { - "description": "Content for channel notification when there are new videos", - "placeholders": { - "channel": { - "type": "String", - "example": "MKBHD" - }, - "count": { - "type": "num", - "format": "compact" - } - } - }, - "playlistNotificationTitle": "New videos in {playlist} playlist", - "@playlistNotificationTitle": { - "description": "Title for the playlist notifications when there are new videos", - "placeholders": { - "playlist": { - "type": "String", - "example": "Lo-Fi girl" - } - } - }, - "playlistNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in the {playlist} playlist", - "@playlistNotificationContent": { - "description": "Content for playlist notification when there are new videos", - "placeholders": { - "playlist": { - "type": "String", - "example": "Lo-Fi girl" - }, - "count": { - "type": "num", - "format": "compact" - } - } - }, - "foregroundServiceNotificationTitle": "Video monitoring", - "@foregroundServiceNotificationTitle": { - "description": "Title for the foreground service running notification when the user wants to receive notifications" - }, - "foregroundServiceNotificationContent": "Will check for new videos once {hours, select, 1{per hour} 24{a day} other{every {hours} hours}}", - "@foregroundServiceNotificationContent": { - "description": "Content for the foreground service running notification when the user wants to receive notifications", - "hours": { - "type": "num", - "format": "compact" - } - }, - "foregroundServiceUpdatingSubscriptions": "Checking subscriptions...", - "@foregroundServiceUpdatingSubscriptions": { - "description": "Foreground service notification text when checking for new subscription videos" - }, - "foregroundServiceUpdatingPlaylist": "Checking playlists...", - "@foregroundServiceUpdatingPlaylist": { - "description": "Foreground service notification text when checking for new playlist videos" - }, - "foregroundServiceUpdatingChannels": "Checking channels...", - "@foregroundServiceUpdatingChannels": { - "description": "Foreground service notification text when checking for new channel videos" - }, - "notificationFrequencySettingsTitle": "New video check frequency", - "@notificationFrequencySettingsTitle": { - "description": "Title for frequency settings" - }, - "notificationFrequencySettingsDescription": "How often the application will check for new videos", - "@notificationFrequencySettingsDescription": { - "description": "Description for frequency settings" - }, - "notificationFrequencySliderLabel": "{hours, select, 24{1d} other{{hours}h}}", - "@notificationFrequencySliderLabel": { - "description": "Short form for a number of hours going up to 1 day", - "hours": { - "type": "num", - "format": "compact" - } - }, - "subtitlesBackground": "Subtitles background", - "@subtitlesBackground": { - "description": "Title for settings to set black background for subtitles" - }, - "subtitlesBackgroundDescription": "Adds a black background to subtitles to make them more readable", - "@subtitlesBackgroundDescription": { - "description": "Description for settings to set black background for subtitles" - }, - "history": "History", - "@history": { - "description": "User view history label" - }, - "deArrowSettingDescription": "Replace click bait titles and thumbnails", - "@deArrowSettingDescription": { - "description": "Description for dearrow" - }, - "deArrowReplaceThumbnails": "Replace thumbnails", - "@deArrowReplaceThumbnails": { - "description": "Settings title for checkbox on whether the thumbnail should be replaced as well" - }, - "deArrowReplaceThumbnailsDescription": "Replace video thumbnails in addition of the titles", - "@deArrowReplaceThumbnailsDescription": { - "description": "Description for DeArrow setting switch" - }, - "deArrowWarning": "Enabling DeArrow can significantly reduce the browsing speed of the app as extra http requests are needed for every single video", - "@deArrowWarning": { - "description": "Warning message when the user enables DeArrow" - }, - "copySettingsAsJson": "Copy settings as JSON to clipboard", - "@copySettingsAsJson": { - "description": "title for settings sections to allow users to copy their settings as json to make debugging easier" - }, - "copySettingsAsJsonDescription": "Copy the settings as JSON to help debugging if you encounter an issue with the app and decide to raise an issue", - "@copySettingsAsJsonDescription": { - "description": "" - }, - "seeking": "Seeking", - "@seeking": { - "description": "category for settings related to seeking in a video" - }, - "skipStep": "Skip forward/backward step", - "@skipStep": { - "description": "Title for the settings to set the skipping step" - }, - "skipStepDescription": "Seconds to skip on forward/backward actions", - "@skipStepDescription": { - "description": "Title for the settings to set the skipping step" - }, - "exponentialSkip": "Exponential skip forward/backward", - "@exponentialSkip": { - "description": "Title for the setting to enable the exponential skipping" - }, - "exponentialSkipDescription": "The more you skip forward, the bigger the step is.", - "@exponentialSkipDescription": { - "description": "Title for the setting to enable the exponential skipping" - }, - "fullscreenOnLandscape": "Full screen on landscape", - "@fullscreenOnLandscape": { - "description": "Setting title to enable full screen on landscape orientation" - }, - "fullscreenOnLandscapeDescription": "Switch to full screen when the device is rotated to landscape mode", - "@fullscreenOnLandscapeDescription": { - "description": "Setting to enable full screen on landscape orientation" - }, - "enabled": "Enabled", - "@enabled": { - "description": "Text to show something is enabled" - }, - "submitFeedback": "Submit feedback", - "@submitFeedback": { - "description": "Title for settings to submit feed back through the app" - }, - "submitFeedbackDescription": "Found a bug or have a suggestion? Use this tool to take screenshot of the app, annotate and submit feedback", - "@submitFeedbackDescription": { - "description": "Setting tile descriptions for feedback submission" - }, - "feedbackDisclaimer": "To submit feedback you will need a GitHub account and your screenshot will be submitted to Imgur anonymously.", - "@feedbackDisclaimer": { - "description": "Content of dialog shown before submitting feedback to make sure the user is ok whith where the data is going" - }, - "feedbackScreenshotError": "Error while uploading screenshot to Imgur", - "@feedbackScreenshotError": { - "description": "Title for dialog if something goes wrong while uploading feedback screenshot" - }, - "channelSortByNewest": "Newest", - "@channelSortByNewest": { - "description": "Sort channel videos from newest to oldest" - }, - "channelSortByOldest": "Oldest", - "@channelSortByOldest": { - "description": "Sort channel videos from oldest to newest" - }, - "channelSortByPopular": "Popular", - "@channelSortByPopular": { - "description": "Sort channel videos by popularity" - } + "subscriptions": "Subscriptions", + "@subscriptions": { + "description": "User subscriptions" + }, + "playlists": "Playlists", + "@playlists": { + "description": "User playlists" + }, + "popular": "Popular", + "@popular": { + "description": "Popular videos title" + }, + "trending": "Trending", + "@trending": { + "description": "Trending videos title" + }, + "noVideoInPlayList": "No video in playlist", + "@noVideoInPlayList": { + "description": "When no videos in the playlist" + }, + "removeFromPlayList": "Remove from playlist", + "@removeFromPlayList": { + "description": "Menu item description to show remove a video from a playlist" + }, + "deletePlayListQ": "Delete Playlist?", + "@deletePlayListQ": { + "description": "Ask user to delete a playlist" + }, + "irreversibleAction": "This action is irreversible", + "@irreversibleAction": { + "description": "Tell the user that the action cannot be undone" + }, + "addPlayList": "Add Playlist", + "@addPlayList": { + "description": "Title for add playlist dialog" + }, + "playListName": "Playlist name", + "@playListName": { + "description": "Place holder for new playlist name text field" + }, + "playlistVisibility": "Visibility", + "@playlistVisibility": { + "description": "Dropdown label for playlist visibility" + }, + "publicPlaylist": "Public", + "@publicPlaylist": { + "description": "Public playlist" + }, + "privatePlaylist": "Private", + "@privatePlaylist": { + "description": "Private playlist" + }, + "cancel": "Cancel", + "@cancel": { + "description": "Cancel button label" + }, + "add": "Add", + "@add": { + "description": "Add button abel" + }, + "unlistedPlaylist": "Unlisted", + "@unlistedPlaylist": { + "description": "Unlisted playlist" + }, + "info": "Info", + "@info": { + "description": "Info label" + }, + "videos": "Videos", + "@videos": { + "description": "Videos label" + }, + "streams": "Streams", + "@streams": { + "description": "Streams label" + }, + "latestVideos": "Latest Videos", + "@latestVideos": { + "description": "Latest channel videos" + }, + "subscribed": "Subscribed", + "@subscribed": { + "description": "When the user is subscribed to a channel" + }, + "subscribe": "Subscribe", + "@subscribe": { + "description": "Label for user to subscribe to a channel" + }, + "nSubscribers": "{count, select, no{No subscribers} other{{count} subscribers}}", + "@nSubscribers": { + "description": "number of subscribers", + "placeholders": { + "count": { + "type": "String" + } + } + }, + "share": "Share", + "@share": { + "description": "asking user if to share" + }, + "shareYoutubeLink": "Share YouTube link", + "@shareYoutubeLink": { + "description": "asking user to share youtube link" + }, + "shareInvidiousLink": "Share Invidious link", + "@shareInvidiousLink": { + "description": "asking user to share invidious link" + }, + "redirectInvidiousLink": "Share Invidious Redirect link", + "@redirectInvidiousLink": { + "description": "asking user to share redirecting invidious link" + }, + "shareLinkWithTimestamp": "Add timestamp", + "@shareLinkWithTimestamp": { + "description": "asking user to share link along with timestamp" + }, + "ok": "OK", + "@ok": { + "description": "Ok" + }, + "noChannels": "No channels", + "@noChannels": { + "description": "when there are no channels to display" + }, + "noPlaylists": "No playlists", + "@noPlaylists": { + "description": "when there are no playlists to display" + }, + "channels": "Channels", + "@channels": { + "description": "Channels label" + }, + "couldntLoadVideo": "Could not load the video", + "@couldntLoadVideo": { + "description": "Message to display when a video can't be loaded" + }, + "comments": "Comments", + "@comments": { + "description": "Comments label" + }, + "recommended": "Recommended", + "@recommended": { + "description": "Recommended label" + }, + "couldntFetchVideos": "Could not fetch videos. Tap to try again.", + "@couldntFetchVideos": { + "description": "Can't load bunch of videos, asking user to try again" + }, + "wizardIntro": "Select a public server or add your own. (Can be changed later in the settings)", + "@wizardIntro": { + "description": "Welcome message on frst time use" + }, + "startUsingClipious": "Start using Clipious", + "@startUsingClipious": { + "description": "button label to start using the app" + }, + "videoAddedToPlaylist": "Video added to playlist", + "@videoAddedToPlaylist": { + "description": "Pop up message when a video was added to a playlist" + }, + "videoAddedToQueue": "Video added to queue", + "@videoAddedToQueue": { + "description": "Pop up message when a video was added at the end of the video queue" + }, + "errorAddingVideoToPlaylist": "Error while adding video to playlist", + "@errorAddingVideoToPlaylist": { + "description": "Error while adding video to playlist" + }, + "itemlistErrorGeneric": "Could not fetch data", + "@itemlistErrorGeneric": { + "description": "Error showing when the data can't be fetch" + }, + "itemListErrorInvalidScope": "You don''t have the permission to see this, if you logged in using the token method try to log out and in again", + "@itemListErrorInvalidScope": { + "description": "Error when the user doesn't have the proper scope to its current token" + }, + "selectPlaylist": "Select playlist", + "@selectPlaylist": { + "description": "Title when users wants to add a video to a playlist" + }, + "createNewPlaylist": "Create new playlist", + "@createNewPlaylist": { + "description": "Button label to create a new playlist when the user wants to add a video to a playlist" + }, + "nReplies": "{count, plural, =0{No replies} =1{1 reply} other{{count} replies}}", + "@nReplies": { + "description": "number of replies to a comment", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, + "loadMore": "Load more", + "@loadMore": { + "description": "CTA to load more" + }, + "topSorting": "Top", + "@topSorting": { + "description": "Content sorting: top" + }, + "newSorting": "New", + "@newSorting": { + "description": "Content sorting: new" + }, + "streamIsLive": "Live", + "@streamIsLive": { + "description": "Label when a video is a live stream" + }, + "sponsorSkipped": "Sponsor skipped", + "@sponsorSkipped": { + "description": "When a sponsor segment is skipped thanks to sponsor block" + }, + "selectBrowsingCountry": "Select browsing country", + "@selectBrowsingCountry": { + "description": "Select country for trending content" + }, + "showOnStart": "Select what to show when the app starts", + "@showOnStart": { + "description": "Title of dialog asking which screen the users prefers to see" + }, + "settings": "Settings", + "@settings": { + "description": "Settings title" + }, + "browsing": "Browsing", + "@browsing": { + "description": "video browsing preferences" + }, + "country": "Country", + "@country": { + "description": "Country label" + }, + "whenAppStartsShow": "When the app starts, show…", + "@whenAppStartsShow": { + "description": "Setting title for selecting the screen to show on start" + }, + "servers": "Servers", + "@servers": { + "description": "Server management settings category" + }, + "manageServers": "Manage servers", + "@manageServers": { + "description": "Settings to manage servers" + }, + "currentServer": "Currently using {current}", + "@currentServer": { + "description": "Which server the user is currently using", + "placeholders": { + "current": { + "type": "String" + } + } + }, + "useSponsorBlock": "Use SponsorBlock", + "@useSponsorBlock": { + "description": "label for sponsorblock checkbox" + }, + "sponsorBlockDescription": "Skip sponsor segments submitted by the community", + "@sponsorBlockDescription": { + "description": "Sponsorblock setting description" + }, + "about": "About", + "@about": { + "description": "About" + }, + "name": "Name", + "@name": { + "description": "NAme label" + }, + "package": "Package", + "@package": { + "description": "package label" + }, + "version": "Version", + "@version": { + "description": "version label" + }, + "build": "Build", + "@build": { + "description": "build label" + }, + "addServer": "Add server", + "@addServer": { + "description": "Add server label" + }, + "useThisServer": "Use this server", + "@useThisServer": { + "description": "Use this server label" + }, + "logIn": "Log in", + "@logIn": { + "description": "CTA to log in to server" + }, + "delete": "Delete", + "@delete": { + "description": "Delete label" + }, + "invalidInvidiousServer": "Invalid Invidious server", + "@invalidInvidiousServer": { + "description": "Error when the user tries to add a server that is not a proper or reachable invidious server" + }, + "yourServers": "Your servers", + "@yourServers": { + "description": "Your servers label" + }, + "loggedIn": "Logged in", + "@loggedIn": { + "description": "Label to tell the user that he is logged in to the server" + }, + "notLoggedIn": "Not logged in", + "@notLoggedIn": { + "description": "Label when the user is not logged in to the server" + }, + "addServerHelpText": "Use the + button to add your own servers or tap on a public server and add it.", + "@addServerHelpText": { + "description": "label for when the user hasn't chosen a server yet" + }, + "publicServers": "Public servers", + "@publicServers": { + "description": "Public servers label" + }, + "loadingPublicServer": "Loading public servers", + "@loadingPublicServer": { + "description": "Message telling users the app is loading the list of public servers" + }, + "tapToAddServer": "Tap to add server to your list", + "@tapToAddServer": { + "description": "public server description" + }, + "publicServersError": "Could not fetch list of public servers. Tap to retry.", + "@publicServersError": { + "description": "Error message when trying to get public servers but it failed" + }, + "appearance": "Appearance", + "@appearance": { + "description": "Settings category title" + }, + "useDynamicTheme": "Dynamic colors", + "@useDynamicTheme": { + "description": "" + }, + "useDynamicThemeDescription": "Use Material You colors (only available on Android 12+)", + "@useDynamicThemeDescription": { + "description": "" + }, + "useDash": "Use DASH", + "@useDash": { + "description": "Label on video options if a user wants to switch to dash urls instead of the regular quality selection" + }, + "useDashDescription": "DASH adaptive streaming can sometimes be problematic, Youtube can throttle it.", + "@useDashDescription": { + "description": "Description for dash in the settings screen" + }, + "videoPlayer": "Video player", + "@videoPlayer": { + "description": "Title for video player related options" + }, + "videoListed": "Public", + "@videoListed": { + "description": "Status of a publicly available video" + }, + "videoUnlisted": "Unlisted", + "@videoUnlisted": { + "description": "Status of a video that is only accessible by link" + }, + "videoIsFamilyFriendly": "Family friendly", + "@videoIsFamilyFriendly": { + "description": "Displayed only when a video is family friendly" + }, + "tapToManage": "Tap to manage", + "@tapToManage": { + "description": "Text shown below a server in the 'Your servers' list" + }, + "authentication": "Authentication", + "@authentication": { + "description": "Label for server settings related to authentications" + }, + "tokenLogin": "Log in with token", + "@tokenLogin": { + "description": "Textto login to a server using the recommended way" + }, + "tokenLoginDescription": "Recommended way to log in", + "@tokenLoginDescription": { + "description": "Recommended way to log in" + }, + "cookieLogin": "Log in with cookie", + "@cookieLogin": { + "description": "Text to login to a server using the cookie jar method" + }, + "cookieLoginDescription": "Use this method if you face issues with the token authentication", + "@cookieLoginDescription": { + "description": "Cookie log in description" + }, + "logout": "Log out", + "@logout": { + "description": "CTA to logout of a server" + }, + "username": "Username", + "@username": { + "description": "Username label for login to a server" + }, + "password": "Password", + "@password": { + "description": "Password label for login to a server" + }, + "wrongUsernamePassword": "Wrong username or password", + "@wrongUsernamePassword": { + "description": "Error message when authentication fails" + }, + "error": "Error", + "@error": {}, + "malformedStatsEndpoint": "/api/v1/stats is not as expected", + "@malformedStatsEndpoint": { + "description": "Title for dialog when adding a server that isn't validated as it should" + }, + "malformedStatsEndpointDescription": "The server stats endpoint did not respond an expected payload, the key \"software.name\" should be equal to \"invidious\".\nResponse from the server:", + "@malformedStatsEndpointDescription": { + "description": "Description of the possible issue for an invalid stats endpoints" + }, + "serverIsNotReachable": "Server is not reachable", + "@serverIsNotReachable": { + "description": "Title for dialog when adding a server that is not reachable" + }, + "videoQueue": "Video queue", + "@videoQueue": { + "description": "Label for button to display the video queue" + }, + "addToQueueList": "Add to queue", + "@addToQueueList": { + "description": "Label on button to add a video to the queue list" + }, + "addToPlaylist": "Add to playlist", + "@addToPlaylist": { + "description": "Label to add a video to a playlist" + }, + "playNext": "Play next", + "@playNext": { + "description": "Label to play the video after the current one." + }, + "playNextAddedToQueue": "Video will play next", + "@playNextAddedToQueue": { + "description": "Pop up message to confirm that the video has been properly set to play next" + }, + "addRecommendedToQueue": "Auto-play recommended next", + "@addRecommendedToQueue": { + "description": "Switch when playing a video to automatically add the recommended videos to the video queue" + }, + "sponsorBlockSettingsQuickDescription": "Select which type of segments to skip", + "@sponsorBlockSettingsQuickDescription": { + "description": "Small description of what the sponsor block settings do" + }, + "sponsorBlockCategorySponsor": "Sponsor", + "@sponsorBlockCategorySponsor": { + "description": "Sponsor block 'Sponsor' Category" + }, + "sponsorBlockCategorySponsorDescription": "Paid promotion, paid referrals and direct advertisements. Not for self-promotion or free shoutouts to causes/creators/websites/products they like.", + "@sponsorBlockCategorySponsorDescription": { + "description": "Sponsor block 'Sponsor' Category description" + }, + "sponsorBlockCategoryUnpaidSelfPromo": "Unpaid/Self Promotion", + "@sponsorBlockCategoryUnpaidSelfPromo": { + "description": "Sponsor block 'Unpaid/Self promotion' Category" + }, + "sponsorBlockCategoryUnpaidSelfPromoDescription": "Similar to \"sponsor\" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated ", + "@sponsorBlockCategoryUnpaidSelfPromoDescription": { + "description": "Sponsor block 'Unpaid/Self promotion' Category description" + }, + "sponsorBlockCategoryInteraction": "Interaction Reminder (Subscribe)", + "@sponsorBlockCategoryInteraction": { + "description": "Sponsor block 'Interaction' Category" + }, + "sponsorBlockCategoryInteractionDescription": "When there is a short reminder to like, subscribe or follow them in the middle of content. If it is long or about something specific, it should be under self promotion instead.", + "@sponsorBlockCategoryInteractionDescription": { + "description": "Sponsor block 'Interaction' Category description" + }, + "sponsorBlockCategoryIntro": "Intermission/Intro Animation", + "@sponsorBlockCategoryIntro": { + "description": "Sponsorblock 'Intro' Category" + }, + "sponsorBlockCategoryIntroDescription": "An interval without actual content. Could be a pause, static frame, repeating animation. This should not be used for transitions containing information.", + "@sponsorBlockCategoryIntroDescription": { + "description": "Sponsorblock 'Intro' Category description" + }, + "sponsorBlockCategoryOutro": "Endcards/Credits", + "@sponsorBlockCategoryOutro": { + "description": "Outro block 'Outro' Category" + }, + "sponsorBlockCategoryOutroDescription": "Credits or when the YouTube endcards appear. Not for conclusions with information.", + "@sponsorBlockCategoryOutroDescription": { + "description": "Outro block 'Outro' Category description" + }, + "sponsorBlockCategoryPreview": "Preview/Recap", + "@sponsorBlockCategoryPreview": { + "description": "Sponsorblock 'Preview' Category" + }, + "sponsorBlockCategoryPreviewDescription": "Collection of clips that show what is coming up in in this video or other videos in a series where all information is repeated later in the video.", + "@sponsorBlockCategoryPreviewDescription": { + "description": "Sponsorblock 'Preview' Category description" + }, + "sponsorBlockCategoryFiller": "Filler Tangent/Jokes", + "@sponsorBlockCategoryFiller": { + "description": "Sponsorblock 'Filler' Category" + }, + "sponsorBlockCategoryFillerDescription": "Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include segments providing context or background details. This is a very aggressive category meant for when you aren''t in the mood for \"fun\".", + "@sponsorBlockCategoryFillerDescription": { + "description": "Sponsorblock 'Filler' Category description" + }, + "sponsorBlockCategoryMusicOffTopic": "Music: Non-Music Section", + "@sponsorBlockCategoryMusicOffTopic": { + "description": "Sponsorblock 'MusicOffTopic' Category" + }, + "sponsorBlockCategoryMusicOffTopicDescription": "Only for use in music videos. This only should be used for sections of music videos that aren''t already covered by another category.", + "@sponsorBlockCategoryMusicOffTopicDescription": { + "description": "Only for use in music videos. This only should be used for sections of music videos that aren't already covered by another category." + }, + "useProxy": "Proxy videos", + "@useProxy": { + "description": "label for settings switch to proxy videos from server" + }, + "useProxyDescription": "By proxying video streams from the server, you can bypass regional blocks or ISP blocking YouTube", + "@useProxyDescription": { + "description": "Description for the use proxy settings" + }, + "pressDownToShowSettings": "Press down to show settings", + "@pressDownToShowSettings": { + "description": "Instruction on how to show video settings when playing a video on TV" + }, + "quality": "Quality", + "@quality": { + "description": "Name of TV ui video settings" + }, + "audio": "Audio", + "@audio": { + "description": "Name TV ui audio settings" + }, + "subtitles": "Subtitles", + "@subtitles": { + "description": "Name of TV ui subtitles settings" + }, + "playbackSpeed": "Playback speed", + "@playbackSpeed": { + "description": "Name of TV ui Playback speed" + }, + "blackBackground": "Black background", + "@blackBackground": { + "description": "Settings name for black background" + }, + "blackBackgroundDescription": "For dark theme on OLED screen", + "@blackBackgroundDescription": { + "description": "Description for dark background setting" + }, + "search": "Search", + "@search": { + "description": "search title" + }, + "subtitleFontSize": "Subtitles font size", + "@subtitleFontSize": { + "description": "Settings label for the size of the subtitles" + }, + "subtitleFontSizeDescription": "Change the size of the subtitles if it is too small or too big on your device", + "@subtitleFontSizeDescription": { + "description": "Settings description for subtitle size" + }, + "skipSslVerification": "Skip SSL certificate verification", + "@skipSslVerification": { + "description": "Setting label to skip ssl certification verification" + }, + "skipSslVerificationDescription": "For using a self-signed SSL certificate, or when having SSL related issues with your server.", + "@skipSslVerificationDescription": { + "description": "Setting description for the skip ssl certification verification" + }, + "themeBrightness": "Theme", + "@themeBrightness": { + "description": "Ask the user to user dark / light / system theme" + }, + "themeLight": "Light", + "@themeLight": { + "description": "Light theme" + }, + "themeDark": "Dark", + "@themeDark": { + "description": "Dark theme" + }, + "followSystem": "Follow system", + "@followSystem": { + "description": "Follow system label" + }, + "requiresRestart": "Requires app restart", + "@requiresRestart": { + "description": "Requires app restart label" + }, + "appLanguage": "App language", + "@appLanguage": { + "description": "Select app language" + }, + "nVideos": "{count, plural, =0{No videos} =1{1 video} other{{count} videos}}", + "@nVideos": { + "description": "One or more videos", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, + "returnYoutubeUrlValidation": "Url must start with http:// or https://", + "@returnYoutubeUrlValidation": { + "description": "error message for invalid custom url for return to youtube" + }, + "returnYoutubeDislikeDescription": "Show estimated video dislikes using API provided by returnyoutubedislike.com", + "@returnYoutubeDislikeDescription": { + "description": "ReturnYoutubeDislike setting description" + }, + "rydCustomInstance": "Custom RYD instance url", + "@rydCustomInstance": { + "description": "title for setting to set a custom ryd instance" + }, + "rydCustomInstanceDescription": "Use a different RYD instance, leave empty to use the default", + "@rydCustomInstanceDescription": { + "description": "description for custom ryd instancr setting" + }, + "autoplayVideoOnLoad": "Automatically play video on load", + "@autoplayVideoOnLoad": { + "description": "Label for settings to enable autoplay when a video loads" + }, + "autoplayVideoOnLoadDescription": "Automatically start playing the video after it has loaded", + "@autoplayVideoOnLoadDescription": { + "description": "Description for the autoplay video on load setting" + }, + "searchHistory": "Search history", + "@searchHistory": { + "description": "Settings label for search history" + }, + "searchHistoryDescription": "Search history settings", + "@searchHistoryDescription": { + "description": "Description for search history settings" + }, + "enableSearchHistory": "Enable search history", + "@enableSearchHistory": { + "description": "Settings label for enabling search history" + }, + "searchHistoryLimit": "Search history limit", + "@searchHistoryLimit": { + "description": "Settings label for search history limit" + }, + "searchHistoryLimitDescription": "Set how many previous searches will show up in suggestions", + "@searchHistoryLimitDescription": { + "description": "Settings label for search history limit description" + }, + "shorts": "Shorts", + "@shorts": { + "description": "Youtube shorts" + }, + "searchUploadDate": "Upload date", + "@searchUploadDate": { + "description": "Filter search result by upload date" + }, + "searchUploadDateAny": "Any date", + "@searchUploadDateAny": { + "description": "Do not filter search result by upload date" + }, + "searchUploadDateHour": "Last Hour", + "@searchUploadDateHour": { + "description": "Search for uploaded in last hour" + }, + "searchUploadDateToday": "Today", + "@searchUploadDateToday": { + "description": "Search for uploaded today" + }, + "searchUploadDateWeek": "This week", + "@searchUploadDateWeek": { + "description": "Search for uploaded this week" + }, + "searchUploadDateMonth": "This month", + "@searchUploadDateMonth": { + "description": "Search for uploaded this month" + }, + "searchUploadDateYear": "This year", + "@searchUploadDateYear": { + "description": "Search for uploaded this year" + }, + "searchDuration": "Duration", + "@searchDuration": { + "description": "Filter search result by duration" + }, + "searchDurationAny": "Any duration", + "@searchDurationAny": { + "description": "Do not filter search result by duration" + }, + "searchDurationShort": "Short (<4 minutes)", + "@searchDurationShort": { + "description": "Search for short videos only" + }, + "searchDurationLong": "Long (>20 minutes)", + "@searchDurationLong": { + "description": "Search for long videos only" + }, + "searchDurationMedium": "Medium (4-20 minutes)", + "@searchDurationMedium": { + "description": "Search for medium videos only" + }, + "searchSortBy": "Sort by", + "@searchSortBy": { + "description": "Search sorting option" + }, + "searchSortRelevance": "Relevance", + "@searchSortRelevance": { + "description": "Sort search by relevance" + }, + "searchSortRating": "Rating", + "@searchSortRating": { + "description": "Sort search by rating" + }, + "searchSortUploadDate": "Upload Date", + "@searchSortUploadDate": { + "description": "Sort search by upload date" + }, + "searchSortViewCount": "View Count", + "@searchSortViewCount": { + "description": "Sort search by view count" + }, + "clearSearchHistory": "Clear search history", + "@clearSearchHistory": { + "description": "Settings label for clearing search history" + }, + "appLogs": "Application Logs", + "@appLogs": { + "description": "Title for settings that leads to application logs" + }, + "appLogsDescription": "Get logs of what is happening in the application, can be useful to report issues", + "@appLogsDescription": { + "description": "Description of the app log settings" + }, + "copyToClipBoard": "Copy to clipboard", + "@copyToClipBoard": { + "description": "Text to copy something to clipboard" + }, + "logsCopied": "Logs copied to clipboard", + "@logsCopied": { + "description": "Message to tell user that logs have been copied to the clipboard" + }, + "rememberSubtitleLanguage": "Remember subtitles language", + "@rememberSubtitleLanguage": { + "description": "Settings label for remembering subtitle language" + }, + "videoFilters": "Video filters", + "@videoFilters": { + "description": "Title for video filter settings" + }, + "nFilters": "{count, plural, =0{No videos} =1{1 filter} other{{count} filters}}", + "@nFilters": { + "description": "One or more video filters", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, + "videoFiltersExplanation": "Hide or Obfuscate videos from all the video feeds in the application based on the filters defined below. This allow you for example to hide sports spoilers or hide shorts from a certain channel.", + "@videoFiltersExplanation": { + "description": "Description on how filter work" + }, + "videoFiltersSettingTileDescriptions": "Define rules to filter out videos", + "@videoFiltersSettingTileDescriptions": { + "description": "Description for the main settings page" + }, + "videoFilterAllChannels": "All channels", + "@videoFilterAllChannels": { + "description": "Title for the sections that applies to all channels" + }, + "addVideoFilter": "Create filter", + "@addVideoFilter": { + "description": "Title when creating a new filter" + }, + "editVideoFilter": "Edit filter", + "@editVideoFilter": { + "description": "Title when editting a filter" + }, + "videoFilterType": "Type", + "@videoFilterType": { + "description": "Label for filter type" + }, + "videoFilterOperation": "Operation", + "@videoFilterOperation": { + "description": "Label for filter operation" + }, + "videoFilterValue": "Value", + "@videoFilterValue": { + "description": "Label for filter value" + }, + "save": "Save", + "@save": { + "description": "Text for save action" + }, + "videoFilterEditDescription": "Select an optional channel, a filter type, operation and a value to filter OUT videos from lists. Example, type: video name, operation: contains, value: test will EXCLUDE all the videos with the word 'test' in their name.", + "@videoFilterEditDescription": { + "description": "Descriptive test for video filter set up" + }, + "optional": "optional", + "@optional": { + "description": "Optional label" + }, + "videoFilterHideLabel": "Hide", + "@videoFilterHideLabel": { + "description": "Label to hide videos" + }, + "videoFilterFilterLabel": "Obfuscate", + "@videoFilterFilterLabel": { + "description": "Label to filter videos" + }, + "videoFilterDescriptionString": "{hideOrFilter} videos where {type} {operation} ''{value}''.", + "@videoFilterDescriptionString": { + "description": "Human readable description of a video filter, in this case is it for string comparison, example: Hide videos where the name of the video does not contain the following string 'test' (Do not translate text between { })", + "placeholders": { + "hideOrFilter": { + "type": "String", + "example": "Hide" + }, + "type": { + "type": "String", + "example": "video title" + }, + "operation": { + "type": "String", + "example": "does not contain" + }, + "value": { + "type": "String", + "example": "some filter text" + } + } + }, + "videoFiltered": "Video filtered for the following reason(s):", + "@videoFiltered": { + "description": "Label shown on video list when it is filtered out" + }, + "videoFilterTapToReveal": "Tap to reveal", + "@videoFilterTapToReveal": { + "description": "Label to tell user to tap to show a filtered video" + }, + "videoFilterHide": "Hide filtered videos", + "@videoFilterHide": { + "description": "Label for settings to hide filtered videos" + }, + "videoFilterHideDescription": "By default filtered videos are not hidden but shown as obfuscated with the reason(s) why it has been filtered. This setting remove the filtered videos from lists.", + "@videoFilterHideDescription": { + "description": "" + }, + "videoFilterNoFilters": "No video filters, tap the '+' button below to start adding filters.", + "@videoFilterNoFilters": { + "description": "Label when there are no video filters" + }, + "videoFilterTypeVideoTitle": "Video title", + "@videoFilterTypeVideoTitle": { + "description": "Label for video filter video title" + }, + "videoFilterTypeChannelName": "Channel name", + "@videoFilterTypeChannelName": { + "description": "Label for video filter channel name" + }, + "videoFilterTypeVideoLength": "Video length (seconds)", + "@videoFilterTypeVideoLength": { + "description": "Label for video filter video length" + }, + "videoFilterOperationContains": "Contains", + "@videoFilterOperationContains": { + "description": "Label for video filter operation Contains" + }, + "videoFilterOperationNotContain": "Does not contain", + "@videoFilterOperationNotContain": { + "description": "Label for video filter operation Does not contain" + }, + "videoFilterOperationLowerThan": "Lower than", + "@videoFilterOperationLowerThan": { + "description": "Label for video filter operation Lower than" + }, + "videoFilterOperationHigherThan": "Higher than", + "@videoFilterOperationHigherThan": { + "description": "Label for video filter operation Higher than" + }, + "channel": "Channel", + "@channel": { + "description": "A single channel" + }, + "videoFilterHideAllFromChannel": "Filter all videos from channel", + "@videoFilterHideAllFromChannel": { + "description": "Label for video filter switch to allow to hide all videos from a channel" + }, + "videoFilterWholeChannel": "{hideOrFilter} all videos from channel", + "@videoFilterWholeChannel": { + "description": "Label for whole channel filtering", + "placeholders": { + "hideOrFilter": { + "type": "String", + "example": "Hide" + } + } + }, + "rememberSubtitleLanguageDescription": "Automatically set subtitles to last language selected, if available", + "@rememberSubtitleLanguageDescription": { + "description": "Settings description for remembering subtitle language" + }, + "lockFullScreenToLandscape": "Lock full screen orientation to video aspect ratio", + "@lockFullScreenToLandscape": { + "description": "Title to force full screen to landscape" + }, + "lockFullScreenToLandscapeDescription": "Locks the full screen orientation based on video format, landscape for wide video and portrait for portrait videos", + "@lockFullScreenToLandscapeDescription": { + "description": "Setting description for forcing video to landscape when in full screen" + }, + "fillFullscreen": "Maximize video to fit screen", + "@fillFullscreen": { + "description": "Title to maximize video to fit screen" + }, + "fillFullscreenDescription": "Adjusts the video to fill the entire screen in landscape mode", + "@fillFullscreenDescription": { + "description": "Setting description for filling video to screen in landscape" + }, + "rememberPlaybackSpeed": "Remember playback speed", + "@rememberPlaybackSpeed": { + "description": "Setting label for remembering playback speed" + }, + "rememberPlaybackSpeedDescription": "Automatically set playback speed to the last speed selected", + "@rememberPlaybackSpeedDescription": { + "description": "Settings description for remembering playback speed" + }, + "downloads": "Downloads", + "@downloads": { + "description": "Downloads" + }, + "download": "Download", + "@download": { + "description": "A single download or CTA for downloading a video" + }, + "videoAlreadyDownloaded": "Video already downloaded", + "@videoAlreadyDownloaded": { + "description": "Message when a user tries to download a video he already has" + }, + "noDownloadedVideos": "No downloaded videos, browse, long press on a video in a list or tap the download button on a video screen to download", + "@noDownloadedVideos": { + "description": "Message showing when the user goes to the download screen but there are no offline videos." + }, + "downloadsPlayAll": "Play all", + "@downloadsPlayAll": { + "description": "Button to play all downloaded videos" + }, + "videoDownloadStarted": "Video download started", + "@videoDownloadStarted": { + "description": "Message when a video starts being downloaded" + }, + "videoFailedDownloadRetry": "Download failed, tap to retry", + "@videoFailedDownloadRetry": { + "description": "Shown on download manager when a download fails and prompt the user to retry" + }, + "videoDownloadAudioOnly": "Audio only", + "@videoDownloadAudioOnly": { + "description": "Label for toggle to download audio only " + }, + "manageSubscriptions": "Manage Subscriptions", + "@manageSubscriptions": { + "description": "Title of manage subscriptions page" + }, + "noSubscriptions": "No subscriptions, browse videos and subscribe to any channel you like.", + "@noSubscriptions": { + "description": "Message when the user has no subs" + }, + "youCanSubscribeAgainLater": "You can subscribe to this channel again later", + "@youCanSubscribeAgainLater": { + "description": "Text for the unscubscribe confirmation dialog" + }, + "unSubscribeQuestion": "Unsubscribe ?", + "@unSubscribeQuestion": { + "description": "Title for dialog if a user wants to unsubscribe in the subscribtion management screen" + }, + "clearHistoryQuestion": "Clear history ?", + "@clearHistoryQuestion": {}, + "clearHistoryQuestionExplanation": "This will clear your viewing history of your account on the Invidious instance you use. This cannot be undone.", + "@clearHistoryQuestionExplanation": { + "description": "Message for dialog before clearing full viewing history" + }, + "noHistory": "No viewing history, watch some videos and it will appear here", + "@noHistory": { + "description": "Message when the user visits the history tab but it's empty" + }, + "homeLayoutEditor": "Edit home layout", + "@homeLayoutEditor": { + "description": "Title of layout editor screen" + }, + "layoutEditorAddVideoSource": "Add video source", + "@layoutEditorAddVideoSource": { + "description": "Label for button to allow user to add more video sources to the home screen" + }, + "layoutEditorExplanation": "You can decide what to display on your home screen, you can have up to 2 small view with horizontal scrolling and one big source.", + "@layoutEditorExplanation": { + "description": "text to explain the home layout editor" + }, + "home": "Home", + "@home": { + "description": "Label for Home browsing tab" + }, + "library": "Library", + "@library": { + "description": "Name for user library" + }, + "customizeAppLayout": "Customize app sections", + "@customizeAppLayout": { + "description": "Settings label for the settings to allow the user to set up the app sections themselves" + }, + "customizeAppLayoutExplanation": "Select which sections you want to appear in the main app navigation bar. Click on the home icon to select which screen shows when the application starts. You can reorder the sections by dragging them around.", + "@customizeAppLayoutExplanation": { + "description": "" + }, + "navigationBarStyle": "Navigation bar style", + "@navigationBarStyle": { + "description": "Label for settings on customizing navigation bar style" + }, + "navigationBarLabelAlwaysShowing": "Label always showing", + "@navigationBarLabelAlwaysShowing": { + "description": "Label always showing option for navigation bar" + }, + "navigationBarLabelShowOnSelect": "Label shown on selected item", + "@navigationBarLabelShowOnSelect": { + "description": "Label only showing when selected option for navigation bar" + }, + "navigationBarLabelNeverShow": "Never show label", + "@navigationBarLabelNeverShow": { + "description": "Never show label option for navigation bar" + }, + "distractionFreeMode": "Distraction free mode", + "@distractionFreeMode": { + "description": "title for distraction free mode settings" + }, + "distractionFreeModeDescription": "Disable video comments and recommendations", + "@distractionFreeModeDescription": { + "description": "Description for distraction free mode" + }, + "secondsShortForm": "secs", + "@secondsShortForm": { + "description": "Short form for the word seconds" + }, + "videoFilterApplyDateToFilter": "Filter videos on given times", + "@videoFilterApplyDateToFilter": { + "description": "Label for switch to allow user to customize video filter and set days of week and time to them" + }, + "videoFilterDayOfWeek": "Select days to apply filters", + "@videoFilterDayOfWeek": { + "description": "Title for day selection for the filter" + }, + "videoFilterDayOfWeekDescription": "You can selectively choose days of the week and time to which the filters apply to, for example, avoid sport events spoilers.", + "@videoFilterDayOfWeekDescription": { + "description": "" + }, + "videoFilterStartTime": "Start time", + "@videoFilterStartTime": { + "description": "Title for filter start time" + }, + "videoFilterEndTime": "End time", + "@videoFilterEndTime": { + "description": "Title for filter end time" + }, + "videoFilterAppliedOn": "Applied on {selectedDays}", + "@videoFilterAppliedOn": { + "description": "Readable text on when the filter should apply", + "placeholders": { + "selectedDays": { + "type": "String", + "example": "Monday, Wednesday, Friday" + } + } + }, + "from": "From", + "@from": { + "description": "From word (as in 'From xx To xx')" + }, + "to": "To", + "@to": { + "description": "To word as in 'From xx To xx')" + }, + "videoFilterTimeOfDayFromTo": "From {from} to {to}", + "@videoFilterTimeOfDayFromTo": { + "description": "Time of day range", + "placeholders": { + "from": { + "type": "String", + "example": "3:00 AM" + }, + "to": { + "type": "String", + "example": "5:00 PM" + } + } + }, + "notifications": "Notifications", + "@notifications": { + "description": "Notification settings title" + }, + "notificationsDescription": "Enable and review what you are notified about", + "@notificationsDescription": { + "description": "Setting description for notifications" + }, + "enableNotificationDescriptions": "Runs foreground service to check and notify you on the changes you are monitoring", + "@enableNotificationDescriptions": { + "description": "" + }, + "subscriptionNotification": "Subscription notifications", + "@subscriptionNotification": { + "description": "Title for subscriptions notifications" + }, + "subscriptionNotificationDescription": "Get notified of new videos from your subscription feed if you are logged in to your current instance", + "@subscriptionNotificationDescription": { + "description": "Description for subscription notifications" + }, + "subscriptionNotificationTitle": "New videos from your subscriptions", + "@subscriptionNotificationTitle": { + "description": "Title for the notification showing that there are new videos from the subscription feed" + }, + "subscriptionNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in your subscription feed", + "@subscriptionNotificationContent": { + "description": "Content for subscription notification", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, + "askForDisableBatteryOptimizationTitle": "Disabling battery optimization required", + "@askForDisableBatteryOptimizationTitle": { + "description": "Title for the dialog asking the user to turn off disabling battery optimization when turning on notifications" + }, + "askForDisableBatteryOptimizationContent": "In order to send notification Clipious needs to run a background service. For it to run smoothly it is required that Clipious is given unrestricted battery usage, tapping ok will open the battery optimization settings.", + "@askForDisableBatteryOptimizationContent": { + "description": "Content for the dialog asking the user to turn off disabling battery optimization when turning on notifications" + }, + "askToEnableBackgroundServiceTitle": "Notifications turned off", + "@askToEnableBackgroundServiceTitle": { + "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" + }, + "askToEnableBackgroundServiceContent": "To get notifications, Clipious notifications need to be enabled, press OK to enable it.", + "@askToEnableBackgroundServiceContent": { + "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" + }, + "otherNotifications": "Other notifications sources (bell icons)", + "@otherNotifications": { + "description": "Title for settings section in the notification settings" + }, + "deleteChannelNotificationTitle": "Delete channel notification ?", + "@deleteChannelNotificationTitle": { + "description": "Title for dialog to confirm whether to delete channel notifications" + }, + "deleteChannelNotificationContent": "You won''t receive anymore notifications from this channel.", + "@deleteChannelNotificationContent": { + "description": "Title for dialog to confirm whether to delete channel notifications" + }, + "deletePlaylistNotificationTitle": "Delete playlist notification ?", + "@deletePlaylistNotificationTitle": { + "description": "Title for dialog to confirm whether to delete playlist notifications" + }, + "deletePlaylistNotificationContent": "You won''t receive anymore notifications from this playlist.", + "@deletePlaylistNotificationContent": { + "description": "Title for dialog to confirm whether to delete playlist notifications" + }, + "channelNotificationTitle": "New videos from {channel}", + "@channelNotificationTitle": { + "description": "Title for the channel notifications when there are new videos", + "placeholders": { + "channel": { + "type": "String", + "example": "MKBHD" + } + } + }, + "channelNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} from {channel}", + "@channelNotificationContent": { + "description": "Content for channel notification when there are new videos", + "placeholders": { + "channel": { + "type": "String", + "example": "MKBHD" + }, + "count": { + "type": "num", + "format": "compact" + } + } + }, + "playlistNotificationTitle": "New videos in {playlist} playlist", + "@playlistNotificationTitle": { + "description": "Title for the playlist notifications when there are new videos", + "placeholders": { + "playlist": { + "type": "String", + "example": "Lo-Fi girl" + } + } + }, + "playlistNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in the {playlist} playlist", + "@playlistNotificationContent": { + "description": "Content for playlist notification when there are new videos", + "placeholders": { + "playlist": { + "type": "String", + "example": "Lo-Fi girl" + }, + "count": { + "type": "num", + "format": "compact" + } + } + }, + "foregroundServiceNotificationTitle": "Video monitoring", + "@foregroundServiceNotificationTitle": { + "description": "Title for the foreground service running notification when the user wants to receive notifications" + }, + "foregroundServiceNotificationContent": "Will check for new videos once {hours, select, 1{per hour} 24{a day} other{every {hours} hours}}", + "@foregroundServiceNotificationContent": { + "description": "Content for the foreground service running notification when the user wants to receive notifications", + "hours": { + "type": "num", + "format": "compact" + } + }, + "foregroundServiceUpdatingSubscriptions": "Checking subscriptions...", + "@foregroundServiceUpdatingSubscriptions": { + "description": "Foreground service notification text when checking for new subscription videos" + }, + "foregroundServiceUpdatingPlaylist": "Checking playlists...", + "@foregroundServiceUpdatingPlaylist": { + "description": "Foreground service notification text when checking for new playlist videos" + }, + "foregroundServiceUpdatingChannels": "Checking channels...", + "@foregroundServiceUpdatingChannels": { + "description": "Foreground service notification text when checking for new channel videos" + }, + "notificationFrequencySettingsTitle": "New video check frequency", + "@notificationFrequencySettingsTitle": { + "description": "Title for frequency settings" + }, + "notificationFrequencySettingsDescription": "How often the application will check for new videos", + "@notificationFrequencySettingsDescription": { + "description": "Description for frequency settings" + }, + "notificationFrequencySliderLabel": "{hours, select, 24{1d} other{{hours}h}}", + "@notificationFrequencySliderLabel": { + "description": "Short form for a number of hours going up to 1 day", + "hours": { + "type": "num", + "format": "compact" + } + }, + "subtitlesBackground": "Subtitles background", + "@subtitlesBackground": { + "description": "Title for settings to set black background for subtitles" + }, + "subtitlesBackgroundDescription": "Adds a black background to subtitles to make them more readable", + "@subtitlesBackgroundDescription": { + "description": "Description for settings to set black background for subtitles" + }, + "history": "History", + "@history": { + "description": "User view history label" + }, + "deArrowSettingDescription": "Replace click bait titles and thumbnails", + "@deArrowSettingDescription": { + "description": "Description for dearrow" + }, + "deArrowReplaceThumbnails": "Replace thumbnails", + "@deArrowReplaceThumbnails": { + "description": "Settings title for checkbox on whether the thumbnail should be replaced as well" + }, + "deArrowReplaceThumbnailsDescription": "Replace video thumbnails in addition of the titles", + "@deArrowReplaceThumbnailsDescription": { + "description": "Description for DeArrow setting switch" + }, + "deArrowWarning": "Enabling DeArrow can significantly reduce the browsing speed of the app as extra http requests are needed for every single video", + "@deArrowWarning": { + "description": "Warning message when the user enables DeArrow" + }, + "copySettingsAsJson": "Copy settings as JSON to clipboard", + "@copySettingsAsJson": { + "description": "title for settings sections to allow users to copy their settings as json to make debugging easier" + }, + "copySettingsAsJsonDescription": "Copy the settings as JSON to help debugging if you encounter an issue with the app and decide to raise an issue", + "@copySettingsAsJsonDescription": { + "description": "" + }, + "seeking": "Seeking", + "@seeking": { + "description": "category for settings related to seeking in a video" + }, + "skipStep": "Skip forward/backward step", + "@skipStep": { + "description": "Title for the settings to set the skipping step" + }, + "skipStepDescription": "Seconds to skip on forward/backward actions", + "@skipStepDescription": { + "description": "Title for the settings to set the skipping step" + }, + "exponentialSkip": "Exponential skip forward/backward", + "@exponentialSkip": { + "description": "Title for the setting to enable the exponential skipping" + }, + "exponentialSkipDescription": "The more you skip forward, the bigger the step is.", + "@exponentialSkipDescription": { + "description": "Title for the setting to enable the exponential skipping" + }, + "fullscreenOnLandscape": "Full screen on landscape", + "@fullscreenOnLandscape": { + "description": "Setting title to enable full screen on landscape orientation" + }, + "fullscreenOnLandscapeDescription": "Switch to full screen when the device is rotated to landscape mode", + "@fullscreenOnLandscapeDescription": { + "description": "Setting to enable full screen on landscape orientation" + }, + "enabled": "Enabled", + "@enabled": { + "description": "Text to show something is enabled" + }, + "submitFeedback": "Submit feedback", + "@submitFeedback": { + "description": "Title for settings to submit feed back through the app" + }, + "submitFeedbackDescription": "Found a bug or have a suggestion? Use this tool to take screenshot of the app, annotate and submit feedback", + "@submitFeedbackDescription": { + "description": "Setting tile descriptions for feedback submission" + }, + "feedbackDisclaimer": "To submit feedback you will need a GitHub account and your screenshot will be submitted to Imgur anonymously.", + "@feedbackDisclaimer": { + "description": "Content of dialog shown before submitting feedback to make sure the user is ok whith where the data is going" + }, + "feedbackScreenshotError": "Error while uploading screenshot to Imgur", + "@feedbackScreenshotError": { + "description": "Title for dialog if something goes wrong while uploading feedback screenshot" + }, + "channelSortByNewest": "Newest", + "@channelSortByNewest": { + "description": "Sort channel videos from newest to oldest" + }, + "channelSortByOldest": "Oldest", + "@channelSortByOldest": { + "description": "Sort channel videos from oldest to newest" + }, + "channelSortByPopular": "Popular", + "@channelSortByPopular": { + "description": "Sort channel videos by popularity" + }, + "invidiousAccount": "Invidious account", + "@invidiousAccount": { + "description": "Text when the user choose where to subscribe to a channel" + }, + "onDeviceSubscriptions": "On device", + "@onDeviceSubscriptions": { + "description": "Text when the user chooses where to subscribe to a channel" + }, + "both": "Both", + "refresh": "Refresh" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b7609380..92693a75 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -857,7 +857,7 @@ "@itemListErrorInvalidScope": { "description": "Error when the user doesn't have the proper scope to its current token" }, - "itemlistErrorGeneric": "Impossible d'obtenir les données", + "itemlistErrorGeneric": "Impossible d''obtenir les données", "@itemlistErrorGeneric": { "description": "Error showing when the data can't be fetch" }, @@ -1317,7 +1317,7 @@ "@rydCustomInstance": { "description": "title for setting to set a custom ryd instance" }, - "askForDisableBatteryOptimizationTitle": "La désactivation de l'optimisation de la batterie est requise", + "askForDisableBatteryOptimizationTitle": "La désactivation de l''optimisation de la batterie est requise", "@askForDisableBatteryOptimizationTitle": { "description": "Title for the dialog asking the user to turn off disabling battery optimization when turning on notifications" }, diff --git a/lib/offline_subscriptions/models/offline_subscription.dart b/lib/offline_subscriptions/models/offline_subscription.dart new file mode 100644 index 00000000..2472372e --- /dev/null +++ b/lib/offline_subscriptions/models/offline_subscription.dart @@ -0,0 +1,18 @@ +import 'dart:core'; + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'offline_subscription.g.dart'; + +part 'offline_subscription.freezed.dart'; + +@freezed +class OfflineSubscription with _$OfflineSubscription { + const factory OfflineSubscription({ + required String channelId, + required String channelName, + }) = _OfflineSubscription; + + factory OfflineSubscription.fromJson(Map json) => + _$OfflineSubscriptionFromJson(json); +} diff --git a/lib/offline_subscriptions/models/offline_subscription.freezed.dart b/lib/offline_subscriptions/models/offline_subscription.freezed.dart new file mode 100644 index 00000000..e0bb286f --- /dev/null +++ b/lib/offline_subscriptions/models/offline_subscription.freezed.dart @@ -0,0 +1,173 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'offline_subscription.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +OfflineSubscription _$OfflineSubscriptionFromJson(Map json) { + return _OfflineSubscription.fromJson(json); +} + +/// @nodoc +mixin _$OfflineSubscription { + String get channelId => throw _privateConstructorUsedError; + String get channelName => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $OfflineSubscriptionCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OfflineSubscriptionCopyWith<$Res> { + factory $OfflineSubscriptionCopyWith( + OfflineSubscription value, $Res Function(OfflineSubscription) then) = + _$OfflineSubscriptionCopyWithImpl<$Res, OfflineSubscription>; + @useResult + $Res call({String channelId, String channelName}); +} + +/// @nodoc +class _$OfflineSubscriptionCopyWithImpl<$Res, $Val extends OfflineSubscription> + implements $OfflineSubscriptionCopyWith<$Res> { + _$OfflineSubscriptionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelId = null, + Object? channelName = null, + }) { + return _then(_value.copyWith( + channelId: null == channelId + ? _value.channelId + : channelId // ignore: cast_nullable_to_non_nullable + as String, + channelName: null == channelName + ? _value.channelName + : channelName // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$OfflineSubscriptionImplCopyWith<$Res> + implements $OfflineSubscriptionCopyWith<$Res> { + factory _$$OfflineSubscriptionImplCopyWith(_$OfflineSubscriptionImpl value, + $Res Function(_$OfflineSubscriptionImpl) then) = + __$$OfflineSubscriptionImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String channelId, String channelName}); +} + +/// @nodoc +class __$$OfflineSubscriptionImplCopyWithImpl<$Res> + extends _$OfflineSubscriptionCopyWithImpl<$Res, _$OfflineSubscriptionImpl> + implements _$$OfflineSubscriptionImplCopyWith<$Res> { + __$$OfflineSubscriptionImplCopyWithImpl(_$OfflineSubscriptionImpl _value, + $Res Function(_$OfflineSubscriptionImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelId = null, + Object? channelName = null, + }) { + return _then(_$OfflineSubscriptionImpl( + channelId: null == channelId + ? _value.channelId + : channelId // ignore: cast_nullable_to_non_nullable + as String, + channelName: null == channelName + ? _value.channelName + : channelName // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OfflineSubscriptionImpl implements _OfflineSubscription { + const _$OfflineSubscriptionImpl( + {required this.channelId, required this.channelName}); + + factory _$OfflineSubscriptionImpl.fromJson(Map json) => + _$$OfflineSubscriptionImplFromJson(json); + + @override + final String channelId; + @override + final String channelName; + + @override + String toString() { + return 'OfflineSubscription(channelId: $channelId, channelName: $channelName)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OfflineSubscriptionImpl && + (identical(other.channelId, channelId) || + other.channelId == channelId) && + (identical(other.channelName, channelName) || + other.channelName == channelName)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, channelId, channelName); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$OfflineSubscriptionImplCopyWith<_$OfflineSubscriptionImpl> get copyWith => + __$$OfflineSubscriptionImplCopyWithImpl<_$OfflineSubscriptionImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$OfflineSubscriptionImplToJson( + this, + ); + } +} + +abstract class _OfflineSubscription implements OfflineSubscription { + const factory _OfflineSubscription( + {required final String channelId, + required final String channelName}) = _$OfflineSubscriptionImpl; + + factory _OfflineSubscription.fromJson(Map json) = + _$OfflineSubscriptionImpl.fromJson; + + @override + String get channelId; + @override + String get channelName; + @override + @JsonKey(ignore: true) + _$$OfflineSubscriptionImplCopyWith<_$OfflineSubscriptionImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/offline_subscriptions/models/offline_subscription.g.dart b/lib/offline_subscriptions/models/offline_subscription.g.dart new file mode 100644 index 00000000..76615c36 --- /dev/null +++ b/lib/offline_subscriptions/models/offline_subscription.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'offline_subscription.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$OfflineSubscriptionImpl _$$OfflineSubscriptionImplFromJson( + Map json) => + _$OfflineSubscriptionImpl( + channelId: json['channelId'] as String, + channelName: json['channelName'] as String, + ); + +Map _$$OfflineSubscriptionImplToJson( + _$OfflineSubscriptionImpl instance) => + { + 'channelId': instance.channelId, + 'channelName': instance.channelName, + }; diff --git a/lib/subscription_management/states/manage_subscriptions.dart b/lib/subscription_management/states/manage_subscriptions.dart index 968ebedb..f8e8997b 100644 --- a/lib/subscription_management/states/manage_subscriptions.dart +++ b/lib/subscription_management/states/manage_subscriptions.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:invidious/extensions.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import 'package:logging/logging.dart'; import '../../globals.dart'; @@ -33,10 +34,24 @@ class ManageSubscriptionCubit extends Cubit { } refreshSubs() async { + final isLoggedIn = await service.isLoggedIn(); emit(state.copyWith(loading: true)); - var subs = - (await service.getSubscriptions()).sortBy((e) => e.author).toList(); - emit(state.copyWith(subs: subs, loading: false)); + List subs = []; + if (isLoggedIn) { + subs = + (await service.getSubscriptions()).sortBy((e) => e.author).toList(); + } + final offlineSubs = await db.getOfflineSubscriptions(); + emit(state.copyWith( + subs: subs, + loading: false, + isLoggedIn: isLoggedIn, + offlineSubs: offlineSubs)); + } + + unsubscribeOffline(String channelId) async { + await db.deleteOfflineSubscription(channelId); + refreshSubs(); } } @@ -44,5 +59,7 @@ class ManageSubscriptionCubit extends Cubit { class ManageSubscriptionsState with _$ManageSubscriptionsState { const factory ManageSubscriptionsState( {@Default([]) List subs, + @Default([]) List offlineSubs, + @Default(false) isLoggedIn, @Default(true) bool loading}) = _ManageSubscriptionsState; } diff --git a/lib/subscription_management/states/manage_subscriptions.freezed.dart b/lib/subscription_management/states/manage_subscriptions.freezed.dart index aec70c78..eed7766f 100644 --- a/lib/subscription_management/states/manage_subscriptions.freezed.dart +++ b/lib/subscription_management/states/manage_subscriptions.freezed.dart @@ -17,6 +17,9 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$ManageSubscriptionsState { List get subs => throw _privateConstructorUsedError; + List get offlineSubs => + throw _privateConstructorUsedError; + dynamic get isLoggedIn => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -30,7 +33,11 @@ abstract class $ManageSubscriptionsStateCopyWith<$Res> { $Res Function(ManageSubscriptionsState) then) = _$ManageSubscriptionsStateCopyWithImpl<$Res, ManageSubscriptionsState>; @useResult - $Res call({List subs, bool loading}); + $Res call( + {List subs, + List offlineSubs, + dynamic isLoggedIn, + bool loading}); } /// @nodoc @@ -48,6 +55,8 @@ class _$ManageSubscriptionsStateCopyWithImpl<$Res, @override $Res call({ Object? subs = null, + Object? offlineSubs = null, + Object? isLoggedIn = freezed, Object? loading = null, }) { return _then(_value.copyWith( @@ -55,6 +64,14 @@ class _$ManageSubscriptionsStateCopyWithImpl<$Res, ? _value.subs : subs // ignore: cast_nullable_to_non_nullable as List, + offlineSubs: null == offlineSubs + ? _value.offlineSubs + : offlineSubs // ignore: cast_nullable_to_non_nullable + as List, + isLoggedIn: freezed == isLoggedIn + ? _value.isLoggedIn + : isLoggedIn // ignore: cast_nullable_to_non_nullable + as dynamic, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -72,7 +89,11 @@ abstract class _$$ManageSubscriptionsStateImplCopyWith<$Res> __$$ManageSubscriptionsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({List subs, bool loading}); + $Res call( + {List subs, + List offlineSubs, + dynamic isLoggedIn, + bool loading}); } /// @nodoc @@ -89,6 +110,8 @@ class __$$ManageSubscriptionsStateImplCopyWithImpl<$Res> @override $Res call({ Object? subs = null, + Object? offlineSubs = null, + Object? isLoggedIn = freezed, Object? loading = null, }) { return _then(_$ManageSubscriptionsStateImpl( @@ -96,6 +119,11 @@ class __$$ManageSubscriptionsStateImplCopyWithImpl<$Res> ? _value._subs : subs // ignore: cast_nullable_to_non_nullable as List, + offlineSubs: null == offlineSubs + ? _value._offlineSubs + : offlineSubs // ignore: cast_nullable_to_non_nullable + as List, + isLoggedIn: freezed == isLoggedIn ? _value.isLoggedIn! : isLoggedIn, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -108,8 +136,12 @@ class __$$ManageSubscriptionsStateImplCopyWithImpl<$Res> class _$ManageSubscriptionsStateImpl implements _ManageSubscriptionsState { const _$ManageSubscriptionsStateImpl( - {final List subs = const [], this.loading = true}) - : _subs = subs; + {final List subs = const [], + final List offlineSubs = const [], + this.isLoggedIn = false, + this.loading = true}) + : _subs = subs, + _offlineSubs = offlineSubs; final List _subs; @override @@ -120,13 +152,25 @@ class _$ManageSubscriptionsStateImpl implements _ManageSubscriptionsState { return EqualUnmodifiableListView(_subs); } + final List _offlineSubs; + @override + @JsonKey() + List get offlineSubs { + if (_offlineSubs is EqualUnmodifiableListView) return _offlineSubs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_offlineSubs); + } + + @override + @JsonKey() + final dynamic isLoggedIn; @override @JsonKey() final bool loading; @override String toString() { - return 'ManageSubscriptionsState(subs: $subs, loading: $loading)'; + return 'ManageSubscriptionsState(subs: $subs, offlineSubs: $offlineSubs, isLoggedIn: $isLoggedIn, loading: $loading)'; } @override @@ -135,12 +179,20 @@ class _$ManageSubscriptionsStateImpl implements _ManageSubscriptionsState { (other.runtimeType == runtimeType && other is _$ManageSubscriptionsStateImpl && const DeepCollectionEquality().equals(other._subs, _subs) && + const DeepCollectionEquality() + .equals(other._offlineSubs, _offlineSubs) && + const DeepCollectionEquality() + .equals(other.isLoggedIn, isLoggedIn) && (identical(other.loading, loading) || other.loading == loading)); } @override int get hashCode => Object.hash( - runtimeType, const DeepCollectionEquality().hash(_subs), loading); + runtimeType, + const DeepCollectionEquality().hash(_subs), + const DeepCollectionEquality().hash(_offlineSubs), + const DeepCollectionEquality().hash(isLoggedIn), + loading); @JsonKey(ignore: true) @override @@ -153,11 +205,17 @@ class _$ManageSubscriptionsStateImpl implements _ManageSubscriptionsState { abstract class _ManageSubscriptionsState implements ManageSubscriptionsState { const factory _ManageSubscriptionsState( {final List subs, + final List offlineSubs, + final dynamic isLoggedIn, final bool loading}) = _$ManageSubscriptionsStateImpl; @override List get subs; @override + List get offlineSubs; + @override + dynamic get isLoggedIn; + @override bool get loading; @override @JsonKey(ignore: true) diff --git a/lib/subscription_management/states/subscribe_button.dart b/lib/subscription_management/states/subscribe_button.dart index 6ea9b920..49422d64 100644 --- a/lib/subscription_management/states/subscribe_button.dart +++ b/lib/subscription_management/states/subscribe_button.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import '../../globals.dart'; @@ -10,28 +11,61 @@ class SubscribeButtonCubit extends Cubit { onReady(); } - toggleSubscription() async { + setAccountSubscription(bool subscribed) async { emit(state.copyWith(loading: true)); - bool wasSubscribed = state.isSubscribed; + bool wasSubscribed = state.isAccountSubscribed; bool success = false; - if (wasSubscribed) { + if (!subscribed) { success = await service.unSubscribe(state.channelId); } else { success = await service.subscribe(state.channelId); } bool isSubscribed = await service.isSubscribedToChannel(state.channelId); if (!success || isSubscribed == wasSubscribed) { - return toggleSubscription(); + return setAccountSubscription(subscribed); } - emit(state.copyWith(loading: false, isSubscribed: isSubscribed)); + emit(state.copyWith(loading: false, isAccountSubscribed: isSubscribed)); + } + + setOfflineSubscription(bool subscribed) async { + emit(state.copyWith(loading: true)); + if (subscribed) { + final channel = await service.getChannel(state.channelId); + await db.addOfflineSubscription(OfflineSubscription( + channelId: state.channelId, channelName: channel.author)); + } else { + await db.deleteOfflineSubscription(state.channelId); + } + emit(state.copyWith(isOfflineSubscribed: subscribed, loading: false)); } Future onReady() async { - bool isSubscribed = await service.isSubscribedToChannel(state.channelId); var isLoggedIn = await service.isLoggedIn(); + + bool isAccountSubscribed = + isLoggedIn && await service.isSubscribedToChannel(state.channelId); + + bool isOfflineSubscribed = await db.isOfflineSubscribed(state.channelId); emit(state.copyWith( - loading: false, isSubscribed: isSubscribed, isLoggedIn: isLoggedIn)); + loading: false, + isOfflineSubscribed: isOfflineSubscribed, + isAccountSubscribed: isAccountSubscribed, + isLoggedIn: isLoggedIn)); + } + + Future unsubscribe() async { + emit(state.copyWith(loading: true)); + + if (state.isAccountSubscribed) { + await setAccountSubscription(false); + } + + if (state.isOfflineSubscribed) { + await setOfflineSubscription(false); + } + + emit(state.copyWith(loading: false)); } } @@ -39,11 +73,16 @@ class SubscribeButtonCubit extends Cubit { class SubscribeButtonState with _$SubscribeButtonState { const factory SubscribeButtonState({ required String channelId, - @Default(false) bool isSubscribed, + @Default(false) bool isOfflineSubscribed, + @Default(false) bool isAccountSubscribed, @Default(true) bool loading, required bool isLoggedIn, }) = _SubscribeButtonState; + const SubscribeButtonState._(); + + bool get isSubscribed => isOfflineSubscribed || isAccountSubscribed; + static SubscribeButtonState init(String channelId) { return SubscribeButtonState(channelId: channelId, isLoggedIn: false); } diff --git a/lib/subscription_management/states/subscribe_button.freezed.dart b/lib/subscription_management/states/subscribe_button.freezed.dart index c27d3400..e61fed3a 100644 --- a/lib/subscription_management/states/subscribe_button.freezed.dart +++ b/lib/subscription_management/states/subscribe_button.freezed.dart @@ -17,7 +17,8 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$SubscribeButtonState { String get channelId => throw _privateConstructorUsedError; - bool get isSubscribed => throw _privateConstructorUsedError; + bool get isOfflineSubscribed => throw _privateConstructorUsedError; + bool get isAccountSubscribed => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; bool get isLoggedIn => throw _privateConstructorUsedError; @@ -33,7 +34,11 @@ abstract class $SubscribeButtonStateCopyWith<$Res> { _$SubscribeButtonStateCopyWithImpl<$Res, SubscribeButtonState>; @useResult $Res call( - {String channelId, bool isSubscribed, bool loading, bool isLoggedIn}); + {String channelId, + bool isOfflineSubscribed, + bool isAccountSubscribed, + bool loading, + bool isLoggedIn}); } /// @nodoc @@ -51,7 +56,8 @@ class _$SubscribeButtonStateCopyWithImpl<$Res, @override $Res call({ Object? channelId = null, - Object? isSubscribed = null, + Object? isOfflineSubscribed = null, + Object? isAccountSubscribed = null, Object? loading = null, Object? isLoggedIn = null, }) { @@ -60,9 +66,13 @@ class _$SubscribeButtonStateCopyWithImpl<$Res, ? _value.channelId : channelId // ignore: cast_nullable_to_non_nullable as String, - isSubscribed: null == isSubscribed - ? _value.isSubscribed - : isSubscribed // ignore: cast_nullable_to_non_nullable + isOfflineSubscribed: null == isOfflineSubscribed + ? _value.isOfflineSubscribed + : isOfflineSubscribed // ignore: cast_nullable_to_non_nullable + as bool, + isAccountSubscribed: null == isAccountSubscribed + ? _value.isAccountSubscribed + : isAccountSubscribed // ignore: cast_nullable_to_non_nullable as bool, loading: null == loading ? _value.loading @@ -85,7 +95,11 @@ abstract class _$$SubscribeButtonStateImplCopyWith<$Res> @override @useResult $Res call( - {String channelId, bool isSubscribed, bool loading, bool isLoggedIn}); + {String channelId, + bool isOfflineSubscribed, + bool isAccountSubscribed, + bool loading, + bool isLoggedIn}); } /// @nodoc @@ -100,7 +114,8 @@ class __$$SubscribeButtonStateImplCopyWithImpl<$Res> @override $Res call({ Object? channelId = null, - Object? isSubscribed = null, + Object? isOfflineSubscribed = null, + Object? isAccountSubscribed = null, Object? loading = null, Object? isLoggedIn = null, }) { @@ -109,9 +124,13 @@ class __$$SubscribeButtonStateImplCopyWithImpl<$Res> ? _value.channelId : channelId // ignore: cast_nullable_to_non_nullable as String, - isSubscribed: null == isSubscribed - ? _value.isSubscribed - : isSubscribed // ignore: cast_nullable_to_non_nullable + isOfflineSubscribed: null == isOfflineSubscribed + ? _value.isOfflineSubscribed + : isOfflineSubscribed // ignore: cast_nullable_to_non_nullable + as bool, + isAccountSubscribed: null == isAccountSubscribed + ? _value.isAccountSubscribed + : isAccountSubscribed // ignore: cast_nullable_to_non_nullable as bool, loading: null == loading ? _value.loading @@ -127,18 +146,23 @@ class __$$SubscribeButtonStateImplCopyWithImpl<$Res> /// @nodoc -class _$SubscribeButtonStateImpl implements _SubscribeButtonState { +class _$SubscribeButtonStateImpl extends _SubscribeButtonState { const _$SubscribeButtonStateImpl( {required this.channelId, - this.isSubscribed = false, + this.isOfflineSubscribed = false, + this.isAccountSubscribed = false, this.loading = true, - required this.isLoggedIn}); + required this.isLoggedIn}) + : super._(); @override final String channelId; @override @JsonKey() - final bool isSubscribed; + final bool isOfflineSubscribed; + @override + @JsonKey() + final bool isAccountSubscribed; @override @JsonKey() final bool loading; @@ -147,7 +171,7 @@ class _$SubscribeButtonStateImpl implements _SubscribeButtonState { @override String toString() { - return 'SubscribeButtonState(channelId: $channelId, isSubscribed: $isSubscribed, loading: $loading, isLoggedIn: $isLoggedIn)'; + return 'SubscribeButtonState(channelId: $channelId, isOfflineSubscribed: $isOfflineSubscribed, isAccountSubscribed: $isAccountSubscribed, loading: $loading, isLoggedIn: $isLoggedIn)'; } @override @@ -157,16 +181,18 @@ class _$SubscribeButtonStateImpl implements _SubscribeButtonState { other is _$SubscribeButtonStateImpl && (identical(other.channelId, channelId) || other.channelId == channelId) && - (identical(other.isSubscribed, isSubscribed) || - other.isSubscribed == isSubscribed) && + (identical(other.isOfflineSubscribed, isOfflineSubscribed) || + other.isOfflineSubscribed == isOfflineSubscribed) && + (identical(other.isAccountSubscribed, isAccountSubscribed) || + other.isAccountSubscribed == isAccountSubscribed) && (identical(other.loading, loading) || other.loading == loading) && (identical(other.isLoggedIn, isLoggedIn) || other.isLoggedIn == isLoggedIn)); } @override - int get hashCode => - Object.hash(runtimeType, channelId, isSubscribed, loading, isLoggedIn); + int get hashCode => Object.hash(runtimeType, channelId, isOfflineSubscribed, + isAccountSubscribed, loading, isLoggedIn); @JsonKey(ignore: true) @override @@ -177,17 +203,21 @@ class _$SubscribeButtonStateImpl implements _SubscribeButtonState { this, _$identity); } -abstract class _SubscribeButtonState implements SubscribeButtonState { +abstract class _SubscribeButtonState extends SubscribeButtonState { const factory _SubscribeButtonState( {required final String channelId, - final bool isSubscribed, + final bool isOfflineSubscribed, + final bool isAccountSubscribed, final bool loading, required final bool isLoggedIn}) = _$SubscribeButtonStateImpl; + const _SubscribeButtonState._() : super._(); @override String get channelId; @override - bool get isSubscribed; + bool get isOfflineSubscribed; + @override + bool get isAccountSubscribed; @override bool get loading; @override diff --git a/lib/subscription_management/view/components/subscribe_button.dart b/lib/subscription_management/view/components/subscribe_button.dart index dd49074c..c4ef5894 100644 --- a/lib/subscription_management/view/components/subscribe_button.dart +++ b/lib/subscription_management/view/components/subscribe_button.dart @@ -12,6 +12,56 @@ class SubscribeButton extends StatelessWidget { const SubscribeButton( {super.key, required this.channelId, required this.subCount}); + static showSubscriptionSheet(BuildContext context) { + final cubit = context.read(); + final locals = AppLocalizations.of(context)!; + + if (cubit.state.isLoggedIn) { + showModalBottomSheet( + context: context, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (cubit.state.isLoggedIn) + TextButton( + onPressed: () async { + await cubit.setAccountSubscription(true); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text(locals.invidiousAccount)), + TextButton( + onPressed: () async { + await cubit.setOfflineSubscription(true); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text(locals.onDeviceSubscriptions)), + if (cubit.state.isLoggedIn) + TextButton( + onPressed: () async { + await Future.wait([ + cubit.setOfflineSubscription(true), + cubit.setAccountSubscription(true) + ]); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text(locals.both)), + ], + ); + }, + ); + } else { + cubit.setOfflineSubscription(true); + } + } + @override Widget build(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -25,42 +75,34 @@ class SubscribeButton extends StatelessWidget { child: BlocBuilder( builder: (context, state) { var cubit = context.read(); - return state.isLoggedIn - ? SizedBox( - height: 25, - child: FilledButton.tonal( - onPressed: cubit.toggleSubscription, - child: Row( - children: [ - state.loading - ? const SizedBox( - width: 15, - height: 15, - child: CircularProgressIndicator( - strokeWidth: 1, - )) - : Icon(state.isSubscribed - ? Icons.done - : Icons.add), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - '${(state.isSubscribed ? locals.subscribed : locals.subscribe)} | $subCount'), - ), - ], - ), - )) - : Row( + return SizedBox( + height: 25, + child: FilledButton.tonal( + onPressed: () { + if (state.isSubscribed) { + cubit.unsubscribe(); + } else { + showSubscriptionSheet(context); + } + }, + child: Row( children: [ - const Icon(Icons.people), + state.loading + ? const SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator( + strokeWidth: 1, + )) + : Icon(state.isSubscribed ? Icons.done : Icons.add), Padding( padding: const EdgeInsets.only(left: 8.0), - // child: Text('${subCount.replaceAll("^0.00\$","no")} subscribers'), - child: Text(locals.nSubscribers( - subCount.replaceAll(RegExp(r'^0.00$'), "no"))), + child: Text( + '${(state.isSubscribed ? locals.subscribed : locals.subscribe)} | $subCount'), ), ], - ); + ), + )); }, ), ), diff --git a/lib/subscription_management/view/screens/manage_subscriptions.dart b/lib/subscription_management/view/screens/manage_subscriptions.dart index 4a046e67..3f93c9d5 100644 --- a/lib/subscription_management/view/screens/manage_subscriptions.dart +++ b/lib/subscription_management/view/screens/manage_subscriptions.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import 'package:invidious/router.dart'; import 'package:invidious/subscription_management/models/subscription.dart'; import 'package:invidious/utils.dart'; @@ -35,59 +36,155 @@ class ManageSubscriptionsScreen extends StatelessWidget { builder: (context, state) { var cubit = context.read(); - return Padding( - padding: const EdgeInsets.all(8.0), - child: !state.loading && state.subs.isEmpty - ? Center(child: Text(locals.noChannels)) - : Stack( - children: [ - RefreshIndicator( - onRefresh: () => cubit.refreshSubs(), - child: ListView.builder( - itemCount: state.subs.length, - itemBuilder: (context, index) { - Subscription sub = state.subs[index]; - - return GestureDetector( - onTap: () => AutoRouter.of(context) - .push(ChannelRoute( - channelId: sub.authorId)) - .then((value) => cubit.refreshSubs()), - child: SimpleListItem( - key: ValueKey(sub.authorId), - index: index, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + return DefaultTabController( + length: state.isLoggedIn ? 2 : 1, + child: Column( + children: [ + TabBar(tabs: [ + if (state.isLoggedIn) + Tab( + text: locals.invidiousAccount, + ), + Tab(text: locals.onDeviceSubscriptions), + ]), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TabBarView( + children: [ + if (state.isLoggedIn) + !state.loading && state.subs.isEmpty + ? Center(child: Text(locals.noChannels)) + : Stack( children: [ - Text(sub.author), - IconButton.filledTonal( - visualDensity: - VisualDensity.compact, - onPressed: () { - okCancelDialog( - context, - locals.unSubscribeQuestion, - locals - .youCanSubscribeAgainLater, - () => cubit.unsubscribe( - sub.authorId)); - }, - icon: const Icon( - Icons.clear, - size: 15, + RefreshIndicator( + onRefresh: () => + cubit.refreshSubs(), + child: ListView.builder( + itemCount: state.subs.length, + itemBuilder: (context, index) { + Subscription sub = + state.subs[index]; + + return GestureDetector( + onTap: () => AutoRouter.of( + context) + .push(ChannelRoute( + channelId: + sub.authorId)) + .then((value) => cubit + .refreshSubs()), + child: SimpleListItem( + key: ValueKey( + sub.authorId), + index: index, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text(sub.author), + IconButton + .filledTonal( + visualDensity: + VisualDensity + .compact, + onPressed: () { + okCancelDialog( + context, + locals + .unSubscribeQuestion, + locals + .youCanSubscribeAgainLater, + () => cubit + .unsubscribe( + sub.authorId)); + }, + icon: const Icon( + Icons.clear, + size: 15, + ), + ) + ], + ), + ), + ); + }, ), - ) + ), + if (state.loading) + const TopListLoading() ], ), + !state.loading && state.offlineSubs.isEmpty + ? Center(child: Text(locals.noChannels)) + : Stack( + children: [ + RefreshIndicator( + onRefresh: () => + cubit.refreshSubs(), + child: ListView.builder( + itemCount: + state.offlineSubs.length, + itemBuilder: (context, index) { + OfflineSubscription sub = + state.offlineSubs[index]; + + return GestureDetector( + onTap: () => AutoRouter.of( + context) + .push(ChannelRoute( + channelId: + sub.channelId)) + .then((value) => + cubit.refreshSubs()), + child: SimpleListItem( + key: + ValueKey(sub.channelId), + index: index, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text(sub.channelName), + IconButton.filledTonal( + visualDensity: + VisualDensity + .compact, + onPressed: () { + okCancelDialog( + context, + locals + .unSubscribeQuestion, + locals + .youCanSubscribeAgainLater, + () => cubit + .unsubscribeOffline( + sub.channelId)); + }, + icon: const Icon( + Icons.clear, + size: 15, + ), + ) + ], + ), + ), + ); + }, + ), + ), + if (state.loading) + const TopListLoading() + ], ), - ); - }, - ), - ), - if (state.loading) const TopListLoading() - ], + ], + ), ), + ), + ], + ), ); }, ), diff --git a/lib/subscription_management/view/tv/tv_subscribe_button.dart b/lib/subscription_management/view/tv/tv_subscribe_button.dart index 36266ee5..b5aae028 100644 --- a/lib/subscription_management/view/tv/tv_subscribe_button.dart +++ b/lib/subscription_management/view/tv/tv_subscribe_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/subscription_management/states/subscribe_button.dart'; +import 'package:invidious/subscription_management/view/components/subscribe_button.dart'; import 'package:invidious/utils/views/tv/components/tv_button.dart'; class TvSubscribeButton extends StatelessWidget { @@ -34,14 +35,19 @@ class TvSubscribeButton extends StatelessWidget { autofocus: autoFocus, onFocusChanged: onFocusChanged, unfocusedColor: colors.surface.withOpacity(0.0), - onPressed: (context) => - state.isLoggedIn ? cubit.toggleSubscription() : null, + onPressed: (context) { + if (state.isSubscribed) { + cubit.unsubscribe(); + } else { + SubscribeButton.showSubscriptionSheet(context); + } + }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16), child: Row( mainAxisSize: MainAxisSize.min, - children: !state.loading && state.isLoggedIn + children: !state.loading ? [ Icon(state.isSubscribed ? Icons.done : Icons.add), Padding( diff --git a/lib/utils/extensions/list_unique.dart b/lib/utils/extensions/list_unique.dart new file mode 100644 index 00000000..9ba951d9 --- /dev/null +++ b/lib/utils/extensions/list_unique.dart @@ -0,0 +1,8 @@ +extension Unique on List { + List unique([Id Function(E element)? id, bool inplace = true]) { + final ids = Set(); + var list = inplace ? this : List.from(this); + list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); + return list; + } +} diff --git a/lib/utils/file_db.dart b/lib/utils/file_db.dart index bf4637eb..d94c9e17 100644 --- a/lib/utils/file_db.dart +++ b/lib/utils/file_db.dart @@ -5,6 +5,7 @@ import 'package:invidious/downloads/models/downloaded_video.dart'; import 'package:invidious/extensions.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/home/models/db/home_layout.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import 'package:invidious/search/models/db/search_history_item.dart'; import 'package:invidious/settings/models/db/app_logs.dart'; import 'package:invidious/settings/models/db/settings.dart'; @@ -468,6 +469,30 @@ class FileDB extends IDbClient { // TODO: implement deleteFromSearchHistory throw UnimplementedError(); } + + @override + Future addOfflineSubscription(OfflineSubscription sub) { + // TODO: implement addOfflineSubscription + throw UnimplementedError(); + } + + @override + Future deleteOfflineSubscription(String sub) { + // TODO: implement deleteOfflineSubscription + throw UnimplementedError(); + } + + @override + Future> getOfflineSubscriptions() { + // TODO: implement getOfflineSubscriptions + throw UnimplementedError(); + } + + @override + Future isOfflineSubscribed(String channelId) { + // TODO: implement isOfflineSubscribed + throw UnimplementedError(); + } } @JsonSerializable() diff --git a/lib/utils/interfaces/db.dart b/lib/utils/interfaces/db.dart index 0214ac9f..389f9d96 100644 --- a/lib/utils/interfaces/db.dart +++ b/lib/utils/interfaces/db.dart @@ -1,5 +1,6 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import '../../downloads/models/downloaded_video.dart'; import '../../home/models/db/home_layout.dart'; @@ -128,4 +129,12 @@ abstract class IDbClient { Future upsertDeArrowCache(DeArrowCache cache); Future deleteFromSearchHistory(String search); + + Future addOfflineSubscription(OfflineSubscription sub); + + Future deleteOfflineSubscription(String channelId); + + Future isOfflineSubscribed(String channelId); + + Future> getOfflineSubscriptions(); } diff --git a/lib/utils/models/paginated_list.dart b/lib/utils/models/paginated_list.dart index aa524e5d..2c3ab78b 100644 --- a/lib/utils/models/paginated_list.dart +++ b/lib/utils/models/paginated_list.dart @@ -1,12 +1,16 @@ +import 'package:invidious/channels/models/channel_videos.dart'; import 'package:invidious/search/models/search_results.dart'; import 'package:invidious/search/models/search_sort_by.dart'; import 'package:invidious/search/models/search_type.dart'; +import 'package:invidious/utils/extensions/list_unique.dart'; import 'package:invidious/utils/models/item_with_continuation.dart'; import 'package:invidious/videos/models/user_feed.dart'; import 'package:invidious/videos/models/video_in_list.dart'; import '../../globals.dart'; +const _done = "NO_MORE_VIDEOS"; + abstract class PaginatedList { Future> getItems(); @@ -127,34 +131,76 @@ class FixedItemList extends PaginatedList { class SubscriptionVideoList extends PaginatedVideoList { final maxResults = 50; int page = 1; - bool hasMore = true; + bool hasMoreOnline = true; + Map continuations = {}; @override Future> getItems() async { - UserFeed feed = - await service.getUserFeed(page: page, maxResults: maxResults); - List subs = []; - subs.addAll(feed.notifications ?? []); - subs.addAll(feed.videos ?? []); - hasMore = subs.length >= maxResults; - return subs; + final isLoggedIn = await service.isLoggedIn(); + + List videos = []; + + if (isLoggedIn) { + UserFeed feed = + await service.getUserFeed(page: page, maxResults: maxResults); + videos.addAll(feed.notifications ?? []); + videos.addAll(feed.videos ?? []); + hasMoreOnline = videos.length >= maxResults; + } + + final offlineSubs = await db.getOfflineSubscriptions(); + List> futures = []; + + for (final offlineSub in offlineSubs) { + var hasContinuation = continuations.containsKey(offlineSub.channelId); + // if we don't have continuation, we've never seen the channel so we retrive videos + // if there is a continuation but it's null, then there's no more videos to fetch + if (!hasContinuation || + (hasContinuation && continuations[offlineSub.channelId] != _done)) { + futures.add(service.getChannelVideos(offlineSub.channelId, + hasContinuation ? continuations[offlineSub.channelId] : null)); + } + } + + if (futures.isNotEmpty) { + List offlineSubVideos = + await Future.wait(futures); + for (final v in offlineSubVideos) { + if (v.videos.isNotEmpty) { + videos.addAll(v.videos); + if (v.videos[0].authorUrl != null) { + continuations[v.videos[0].authorUrl!] = v.continuation ?? _done; + } + } + } + } + + videos = videos.unique( + (element) => element.videoId, + ); + videos.sort((a, b) => (b.published ?? 0).compareTo(a.published ?? 0)); + + return videos; } @override Future> getMoreItems() async { - page = page + 1; + if (hasMoreOnline) { + page = page + 1; + } return getItems(); } @override Future> refresh() async { page = 1; + continuations = {}; return getItems(); } @override bool getHasMore() { - return hasMore; + return hasMoreOnline || continuations.values.any((v) => v != _done); } @override diff --git a/lib/utils/sembast_sqflite_database.dart b/lib/utils/sembast_sqflite_database.dart index f3690247..03fb0297 100644 --- a/lib/utils/sembast_sqflite_database.dart +++ b/lib/utils/sembast_sqflite_database.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:invidious/downloads/models/downloaded_video.dart'; import 'package:invidious/home/models/db/home_layout.dart'; +import 'package:invidious/offline_subscriptions/models/offline_subscription.dart'; import 'package:invidious/search/models/db/search_history_item.dart'; import 'package:invidious/settings/models/db/app_logs.dart'; import 'package:invidious/settings/models/db/server.dart'; @@ -45,6 +46,8 @@ class SembastSqfDb extends IDbClient { final historyVideoCacheStore = stringMapStoreFactory.store('historyVideoCache'); // use historyVideoCache final progressStore = stringMapStoreFactory.store('progress'); + final offlineSubscriptions = + stringMapStoreFactory.store('offline_subscriptions'); SembastSqfDb(this.db); @@ -311,4 +314,25 @@ class SembastSqfDb extends IDbClient { await serversStore.record(server.url).put(db, server.toJson()); } + + @override + Future addOfflineSubscription(OfflineSubscription sub) async { + await offlineSubscriptions.record(sub.channelId).put(db, sub.toJson()); + } + + @override + Future deleteOfflineSubscription(String sub) async { + await offlineSubscriptions.record(sub).delete(db); + } + + @override + Future> getOfflineSubscriptions() { + return offlineSubscriptions.find(db).then((values) => + values.map((e) => OfflineSubscription.fromJson(e.value)).toList()); + } + + @override + Future isOfflineSubscribed(String channelId) { + return offlineSubscriptions.record(channelId).exists(db); + } } diff --git a/lib/videos/views/components/video_list.dart b/lib/videos/views/components/video_list.dart index d35a764d..b77d2ff6 100644 --- a/lib/videos/views/components/video_list.dart +++ b/lib/videos/views/components/video_list.dart @@ -85,58 +85,62 @@ class VideoList extends StatelessWidget { : textTheme.bodyMedium, )), ) - : Padding( - padding: EdgeInsets.only(top: small ? 0.0 : 4.0), - child: RefreshIndicator( - onRefresh: () async => - !small && state.itemList.hasRefresh() - ? await cubit.refreshItems() - : Future.delayed(Duration.zero), - child: GridView.count( - crossAxisCount: gridCount, - controller: cubit.scrollController, - scrollDirection: scrollDirection, - crossAxisSpacing: small ? 8 : 5, - mainAxisSpacing: small ? 8 : 5, - childAspectRatio: small - ? smallVideoAspectRatio - : getGridAspectRatio(context), - children: [ - ...items.map((v) { - VideoInList? onlineVideo; - DownloadedVideo? offlineVideo; + : items.isEmpty && !state.loading + ? FilledButton.tonal( + onPressed: cubit.refreshItems, + child: Text(locals.refresh)) + : Padding( + padding: EdgeInsets.only(top: small ? 0.0 : 4.0), + child: RefreshIndicator( + onRefresh: () async => + !small && state.itemList.hasRefresh() + ? await cubit.refreshItems() + : Future.delayed(Duration.zero), + child: GridView.count( + crossAxisCount: gridCount, + controller: cubit.scrollController, + scrollDirection: scrollDirection, + crossAxisSpacing: small ? 8 : 5, + mainAxisSpacing: small ? 8 : 5, + childAspectRatio: small + ? smallVideoAspectRatio + : getGridAspectRatio(context), + children: [ + ...items.map((v) { + VideoInList? onlineVideo; + DownloadedVideo? offlineVideo; - if (v is VideoInList) { - onlineVideo = v; - } + if (v is VideoInList) { + onlineVideo = v; + } - if (v is DownloadedVideo) { - offlineVideo = v; - } + if (v is DownloadedVideo) { + offlineVideo = v; + } - return VideoListItem( - small: small, - showMetrics: showMetrics, - key: ValueKey( - '${v.videoId}-${small.toString()}'), - video: onlineVideo, - offlineVideo: offlineVideo, - animateDownload: animateDownload, - showVideoModalSheet: showVideoModalSheet, - allowModalSheet: allowModalSheet, - openVideoOverride: openVideoOverride, - ); - }), - if (state.loading) - ...repeatWidget( - () => VideoListItemPlaceHolder( - small: small, - ), - count: 5 * gridCount) - ], - ), - ), - ) + return VideoListItem( + small: small, + showMetrics: showMetrics, + key: ValueKey( + '${v.videoId}-${small.toString()}'), + video: onlineVideo, + offlineVideo: offlineVideo, + animateDownload: animateDownload, + showVideoModalSheet: showVideoModalSheet, + allowModalSheet: allowModalSheet, + openVideoOverride: openVideoOverride, + ); + }), + if (state.loading) + ...repeatWidget( + () => VideoListItemPlaceHolder( + small: small, + ), + count: 5 * gridCount) + ], + ), + ), + ) ], ); }, diff --git a/pubspec.yaml b/pubspec.yaml index b212aeec..e1f697cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: invidious -version: 1.19.12+4057 +version: 1.20.0+4058 publish_to: none description: A new Flutter project. environment: