mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-13 17:30:31 -07:00
feature: Add new home carousel (#58)
## Pull Request Description This adds a new home carousel better suited for mobile The old one is still available ## Checklist - [x] If a new package was added, did you ensure it works for all supported platforms? Is the package also well maintained? - [x] Did you add localization for any text? If yes, did you sort the .arb file using ```arb_utils sort <INPUT_FILE>```? - [x] Check that any changes are related to the issue at hand. Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
2a2502147a
commit
d572884e61
12 changed files with 1696 additions and 393 deletions
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
"@about": {},
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
|
"@accept": {},
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"@active": {},
|
||||||
"actor": "{count, plural, other{Actors} one{Actor}}",
|
"actor": "{count, plural, other{Actors} one{Actor}}",
|
||||||
"@actor": {
|
"@actor": {
|
||||||
"description": "actor",
|
"description": "actor",
|
||||||
|
|
@ -14,14 +17,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addAsFavorite": "Add as favorite",
|
"addAsFavorite": "Add as favorite",
|
||||||
|
"@addAsFavorite": {},
|
||||||
"addToCollection": "Add to collection",
|
"addToCollection": "Add to collection",
|
||||||
|
"@addToCollection": {},
|
||||||
"addToPlaylist": "Add to playlist",
|
"addToPlaylist": "Add to playlist",
|
||||||
|
"@addToPlaylist": {},
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
|
"@advanced": {},
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
"@all": {},
|
||||||
"amoledBlack": "Amoled black",
|
"amoledBlack": "Amoled black",
|
||||||
|
"@amoledBlack": {},
|
||||||
"appLockAutoLogin": "Auto login",
|
"appLockAutoLogin": "Auto login",
|
||||||
|
"@appLockAutoLogin": {},
|
||||||
"appLockBiometrics": "Biometrics",
|
"appLockBiometrics": "Biometrics",
|
||||||
|
"@appLockBiometrics": {},
|
||||||
"appLockPasscode": "Passcode",
|
"appLockPasscode": "Passcode",
|
||||||
|
"@appLockPasscode": {},
|
||||||
"appLockTitle": "Set the log-in method for {userName}",
|
"appLockTitle": "Set the log-in method for {userName}",
|
||||||
"@appLockTitle": {
|
"@appLockTitle": {
|
||||||
"description": "Pop-up to pick a login method",
|
"description": "Pop-up to pick a login method",
|
||||||
|
|
@ -32,15 +44,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ascending": "Ascending",
|
"ascending": "Ascending",
|
||||||
|
"@ascending": {},
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
|
"@audio": {},
|
||||||
"autoPlay": "Auto-play",
|
"autoPlay": "Auto-play",
|
||||||
|
"@autoPlay": {},
|
||||||
"backgroundBlur": "Background blur",
|
"backgroundBlur": "Background blur",
|
||||||
|
"@backgroundBlur": {},
|
||||||
"backgroundOpacity": "Background opacity",
|
"backgroundOpacity": "Background opacity",
|
||||||
"biometricsFailedCheckAgain": "Biometrics failed check settings and try again",
|
"@backgroundOpacity": {},
|
||||||
|
"biometricsFailedCheckAgain": "Biometrics failed. Check settings and try again.",
|
||||||
|
"@biometricsFailedCheckAgain": {},
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
|
"@bold": {},
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"@cancel": {},
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
|
"@change": {},
|
||||||
|
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
|
||||||
"@chapter": {
|
"@chapter": {
|
||||||
"description": "chapter",
|
"description": "chapter",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -51,16 +72,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"@clear": {},
|
||||||
"clearAllSettings": "Clear all settings",
|
"clearAllSettings": "Clear all settings",
|
||||||
|
"@clearAllSettings": {},
|
||||||
"clearAllSettingsQuestion": "Clear all settings?",
|
"clearAllSettingsQuestion": "Clear all settings?",
|
||||||
|
"@clearAllSettingsQuestion": {},
|
||||||
"clearChanges": "Clear changes",
|
"clearChanges": "Clear changes",
|
||||||
|
"@clearChanges": {},
|
||||||
"clearSelection": "Clear selection",
|
"clearSelection": "Clear selection",
|
||||||
|
"@clearSelection": {},
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
"@close": {},
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
|
"@code": {},
|
||||||
"collectionFolder": "Collection folder",
|
"collectionFolder": "Collection folder",
|
||||||
|
"@collectionFolder": {},
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
|
"@color": {},
|
||||||
"combined": "Combined",
|
"combined": "Combined",
|
||||||
|
"@combined": {},
|
||||||
"communityRating": "Community Rating",
|
"communityRating": "Community Rating",
|
||||||
|
"@communityRating": {},
|
||||||
"continuePage": "Continue - page {page}",
|
"continuePage": "Continue - page {page}",
|
||||||
"@continuePage": {
|
"@continuePage": {
|
||||||
"description": "Continue - page 1",
|
"description": "Continue - page 1",
|
||||||
|
|
@ -71,12 +103,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controls": "Controls",
|
"controls": "Controls",
|
||||||
|
"@controls": {},
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
"@dashboard": {},
|
||||||
"dashboardContinue": "Continue",
|
"dashboardContinue": "Continue",
|
||||||
|
"@dashboardContinue": {},
|
||||||
"dashboardContinueListening": "Continue Listening",
|
"dashboardContinueListening": "Continue Listening",
|
||||||
|
"@dashboardContinueListening": {},
|
||||||
"dashboardContinueReading": "Continue Reading",
|
"dashboardContinueReading": "Continue Reading",
|
||||||
|
"@dashboardContinueReading": {},
|
||||||
"dashboardContinueWatching": "Continue Watching",
|
"dashboardContinueWatching": "Continue Watching",
|
||||||
|
"@dashboardContinueWatching": {},
|
||||||
"dashboardNextUp": "Next-up",
|
"dashboardNextUp": "Next-up",
|
||||||
|
"@dashboardNextUp": {},
|
||||||
"dashboardRecentlyAdded": "Recently added in {name}",
|
"dashboardRecentlyAdded": "Recently added in {name}",
|
||||||
"@dashboardRecentlyAdded": {
|
"@dashboardRecentlyAdded": {
|
||||||
"description": "Recently added on home screen",
|
"description": "Recently added on home screen",
|
||||||
|
|
@ -87,10 +126,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dateAdded": "Date added",
|
"dateAdded": "Date added",
|
||||||
|
"@dateAdded": {},
|
||||||
"dateLastContentAdded": "Date last content added",
|
"dateLastContentAdded": "Date last content added",
|
||||||
|
"@dateLastContentAdded": {},
|
||||||
"datePlayed": "Date played",
|
"datePlayed": "Date played",
|
||||||
|
"@datePlayed": {},
|
||||||
"days": "Days",
|
"days": "Days",
|
||||||
|
"@days": {},
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"@delete": {},
|
||||||
"deleteFileFromSystem": "Deleting this item {item} will delete it from both the file system and your media library.\nAre you sure you wish to continue?",
|
"deleteFileFromSystem": "Deleting this item {item} will delete it from both the file system and your media library.\nAre you sure you wish to continue?",
|
||||||
"@deleteFileFromSystem": {
|
"@deleteFileFromSystem": {
|
||||||
"description": "Delete file from system",
|
"description": "Delete file from system",
|
||||||
|
|
@ -110,7 +154,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descending": "Descending",
|
"descending": "Descending",
|
||||||
"director": "{count, plural, other{Director} two{Directors}}",
|
"@descending": {},
|
||||||
|
"director": "{count, plural, other{Director} two{Directors}}",
|
||||||
"@director": {
|
"@director": {
|
||||||
"description": "director",
|
"description": "director",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -120,19 +165,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disableFilters": "Disable filters",
|
"disableFilters": "Turn off filters",
|
||||||
"disabled": "Disabled",
|
"@disableFilters": {},
|
||||||
|
"disabled": "Off",
|
||||||
|
"@disabled": {},
|
||||||
"discovered": "Discovered",
|
"discovered": "Discovered",
|
||||||
|
"@discovered": {},
|
||||||
"displayLanguage": "Display language",
|
"displayLanguage": "Display language",
|
||||||
"downloadsClearDesc": "Are you sure you want to remove all synced data?\nThis will clear all data for every synced user!",
|
"@displayLanguage": {},
|
||||||
|
"downloadsClearDesc": "Remove all synced data, clearing\nall data for every synced user?",
|
||||||
|
"@downloadsClearDesc": {},
|
||||||
"downloadsClearTitle": "Clear synced data",
|
"downloadsClearTitle": "Clear synced data",
|
||||||
|
"@downloadsClearTitle": {},
|
||||||
"downloadsPath": "Path",
|
"downloadsPath": "Path",
|
||||||
|
"@downloadsPath": {},
|
||||||
"downloadsSyncedData": "Synced data",
|
"downloadsSyncedData": "Synced data",
|
||||||
|
"@downloadsSyncedData": {},
|
||||||
"downloadsTitle": "Downloads",
|
"downloadsTitle": "Downloads",
|
||||||
|
"@downloadsTitle": {},
|
||||||
"dynamicText": "Dynamic",
|
"dynamicText": "Dynamic",
|
||||||
|
"@dynamicText": {},
|
||||||
"editMetadata": "Edit metadata",
|
"editMetadata": "Edit metadata",
|
||||||
|
"@editMetadata": {},
|
||||||
"empty": "Empty",
|
"empty": "Empty",
|
||||||
"enabled": "Enabled",
|
"@empty": {},
|
||||||
|
"enabled": "On",
|
||||||
|
"@enabled": {},
|
||||||
"endsAt": "ends at {date}",
|
"endsAt": "ends at {date}",
|
||||||
"@endsAt": {
|
"@endsAt": {
|
||||||
"description": "endsAt",
|
"description": "endsAt",
|
||||||
|
|
@ -154,10 +212,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failedToLoadImage": "Failed to load image",
|
"@error": {},
|
||||||
|
"failedToLoadImage": "Could not load image",
|
||||||
|
"@failedToLoadImage": {},
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
|
"@favorite": {},
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
"fetchingLibrary": "Fetching library items",
|
"@favorites": {},
|
||||||
|
"fetchingLibrary": "Fetching library items…",
|
||||||
|
"@fetchingLibrary": {},
|
||||||
"filter": "{count, plural, other{Filters} one{Filter}}",
|
"filter": "{count, plural, other{Filters} one{Filter}}",
|
||||||
"@filter": {
|
"@filter": {
|
||||||
"description": "filter",
|
"description": "filter",
|
||||||
|
|
@ -169,9 +232,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
|
"@folders": {},
|
||||||
"fontColor": "Font color",
|
"fontColor": "Font color",
|
||||||
|
"@fontColor": {},
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
|
"@fontSize": {},
|
||||||
"forceRefresh": "Force refresh",
|
"forceRefresh": "Force refresh",
|
||||||
|
"@forceRefresh": {},
|
||||||
"genre": "{count, plural, other{Genres} one{Genre}}",
|
"genre": "{count, plural, other{Genres} one{Genre}}",
|
||||||
"@genre": {
|
"@genre": {
|
||||||
"description": "genre",
|
"description": "genre",
|
||||||
|
|
@ -183,19 +250,35 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"goTo": "Go To",
|
"goTo": "Go To",
|
||||||
|
"@goTo": {},
|
||||||
"grid": "Grid",
|
"grid": "Grid",
|
||||||
|
"@grid": {},
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
|
"@group": {},
|
||||||
"groupBy": "Group by",
|
"groupBy": "Group by",
|
||||||
|
"@groupBy": {},
|
||||||
"heightOffset": "Height offset",
|
"heightOffset": "Height offset",
|
||||||
|
"@heightOffset": {},
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
|
"@hide": {},
|
||||||
"hideEmpty": "Hide empty",
|
"hideEmpty": "Hide empty",
|
||||||
|
"@hideEmpty": {},
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
|
"@home": {},
|
||||||
|
"homeBannerBanner": "Banner",
|
||||||
|
"homeBannerCarousel": "Carousel",
|
||||||
"identify": "Identify",
|
"identify": "Identify",
|
||||||
|
"@identify": {},
|
||||||
"immediately": "Immediately",
|
"immediately": "Immediately",
|
||||||
"incorrectPinTryAgain": "Incorrect pin try again",
|
"@immediately": {},
|
||||||
|
"incorrectPinTryAgain": "Incorrect PIN. Try again.",
|
||||||
|
"@incorrectPinTryAgain": {},
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"invalidUrl": "Invalid url",
|
"@info": {},
|
||||||
"invalidUrlDesc": "Url needs to start with http(s)://",
|
"invalidUrl": "Invalid URL",
|
||||||
|
"@invalidUrl": {},
|
||||||
|
"invalidUrlDesc": "URL needs to start with http(s)://",
|
||||||
|
"@invalidUrlDesc": {},
|
||||||
"itemCount": "Item count: {count}",
|
"itemCount": "Item count: {count}",
|
||||||
"@itemCount": {
|
"@itemCount": {
|
||||||
"description": "Item count",
|
"description": "Item count",
|
||||||
|
|
@ -205,7 +288,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"label": "{count, plural, other{Labels} one{Label}}",
|
"label": "{count, plural, other{Labels} one{Label}}",
|
||||||
"@label": {
|
"@label": {
|
||||||
"description": "label",
|
"description": "label",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -225,16 +308,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libraryFetchNoItemsFound": "No items found, try different settings.",
|
"libraryFetchNoItemsFound": "No items found. Try different settings.",
|
||||||
"libraryPageSizeDesc": "Set the amount to load at a time. 0 disables paging",
|
"@libraryFetchNoItemsFound": {},
|
||||||
|
"libraryPageSizeDesc": "Set the amount to load at a time. 0 turns off paging.",
|
||||||
|
"@libraryPageSizeDesc": {},
|
||||||
"libraryPageSizeTitle": "Library page size",
|
"libraryPageSizeTitle": "Library page size",
|
||||||
|
"@libraryPageSizeTitle": {},
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
|
"@light": {},
|
||||||
"list": "List",
|
"list": "List",
|
||||||
|
"@list": {},
|
||||||
"lockscreen": "Lockscreen",
|
"lockscreen": "Lockscreen",
|
||||||
|
"@lockscreen": {},
|
||||||
"loggedIn": "Logged-in",
|
"loggedIn": "Logged-in",
|
||||||
"login": "Login",
|
"@loggedIn": {},
|
||||||
"logout": "Logout",
|
"login": "Log in",
|
||||||
"logoutUserPopupContent": "This will log-out {userName} and delete te user from the app.\nYou will have to log back in to {serverName}.",
|
"@login": {},
|
||||||
|
"logout": "Log out",
|
||||||
|
"@logout": {},
|
||||||
|
"logoutUserPopupContent": "This will log out {userName} and delete the user from the app.\nYou will have to log back in on {serverName}.",
|
||||||
"@logoutUserPopupContent": {
|
"@logoutUserPopupContent": {
|
||||||
"description": "Pop-up for loging out the user description",
|
"description": "Pop-up for loging out the user description",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -246,7 +338,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logoutUserPopupTitle": "Log-out user {userName}?",
|
"logoutUserPopupTitle": "Log out {userName}?",
|
||||||
"@logoutUserPopupTitle": {
|
"@logoutUserPopupTitle": {
|
||||||
"description": "Pop-up for loging out the user",
|
"description": "Pop-up for loging out the user",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -256,21 +348,37 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loop": "Loop",
|
"loop": "Loop",
|
||||||
|
"@loop": {},
|
||||||
"markAsUnwatched": "Mark as unwatched",
|
"markAsUnwatched": "Mark as unwatched",
|
||||||
|
"@markAsUnwatched": {},
|
||||||
"markAsWatched": "Mark as watched",
|
"markAsWatched": "Mark as watched",
|
||||||
|
"@markAsWatched": {},
|
||||||
"masonry": "Masonry",
|
"masonry": "Masonry",
|
||||||
|
"@masonry": {},
|
||||||
"mediaTypeBase": "Base Type",
|
"mediaTypeBase": "Base Type",
|
||||||
|
"@mediaTypeBase": {},
|
||||||
"mediaTypeBook": "Book",
|
"mediaTypeBook": "Book",
|
||||||
|
"@mediaTypeBook": {},
|
||||||
"mediaTypeBoxset": "Boxset",
|
"mediaTypeBoxset": "Boxset",
|
||||||
|
"@mediaTypeBoxset": {},
|
||||||
"mediaTypeEpisode": "Episode",
|
"mediaTypeEpisode": "Episode",
|
||||||
|
"@mediaTypeEpisode": {},
|
||||||
"mediaTypeFolder": "Folder",
|
"mediaTypeFolder": "Folder",
|
||||||
|
"@mediaTypeFolder": {},
|
||||||
"mediaTypeMovie": "Movie",
|
"mediaTypeMovie": "Movie",
|
||||||
|
"@mediaTypeMovie": {},
|
||||||
"mediaTypePerson": "Person",
|
"mediaTypePerson": "Person",
|
||||||
|
"@mediaTypePerson": {},
|
||||||
"mediaTypePhoto": "Photo",
|
"mediaTypePhoto": "Photo",
|
||||||
|
"@mediaTypePhoto": {},
|
||||||
"mediaTypePhotoAlbum": "Photo Album",
|
"mediaTypePhotoAlbum": "Photo Album",
|
||||||
|
"@mediaTypePhotoAlbum": {},
|
||||||
"mediaTypePlaylist": "Playlist",
|
"mediaTypePlaylist": "Playlist",
|
||||||
|
"@mediaTypePlaylist": {},
|
||||||
"mediaTypeSeason": "Season",
|
"mediaTypeSeason": "Season",
|
||||||
|
"@mediaTypeSeason": {},
|
||||||
"mediaTypeSeries": "Series",
|
"mediaTypeSeries": "Series",
|
||||||
|
"@mediaTypeSeries": {},
|
||||||
"metaDataSavedFor": "Metadata saved for {item}",
|
"metaDataSavedFor": "Metadata saved for {item}",
|
||||||
"@metaDataSavedFor": {
|
"@metaDataSavedFor": {
|
||||||
"description": "metaDataSavedFor",
|
"description": "metaDataSavedFor",
|
||||||
|
|
@ -281,8 +389,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadataRefreshDefault": "Scan for new and updated files",
|
"metadataRefreshDefault": "Scan for new and updated files",
|
||||||
|
"@metadataRefreshDefault": {},
|
||||||
"metadataRefreshFull": "Replace all metadata",
|
"metadataRefreshFull": "Replace all metadata",
|
||||||
|
"@metadataRefreshFull": {},
|
||||||
"metadataRefreshValidation": "Search for missing metadata",
|
"metadataRefreshValidation": "Search for missing metadata",
|
||||||
|
"@metadataRefreshValidation": {},
|
||||||
"minutes": "{count, plural, other{Minutes} one{Minute} }",
|
"minutes": "{count, plural, other{Minutes} one{Minute} }",
|
||||||
"@minutes": {
|
"@minutes": {
|
||||||
"description": "minute",
|
"description": "minute",
|
||||||
|
|
@ -294,6 +405,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"@mode": {},
|
||||||
"moreFrom": "More from {info}",
|
"moreFrom": "More from {info}",
|
||||||
"@moreFrom": {
|
"@moreFrom": {
|
||||||
"description": "More from",
|
"description": "More from",
|
||||||
|
|
@ -304,32 +416,61 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moreOptions": "More options",
|
"moreOptions": "More options",
|
||||||
|
"@moreOptions": {},
|
||||||
"mouseDragSupport": "Drag using mouse",
|
"mouseDragSupport": "Drag using mouse",
|
||||||
|
"@mouseDragSupport": {},
|
||||||
"musicAlbum": "Album",
|
"musicAlbum": "Album",
|
||||||
|
"@musicAlbum": {},
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"@name": {},
|
||||||
|
"nativeName": "English",
|
||||||
|
"@nativeName": {},
|
||||||
"navigation": "Navigation",
|
"navigation": "Navigation",
|
||||||
|
"@navigation": {},
|
||||||
"navigationDashboard": "Dashboard",
|
"navigationDashboard": "Dashboard",
|
||||||
|
"@navigationDashboard": {},
|
||||||
"navigationFavorites": "Favorites",
|
"navigationFavorites": "Favorites",
|
||||||
|
"@navigationFavorites": {},
|
||||||
"navigationSync": "Synced",
|
"navigationSync": "Synced",
|
||||||
|
"@navigationSync": {},
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
|
"@never": {},
|
||||||
"nextUp": "Next Up",
|
"nextUp": "Next Up",
|
||||||
|
"@nextUp": {},
|
||||||
"noItemsSynced": "No items synced",
|
"noItemsSynced": "No items synced",
|
||||||
|
"@noItemsSynced": {},
|
||||||
"noItemsToShow": "No items to show",
|
"noItemsToShow": "No items to show",
|
||||||
|
"@noItemsToShow": {},
|
||||||
"noRating": "No rating",
|
"noRating": "No rating",
|
||||||
|
"@noRating": {},
|
||||||
"noResults": "No results",
|
"noResults": "No results",
|
||||||
|
"@noResults": {},
|
||||||
"noServersFound": "No new servers found",
|
"noServersFound": "No new servers found",
|
||||||
|
"@noServersFound": {},
|
||||||
"noSuggestionsFound": "No suggestions found",
|
"noSuggestionsFound": "No suggestions found",
|
||||||
|
"@noSuggestionsFound": {},
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
"@none": {},
|
||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
|
"@normal": {},
|
||||||
"notPartOfAlbum": "Not part of a album",
|
"notPartOfAlbum": "Not part of a album",
|
||||||
|
"@notPartOfAlbum": {},
|
||||||
"openParent": "Open parent",
|
"openParent": "Open parent",
|
||||||
|
"@openParent": {},
|
||||||
"openShow": "Open show",
|
"openShow": "Open show",
|
||||||
|
"@openShow": {},
|
||||||
"openWebLink": "Open web link",
|
"openWebLink": "Open web link",
|
||||||
|
"@openWebLink": {},
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
|
"@options": {},
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
|
"@other": {},
|
||||||
"outlineColor": "Outline color",
|
"outlineColor": "Outline color",
|
||||||
|
"@outlineColor": {},
|
||||||
"outlineSize": "Outline size",
|
"outlineSize": "Outline size",
|
||||||
|
"@outlineSize": {},
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
|
"@overview": {},
|
||||||
"page": "Page {index}",
|
"page": "Page {index}",
|
||||||
"@page": {
|
"@page": {
|
||||||
"description": "page",
|
"description": "page",
|
||||||
|
|
@ -340,11 +481,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parentalRating": "Parental Rating",
|
"parentalRating": "Parental Rating",
|
||||||
|
"@parentalRating": {},
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"@password": {},
|
||||||
"pathClearTitle": "Clear downloads path",
|
"pathClearTitle": "Clear downloads path",
|
||||||
|
"@pathClearTitle": {},
|
||||||
"pathEditDesc": "This location is set for all users, any synced data will no longer be accessible.\nIt will remain on your storage.",
|
"pathEditDesc": "This location is set for all users, any synced data will no longer be accessible.\nIt will remain on your storage.",
|
||||||
|
"@pathEditDesc": {},
|
||||||
"pathEditSelect": "Select downloads destination",
|
"pathEditSelect": "Select downloads destination",
|
||||||
|
"@pathEditSelect": {},
|
||||||
"pathEditTitle": "Change location",
|
"pathEditTitle": "Change location",
|
||||||
|
"@pathEditTitle": {},
|
||||||
"play": "Play {item}",
|
"play": "Play {item}",
|
||||||
"@play": {
|
"@play": {
|
||||||
"description": "Play with",
|
"description": "Play with",
|
||||||
|
|
@ -355,6 +502,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playCount": "Play count",
|
"playCount": "Play count",
|
||||||
|
"@playCount": {},
|
||||||
"playFrom": "Play from {name}",
|
"playFrom": "Play from {name}",
|
||||||
"@playFrom": {
|
"@playFrom": {
|
||||||
"description": "playFrom",
|
"description": "playFrom",
|
||||||
|
|
@ -374,13 +522,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playLabel": "Play",
|
"playLabel": "Play",
|
||||||
|
"@playLabel": {},
|
||||||
"playVideos": "Play videos",
|
"playVideos": "Play videos",
|
||||||
|
"@playVideos": {},
|
||||||
"played": "Played",
|
"played": "Played",
|
||||||
|
"@played": {},
|
||||||
"quickConnectAction": "Enter quick connect code for",
|
"quickConnectAction": "Enter quick connect code for",
|
||||||
|
"@quickConnectAction": {},
|
||||||
"quickConnectInputACode": "Input a code",
|
"quickConnectInputACode": "Input a code",
|
||||||
|
"@quickConnectInputACode": {},
|
||||||
"quickConnectTitle": "Quick-connect",
|
"quickConnectTitle": "Quick-connect",
|
||||||
|
"@quickConnectTitle": {},
|
||||||
"quickConnectWrongCode": "Wrong code",
|
"quickConnectWrongCode": "Wrong code",
|
||||||
|
"@quickConnectWrongCode": {},
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
|
"@random": {},
|
||||||
"rating": "{count, plural, other{Ratings} one{Rating}}",
|
"rating": "{count, plural, other{Ratings} one{Rating}}",
|
||||||
"@rating": {
|
"@rating": {
|
||||||
"description": "rating",
|
"description": "rating",
|
||||||
|
|
@ -392,6 +548,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reWatch": "Rewatch",
|
"reWatch": "Rewatch",
|
||||||
|
"@reWatch": {},
|
||||||
"read": "Read {item}",
|
"read": "Read {item}",
|
||||||
"@read": {
|
"@read": {
|
||||||
"description": "read",
|
"description": "read",
|
||||||
|
|
@ -411,8 +568,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recursive": "Recursive",
|
"recursive": "Recursive",
|
||||||
|
"@recursive": {},
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
"@refresh": {},
|
||||||
"refreshMetadata": "Refresh metadata",
|
"refreshMetadata": "Refresh metadata",
|
||||||
|
"@refreshMetadata": {},
|
||||||
"refreshPopup": "Refresh - {name}",
|
"refreshPopup": "Refresh - {name}",
|
||||||
"@refreshPopup": {
|
"@refreshPopup": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -421,17 +581,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"refreshPopupContentMetadata": "Metadata is refreshed based on settings and internet services that are enabled in the Dashboard.",
|
"refreshPopupContentMetadata": "Metadata is refreshed based on settings and Internet services turned on in the dashboard.",
|
||||||
|
"@refreshPopupContentMetadata": {},
|
||||||
"related": "Related",
|
"related": "Related",
|
||||||
|
"@related": {},
|
||||||
"releaseDate": "Release date",
|
"releaseDate": "Release date",
|
||||||
|
"@releaseDate": {},
|
||||||
"removeAsFavorite": "Remove as favorite",
|
"removeAsFavorite": "Remove as favorite",
|
||||||
"removeFromCollection": "Remove to collection",
|
"@removeAsFavorite": {},
|
||||||
"removeFromPlaylist": "Remove to playlist",
|
"removeFromCollection": "Remove from collection",
|
||||||
|
"@removeFromCollection": {},
|
||||||
|
"removeFromPlaylist": "Remove from playlist",
|
||||||
|
"@removeFromPlaylist": {},
|
||||||
"replaceAllImages": "Replace all images",
|
"replaceAllImages": "Replace all images",
|
||||||
|
"@replaceAllImages": {},
|
||||||
"replaceExistingImages": "Replace existing images",
|
"replaceExistingImages": "Replace existing images",
|
||||||
|
"@replaceExistingImages": {},
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
|
"@restart": {},
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
|
"@result": {},
|
||||||
"resumable": "Resumable",
|
"resumable": "Resumable",
|
||||||
|
"@resumable": {},
|
||||||
"resume": "Resume {item}",
|
"resume": "Resume {item}",
|
||||||
"@resume": {
|
"@resume": {
|
||||||
"description": "resume",
|
"description": "resume",
|
||||||
|
|
@ -442,12 +613,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"retrievePublicListOfUsers": "Retrieve public list of users",
|
"retrievePublicListOfUsers": "Retrieve public list of users",
|
||||||
|
"@retrievePublicListOfUsers": {},
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
|
"@retry": {},
|
||||||
"runTime": "Run time",
|
"runTime": "Run time",
|
||||||
|
"@runTime": {},
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"@save": {},
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
|
"@saved": {},
|
||||||
"scanBiometricHint": "Verify identity",
|
"scanBiometricHint": "Verify identity",
|
||||||
|
"@scanBiometricHint": {},
|
||||||
"scanLibrary": "Scan library",
|
"scanLibrary": "Scan library",
|
||||||
|
"@scanLibrary": {},
|
||||||
"scanYourFingerprintToAuthenticate": "Scan your fingerprint to authenticate {user}",
|
"scanYourFingerprintToAuthenticate": "Scan your fingerprint to authenticate {user}",
|
||||||
"@scanYourFingerprintToAuthenticate": {
|
"@scanYourFingerprintToAuthenticate": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -456,7 +634,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scanningName": "Scanning - {name}",
|
"scanningName": "Scanning - {name}…",
|
||||||
"@scanningName": {
|
"@scanningName": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
|
|
@ -465,7 +643,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scrollToTop": "Scroll to top",
|
"scrollToTop": "Scroll to top",
|
||||||
|
"@scrollToTop": {},
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
"@search": {},
|
||||||
"season": "{count, plural, other{Seasons} one{Season} }",
|
"season": "{count, plural, other{Seasons} one{Season} }",
|
||||||
"@season": {
|
"@season": {
|
||||||
"description": "season",
|
"description": "season",
|
||||||
|
|
@ -487,9 +667,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selectAll": "Select all",
|
"selectAll": "Select all",
|
||||||
|
"@selectAll": {},
|
||||||
"selectTime": "Select time",
|
"selectTime": "Select time",
|
||||||
|
"@selectTime": {},
|
||||||
"selectViewType": "Select view type",
|
"selectViewType": "Select view type",
|
||||||
|
"@selectViewType": {},
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
|
"@selected": {},
|
||||||
"selectedWith": "Selected {info}",
|
"selectedWith": "Selected {info}",
|
||||||
"@selectedWith": {
|
"@selectedWith": {
|
||||||
"description": "selected",
|
"description": "selected",
|
||||||
|
|
@ -500,7 +684,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"separate": "Separate",
|
"separate": "Separate",
|
||||||
|
"@separate": {},
|
||||||
"server": "Server",
|
"server": "Server",
|
||||||
|
"@server": {},
|
||||||
"set": "Set",
|
"set": "Set",
|
||||||
"@set": {
|
"@set": {
|
||||||
"description": "Use for setting a certain value",
|
"description": "Use for setting a certain value",
|
||||||
|
|
@ -516,50 +702,95 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settingSecurityApplockTitle": "App lock",
|
"settingSecurityApplockTitle": "App lock",
|
||||||
|
"@settingSecurityApplockTitle": {},
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"@settings": {},
|
||||||
"settingsBlurEpisodesDesc": "Blur all upcoming episodes",
|
"settingsBlurEpisodesDesc": "Blur all upcoming episodes",
|
||||||
|
"@settingsBlurEpisodesDesc": {},
|
||||||
"settingsBlurEpisodesTitle": "Blur next-up episodes",
|
"settingsBlurEpisodesTitle": "Blur next-up episodes",
|
||||||
|
"@settingsBlurEpisodesTitle": {},
|
||||||
"settingsBlurredPlaceholderDesc": "Show blurred background when loading posters",
|
"settingsBlurredPlaceholderDesc": "Show blurred background when loading posters",
|
||||||
|
"@settingsBlurredPlaceholderDesc": {},
|
||||||
"settingsBlurredPlaceholderTitle": "Blurred placeholder",
|
"settingsBlurredPlaceholderTitle": "Blurred placeholder",
|
||||||
|
"@settingsBlurredPlaceholderTitle": {},
|
||||||
"settingsClientDesc": "General, Time-out, Layout, Theme",
|
"settingsClientDesc": "General, Time-out, Layout, Theme",
|
||||||
|
"@settingsClientDesc": {},
|
||||||
"settingsClientTitle": "Fladder",
|
"settingsClientTitle": "Fladder",
|
||||||
|
"@settingsClientTitle": {},
|
||||||
"settingsContinue": "Continue",
|
"settingsContinue": "Continue",
|
||||||
|
"@settingsContinue": {},
|
||||||
"settingsEnableOsMediaControls": "Enable OS media controls",
|
"settingsEnableOsMediaControls": "Enable OS media controls",
|
||||||
"settingsHomeCarouselDesc": "Shows a carousel on the dashboard screen",
|
"@settingsEnableOsMediaControls": {},
|
||||||
"settingsHomeCarouselTitle": "Dashboard carousel",
|
"settingsHomeBannerDescription": "Switch between a banner or scrollable carousel",
|
||||||
|
"settingsHomeBannerTitle": "Home banner",
|
||||||
|
"settingsHomeCarouselDesc": "Shows a banner on the dashboard screen",
|
||||||
|
"settingsHomeCarouselTitle": "Dashboard banner",
|
||||||
"settingsHomeNextUpDesc": "Type of posters shown in the dashboard screen",
|
"settingsHomeNextUpDesc": "Type of posters shown in the dashboard screen",
|
||||||
|
"@settingsHomeNextUpDesc": {},
|
||||||
"settingsHomeNextUpTitle": "Next-up posters",
|
"settingsHomeNextUpTitle": "Next-up posters",
|
||||||
|
"@settingsHomeNextUpTitle": {},
|
||||||
"settingsNextUpCutoffDays": "Next-up cutoff days",
|
"settingsNextUpCutoffDays": "Next-up cutoff days",
|
||||||
|
"@settingsNextUpCutoffDays": {},
|
||||||
"settingsPlayerCustomSubtitlesDesc": "Customize Size, Color, Position, Outline",
|
"settingsPlayerCustomSubtitlesDesc": "Customize Size, Color, Position, Outline",
|
||||||
|
"@settingsPlayerCustomSubtitlesDesc": {},
|
||||||
"settingsPlayerCustomSubtitlesTitle": "Customize subtitles",
|
"settingsPlayerCustomSubtitlesTitle": "Customize subtitles",
|
||||||
|
"@settingsPlayerCustomSubtitlesTitle": {},
|
||||||
"settingsPlayerDesc": "Aspect-ratio, Advanced",
|
"settingsPlayerDesc": "Aspect-ratio, Advanced",
|
||||||
"settingsPlayerMobileWarning": "Enabling Hardware acceleration and native libass subtitles on Android might cause some subtitles to not render.",
|
"@settingsPlayerDesc": {},
|
||||||
|
"settingsPlayerMobileWarning": "Turning on hardware acceleration and native libass subtitles on Android might cause some subtitles to not render.",
|
||||||
|
"@settingsPlayerMobileWarning": {},
|
||||||
"settingsPlayerNativeLibassAccelDesc": "Use video player libass subtitle renderer",
|
"settingsPlayerNativeLibassAccelDesc": "Use video player libass subtitle renderer",
|
||||||
|
"@settingsPlayerNativeLibassAccelDesc": {},
|
||||||
"settingsPlayerNativeLibassAccelTitle": "Native libass subtitles",
|
"settingsPlayerNativeLibassAccelTitle": "Native libass subtitles",
|
||||||
|
"@settingsPlayerNativeLibassAccelTitle": {},
|
||||||
"settingsPlayerTitle": "Player",
|
"settingsPlayerTitle": "Player",
|
||||||
"settingsPlayerVideoHWAccelDesc": "Use the gpu to render video (recommended)",
|
"@settingsPlayerTitle": {},
|
||||||
|
"settingsPlayerVideoHWAccelDesc": "Use the GPU to render video (recommended)",
|
||||||
|
"@settingsPlayerVideoHWAccelDesc": {},
|
||||||
"settingsPlayerVideoHWAccelTitle": "Hardware acceleration",
|
"settingsPlayerVideoHWAccelTitle": "Hardware acceleration",
|
||||||
|
"@settingsPlayerVideoHWAccelTitle": {},
|
||||||
"settingsPosterPinch": "Pinch-zoom to scale posters",
|
"settingsPosterPinch": "Pinch-zoom to scale posters",
|
||||||
|
"@settingsPosterPinch": {},
|
||||||
"settingsPosterSize": "Poster size",
|
"settingsPosterSize": "Poster size",
|
||||||
|
"@settingsPosterSize": {},
|
||||||
"settingsPosterSlider": "Show scale slider",
|
"settingsPosterSlider": "Show scale slider",
|
||||||
|
"@settingsPosterSlider": {},
|
||||||
"settingsProfileDesc": "Lockscreen",
|
"settingsProfileDesc": "Lockscreen",
|
||||||
|
"@settingsProfileDesc": {},
|
||||||
"settingsProfileTitle": "Profile",
|
"settingsProfileTitle": "Profile",
|
||||||
|
"@settingsProfileTitle": {},
|
||||||
"settingsQuickConnectTitle": "Quick connect",
|
"settingsQuickConnectTitle": "Quick connect",
|
||||||
|
"@settingsQuickConnectTitle": {},
|
||||||
"settingsSecurity": "Security",
|
"settingsSecurity": "Security",
|
||||||
|
"@settingsSecurity": {},
|
||||||
"settingsShowScaleSlider": "Show poster size slide",
|
"settingsShowScaleSlider": "Show poster size slide",
|
||||||
|
"@settingsShowScaleSlider": {},
|
||||||
"settingsVisual": "Visual",
|
"settingsVisual": "Visual",
|
||||||
|
"@settingsVisual": {},
|
||||||
"shadow": "Shadow",
|
"shadow": "Shadow",
|
||||||
|
"@shadow": {},
|
||||||
"showAlbum": "Show album",
|
"showAlbum": "Show album",
|
||||||
|
"@showAlbum": {},
|
||||||
"showDetails": "Show details",
|
"showDetails": "Show details",
|
||||||
|
"@showDetails": {},
|
||||||
"showEmpty": "Show empty",
|
"showEmpty": "Show empty",
|
||||||
|
"@showEmpty": {},
|
||||||
"shuffleGallery": "Shuffle gallery",
|
"shuffleGallery": "Shuffle gallery",
|
||||||
|
"@shuffleGallery": {},
|
||||||
"shuffleVideos": "Shuffle videos",
|
"shuffleVideos": "Shuffle videos",
|
||||||
|
"@shuffleVideos": {},
|
||||||
"somethingWentWrong": "Something went wrong",
|
"somethingWentWrong": "Something went wrong",
|
||||||
"somethingWentWrongPasswordCheck": "Something went wrong, check your password",
|
"@somethingWentWrong": {},
|
||||||
|
"somethingWentWrongPasswordCheck": "Something went wrong. Check your password.",
|
||||||
|
"@somethingWentWrongPasswordCheck": {},
|
||||||
"sortBy": "Sort by",
|
"sortBy": "Sort by",
|
||||||
|
"@sortBy": {},
|
||||||
"sortName": "Name",
|
"sortName": "Name",
|
||||||
|
"@sortName": {},
|
||||||
"sortOrder": "Sort order",
|
"sortOrder": "Sort order",
|
||||||
|
"@sortOrder": {},
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"@start": {},
|
||||||
"studio": "{count, plural, other{Studios} one{Studio}}",
|
"studio": "{count, plural, other{Studios} one{Studio}}",
|
||||||
"@studio": {
|
"@studio": {
|
||||||
"description": "studio",
|
"description": "studio",
|
||||||
|
|
@ -571,10 +802,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subtitleConfigurator": "Subtitle configurator",
|
"subtitleConfigurator": "Subtitle configurator",
|
||||||
|
"@subtitleConfigurator": {},
|
||||||
"subtitleConfiguratorPlaceHolder": "This is placeholder text, \n nothing to see here.",
|
"subtitleConfiguratorPlaceHolder": "This is placeholder text, \n nothing to see here.",
|
||||||
|
"@subtitleConfiguratorPlaceHolder": {},
|
||||||
"subtitles": "Subtitles",
|
"subtitles": "Subtitles",
|
||||||
|
"@subtitles": {},
|
||||||
"switchUser": "Switch user",
|
"switchUser": "Switch user",
|
||||||
|
"@switchUser": {},
|
||||||
"sync": "Sync",
|
"sync": "Sync",
|
||||||
|
"@sync": {},
|
||||||
"syncDeleteItemDesc": "Delete all synced data for?\n{item}",
|
"syncDeleteItemDesc": "Delete all synced data for?\n{item}",
|
||||||
"@syncDeleteItemDesc": {
|
"@syncDeleteItemDesc": {
|
||||||
"description": "Sync delete item pop-up window",
|
"description": "Sync delete item pop-up window",
|
||||||
|
|
@ -585,12 +821,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"syncDeleteItemTitle": "Delete synced item",
|
"syncDeleteItemTitle": "Delete synced item",
|
||||||
|
"@syncDeleteItemTitle": {},
|
||||||
"syncDeletePopupPermanent": "This action is permanent and will remove all localy synced files",
|
"syncDeletePopupPermanent": "This action is permanent and will remove all localy synced files",
|
||||||
|
"@syncDeletePopupPermanent": {},
|
||||||
"syncDetails": "Sync details",
|
"syncDetails": "Sync details",
|
||||||
|
"@syncDetails": {},
|
||||||
"syncOpenParent": "Open parent",
|
"syncOpenParent": "Open parent",
|
||||||
|
"@syncOpenParent": {},
|
||||||
"syncRemoveDataDesc": "Delete synced video data? This is permanent and you will need to re-sync the files",
|
"syncRemoveDataDesc": "Delete synced video data? This is permanent and you will need to re-sync the files",
|
||||||
|
"@syncRemoveDataDesc": {},
|
||||||
"syncRemoveDataTitle": "Remove synced data?",
|
"syncRemoveDataTitle": "Remove synced data?",
|
||||||
|
"@syncRemoveDataTitle": {},
|
||||||
"syncedItems": "Synced items",
|
"syncedItems": "Synced items",
|
||||||
|
"@syncedItems": {},
|
||||||
"tag": "{count, plural, one{Tag} other{Tags}}",
|
"tag": "{count, plural, one{Tag} other{Tags}}",
|
||||||
"@tag": {
|
"@tag": {
|
||||||
"description": "tag",
|
"description": "tag",
|
||||||
|
|
@ -602,10 +845,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
|
"@theme": {},
|
||||||
"themeColor": "Theme color",
|
"themeColor": "Theme color",
|
||||||
|
"@themeColor": {},
|
||||||
"themeModeDark": "Dark",
|
"themeModeDark": "Dark",
|
||||||
|
"@themeModeDark": {},
|
||||||
"themeModeLight": "Light",
|
"themeModeLight": "Light",
|
||||||
|
"@themeModeLight": {},
|
||||||
"themeModeSystem": "System",
|
"themeModeSystem": "System",
|
||||||
|
"@themeModeSystem": {},
|
||||||
"timeAndAnnotation": "{minutes} and {seconds}",
|
"timeAndAnnotation": "{minutes} and {seconds}",
|
||||||
"@timeAndAnnotation": {
|
"@timeAndAnnotation": {
|
||||||
"description": "timeAndAnnotation",
|
"description": "timeAndAnnotation",
|
||||||
|
|
@ -619,6 +867,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeOut": "Time-out",
|
"timeOut": "Time-out",
|
||||||
|
"@timeOut": {},
|
||||||
"totalSize": "Total size: {size}",
|
"totalSize": "Total size: {size}",
|
||||||
"@totalSize": {
|
"@totalSize": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -638,24 +887,43 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unPlayed": "Unplayed",
|
"unPlayed": "Unplayed",
|
||||||
|
"@unPlayed": {},
|
||||||
"unableToConnectHost": "Unable to connect to host",
|
"unableToConnectHost": "Unable to connect to host",
|
||||||
"unableToReverseAction": "This action can not be reversed, it will remove all settings.",
|
"@unableToConnectHost": {},
|
||||||
|
"unableToReverseAction": "This action can not be reversed. It will remove all settings.",
|
||||||
|
"@unableToReverseAction": {},
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
|
"@unknown": {},
|
||||||
"useDefaults": "Use defaults",
|
"useDefaults": "Use defaults",
|
||||||
|
"@useDefaults": {},
|
||||||
"userName": "Username",
|
"userName": "Username",
|
||||||
|
"@userName": {},
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
|
"@video": {},
|
||||||
"videoScaling": "Video scaling",
|
"videoScaling": "Video scaling",
|
||||||
|
"@videoScaling": {},
|
||||||
"videoScalingContain": "Contain",
|
"videoScalingContain": "Contain",
|
||||||
|
"@videoScalingContain": {},
|
||||||
"videoScalingCover": "Cover",
|
"videoScalingCover": "Cover",
|
||||||
|
"@videoScalingCover": {},
|
||||||
"videoScalingFill": "Fill",
|
"videoScalingFill": "Fill",
|
||||||
|
"@videoScalingFill": {},
|
||||||
"videoScalingFillScreenDesc": "Fill the navigation and statusbar",
|
"videoScalingFillScreenDesc": "Fill the navigation and statusbar",
|
||||||
|
"@videoScalingFillScreenDesc": {},
|
||||||
"videoScalingFillScreenNotif": "Fill-screen overwrites video fit, in horizontal rotation",
|
"videoScalingFillScreenNotif": "Fill-screen overwrites video fit, in horizontal rotation",
|
||||||
|
"@videoScalingFillScreenNotif": {},
|
||||||
"videoScalingFillScreenTitle": "Fill screen",
|
"videoScalingFillScreenTitle": "Fill screen",
|
||||||
|
"@videoScalingFillScreenTitle": {},
|
||||||
"videoScalingFitHeight": "Fit Height",
|
"videoScalingFitHeight": "Fit Height",
|
||||||
|
"@videoScalingFitHeight": {},
|
||||||
"videoScalingFitWidth": "Fit Width",
|
"videoScalingFitWidth": "Fit Width",
|
||||||
|
"@videoScalingFitWidth": {},
|
||||||
"videoScalingScaleDown": "ScaleDown",
|
"videoScalingScaleDown": "ScaleDown",
|
||||||
|
"@videoScalingScaleDown": {},
|
||||||
"viewPhotos": "View photos",
|
"viewPhotos": "View photos",
|
||||||
|
"@viewPhotos": {},
|
||||||
"watchOn": "Watch on",
|
"watchOn": "Watch on",
|
||||||
|
"@watchOn": {},
|
||||||
"writer": "{count, plural, other{Writer} two{Writers}}",
|
"writer": "{count, plural, other{Writer} two{Writers}}",
|
||||||
"@writer": {
|
"@writer": {
|
||||||
"description": "writer",
|
"description": "writer",
|
||||||
|
|
|
||||||
|
|
@ -1 +1,41 @@
|
||||||
{}
|
{
|
||||||
|
"es": [
|
||||||
|
"nativeName",
|
||||||
|
"settingsHomeBannerTitle",
|
||||||
|
"settingsHomeBannerDescription",
|
||||||
|
"homeBannerBanner",
|
||||||
|
"homeBannerCarousel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"nativeName",
|
||||||
|
"settingsHomeBannerTitle",
|
||||||
|
"settingsHomeBannerDescription",
|
||||||
|
"homeBannerBanner",
|
||||||
|
"homeBannerCarousel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ja": [
|
||||||
|
"nativeName",
|
||||||
|
"settingsHomeBannerTitle",
|
||||||
|
"settingsHomeBannerDescription",
|
||||||
|
"homeBannerBanner",
|
||||||
|
"homeBannerCarousel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"nl": [
|
||||||
|
"nativeName",
|
||||||
|
"settingsHomeBannerTitle",
|
||||||
|
"settingsHomeBannerDescription",
|
||||||
|
"homeBannerBanner",
|
||||||
|
"homeBannerCarousel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"nativeName",
|
||||||
|
"settingsHomeBannerTitle",
|
||||||
|
"settingsHomeBannerDescription",
|
||||||
|
"homeBannerBanner",
|
||||||
|
"homeBannerCarousel"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import 'dart:developer';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:fladder/util/custom_color_themes.dart';
|
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/util/custom_color_themes.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
part 'client_settings_model.freezed.dart';
|
part 'client_settings_model.freezed.dart';
|
||||||
part 'client_settings_model.g.dart';
|
part 'client_settings_model.g.dart';
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
|
||||||
Duration? nextUpDateCutoff,
|
Duration? nextUpDateCutoff,
|
||||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||||
ColorThemes? themeColor,
|
ColorThemes? themeColor,
|
||||||
|
@Default(HomeBanner.carousel) HomeBanner homeBanner,
|
||||||
@Default(false) bool amoledBlack,
|
@Default(false) bool amoledBlack,
|
||||||
@Default(false) bool blurPlaceHolders,
|
@Default(false) bool blurPlaceHolders,
|
||||||
@Default(false) bool blurUpcomingEpisodes,
|
@Default(false) bool blurUpcomingEpisodes,
|
||||||
|
|
@ -71,6 +73,18 @@ class LocaleConvert implements JsonConverter<Locale?, String?> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HomeBanner {
|
||||||
|
carousel,
|
||||||
|
banner;
|
||||||
|
|
||||||
|
const HomeBanner();
|
||||||
|
|
||||||
|
String label(BuildContext context) => switch (this) {
|
||||||
|
HomeBanner.carousel => context.localized.homeBannerCarousel,
|
||||||
|
HomeBanner.banner => context.localized.homeBannerBanner,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class Vector2 {
|
class Vector2 {
|
||||||
final double x;
|
final double x;
|
||||||
final double y;
|
final double y;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ mixin _$ClientSettingsModel {
|
||||||
Duration? get nextUpDateCutoff => throw _privateConstructorUsedError;
|
Duration? get nextUpDateCutoff => throw _privateConstructorUsedError;
|
||||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||||
ColorThemes? get themeColor => throw _privateConstructorUsedError;
|
ColorThemes? get themeColor => throw _privateConstructorUsedError;
|
||||||
|
HomeBanner get homeBanner => throw _privateConstructorUsedError;
|
||||||
bool get amoledBlack => throw _privateConstructorUsedError;
|
bool get amoledBlack => throw _privateConstructorUsedError;
|
||||||
bool get blurPlaceHolders => throw _privateConstructorUsedError;
|
bool get blurPlaceHolders => throw _privateConstructorUsedError;
|
||||||
bool get blurUpcomingEpisodes => throw _privateConstructorUsedError;
|
bool get blurUpcomingEpisodes => throw _privateConstructorUsedError;
|
||||||
|
|
@ -38,8 +39,12 @@ mixin _$ClientSettingsModel {
|
||||||
bool get mouseDragSupport => throw _privateConstructorUsedError;
|
bool get mouseDragSupport => throw _privateConstructorUsedError;
|
||||||
int? get libraryPageSize => throw _privateConstructorUsedError;
|
int? get libraryPageSize => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this ClientSettingsModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
|
||||||
|
/// Create a copy of ClientSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
$ClientSettingsModelCopyWith<ClientSettingsModel> get copyWith =>
|
$ClientSettingsModelCopyWith<ClientSettingsModel> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +63,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
||||||
Duration? nextUpDateCutoff,
|
Duration? nextUpDateCutoff,
|
||||||
ThemeMode themeMode,
|
ThemeMode themeMode,
|
||||||
ColorThemes? themeColor,
|
ColorThemes? themeColor,
|
||||||
|
HomeBanner homeBanner,
|
||||||
bool amoledBlack,
|
bool amoledBlack,
|
||||||
bool blurPlaceHolders,
|
bool blurPlaceHolders,
|
||||||
bool blurUpcomingEpisodes,
|
bool blurUpcomingEpisodes,
|
||||||
|
|
@ -79,6 +85,8 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ClientSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -89,6 +97,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
Object? nextUpDateCutoff = freezed,
|
Object? nextUpDateCutoff = freezed,
|
||||||
Object? themeMode = null,
|
Object? themeMode = null,
|
||||||
Object? themeColor = freezed,
|
Object? themeColor = freezed,
|
||||||
|
Object? homeBanner = null,
|
||||||
Object? amoledBlack = null,
|
Object? amoledBlack = null,
|
||||||
Object? blurPlaceHolders = null,
|
Object? blurPlaceHolders = null,
|
||||||
Object? blurUpcomingEpisodes = null,
|
Object? blurUpcomingEpisodes = null,
|
||||||
|
|
@ -128,6 +137,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
? _value.themeColor
|
? _value.themeColor
|
||||||
: themeColor // ignore: cast_nullable_to_non_nullable
|
: themeColor // ignore: cast_nullable_to_non_nullable
|
||||||
as ColorThemes?,
|
as ColorThemes?,
|
||||||
|
homeBanner: null == homeBanner
|
||||||
|
? _value.homeBanner
|
||||||
|
: homeBanner // ignore: cast_nullable_to_non_nullable
|
||||||
|
as HomeBanner,
|
||||||
amoledBlack: null == amoledBlack
|
amoledBlack: null == amoledBlack
|
||||||
? _value.amoledBlack
|
? _value.amoledBlack
|
||||||
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -184,6 +197,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
||||||
Duration? nextUpDateCutoff,
|
Duration? nextUpDateCutoff,
|
||||||
ThemeMode themeMode,
|
ThemeMode themeMode,
|
||||||
ColorThemes? themeColor,
|
ColorThemes? themeColor,
|
||||||
|
HomeBanner homeBanner,
|
||||||
bool amoledBlack,
|
bool amoledBlack,
|
||||||
bool blurPlaceHolders,
|
bool blurPlaceHolders,
|
||||||
bool blurUpcomingEpisodes,
|
bool blurUpcomingEpisodes,
|
||||||
|
|
@ -203,6 +217,8 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
$Res Function(_$ClientSettingsModelImpl) _then)
|
$Res Function(_$ClientSettingsModelImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of ClientSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -213,6 +229,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? nextUpDateCutoff = freezed,
|
Object? nextUpDateCutoff = freezed,
|
||||||
Object? themeMode = null,
|
Object? themeMode = null,
|
||||||
Object? themeColor = freezed,
|
Object? themeColor = freezed,
|
||||||
|
Object? homeBanner = null,
|
||||||
Object? amoledBlack = null,
|
Object? amoledBlack = null,
|
||||||
Object? blurPlaceHolders = null,
|
Object? blurPlaceHolders = null,
|
||||||
Object? blurUpcomingEpisodes = null,
|
Object? blurUpcomingEpisodes = null,
|
||||||
|
|
@ -252,6 +269,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value.themeColor
|
? _value.themeColor
|
||||||
: themeColor // ignore: cast_nullable_to_non_nullable
|
: themeColor // ignore: cast_nullable_to_non_nullable
|
||||||
as ColorThemes?,
|
as ColorThemes?,
|
||||||
|
homeBanner: null == homeBanner
|
||||||
|
? _value.homeBanner
|
||||||
|
: homeBanner // ignore: cast_nullable_to_non_nullable
|
||||||
|
as HomeBanner,
|
||||||
amoledBlack: null == amoledBlack
|
amoledBlack: null == amoledBlack
|
||||||
? _value.amoledBlack
|
? _value.amoledBlack
|
||||||
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -304,6 +325,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
this.nextUpDateCutoff,
|
this.nextUpDateCutoff,
|
||||||
this.themeMode = ThemeMode.system,
|
this.themeMode = ThemeMode.system,
|
||||||
this.themeColor,
|
this.themeColor,
|
||||||
|
this.homeBanner = HomeBanner.carousel,
|
||||||
this.amoledBlack = false,
|
this.amoledBlack = false,
|
||||||
this.blurPlaceHolders = false,
|
this.blurPlaceHolders = false,
|
||||||
this.blurUpcomingEpisodes = false,
|
this.blurUpcomingEpisodes = false,
|
||||||
|
|
@ -338,6 +360,9 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
final ColorThemes? themeColor;
|
final ColorThemes? themeColor;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
|
final HomeBanner homeBanner;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
final bool amoledBlack;
|
final bool amoledBlack;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
|
|
@ -365,7 +390,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, libraryPageSize: $libraryPageSize)';
|
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, homeBanner: $homeBanner, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, libraryPageSize: $libraryPageSize)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -380,6 +405,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
|
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
|
||||||
..add(DiagnosticsProperty('themeMode', themeMode))
|
..add(DiagnosticsProperty('themeMode', themeMode))
|
||||||
..add(DiagnosticsProperty('themeColor', themeColor))
|
..add(DiagnosticsProperty('themeColor', themeColor))
|
||||||
|
..add(DiagnosticsProperty('homeBanner', homeBanner))
|
||||||
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
|
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
|
||||||
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
|
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
|
||||||
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
|
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
|
||||||
|
|
@ -408,6 +434,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
other.themeMode == themeMode) &&
|
other.themeMode == themeMode) &&
|
||||||
(identical(other.themeColor, themeColor) ||
|
(identical(other.themeColor, themeColor) ||
|
||||||
other.themeColor == themeColor) &&
|
other.themeColor == themeColor) &&
|
||||||
|
(identical(other.homeBanner, homeBanner) ||
|
||||||
|
other.homeBanner == homeBanner) &&
|
||||||
(identical(other.amoledBlack, amoledBlack) ||
|
(identical(other.amoledBlack, amoledBlack) ||
|
||||||
other.amoledBlack == amoledBlack) &&
|
other.amoledBlack == amoledBlack) &&
|
||||||
(identical(other.blurPlaceHolders, blurPlaceHolders) ||
|
(identical(other.blurPlaceHolders, blurPlaceHolders) ||
|
||||||
|
|
@ -428,7 +456,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
other.libraryPageSize == libraryPageSize));
|
other.libraryPageSize == libraryPageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
runtimeType,
|
runtimeType,
|
||||||
|
|
@ -439,6 +467,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
nextUpDateCutoff,
|
nextUpDateCutoff,
|
||||||
themeMode,
|
themeMode,
|
||||||
themeColor,
|
themeColor,
|
||||||
|
homeBanner,
|
||||||
amoledBlack,
|
amoledBlack,
|
||||||
blurPlaceHolders,
|
blurPlaceHolders,
|
||||||
blurUpcomingEpisodes,
|
blurUpcomingEpisodes,
|
||||||
|
|
@ -449,7 +478,9 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
mouseDragSupport,
|
mouseDragSupport,
|
||||||
libraryPageSize);
|
libraryPageSize);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
/// Create a copy of ClientSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
||||||
|
|
@ -473,6 +504,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
final Duration? nextUpDateCutoff,
|
final Duration? nextUpDateCutoff,
|
||||||
final ThemeMode themeMode,
|
final ThemeMode themeMode,
|
||||||
final ColorThemes? themeColor,
|
final ColorThemes? themeColor,
|
||||||
|
final HomeBanner homeBanner,
|
||||||
final bool amoledBlack,
|
final bool amoledBlack,
|
||||||
final bool blurPlaceHolders,
|
final bool blurPlaceHolders,
|
||||||
final bool blurUpcomingEpisodes,
|
final bool blurUpcomingEpisodes,
|
||||||
|
|
@ -502,6 +534,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
@override
|
@override
|
||||||
ColorThemes? get themeColor;
|
ColorThemes? get themeColor;
|
||||||
@override
|
@override
|
||||||
|
HomeBanner get homeBanner;
|
||||||
|
@override
|
||||||
bool get amoledBlack;
|
bool get amoledBlack;
|
||||||
@override
|
@override
|
||||||
bool get blurPlaceHolders;
|
bool get blurPlaceHolders;
|
||||||
|
|
@ -520,8 +554,11 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
bool get mouseDragSupport;
|
bool get mouseDragSupport;
|
||||||
@override
|
@override
|
||||||
int? get libraryPageSize;
|
int? get libraryPageSize;
|
||||||
|
|
||||||
|
/// Create a copy of ClientSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
||||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||||
ThemeMode.system,
|
ThemeMode.system,
|
||||||
themeColor: $enumDecodeNullable(_$ColorThemesEnumMap, json['themeColor']),
|
themeColor: $enumDecodeNullable(_$ColorThemesEnumMap, json['themeColor']),
|
||||||
|
homeBanner:
|
||||||
|
$enumDecodeNullable(_$HomeBannerEnumMap, json['homeBanner']) ??
|
||||||
|
HomeBanner.carousel,
|
||||||
amoledBlack: json['amoledBlack'] as bool? ?? false,
|
amoledBlack: json['amoledBlack'] as bool? ?? false,
|
||||||
blurPlaceHolders: json['blurPlaceHolders'] as bool? ?? false,
|
blurPlaceHolders: json['blurPlaceHolders'] as bool? ?? false,
|
||||||
blurUpcomingEpisodes: json['blurUpcomingEpisodes'] as bool? ?? false,
|
blurUpcomingEpisodes: json['blurUpcomingEpisodes'] as bool? ?? false,
|
||||||
|
|
@ -47,6 +50,7 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
||||||
'nextUpDateCutoff': instance.nextUpDateCutoff?.inMicroseconds,
|
'nextUpDateCutoff': instance.nextUpDateCutoff?.inMicroseconds,
|
||||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||||
'themeColor': _$ColorThemesEnumMap[instance.themeColor],
|
'themeColor': _$ColorThemesEnumMap[instance.themeColor],
|
||||||
|
'homeBanner': _$HomeBannerEnumMap[instance.homeBanner]!,
|
||||||
'amoledBlack': instance.amoledBlack,
|
'amoledBlack': instance.amoledBlack,
|
||||||
'blurPlaceHolders': instance.blurPlaceHolders,
|
'blurPlaceHolders': instance.blurPlaceHolders,
|
||||||
'blurUpcomingEpisodes': instance.blurUpcomingEpisodes,
|
'blurUpcomingEpisodes': instance.blurUpcomingEpisodes,
|
||||||
|
|
@ -81,3 +85,8 @@ const _$ColorThemesEnumMap = {
|
||||||
ColorThemes.deepPurple: 'deepPurple',
|
ColorThemes.deepPurple: 'deepPurple',
|
||||||
ColorThemes.blueGrey: 'blueGrey',
|
ColorThemes.blueGrey: 'blueGrey',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$HomeBannerEnumMap = {
|
||||||
|
HomeBanner.carousel: 'carousel',
|
||||||
|
HomeBanner.banner: 'banner',
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
import 'package:fladder/providers/views_provider.dart';
|
import 'package:fladder/providers/views_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.dart';
|
import 'package:fladder/routes/auto_router.gr.dart';
|
||||||
import 'package:fladder/screens/shared/media/carousel_banner.dart';
|
import 'package:fladder/screens/dashboard/top_posters_row.dart';
|
||||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||||
|
|
@ -104,20 +104,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Transform.translate(
|
child: Transform.translate(
|
||||||
offset: Offset(0, AdaptiveLayout.layoutOf(context) == LayoutState.phone ? -14 : 0),
|
offset: Offset(0, AdaptiveLayout.layoutOf(context) == LayoutState.phone ? -14 : 0),
|
||||||
child: ConstrainedBox(
|
child: TopPostersRow(posters: homeCarouselItems),
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: AdaptiveLayout.of(context).isDesktop ? 350 : 275,
|
|
||||||
maxHeight: (MediaQuery.sizeOf(context).height * 0.25).clamp(400, double.infinity)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.6,
|
|
||||||
child: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: CarouselBanner(
|
|
||||||
items: homeCarouselItems,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
} else if (AdaptiveLayout.of(context).isDesktop)
|
} else if (AdaptiveLayout.of(context).isDesktop)
|
||||||
|
|
|
||||||
36
lib/screens/dashboard/top_posters_row.dart
Normal file
36
lib/screens/dashboard/top_posters_row.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
|
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||||
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
|
import 'package:fladder/screens/shared/media/carousel_banner.dart';
|
||||||
|
import 'package:fladder/screens/shared/media/media_banner.dart';
|
||||||
|
|
||||||
|
class TopPostersRow extends ConsumerWidget {
|
||||||
|
final List<ItemBaseModel> posters;
|
||||||
|
const TopPostersRow({required this.posters, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final bannerType = ref.watch(clientSettingsProvider.select((value) => value.homeBanner));
|
||||||
|
final maxHeight = (MediaQuery.sizeOf(context).shortestSide * 0.6).clamp(125.0, 350.0);
|
||||||
|
return switch (bannerType) {
|
||||||
|
HomeBanner.carousel => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CarouselBanner(
|
||||||
|
items: posters,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
HomeBanner.banner => MediaBanner(
|
||||||
|
items: posters,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||||
|
|
@ -186,6 +187,28 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsHomeBannerTitle),
|
||||||
|
subLabel: Text(context.localized.settingsHomeBannerDescription),
|
||||||
|
trailing: EnumBox(
|
||||||
|
current: ref.watch(
|
||||||
|
clientSettingsProvider.select(
|
||||||
|
(value) => value.homeBanner.label(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => HomeBanner.values
|
||||||
|
.map(
|
||||||
|
(entry) => PopupMenuItem(
|
||||||
|
value: entry,
|
||||||
|
child: Text(entry.label(context)),
|
||||||
|
onTap: () => ref
|
||||||
|
.read(clientSettingsProvider.notifier)
|
||||||
|
.update((context) => context.copyWith(homeBanner: entry)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsHomeNextUpTitle),
|
label: Text(context.localized.settingsHomeNextUpTitle),
|
||||||
subLabel: Text(context.localized.settingsHomeNextUpDesc),
|
subLabel: Text(context.localized.settingsHomeNextUpDesc),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
@ -45,7 +44,7 @@ class SettingsScaffold extends ConsumerWidget {
|
||||||
leading: context.router.backButton(),
|
leading: context.router.backButton(),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
titlePadding: const EdgeInsets.symmetric(horizontal: 16)
|
titlePadding: const EdgeInsets.symmetric(horizontal: 16)
|
||||||
.add(EdgeInsets.only(left: padding.left, right: padding.right)),
|
.add(EdgeInsets.only(left: padding.left, right: padding.right, bottom: 4)),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: Theme.of(context).textTheme.headlineLarge),
|
Text(label, style: Theme.of(context).textTheme.headlineLarge),
|
||||||
|
|
@ -75,8 +74,7 @@ class SettingsScaffold extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: MediaQuery.paddingOf(context)
|
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8),
|
||||||
.copyWith(top: AdaptiveLayout.of(context).isDesktop || kIsWeb ? 0 : null),
|
|
||||||
sliver: SliverList(
|
sliver: SliverList(
|
||||||
delegate: SliverChildListDelegate(items),
|
delegate: SliverChildListDelegate(items),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,25 @@
|
||||||
import 'package:async/async.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/movie_model.dart';
|
|
||||||
import 'package:fladder/screens/shared/media/components/media_play_button.dart';
|
|
||||||
import 'package:fladder/util/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/fladder_image.dart';
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:fladder/util/themes_data.dart';
|
import 'package:fladder/widgets/shared/fladder_carousel.dart';
|
||||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
class CarouselBanner extends ConsumerStatefulWidget {
|
class CarouselBanner extends ConsumerStatefulWidget {
|
||||||
final PageController? controller;
|
final PageController? controller;
|
||||||
final List<ItemBaseModel> items;
|
final List<ItemBaseModel> items;
|
||||||
|
final double maxHeight;
|
||||||
const CarouselBanner({
|
const CarouselBanner({
|
||||||
this.controller,
|
this.controller,
|
||||||
required this.items,
|
required this.items,
|
||||||
|
this.maxHeight = 250,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -29,350 +28,120 @@ class CarouselBanner extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CarouselBannerState extends ConsumerState<CarouselBanner> {
|
class _CarouselBannerState extends ConsumerState<CarouselBanner> {
|
||||||
bool showControls = false;
|
|
||||||
bool interacting = false;
|
|
||||||
int currentPage = 0;
|
|
||||||
double dragOffset = 0;
|
|
||||||
double dragIntensity = 1;
|
|
||||||
double slidePosition = 1;
|
|
||||||
|
|
||||||
late final RestartableTimer timer = RestartableTimer(const Duration(seconds: 8), () => nextSlide());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
timer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void nextSlide() {
|
|
||||||
if (!interacting) {
|
|
||||||
setState(() {
|
|
||||||
if (currentPage == widget.items.length - 1) {
|
|
||||||
currentPage = 0;
|
|
||||||
} else {
|
|
||||||
currentPage++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
timer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void previousSlide() {
|
|
||||||
if (!interacting) {
|
|
||||||
setState(() {
|
|
||||||
if (currentPage == 0) {
|
|
||||||
currentPage = widget.items.length - 1;
|
|
||||||
} else {
|
|
||||||
currentPage--;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
timer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final overlayColor = ThemesData.of(context).dark.colorScheme.onSecondary;
|
return ConstrainedBox(
|
||||||
final shadows = [
|
constraints: BoxConstraints(maxHeight: widget.maxHeight),
|
||||||
BoxShadow(blurRadius: 12, spreadRadius: 8, color: overlayColor),
|
child: LayoutBuilder(
|
||||||
];
|
builder: (context, constraints) {
|
||||||
final currentItem = widget.items[currentPage.clamp(0, widget.items.length - 1)];
|
final maxExtent = (constraints.maxHeight * 2.1).clamp(250.0, MediaQuery.sizeOf(context).shortestSide * 0.75);
|
||||||
final actions = currentItem.generateActions(context, ref);
|
final border = BorderRadius.circular(18);
|
||||||
|
return FladderCarousel(
|
||||||
final double dragOpacity = (1 - dragOffset.abs()).clamp(0, 1);
|
shape: RoundedRectangleBorder(borderRadius: border),
|
||||||
|
onTap: (index) => widget.items[index].navigateTo(context),
|
||||||
return Column(
|
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
||||||
mainAxisSize: MainAxisSize.min,
|
? null
|
||||||
children: [
|
: (index) {
|
||||||
Flexible(
|
final poster = widget.items[index];
|
||||||
child: Card(
|
showBottomSheetPill(
|
||||||
elevation: 16,
|
context: context,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
item: poster,
|
||||||
surfaceTintColor: overlayColor,
|
content: (scrollContext, scrollController) => ListView(
|
||||||
color: overlayColor,
|
shrinkWrap: true,
|
||||||
child: GestureDetector(
|
controller: scrollController,
|
||||||
onTap: () => currentItem.navigateTo(context),
|
children: poster.generateActions(context, ref).listTileItems(scrollContext, useIcons: true),
|
||||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
|
||||||
? () async {
|
|
||||||
interacting = true;
|
|
||||||
await showBottomSheetPill(
|
|
||||||
context: context,
|
|
||||||
content: (context, scrollController) => ListView(
|
|
||||||
controller: scrollController,
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: actions.listTileItems(context, useIcons: true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
interacting = false;
|
|
||||||
timer.reset();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: MouseRegion(
|
|
||||||
onEnter: (event) => setState(() => showControls = true),
|
|
||||||
onHover: (event) => timer.reset(),
|
|
||||||
onExit: (event) => setState(() => showControls = false),
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Dismissible(
|
|
||||||
key: const Key("Dismissable"),
|
|
||||||
direction: DismissDirection.horizontal,
|
|
||||||
onUpdate: (details) {
|
|
||||||
setState(() {
|
|
||||||
dragOffset = details.progress * 4;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
confirmDismiss: (direction) async {
|
|
||||||
if (direction == DismissDirection.startToEnd) {
|
|
||||||
previousSlide();
|
|
||||||
} else {
|
|
||||||
nextSlide();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
duration: const Duration(milliseconds: 125),
|
|
||||||
opacity: dragOpacity.abs(),
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 125),
|
|
||||||
child: Container(
|
|
||||||
key: Key(currentItem.id),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
foregroundDecoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.white.withOpacity(0.10), strokeAlign: BorderSide.strokeAlignInside),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.bottomLeft,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
overlayColor.withOpacity(1),
|
|
||||||
overlayColor.withOpacity(0.75),
|
|
||||||
overlayColor.withOpacity(0.45),
|
|
||||||
overlayColor.withOpacity(0.15),
|
|
||||||
overlayColor.withOpacity(0),
|
|
||||||
overlayColor.withOpacity(0),
|
|
||||||
overlayColor.withOpacity(0.1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(1),
|
|
||||||
child: FladderImage(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: currentItem.bannerImage,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
Row(
|
},
|
||||||
children: [
|
onSecondaryTap: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||||
Expanded(
|
? null
|
||||||
child: Padding(
|
: (details) async {
|
||||||
padding: const EdgeInsets.all(16),
|
Offset localPosition = details.$2.globalPosition;
|
||||||
child: Column(
|
RelativeRect position = RelativeRect.fromLTRB(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final poster = widget.items[details.$1];
|
||||||
children: [
|
|
||||||
Flexible(
|
await showMenu(
|
||||||
child: IgnorePointer(
|
context: context,
|
||||||
child: Column(
|
position: position,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||||
mainAxisSize: MainAxisSize.min,
|
);
|
||||||
children: [
|
},
|
||||||
Flexible(
|
itemExtent: maxExtent,
|
||||||
child: Text(
|
children: [
|
||||||
currentItem.title,
|
...widget.items.mapIndexed(
|
||||||
maxLines: 3,
|
(index, e) => LayoutBuilder(builder: (context, constraints) {
|
||||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
final opacity = (constraints.maxWidth / maxExtent);
|
||||||
shadows: shadows,
|
return Stack(
|
||||||
color: Colors.white,
|
clipBehavior: Clip.none,
|
||||||
),
|
children: [
|
||||||
),
|
FladderImage(image: e.bannerImage),
|
||||||
),
|
AnimatedOpacity(
|
||||||
if (currentItem.label(context) != null && currentItem is! MovieModel)
|
duration: const Duration(milliseconds: 250),
|
||||||
Flexible(
|
opacity: opacity.clamp(0, 1),
|
||||||
child: Text(
|
child: Stack(
|
||||||
currentItem.label(context)!,
|
children: [
|
||||||
maxLines: 3,
|
Positioned.fill(
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
child: Container(
|
||||||
shadows: shadows,
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.75),
|
gradient: LinearGradient(
|
||||||
),
|
begin: Alignment.bottomLeft,
|
||||||
),
|
end: Alignment.topCenter,
|
||||||
),
|
colors: [
|
||||||
if (currentItem.overview.summary.isNotEmpty &&
|
Theme.of(context).colorScheme.primaryContainer.withOpacity(0.75),
|
||||||
AdaptiveLayout.layoutOf(context) != LayoutState.phone)
|
Colors.transparent,
|
||||||
Flexible(
|
],
|
||||||
child: Text(
|
|
||||||
currentItem.overview.summary,
|
|
||||||
maxLines: 3,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
||||||
shadows: shadows,
|
|
||||||
color: Colors.white.withOpacity(0.75),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
].addInBetween(const SizedBox(height: 6)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
),
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
if (currentItem.playAble)
|
|
||||||
MediaPlayButton(
|
|
||||||
item: currentItem,
|
|
||||||
onPressed: () async {
|
|
||||||
await currentItem.play(
|
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
].addInBetween(const SizedBox(height: 16)),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: showControls ? 1 : 0,
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: () => nextSlide(),
|
|
||||||
icon: const Icon(IconsaxOutline.arrow_right_3),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Card(
|
child: Column(
|
||||||
child: PopupMenuButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
onOpened: () => interacting = true,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onCanceled: () {
|
children: [
|
||||||
interacting = false;
|
Text(
|
||||||
timer.reset();
|
e.title,
|
||||||
},
|
maxLines: 2,
|
||||||
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
|
softWrap: e.title.length > 25,
|
||||||
),
|
textWidthBasis: TextWidthBasis.parent,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
if (e.label(context) != null)
|
||||||
|
Text(
|
||||||
|
e.label(context)!,
|
||||||
|
maxLines: 2,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
].addInBetween(const SizedBox(height: 4)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
),
|
border: Border.all(
|
||||||
),
|
color: Colors.white.withOpacity(0.1),
|
||||||
),
|
width: 1.0,
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onHorizontalDragUpdate: (details) {
|
|
||||||
final delta = (details.primaryDelta ?? 0) / 20;
|
|
||||||
slidePosition += delta;
|
|
||||||
if (slidePosition > 1) {
|
|
||||||
nextSlide();
|
|
||||||
slidePosition = 0;
|
|
||||||
} else if (slidePosition < -1) {
|
|
||||||
previousSlide();
|
|
||||||
slidePosition = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onHorizontalDragStart: (details) {
|
|
||||||
slidePosition = 0;
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
children: widget.items.mapIndexed((index, e) {
|
|
||||||
return Tooltip(
|
|
||||||
message: '${e.name}\n${e.detailedName}',
|
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTapUp: currentPage == index
|
|
||||||
? null
|
|
||||||
: (details) {
|
|
||||||
animateToTarget(index);
|
|
||||||
timer.reset();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
color: Colors.red.withOpacity(0),
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 125),
|
|
||||||
width: currentItem == e ? 22 : 6,
|
|
||||||
height: currentItem == e ? 10 : 6,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: currentItem == e
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Theme.of(context).colorScheme.primary.withOpacity(0.25),
|
|
||||||
),
|
),
|
||||||
),
|
borderRadius: border),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
),
|
)
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
)
|
},
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void animateToTarget(int nextIndex) {
|
|
||||||
int step = currentPage < nextIndex ? 1 : -1;
|
|
||||||
void updateItem(int item) {
|
|
||||||
Future.delayed(Duration(milliseconds: 64 ~/ ((currentPage - nextIndex).abs() / 3)), () {
|
|
||||||
setState(() {
|
|
||||||
currentPage = item;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentPage != nextIndex) {
|
|
||||||
updateItem(item + step);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
timer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItem(currentPage + step);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
370
lib/screens/shared/media/media_banner.dart
Normal file
370
lib/screens/shared/media/media_banner.dart
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
|
import 'package:fladder/models/items/movie_model.dart';
|
||||||
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
|
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||||
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
import 'package:fladder/util/themes_data.dart';
|
||||||
|
import 'package:fladder/widgets/shared/fladder_carousel.dart';
|
||||||
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
|
|
||||||
|
class MediaBanner extends ConsumerStatefulWidget {
|
||||||
|
final PageController? controller;
|
||||||
|
final List<ItemBaseModel> items;
|
||||||
|
final double maxHeight;
|
||||||
|
|
||||||
|
const MediaBanner({
|
||||||
|
this.controller,
|
||||||
|
required this.items,
|
||||||
|
this.maxHeight = 250,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _MediaBannerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaBannerState extends ConsumerState<MediaBanner> {
|
||||||
|
bool showControls = false;
|
||||||
|
bool interacting = false;
|
||||||
|
int currentPage = 0;
|
||||||
|
double dragOffset = 0;
|
||||||
|
double dragIntensity = 1;
|
||||||
|
double slidePosition = 1;
|
||||||
|
|
||||||
|
late final RestartableTimer timer = RestartableTimer(const Duration(seconds: 8), () => nextSlide());
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextSlide() {
|
||||||
|
if (!interacting) {
|
||||||
|
setState(() {
|
||||||
|
if (currentPage == widget.items.length - 1) {
|
||||||
|
currentPage = 0;
|
||||||
|
} else {
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void previousSlide() {
|
||||||
|
if (!interacting) {
|
||||||
|
setState(() {
|
||||||
|
if (currentPage == 0) {
|
||||||
|
currentPage = widget.items.length - 1;
|
||||||
|
} else {
|
||||||
|
currentPage--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
final controller = FladderCarouselController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final overlayColor = ThemesData.of(context).dark.colorScheme.onSecondary;
|
||||||
|
final shadows = [
|
||||||
|
BoxShadow(blurRadius: 12, spreadRadius: 8, color: overlayColor),
|
||||||
|
];
|
||||||
|
final currentItem = widget.items[currentPage.clamp(0, widget.items.length - 1)];
|
||||||
|
final actions = currentItem.generateActions(context, ref);
|
||||||
|
final double dragOpacity = (1 - dragOffset.abs()).clamp(0, 1);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: widget.maxHeight),
|
||||||
|
child: Card(
|
||||||
|
elevation: 16,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
surfaceTintColor: overlayColor,
|
||||||
|
color: overlayColor,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => currentItem.navigateTo(context),
|
||||||
|
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||||
|
? () async {
|
||||||
|
interacting = true;
|
||||||
|
final poster = currentItem;
|
||||||
|
showBottomSheetPill(
|
||||||
|
context: context,
|
||||||
|
item: poster,
|
||||||
|
content: (scrollContext, scrollController) => ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
controller: scrollController,
|
||||||
|
children: poster.generateActions(context, ref).listTileItems(scrollContext, useIcons: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
interacting = false;
|
||||||
|
timer.reset();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (event) => setState(() => showControls = true),
|
||||||
|
onHover: (event) => timer.reset(),
|
||||||
|
onExit: (event) => setState(() => showControls = false),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Dismissible(
|
||||||
|
key: const Key("Dismissable"),
|
||||||
|
direction: DismissDirection.horizontal,
|
||||||
|
onUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
dragOffset = details.progress * 4;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
confirmDismiss: (direction) async {
|
||||||
|
if (direction == DismissDirection.startToEnd) {
|
||||||
|
previousSlide();
|
||||||
|
} else {
|
||||||
|
nextSlide();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
|
opacity: dragOpacity.abs(),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
|
child: Container(
|
||||||
|
key: Key(currentItem.id),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.10), strokeAlign: BorderSide.strokeAlignInside),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomLeft,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
overlayColor.withOpacity(1),
|
||||||
|
overlayColor.withOpacity(0.75),
|
||||||
|
overlayColor.withOpacity(0.45),
|
||||||
|
overlayColor.withOpacity(0.15),
|
||||||
|
overlayColor.withOpacity(0),
|
||||||
|
overlayColor.withOpacity(0),
|
||||||
|
overlayColor.withOpacity(0.1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(1),
|
||||||
|
child: FladderImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: currentItem.bannerImage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
currentItem.title,
|
||||||
|
maxLines: 2,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
shadows: shadows,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentItem.label(context) != null && currentItem is! MovieModel)
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
currentItem.label(context)!,
|
||||||
|
maxLines: 2,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
shadows: shadows,
|
||||||
|
color: Colors.white.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentItem.overview.summary.isNotEmpty &&
|
||||||
|
AdaptiveLayout.layoutOf(context) != LayoutState.phone)
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
currentItem.overview.summary,
|
||||||
|
maxLines: 2,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
shadows: shadows,
|
||||||
|
color: Colors.white.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].addInBetween(const SizedBox(height: 6)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].addInBetween(const SizedBox(height: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => nextSlide(),
|
||||||
|
icon: const Icon(IconsaxOutline.arrow_right_3),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Card(
|
||||||
|
child: PopupMenuButton(
|
||||||
|
onOpened: () => interacting = true,
|
||||||
|
onCanceled: () {
|
||||||
|
interacting = false;
|
||||||
|
timer.reset();
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onHorizontalDragUpdate: (details) {
|
||||||
|
final delta = (details.primaryDelta ?? 0) / 20;
|
||||||
|
slidePosition += delta;
|
||||||
|
if (slidePosition > 1) {
|
||||||
|
nextSlide();
|
||||||
|
slidePosition = 0;
|
||||||
|
} else if (slidePosition < -1) {
|
||||||
|
previousSlide();
|
||||||
|
slidePosition = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHorizontalDragStart: (details) {
|
||||||
|
slidePosition = 0;
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
children: widget.items.mapIndexed((index, e) {
|
||||||
|
return Tooltip(
|
||||||
|
message: '${e.name}\n${e.detailedName}',
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTapUp: currentPage == index
|
||||||
|
? null
|
||||||
|
: (details) {
|
||||||
|
animateToTarget(index);
|
||||||
|
timer.reset();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: Colors.red.withOpacity(0),
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
|
width: currentItem == e ? 22 : 6,
|
||||||
|
height: currentItem == e ? 10 : 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: currentItem == e
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).colorScheme.primary.withOpacity(0.25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void animateToTarget(int nextIndex) {
|
||||||
|
int step = currentPage < nextIndex ? 1 : -1;
|
||||||
|
void updateItem(int item) {
|
||||||
|
Future.delayed(Duration(milliseconds: 64 ~/ ((currentPage - nextIndex).abs() / 3)), () {
|
||||||
|
setState(() {
|
||||||
|
currentPage = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentPage != nextIndex) {
|
||||||
|
updateItem(item + step);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItem(currentPage + step);
|
||||||
|
}
|
||||||
|
}
|
||||||
752
lib/widgets/shared/fladder_carousel.dart
Normal file
752
lib/widgets/shared/fladder_carousel.dart
Normal file
|
|
@ -0,0 +1,752 @@
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
//This is a copy of the CarouselView widget with some minor changes.
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class MyCustomScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
@override
|
||||||
|
Set<PointerDeviceKind> get dragDevices => PointerDeviceKind.values.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Material Design carousel widget.
|
||||||
|
///
|
||||||
|
/// The [FladderCarousel] present a scrollable list of items, each of which can dynamically
|
||||||
|
/// change size based on the chosen layout.
|
||||||
|
///
|
||||||
|
/// This widget supports uncontained carousel layout. It shows items that scroll
|
||||||
|
/// to the edge of the container, behaving similarly to a [ListView] where all
|
||||||
|
/// children are a uniform size.
|
||||||
|
///
|
||||||
|
/// The [FladderCarouselController] is used to control the [FladderCarouselController.initialItem].
|
||||||
|
///
|
||||||
|
/// The [FladderCarousel.itemExtent] property must be non-null and defines the base
|
||||||
|
/// size of items. While items typically maintain this size, the first and last
|
||||||
|
/// visible items may be slightly compressed during scrolling. The [shrinkExtent]
|
||||||
|
/// property controls the minimum allowable size for these compressed items.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// Here is an example of [FladderCarousel] to show the uncontained layout. Each carousel
|
||||||
|
/// item has the same size but can be "squished" to the [shrinkExtent] when they
|
||||||
|
/// are show on the view and out of view.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/carousel/carousel.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [FladderCarouselController], which controls the first visible item in the carousel.
|
||||||
|
/// * [PageView], which is a scrollable list that works page by page.
|
||||||
|
class FladderCarousel extends StatefulWidget {
|
||||||
|
/// Creates a Material Design carousel.
|
||||||
|
const FladderCarousel({
|
||||||
|
super.key,
|
||||||
|
this.padding,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.elevation,
|
||||||
|
this.shape,
|
||||||
|
this.overlayColor,
|
||||||
|
this.itemSnapping = false,
|
||||||
|
this.shrinkExtent = 0.0,
|
||||||
|
this.controller,
|
||||||
|
this.scrollDirection = Axis.horizontal,
|
||||||
|
this.reverse = false,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onSecondaryTap,
|
||||||
|
required this.itemExtent,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The amount of space to surround each carousel item with.
|
||||||
|
///
|
||||||
|
/// Defaults to [EdgeInsets.all] of 4 pixels.
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
/// The background color for each carousel item.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorScheme.surface].
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// The z-coordinate of each carousel item.
|
||||||
|
///
|
||||||
|
/// Defaults to 0.0.
|
||||||
|
final double? elevation;
|
||||||
|
|
||||||
|
/// The shape of each carousel item's [Material].
|
||||||
|
///
|
||||||
|
/// Defines each item's [Material.shape].
|
||||||
|
///
|
||||||
|
/// Defaults to a [RoundedRectangleBorder] with a circular corner radius
|
||||||
|
/// of 28.0.
|
||||||
|
final ShapeBorder? shape;
|
||||||
|
|
||||||
|
/// The highlight color to indicate the carousel items are in pressed, hovered
|
||||||
|
/// or focused states.
|
||||||
|
///
|
||||||
|
/// The default values are:
|
||||||
|
/// * [WidgetState.pressed] - [ColorScheme.onSurface] with an opacity of 0.1
|
||||||
|
/// * [WidgetState.hovered] - [ColorScheme.onSurface] with an opacity of 0.08
|
||||||
|
/// * [WidgetState.focused] - [ColorScheme.onSurface] with an opacity of 0.1
|
||||||
|
final WidgetStateProperty<Color?>? overlayColor;
|
||||||
|
|
||||||
|
/// The minimum allowable extent (size) in the main axis for carousel items
|
||||||
|
/// during scrolling transitions.
|
||||||
|
///
|
||||||
|
/// As the carousel scrolls, the first visible item is pinned and gradually
|
||||||
|
/// shrinks until it reaches this minimum extent before scrolling off-screen.
|
||||||
|
/// Similarly, the last visible item enters the viewport at this minimum size
|
||||||
|
/// and expands to its full [itemExtent].
|
||||||
|
///
|
||||||
|
/// In cases where the remaining viewport space for the last visible item is
|
||||||
|
/// larger than the defined [shrinkExtent], the [shrinkExtent] is dynamically
|
||||||
|
/// adjusted to match this remaining space, ensuring a smooth size transition.
|
||||||
|
///
|
||||||
|
/// Defaults to 0.0. Setting to 0.0 allows items to shrink/expand completely,
|
||||||
|
/// transitioning between 0.0 and the full [itemExtent]. In cases where the
|
||||||
|
/// remaining viewport space for the last visible item is larger than the
|
||||||
|
/// defined [shrinkExtent], the [shrinkExtent] is dynamically adjusted to match
|
||||||
|
/// this remaining space, ensuring a smooth size transition.
|
||||||
|
final double shrinkExtent;
|
||||||
|
|
||||||
|
/// Whether the carousel should keep scrolling to the next/previous items to
|
||||||
|
/// maintain the original layout.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool itemSnapping;
|
||||||
|
|
||||||
|
/// An object that can be used to control the position to which this scroll
|
||||||
|
/// view is scrolled.
|
||||||
|
final FladderCarouselController? controller;
|
||||||
|
|
||||||
|
/// The [Axis] along which the scroll view's offset increases with each item.
|
||||||
|
///
|
||||||
|
/// Defaults to [Axis.horizontal].
|
||||||
|
final Axis scrollDirection;
|
||||||
|
|
||||||
|
/// Whether the carousel list scrolls in the reading direction.
|
||||||
|
///
|
||||||
|
/// For example, if the reading direction is left-to-right and
|
||||||
|
/// [scrollDirection] is [Axis.horizontal], then the carousel scrolls from
|
||||||
|
/// left to right when [reverse] is false and from right to left when
|
||||||
|
/// [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Similarly, if [scrollDirection] is [Axis.vertical], then the carousel view
|
||||||
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||||
|
/// when [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// Called when one of the [children] is tapped.
|
||||||
|
final ValueChanged<int>? onTap;
|
||||||
|
|
||||||
|
/// Called when one of the [children] is longPressed.
|
||||||
|
final ValueChanged<int>? onLongPress;
|
||||||
|
|
||||||
|
final ValueChanged<(int, TapDownDetails)>? onSecondaryTap;
|
||||||
|
|
||||||
|
/// The extent the children are forced to have in the main axis.
|
||||||
|
///
|
||||||
|
/// The item extent should not exceed the available space that the carousel
|
||||||
|
/// occupies to ensure at least one item is fully visible.
|
||||||
|
///
|
||||||
|
/// This must be non-null.
|
||||||
|
final double itemExtent;
|
||||||
|
|
||||||
|
/// The child widgets for the carousel.
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FladderCarousel> createState() => _CarouselViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarouselViewState extends State<FladderCarousel> {
|
||||||
|
late double _itemExtent;
|
||||||
|
FladderCarouselController? _internalController;
|
||||||
|
FladderCarouselController get _controller => widget.controller ?? _internalController!;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.controller == null) {
|
||||||
|
_internalController = FladderCarouselController();
|
||||||
|
}
|
||||||
|
_controller._attach(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_itemExtent = widget.itemExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant FladderCarousel oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.controller != oldWidget.controller) {
|
||||||
|
oldWidget.controller?._detach(this);
|
||||||
|
if (widget.controller != null) {
|
||||||
|
_internalController?._detach(this);
|
||||||
|
_internalController = null;
|
||||||
|
widget.controller?._attach(this);
|
||||||
|
} else {
|
||||||
|
// widget.controller == null && oldWidget.controller != null
|
||||||
|
assert(_internalController == null);
|
||||||
|
_internalController = FladderCarouselController();
|
||||||
|
_controller._attach(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (widget.itemExtent != oldWidget.itemExtent) {
|
||||||
|
_itemExtent = widget.itemExtent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller._detach(this);
|
||||||
|
_internalController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
AxisDirection _getDirection(BuildContext context) {
|
||||||
|
switch (widget.scrollDirection) {
|
||||||
|
case Axis.horizontal:
|
||||||
|
assert(debugCheckHasDirectionality(context));
|
||||||
|
final TextDirection textDirection = Directionality.of(context);
|
||||||
|
final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
|
||||||
|
return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
|
||||||
|
case Axis.vertical:
|
||||||
|
return widget.reverse ? AxisDirection.up : AxisDirection.down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final AxisDirection axisDirection = _getDirection(context);
|
||||||
|
final ScrollPhysics physics =
|
||||||
|
widget.itemSnapping ? const CarouselScrollPhysics() : ScrollConfiguration.of(context).getScrollPhysics(context);
|
||||||
|
final EdgeInsets effectivePadding = widget.padding ?? const EdgeInsets.all(4.0);
|
||||||
|
final Color effectiveBackgroundColor = widget.backgroundColor ?? Theme.of(context).colorScheme.surface;
|
||||||
|
final double effectiveElevation = widget.elevation ?? 0.0;
|
||||||
|
final ShapeBorder effectiveShape =
|
||||||
|
widget.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0)));
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
final double mainAxisExtent = switch (widget.scrollDirection) {
|
||||||
|
Axis.horizontal => constraints.maxWidth,
|
||||||
|
Axis.vertical => constraints.maxHeight,
|
||||||
|
};
|
||||||
|
_itemExtent = clampDouble(_itemExtent, 0, mainAxisExtent);
|
||||||
|
|
||||||
|
return Scrollable(
|
||||||
|
axisDirection: axisDirection,
|
||||||
|
scrollBehavior: MyCustomScrollBehavior(),
|
||||||
|
controller: _controller,
|
||||||
|
physics: physics,
|
||||||
|
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
||||||
|
return Viewport(
|
||||||
|
cacheExtent: 0.0,
|
||||||
|
cacheExtentStyle: CacheExtentStyle.viewport,
|
||||||
|
axisDirection: axisDirection,
|
||||||
|
offset: position,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
slivers: <Widget>[
|
||||||
|
_SliverFixedExtentCarousel(
|
||||||
|
itemExtent: _itemExtent,
|
||||||
|
minExtent: widget.shrinkExtent,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(BuildContext context, int index) {
|
||||||
|
return Padding(
|
||||||
|
padding: effectivePadding,
|
||||||
|
child: Material(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
color: effectiveBackgroundColor,
|
||||||
|
elevation: effectiveElevation,
|
||||||
|
shape: effectiveShape,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: <Widget>[
|
||||||
|
widget.children.elementAt(index),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: widget.onTap != null ? () => widget.onTap!.call(index) : null,
|
||||||
|
onLongPress: widget.onLongPress != null ? () => widget.onLongPress!.call(index) : null,
|
||||||
|
onSecondaryTapDown: widget.onSecondaryTap != null
|
||||||
|
? (details) => widget.onSecondaryTap!.call((index, details))
|
||||||
|
: null,
|
||||||
|
overlayColor: widget.overlayColor ??
|
||||||
|
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return theme.colorScheme.onSurface.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered)) {
|
||||||
|
return theme.colorScheme.onSurface.withOpacity(0.08);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return theme.colorScheme.onSurface.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: widget.children.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sliver that displays its box children in a linear array with a fixed extent
|
||||||
|
/// per item.
|
||||||
|
///
|
||||||
|
/// _To learn more about slivers, see [CustomScrollView.slivers]._
|
||||||
|
///
|
||||||
|
/// This sliver list arranges its children in a line along the main axis starting
|
||||||
|
/// at offset zero and without gaps. Each child is constrained to a fixed extent
|
||||||
|
/// along the main axis and the [SliverConstraints.crossAxisExtent]
|
||||||
|
/// along the cross axis. The difference between this and a list view with a fixed
|
||||||
|
/// extent is the first item and last item can be squished a little during scrolling
|
||||||
|
/// transition. This compression is controlled by the `minExtent` property and
|
||||||
|
/// aligns with the [Material Design Carousel specifications]
|
||||||
|
/// (https://m3.material.io/components/carousel/guidelines#96c5c157-fe5b-4ee3-a9b4-72bf8efab7e9).
|
||||||
|
class _SliverFixedExtentCarousel extends SliverMultiBoxAdaptorWidget {
|
||||||
|
const _SliverFixedExtentCarousel({
|
||||||
|
required super.delegate,
|
||||||
|
required this.minExtent,
|
||||||
|
required this.itemExtent,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double itemExtent;
|
||||||
|
final double minExtent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderSliverFixedExtentBoxAdaptor createRenderObject(BuildContext context) {
|
||||||
|
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
|
||||||
|
return _RenderSliverFixedExtentCarousel(
|
||||||
|
childManager: element,
|
||||||
|
minExtent: minExtent,
|
||||||
|
maxExtent: itemExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, _RenderSliverFixedExtentCarousel renderObject) {
|
||||||
|
renderObject.maxExtent = itemExtent;
|
||||||
|
renderObject.minExtent = minExtent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderSliverFixedExtentCarousel extends RenderSliverFixedExtentBoxAdaptor {
|
||||||
|
_RenderSliverFixedExtentCarousel({
|
||||||
|
required super.childManager,
|
||||||
|
required double maxExtent,
|
||||||
|
required double minExtent,
|
||||||
|
}) : _maxExtent = maxExtent,
|
||||||
|
_minExtent = minExtent;
|
||||||
|
|
||||||
|
double get maxExtent => _maxExtent;
|
||||||
|
double _maxExtent;
|
||||||
|
set maxExtent(double value) {
|
||||||
|
if (_maxExtent == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_maxExtent = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get minExtent => _minExtent;
|
||||||
|
double _minExtent;
|
||||||
|
set minExtent(double value) {
|
||||||
|
if (_minExtent == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_minExtent = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implements the [itemExtentBuilder] callback.
|
||||||
|
double _buildItemExtent(int index, SliverLayoutDimensions currentLayoutDimensions) {
|
||||||
|
final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor();
|
||||||
|
|
||||||
|
// Calculate how many items have been completely scroll off screen.
|
||||||
|
final int offscreenItems = (constraints.scrollOffset / maxExtent).floor();
|
||||||
|
|
||||||
|
// If an item is partially off screen and partially on screen,
|
||||||
|
// `constraints.scrollOffset` must be greater than
|
||||||
|
// `offscreenItems * maxExtent`, so the difference between these two is how
|
||||||
|
// much the current first visible item is off screen.
|
||||||
|
final double offscreenExtent = constraints.scrollOffset - offscreenItems * maxExtent;
|
||||||
|
|
||||||
|
// If there is not enough space to place the last visible item but the remaining
|
||||||
|
// space is larger than `minExtent`, the extent for last item should be at
|
||||||
|
// least the remaining extent to ensure a smooth size transition.
|
||||||
|
final double effectiveMinExtent = math.max(constraints.remainingPaintExtent % maxExtent, minExtent);
|
||||||
|
|
||||||
|
// Two special cases are the first and last visible items. Other items' extent
|
||||||
|
// should all return `maxExtent`.
|
||||||
|
if (index == firstVisibleIndex) {
|
||||||
|
final double effectiveExtent = maxExtent - offscreenExtent;
|
||||||
|
return math.max(effectiveExtent, effectiveMinExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final double scrollOffsetForLastIndex = constraints.scrollOffset + constraints.remainingPaintExtent;
|
||||||
|
if (index == getMaxChildIndexForScrollOffset(scrollOffsetForLastIndex, maxExtent)) {
|
||||||
|
return clampDouble(scrollOffsetForLastIndex - maxExtent * index, effectiveMinExtent, maxExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
late SliverLayoutDimensions _currentLayoutDimensions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
_currentLayoutDimensions = SliverLayoutDimensions(
|
||||||
|
scrollOffset: constraints.scrollOffset,
|
||||||
|
precedingScrollExtent: constraints.precedingScrollExtent,
|
||||||
|
viewportMainAxisExtent: constraints.viewportMainAxisExtent,
|
||||||
|
crossAxisExtent: constraints.crossAxisExtent,
|
||||||
|
);
|
||||||
|
super.performLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layout offset for the child with the given index.
|
||||||
|
@override
|
||||||
|
double indexToLayoutOffset(
|
||||||
|
@Deprecated('The itemExtent is already available within the scope of this function. '
|
||||||
|
'This feature was deprecated after v3.20.0-7.0.pre.')
|
||||||
|
double itemExtent,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor();
|
||||||
|
|
||||||
|
// If there is not enough space to place the last visible item but the remaining
|
||||||
|
// space is larger than `minExtent`, the extent for last item should be at
|
||||||
|
// least the remaining extent to make sure a smooth size transition.
|
||||||
|
final double effectiveMinExtent = math.max(constraints.remainingPaintExtent % maxExtent, minExtent);
|
||||||
|
if (index == firstVisibleIndex) {
|
||||||
|
final double firstVisibleItemExtent = _buildItemExtent(index, _currentLayoutDimensions);
|
||||||
|
|
||||||
|
// If the first item is squished to be less than `effectievMinExtent`,
|
||||||
|
// then it should stop changinng its size and should start to scroll off screen.
|
||||||
|
if (firstVisibleItemExtent <= effectiveMinExtent) {
|
||||||
|
return maxExtent * index - effectiveMinExtent + maxExtent;
|
||||||
|
}
|
||||||
|
return constraints.scrollOffset;
|
||||||
|
}
|
||||||
|
return maxExtent * index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The minimum child index that is visible at the given scroll offset.
|
||||||
|
@override
|
||||||
|
int getMinChildIndexForScrollOffset(
|
||||||
|
double scrollOffset,
|
||||||
|
@Deprecated('The itemExtent is already available within the scope of this function. '
|
||||||
|
'This feature was deprecated after v3.20.0-7.0.pre.')
|
||||||
|
double itemExtent,
|
||||||
|
) {
|
||||||
|
final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor();
|
||||||
|
return math.max(firstVisibleIndex, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum child index that is visible at the given scroll offset.
|
||||||
|
@override
|
||||||
|
int getMaxChildIndexForScrollOffset(
|
||||||
|
double scrollOffset,
|
||||||
|
@Deprecated('The itemExtent is already available within the scope of this function. '
|
||||||
|
'This feature was deprecated after v3.20.0-7.0.pre.')
|
||||||
|
double itemExtent,
|
||||||
|
) {
|
||||||
|
if (maxExtent > 0.0) {
|
||||||
|
final double actual = scrollOffset / maxExtent - 1;
|
||||||
|
final int round = actual.round();
|
||||||
|
if ((actual * maxExtent - round * maxExtent).abs() < precisionErrorTolerance) {
|
||||||
|
return math.max(0, round);
|
||||||
|
}
|
||||||
|
return math.max(0, actual.ceil());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get itemExtent => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ItemExtentBuilder? get itemExtentBuilder => _buildItemExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll physics used by a [FladderCarousel].
|
||||||
|
///
|
||||||
|
/// These physics cause the carousel item to snap to item boundaries.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ScrollPhysics], the base class which defines the API for scrolling
|
||||||
|
/// physics.
|
||||||
|
/// * [PageScrollPhysics], scroll physics used by a [PageView].
|
||||||
|
class CarouselScrollPhysics extends ScrollPhysics {
|
||||||
|
/// Creates physics for a [FladderCarousel].
|
||||||
|
const CarouselScrollPhysics({super.parent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
CarouselScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||||
|
return CarouselScrollPhysics(parent: buildParent(ancestor));
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getTargetPixels(
|
||||||
|
_CarouselPosition position,
|
||||||
|
Tolerance tolerance,
|
||||||
|
double velocity,
|
||||||
|
) {
|
||||||
|
double fraction;
|
||||||
|
fraction = position.itemExtent! / position.viewportDimension;
|
||||||
|
|
||||||
|
final double itemWidth = position.viewportDimension * fraction;
|
||||||
|
|
||||||
|
final double actual = math.max(0.0, position.pixels) / itemWidth;
|
||||||
|
final double round = actual.roundToDouble();
|
||||||
|
double item;
|
||||||
|
if ((actual - round).abs() < precisionErrorTolerance) {
|
||||||
|
item = round;
|
||||||
|
} else {
|
||||||
|
item = actual;
|
||||||
|
}
|
||||||
|
if (velocity < -tolerance.velocity) {
|
||||||
|
item -= 0.5;
|
||||||
|
} else if (velocity > tolerance.velocity) {
|
||||||
|
item += 0.5;
|
||||||
|
}
|
||||||
|
return item.roundToDouble() * itemWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Simulation? createBallisticSimulation(
|
||||||
|
ScrollMetrics position,
|
||||||
|
double velocity,
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
position is _CarouselPosition,
|
||||||
|
'CarouselScrollPhysics can only be used with Scrollables that uses '
|
||||||
|
'the FladderCarouselController',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _CarouselPosition metrics = position as _CarouselPosition;
|
||||||
|
if ((velocity <= 0.0 && metrics.pixels <= metrics.minScrollExtent) ||
|
||||||
|
(velocity >= 0.0 && metrics.pixels >= metrics.maxScrollExtent)) {
|
||||||
|
return super.createBallisticSimulation(metrics, velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Tolerance tolerance = toleranceFor(metrics);
|
||||||
|
final double target = _getTargetPixels(metrics, tolerance, velocity);
|
||||||
|
if (target != metrics.pixels) {
|
||||||
|
return ScrollSpringSimulation(
|
||||||
|
spring,
|
||||||
|
metrics.pixels,
|
||||||
|
target,
|
||||||
|
velocity,
|
||||||
|
tolerance: tolerance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get allowImplicitScrolling => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metrics for a [FladderCarousel].
|
||||||
|
class _CarouselMetrics extends FixedScrollMetrics {
|
||||||
|
/// Creates an immutable snapshot of values associated with a [FladderCarousel].
|
||||||
|
_CarouselMetrics({
|
||||||
|
required super.minScrollExtent,
|
||||||
|
required super.maxScrollExtent,
|
||||||
|
required super.pixels,
|
||||||
|
required super.viewportDimension,
|
||||||
|
required super.axisDirection,
|
||||||
|
this.itemExtent,
|
||||||
|
required super.devicePixelRatio,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Extent for the carousel item.
|
||||||
|
///
|
||||||
|
/// Used to compute the first item from the current [pixels].
|
||||||
|
final double? itemExtent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CarouselMetrics copyWith({
|
||||||
|
double? minScrollExtent,
|
||||||
|
double? maxScrollExtent,
|
||||||
|
double? pixels,
|
||||||
|
double? viewportDimension,
|
||||||
|
AxisDirection? axisDirection,
|
||||||
|
double? itemExtent,
|
||||||
|
double? devicePixelRatio,
|
||||||
|
}) {
|
||||||
|
return _CarouselMetrics(
|
||||||
|
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
|
||||||
|
maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
|
||||||
|
pixels: pixels ?? (hasPixels ? this.pixels : null),
|
||||||
|
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
|
||||||
|
axisDirection: axisDirection ?? this.axisDirection,
|
||||||
|
itemExtent: itemExtent ?? this.itemExtent,
|
||||||
|
devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarouselPosition extends ScrollPositionWithSingleContext implements _CarouselMetrics {
|
||||||
|
_CarouselPosition({
|
||||||
|
required super.physics,
|
||||||
|
required super.context,
|
||||||
|
this.initialItem = 0,
|
||||||
|
required this.itemExtent,
|
||||||
|
super.oldPosition,
|
||||||
|
}) : _itemToShowOnStartup = initialItem.toDouble(),
|
||||||
|
super(initialPixels: null);
|
||||||
|
|
||||||
|
final int initialItem;
|
||||||
|
final double _itemToShowOnStartup;
|
||||||
|
// When the viewport has a zero-size, the item can not
|
||||||
|
// be retrieved by `getItemFromPixels`, so we need to cache the item
|
||||||
|
// for use when resizing the viewport to non-zero next time.
|
||||||
|
double? _cachedItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? itemExtent;
|
||||||
|
|
||||||
|
double getItemFromPixels(double pixels, double viewportDimension) {
|
||||||
|
assert(viewportDimension > 0.0);
|
||||||
|
final double fraction = itemExtent! / viewportDimension;
|
||||||
|
|
||||||
|
final double actual = math.max(0.0, pixels) / (viewportDimension * fraction);
|
||||||
|
final double round = actual.roundToDouble();
|
||||||
|
if ((actual - round).abs() < precisionErrorTolerance) {
|
||||||
|
return round;
|
||||||
|
}
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
double getPixelsFromItem(double item) {
|
||||||
|
final double fraction = itemExtent! / viewportDimension;
|
||||||
|
|
||||||
|
return item * viewportDimension * fraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool applyViewportDimension(double viewportDimension) {
|
||||||
|
final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
|
||||||
|
if (viewportDimension == oldViewportDimensions) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final bool result = super.applyViewportDimension(viewportDimension);
|
||||||
|
final double? oldPixels = hasPixels ? pixels : null;
|
||||||
|
double item;
|
||||||
|
if (oldPixels == null) {
|
||||||
|
item = _itemToShowOnStartup;
|
||||||
|
} else if (oldViewportDimensions == 0.0) {
|
||||||
|
// If resize from zero, we should use the _cachedItem to recover the state.
|
||||||
|
item = _cachedItem!;
|
||||||
|
} else {
|
||||||
|
item = getItemFromPixels(oldPixels, oldViewportDimensions!);
|
||||||
|
}
|
||||||
|
final double newPixels = getPixelsFromItem(item);
|
||||||
|
// If the viewportDimension is zero, cache the item
|
||||||
|
// in case the viewport is resized to be non-zero.
|
||||||
|
_cachedItem = (viewportDimension == 0.0) ? item : null;
|
||||||
|
|
||||||
|
if (newPixels != oldPixels) {
|
||||||
|
correctPixels(newPixels);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CarouselMetrics copyWith({
|
||||||
|
double? minScrollExtent,
|
||||||
|
double? maxScrollExtent,
|
||||||
|
double? pixels,
|
||||||
|
double? viewportDimension,
|
||||||
|
AxisDirection? axisDirection,
|
||||||
|
double? itemExtent,
|
||||||
|
List<int>? layoutWeights,
|
||||||
|
double? devicePixelRatio,
|
||||||
|
}) {
|
||||||
|
return _CarouselMetrics(
|
||||||
|
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
|
||||||
|
maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
|
||||||
|
pixels: pixels ?? (hasPixels ? this.pixels : null),
|
||||||
|
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
|
||||||
|
axisDirection: axisDirection ?? this.axisDirection,
|
||||||
|
itemExtent: itemExtent ?? this.itemExtent,
|
||||||
|
devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A controller for [FladderCarousel].
|
||||||
|
///
|
||||||
|
/// Using a carousel controller helps to show the first visible item on the
|
||||||
|
/// carousel list.
|
||||||
|
class FladderCarouselController extends ScrollController {
|
||||||
|
/// Creates a carousel controller.
|
||||||
|
FladderCarouselController({
|
||||||
|
this.initialItem = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The item that expands to the maximum size when first creating the [FladderCarousel].
|
||||||
|
final int initialItem;
|
||||||
|
|
||||||
|
_CarouselViewState? _carouselState;
|
||||||
|
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void _attach(_CarouselViewState anchor) {
|
||||||
|
_carouselState = anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _detach(_CarouselViewState anchor) {
|
||||||
|
if (_carouselState == anchor) {
|
||||||
|
_carouselState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
|
||||||
|
assert(_carouselState != null);
|
||||||
|
final double itemExtent = _carouselState!._itemExtent;
|
||||||
|
|
||||||
|
return _CarouselPosition(
|
||||||
|
physics: physics,
|
||||||
|
context: context,
|
||||||
|
initialItem: initialItem,
|
||||||
|
itemExtent: itemExtent,
|
||||||
|
oldPosition: oldPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void attach(ScrollPosition position) {
|
||||||
|
super.attach(position);
|
||||||
|
final _CarouselPosition carouselPosition = position as _CarouselPosition;
|
||||||
|
carouselPosition.itemExtent = _carouselState!._itemExtent;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue