mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08: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",
|
||||
"about": "About",
|
||||
"@about": {},
|
||||
"accept": "Accept",
|
||||
"@accept": {},
|
||||
"active": "Active",
|
||||
"@active": {},
|
||||
"actor": "{count, plural, other{Actors} one{Actor}}",
|
||||
"@actor": {
|
||||
"description": "actor",
|
||||
|
|
@ -14,14 +17,23 @@
|
|||
}
|
||||
},
|
||||
"addAsFavorite": "Add as favorite",
|
||||
"@addAsFavorite": {},
|
||||
"addToCollection": "Add to collection",
|
||||
"@addToCollection": {},
|
||||
"addToPlaylist": "Add to playlist",
|
||||
"@addToPlaylist": {},
|
||||
"advanced": "Advanced",
|
||||
"@advanced": {},
|
||||
"all": "All",
|
||||
"@all": {},
|
||||
"amoledBlack": "Amoled black",
|
||||
"@amoledBlack": {},
|
||||
"appLockAutoLogin": "Auto login",
|
||||
"@appLockAutoLogin": {},
|
||||
"appLockBiometrics": "Biometrics",
|
||||
"@appLockBiometrics": {},
|
||||
"appLockPasscode": "Passcode",
|
||||
"@appLockPasscode": {},
|
||||
"appLockTitle": "Set the log-in method for {userName}",
|
||||
"@appLockTitle": {
|
||||
"description": "Pop-up to pick a login method",
|
||||
|
|
@ -32,15 +44,24 @@
|
|||
}
|
||||
},
|
||||
"ascending": "Ascending",
|
||||
"@ascending": {},
|
||||
"audio": "Audio",
|
||||
"@audio": {},
|
||||
"autoPlay": "Auto-play",
|
||||
"@autoPlay": {},
|
||||
"backgroundBlur": "Background blur",
|
||||
"@backgroundBlur": {},
|
||||
"backgroundOpacity": "Background opacity",
|
||||
"biometricsFailedCheckAgain": "Biometrics failed check settings and try again",
|
||||
"@backgroundOpacity": {},
|
||||
"biometricsFailedCheckAgain": "Biometrics failed. Check settings and try again.",
|
||||
"@biometricsFailedCheckAgain": {},
|
||||
"bold": "Bold",
|
||||
"@bold": {},
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {},
|
||||
"change": "Change",
|
||||
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
|
||||
"@change": {},
|
||||
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
|
||||
"@chapter": {
|
||||
"description": "chapter",
|
||||
"placeholders": {
|
||||
|
|
@ -51,16 +72,27 @@
|
|||
}
|
||||
},
|
||||
"clear": "Clear",
|
||||
"@clear": {},
|
||||
"clearAllSettings": "Clear all settings",
|
||||
"@clearAllSettings": {},
|
||||
"clearAllSettingsQuestion": "Clear all settings?",
|
||||
"@clearAllSettingsQuestion": {},
|
||||
"clearChanges": "Clear changes",
|
||||
"@clearChanges": {},
|
||||
"clearSelection": "Clear selection",
|
||||
"@clearSelection": {},
|
||||
"close": "Close",
|
||||
"@close": {},
|
||||
"code": "Code",
|
||||
"@code": {},
|
||||
"collectionFolder": "Collection folder",
|
||||
"@collectionFolder": {},
|
||||
"color": "Color",
|
||||
"@color": {},
|
||||
"combined": "Combined",
|
||||
"@combined": {},
|
||||
"communityRating": "Community Rating",
|
||||
"@communityRating": {},
|
||||
"continuePage": "Continue - page {page}",
|
||||
"@continuePage": {
|
||||
"description": "Continue - page 1",
|
||||
|
|
@ -71,12 +103,19 @@
|
|||
}
|
||||
},
|
||||
"controls": "Controls",
|
||||
"@controls": {},
|
||||
"dashboard": "Dashboard",
|
||||
"@dashboard": {},
|
||||
"dashboardContinue": "Continue",
|
||||
"@dashboardContinue": {},
|
||||
"dashboardContinueListening": "Continue Listening",
|
||||
"@dashboardContinueListening": {},
|
||||
"dashboardContinueReading": "Continue Reading",
|
||||
"@dashboardContinueReading": {},
|
||||
"dashboardContinueWatching": "Continue Watching",
|
||||
"@dashboardContinueWatching": {},
|
||||
"dashboardNextUp": "Next-up",
|
||||
"@dashboardNextUp": {},
|
||||
"dashboardRecentlyAdded": "Recently added in {name}",
|
||||
"@dashboardRecentlyAdded": {
|
||||
"description": "Recently added on home screen",
|
||||
|
|
@ -87,10 +126,15 @@
|
|||
}
|
||||
},
|
||||
"dateAdded": "Date added",
|
||||
"@dateAdded": {},
|
||||
"dateLastContentAdded": "Date last content added",
|
||||
"@dateLastContentAdded": {},
|
||||
"datePlayed": "Date played",
|
||||
"@datePlayed": {},
|
||||
"days": "Days",
|
||||
"@days": {},
|
||||
"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": {
|
||||
"description": "Delete file from system",
|
||||
|
|
@ -110,7 +154,8 @@
|
|||
}
|
||||
},
|
||||
"descending": "Descending",
|
||||
"director": "{count, plural, other{Director} two{Directors}}",
|
||||
"@descending": {},
|
||||
"director": "{count, plural, other{Director} two{Directors}}",
|
||||
"@director": {
|
||||
"description": "director",
|
||||
"placeholders": {
|
||||
|
|
@ -120,19 +165,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"disableFilters": "Disable filters",
|
||||
"disabled": "Disabled",
|
||||
"disableFilters": "Turn off filters",
|
||||
"@disableFilters": {},
|
||||
"disabled": "Off",
|
||||
"@disabled": {},
|
||||
"discovered": "Discovered",
|
||||
"@discovered": {},
|
||||
"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": {},
|
||||
"downloadsPath": "Path",
|
||||
"@downloadsPath": {},
|
||||
"downloadsSyncedData": "Synced data",
|
||||
"@downloadsSyncedData": {},
|
||||
"downloadsTitle": "Downloads",
|
||||
"@downloadsTitle": {},
|
||||
"dynamicText": "Dynamic",
|
||||
"@dynamicText": {},
|
||||
"editMetadata": "Edit metadata",
|
||||
"@editMetadata": {},
|
||||
"empty": "Empty",
|
||||
"enabled": "Enabled",
|
||||
"@empty": {},
|
||||
"enabled": "On",
|
||||
"@enabled": {},
|
||||
"endsAt": "ends at {date}",
|
||||
"@endsAt": {
|
||||
"description": "endsAt",
|
||||
|
|
@ -154,10 +212,15 @@
|
|||
}
|
||||
},
|
||||
"error": "Error",
|
||||
"failedToLoadImage": "Failed to load image",
|
||||
"@error": {},
|
||||
"failedToLoadImage": "Could not load image",
|
||||
"@failedToLoadImage": {},
|
||||
"favorite": "Favorite",
|
||||
"@favorite": {},
|
||||
"favorites": "Favorites",
|
||||
"fetchingLibrary": "Fetching library items",
|
||||
"@favorites": {},
|
||||
"fetchingLibrary": "Fetching library items…",
|
||||
"@fetchingLibrary": {},
|
||||
"filter": "{count, plural, other{Filters} one{Filter}}",
|
||||
"@filter": {
|
||||
"description": "filter",
|
||||
|
|
@ -169,9 +232,13 @@
|
|||
}
|
||||
},
|
||||
"folders": "Folders",
|
||||
"@folders": {},
|
||||
"fontColor": "Font color",
|
||||
"@fontColor": {},
|
||||
"fontSize": "Font size",
|
||||
"@fontSize": {},
|
||||
"forceRefresh": "Force refresh",
|
||||
"@forceRefresh": {},
|
||||
"genre": "{count, plural, other{Genres} one{Genre}}",
|
||||
"@genre": {
|
||||
"description": "genre",
|
||||
|
|
@ -183,19 +250,35 @@
|
|||
}
|
||||
},
|
||||
"goTo": "Go To",
|
||||
"@goTo": {},
|
||||
"grid": "Grid",
|
||||
"@grid": {},
|
||||
"group": "Group",
|
||||
"@group": {},
|
||||
"groupBy": "Group by",
|
||||
"@groupBy": {},
|
||||
"heightOffset": "Height offset",
|
||||
"@heightOffset": {},
|
||||
"hide": "Hide",
|
||||
"@hide": {},
|
||||
"hideEmpty": "Hide empty",
|
||||
"@hideEmpty": {},
|
||||
"home": "Home",
|
||||
"@home": {},
|
||||
"homeBannerBanner": "Banner",
|
||||
"homeBannerCarousel": "Carousel",
|
||||
"identify": "Identify",
|
||||
"@identify": {},
|
||||
"immediately": "Immediately",
|
||||
"incorrectPinTryAgain": "Incorrect pin try again",
|
||||
"@immediately": {},
|
||||
"incorrectPinTryAgain": "Incorrect PIN. Try again.",
|
||||
"@incorrectPinTryAgain": {},
|
||||
"info": "Info",
|
||||
"invalidUrl": "Invalid url",
|
||||
"invalidUrlDesc": "Url needs to start with http(s)://",
|
||||
"@info": {},
|
||||
"invalidUrl": "Invalid URL",
|
||||
"@invalidUrl": {},
|
||||
"invalidUrlDesc": "URL needs to start with http(s)://",
|
||||
"@invalidUrlDesc": {},
|
||||
"itemCount": "Item count: {count}",
|
||||
"@itemCount": {
|
||||
"description": "Item count",
|
||||
|
|
@ -205,7 +288,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"label": "{count, plural, other{Labels} one{Label}}",
|
||||
"label": "{count, plural, other{Labels} one{Label}}",
|
||||
"@label": {
|
||||
"description": "label",
|
||||
"placeholders": {
|
||||
|
|
@ -225,16 +308,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"libraryFetchNoItemsFound": "No items found, try different settings.",
|
||||
"libraryPageSizeDesc": "Set the amount to load at a time. 0 disables paging",
|
||||
"libraryFetchNoItemsFound": "No items found. Try different settings.",
|
||||
"@libraryFetchNoItemsFound": {},
|
||||
"libraryPageSizeDesc": "Set the amount to load at a time. 0 turns off paging.",
|
||||
"@libraryPageSizeDesc": {},
|
||||
"libraryPageSizeTitle": "Library page size",
|
||||
"@libraryPageSizeTitle": {},
|
||||
"light": "Light",
|
||||
"@light": {},
|
||||
"list": "List",
|
||||
"@list": {},
|
||||
"lockscreen": "Lockscreen",
|
||||
"@lockscreen": {},
|
||||
"loggedIn": "Logged-in",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"logoutUserPopupContent": "This will log-out {userName} and delete te user from the app.\nYou will have to log back in to {serverName}.",
|
||||
"@loggedIn": {},
|
||||
"login": "Log in",
|
||||
"@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": {
|
||||
"description": "Pop-up for loging out the user description",
|
||||
"placeholders": {
|
||||
|
|
@ -246,7 +338,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"logoutUserPopupTitle": "Log-out user {userName}?",
|
||||
"logoutUserPopupTitle": "Log out {userName}?",
|
||||
"@logoutUserPopupTitle": {
|
||||
"description": "Pop-up for loging out the user",
|
||||
"placeholders": {
|
||||
|
|
@ -256,21 +348,37 @@
|
|||
}
|
||||
},
|
||||
"loop": "Loop",
|
||||
"@loop": {},
|
||||
"markAsUnwatched": "Mark as unwatched",
|
||||
"@markAsUnwatched": {},
|
||||
"markAsWatched": "Mark as watched",
|
||||
"@markAsWatched": {},
|
||||
"masonry": "Masonry",
|
||||
"@masonry": {},
|
||||
"mediaTypeBase": "Base Type",
|
||||
"@mediaTypeBase": {},
|
||||
"mediaTypeBook": "Book",
|
||||
"@mediaTypeBook": {},
|
||||
"mediaTypeBoxset": "Boxset",
|
||||
"@mediaTypeBoxset": {},
|
||||
"mediaTypeEpisode": "Episode",
|
||||
"@mediaTypeEpisode": {},
|
||||
"mediaTypeFolder": "Folder",
|
||||
"@mediaTypeFolder": {},
|
||||
"mediaTypeMovie": "Movie",
|
||||
"@mediaTypeMovie": {},
|
||||
"mediaTypePerson": "Person",
|
||||
"@mediaTypePerson": {},
|
||||
"mediaTypePhoto": "Photo",
|
||||
"@mediaTypePhoto": {},
|
||||
"mediaTypePhotoAlbum": "Photo Album",
|
||||
"@mediaTypePhotoAlbum": {},
|
||||
"mediaTypePlaylist": "Playlist",
|
||||
"@mediaTypePlaylist": {},
|
||||
"mediaTypeSeason": "Season",
|
||||
"@mediaTypeSeason": {},
|
||||
"mediaTypeSeries": "Series",
|
||||
"@mediaTypeSeries": {},
|
||||
"metaDataSavedFor": "Metadata saved for {item}",
|
||||
"@metaDataSavedFor": {
|
||||
"description": "metaDataSavedFor",
|
||||
|
|
@ -281,8 +389,11 @@
|
|||
}
|
||||
},
|
||||
"metadataRefreshDefault": "Scan for new and updated files",
|
||||
"@metadataRefreshDefault": {},
|
||||
"metadataRefreshFull": "Replace all metadata",
|
||||
"@metadataRefreshFull": {},
|
||||
"metadataRefreshValidation": "Search for missing metadata",
|
||||
"@metadataRefreshValidation": {},
|
||||
"minutes": "{count, plural, other{Minutes} one{Minute} }",
|
||||
"@minutes": {
|
||||
"description": "minute",
|
||||
|
|
@ -294,6 +405,7 @@
|
|||
}
|
||||
},
|
||||
"mode": "Mode",
|
||||
"@mode": {},
|
||||
"moreFrom": "More from {info}",
|
||||
"@moreFrom": {
|
||||
"description": "More from",
|
||||
|
|
@ -304,32 +416,61 @@
|
|||
}
|
||||
},
|
||||
"moreOptions": "More options",
|
||||
"@moreOptions": {},
|
||||
"mouseDragSupport": "Drag using mouse",
|
||||
"@mouseDragSupport": {},
|
||||
"musicAlbum": "Album",
|
||||
"@musicAlbum": {},
|
||||
"name": "Name",
|
||||
"@name": {},
|
||||
"nativeName": "English",
|
||||
"@nativeName": {},
|
||||
"navigation": "Navigation",
|
||||
"@navigation": {},
|
||||
"navigationDashboard": "Dashboard",
|
||||
"@navigationDashboard": {},
|
||||
"navigationFavorites": "Favorites",
|
||||
"@navigationFavorites": {},
|
||||
"navigationSync": "Synced",
|
||||
"@navigationSync": {},
|
||||
"never": "Never",
|
||||
"@never": {},
|
||||
"nextUp": "Next Up",
|
||||
"@nextUp": {},
|
||||
"noItemsSynced": "No items synced",
|
||||
"@noItemsSynced": {},
|
||||
"noItemsToShow": "No items to show",
|
||||
"@noItemsToShow": {},
|
||||
"noRating": "No rating",
|
||||
"@noRating": {},
|
||||
"noResults": "No results",
|
||||
"@noResults": {},
|
||||
"noServersFound": "No new servers found",
|
||||
"@noServersFound": {},
|
||||
"noSuggestionsFound": "No suggestions found",
|
||||
"@noSuggestionsFound": {},
|
||||
"none": "None",
|
||||
"@none": {},
|
||||
"normal": "Normal",
|
||||
"@normal": {},
|
||||
"notPartOfAlbum": "Not part of a album",
|
||||
"@notPartOfAlbum": {},
|
||||
"openParent": "Open parent",
|
||||
"@openParent": {},
|
||||
"openShow": "Open show",
|
||||
"@openShow": {},
|
||||
"openWebLink": "Open web link",
|
||||
"@openWebLink": {},
|
||||
"options": "Options",
|
||||
"@options": {},
|
||||
"other": "Other",
|
||||
"@other": {},
|
||||
"outlineColor": "Outline color",
|
||||
"@outlineColor": {},
|
||||
"outlineSize": "Outline size",
|
||||
"@outlineSize": {},
|
||||
"overview": "Overview",
|
||||
"@overview": {},
|
||||
"page": "Page {index}",
|
||||
"@page": {
|
||||
"description": "page",
|
||||
|
|
@ -340,11 +481,17 @@
|
|||
}
|
||||
},
|
||||
"parentalRating": "Parental Rating",
|
||||
"@parentalRating": {},
|
||||
"password": "Password",
|
||||
"@password": {},
|
||||
"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": {},
|
||||
"pathEditSelect": "Select downloads destination",
|
||||
"@pathEditSelect": {},
|
||||
"pathEditTitle": "Change location",
|
||||
"@pathEditTitle": {},
|
||||
"play": "Play {item}",
|
||||
"@play": {
|
||||
"description": "Play with",
|
||||
|
|
@ -355,6 +502,7 @@
|
|||
}
|
||||
},
|
||||
"playCount": "Play count",
|
||||
"@playCount": {},
|
||||
"playFrom": "Play from {name}",
|
||||
"@playFrom": {
|
||||
"description": "playFrom",
|
||||
|
|
@ -374,13 +522,21 @@
|
|||
}
|
||||
},
|
||||
"playLabel": "Play",
|
||||
"@playLabel": {},
|
||||
"playVideos": "Play videos",
|
||||
"@playVideos": {},
|
||||
"played": "Played",
|
||||
"@played": {},
|
||||
"quickConnectAction": "Enter quick connect code for",
|
||||
"@quickConnectAction": {},
|
||||
"quickConnectInputACode": "Input a code",
|
||||
"@quickConnectInputACode": {},
|
||||
"quickConnectTitle": "Quick-connect",
|
||||
"@quickConnectTitle": {},
|
||||
"quickConnectWrongCode": "Wrong code",
|
||||
"@quickConnectWrongCode": {},
|
||||
"random": "Random",
|
||||
"@random": {},
|
||||
"rating": "{count, plural, other{Ratings} one{Rating}}",
|
||||
"@rating": {
|
||||
"description": "rating",
|
||||
|
|
@ -392,6 +548,7 @@
|
|||
}
|
||||
},
|
||||
"reWatch": "Rewatch",
|
||||
"@reWatch": {},
|
||||
"read": "Read {item}",
|
||||
"@read": {
|
||||
"description": "read",
|
||||
|
|
@ -411,8 +568,11 @@
|
|||
}
|
||||
},
|
||||
"recursive": "Recursive",
|
||||
"@recursive": {},
|
||||
"refresh": "Refresh",
|
||||
"@refresh": {},
|
||||
"refreshMetadata": "Refresh metadata",
|
||||
"@refreshMetadata": {},
|
||||
"refreshPopup": "Refresh - {name}",
|
||||
"@refreshPopup": {
|
||||
"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": {},
|
||||
"releaseDate": "Release date",
|
||||
"@releaseDate": {},
|
||||
"removeAsFavorite": "Remove as favorite",
|
||||
"removeFromCollection": "Remove to collection",
|
||||
"removeFromPlaylist": "Remove to playlist",
|
||||
"@removeAsFavorite": {},
|
||||
"removeFromCollection": "Remove from collection",
|
||||
"@removeFromCollection": {},
|
||||
"removeFromPlaylist": "Remove from playlist",
|
||||
"@removeFromPlaylist": {},
|
||||
"replaceAllImages": "Replace all images",
|
||||
"@replaceAllImages": {},
|
||||
"replaceExistingImages": "Replace existing images",
|
||||
"@replaceExistingImages": {},
|
||||
"restart": "Restart",
|
||||
"@restart": {},
|
||||
"result": "Result",
|
||||
"@result": {},
|
||||
"resumable": "Resumable",
|
||||
"@resumable": {},
|
||||
"resume": "Resume {item}",
|
||||
"@resume": {
|
||||
"description": "resume",
|
||||
|
|
@ -442,12 +613,19 @@
|
|||
}
|
||||
},
|
||||
"retrievePublicListOfUsers": "Retrieve public list of users",
|
||||
"@retrievePublicListOfUsers": {},
|
||||
"retry": "Retry",
|
||||
"@retry": {},
|
||||
"runTime": "Run time",
|
||||
"@runTime": {},
|
||||
"save": "Save",
|
||||
"@save": {},
|
||||
"saved": "Saved",
|
||||
"@saved": {},
|
||||
"scanBiometricHint": "Verify identity",
|
||||
"@scanBiometricHint": {},
|
||||
"scanLibrary": "Scan library",
|
||||
"@scanLibrary": {},
|
||||
"scanYourFingerprintToAuthenticate": "Scan your fingerprint to authenticate {user}",
|
||||
"@scanYourFingerprintToAuthenticate": {
|
||||
"placeholders": {
|
||||
|
|
@ -456,7 +634,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"scanningName": "Scanning - {name}",
|
||||
"scanningName": "Scanning - {name}…",
|
||||
"@scanningName": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
|
@ -465,7 +643,9 @@
|
|||
}
|
||||
},
|
||||
"scrollToTop": "Scroll to top",
|
||||
"@scrollToTop": {},
|
||||
"search": "Search",
|
||||
"@search": {},
|
||||
"season": "{count, plural, other{Seasons} one{Season} }",
|
||||
"@season": {
|
||||
"description": "season",
|
||||
|
|
@ -487,9 +667,13 @@
|
|||
}
|
||||
},
|
||||
"selectAll": "Select all",
|
||||
"@selectAll": {},
|
||||
"selectTime": "Select time",
|
||||
"@selectTime": {},
|
||||
"selectViewType": "Select view type",
|
||||
"@selectViewType": {},
|
||||
"selected": "Selected",
|
||||
"@selected": {},
|
||||
"selectedWith": "Selected {info}",
|
||||
"@selectedWith": {
|
||||
"description": "selected",
|
||||
|
|
@ -500,7 +684,9 @@
|
|||
}
|
||||
},
|
||||
"separate": "Separate",
|
||||
"@separate": {},
|
||||
"server": "Server",
|
||||
"@server": {},
|
||||
"set": "Set",
|
||||
"@set": {
|
||||
"description": "Use for setting a certain value",
|
||||
|
|
@ -516,50 +702,95 @@
|
|||
}
|
||||
},
|
||||
"settingSecurityApplockTitle": "App lock",
|
||||
"@settingSecurityApplockTitle": {},
|
||||
"settings": "Settings",
|
||||
"@settings": {},
|
||||
"settingsBlurEpisodesDesc": "Blur all upcoming episodes",
|
||||
"@settingsBlurEpisodesDesc": {},
|
||||
"settingsBlurEpisodesTitle": "Blur next-up episodes",
|
||||
"@settingsBlurEpisodesTitle": {},
|
||||
"settingsBlurredPlaceholderDesc": "Show blurred background when loading posters",
|
||||
"@settingsBlurredPlaceholderDesc": {},
|
||||
"settingsBlurredPlaceholderTitle": "Blurred placeholder",
|
||||
"@settingsBlurredPlaceholderTitle": {},
|
||||
"settingsClientDesc": "General, Time-out, Layout, Theme",
|
||||
"@settingsClientDesc": {},
|
||||
"settingsClientTitle": "Fladder",
|
||||
"@settingsClientTitle": {},
|
||||
"settingsContinue": "Continue",
|
||||
"@settingsContinue": {},
|
||||
"settingsEnableOsMediaControls": "Enable OS media controls",
|
||||
"settingsHomeCarouselDesc": "Shows a carousel on the dashboard screen",
|
||||
"settingsHomeCarouselTitle": "Dashboard carousel",
|
||||
"@settingsEnableOsMediaControls": {},
|
||||
"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": {},
|
||||
"settingsHomeNextUpTitle": "Next-up posters",
|
||||
"@settingsHomeNextUpTitle": {},
|
||||
"settingsNextUpCutoffDays": "Next-up cutoff days",
|
||||
"@settingsNextUpCutoffDays": {},
|
||||
"settingsPlayerCustomSubtitlesDesc": "Customize Size, Color, Position, Outline",
|
||||
"@settingsPlayerCustomSubtitlesDesc": {},
|
||||
"settingsPlayerCustomSubtitlesTitle": "Customize subtitles",
|
||||
"@settingsPlayerCustomSubtitlesTitle": {},
|
||||
"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": {},
|
||||
"settingsPlayerNativeLibassAccelTitle": "Native libass subtitles",
|
||||
"@settingsPlayerNativeLibassAccelTitle": {},
|
||||
"settingsPlayerTitle": "Player",
|
||||
"settingsPlayerVideoHWAccelDesc": "Use the gpu to render video (recommended)",
|
||||
"@settingsPlayerTitle": {},
|
||||
"settingsPlayerVideoHWAccelDesc": "Use the GPU to render video (recommended)",
|
||||
"@settingsPlayerVideoHWAccelDesc": {},
|
||||
"settingsPlayerVideoHWAccelTitle": "Hardware acceleration",
|
||||
"@settingsPlayerVideoHWAccelTitle": {},
|
||||
"settingsPosterPinch": "Pinch-zoom to scale posters",
|
||||
"@settingsPosterPinch": {},
|
||||
"settingsPosterSize": "Poster size",
|
||||
"@settingsPosterSize": {},
|
||||
"settingsPosterSlider": "Show scale slider",
|
||||
"@settingsPosterSlider": {},
|
||||
"settingsProfileDesc": "Lockscreen",
|
||||
"@settingsProfileDesc": {},
|
||||
"settingsProfileTitle": "Profile",
|
||||
"@settingsProfileTitle": {},
|
||||
"settingsQuickConnectTitle": "Quick connect",
|
||||
"@settingsQuickConnectTitle": {},
|
||||
"settingsSecurity": "Security",
|
||||
"@settingsSecurity": {},
|
||||
"settingsShowScaleSlider": "Show poster size slide",
|
||||
"@settingsShowScaleSlider": {},
|
||||
"settingsVisual": "Visual",
|
||||
"@settingsVisual": {},
|
||||
"shadow": "Shadow",
|
||||
"@shadow": {},
|
||||
"showAlbum": "Show album",
|
||||
"@showAlbum": {},
|
||||
"showDetails": "Show details",
|
||||
"@showDetails": {},
|
||||
"showEmpty": "Show empty",
|
||||
"@showEmpty": {},
|
||||
"shuffleGallery": "Shuffle gallery",
|
||||
"@shuffleGallery": {},
|
||||
"shuffleVideos": "Shuffle videos",
|
||||
"@shuffleVideos": {},
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"somethingWentWrongPasswordCheck": "Something went wrong, check your password",
|
||||
"@somethingWentWrong": {},
|
||||
"somethingWentWrongPasswordCheck": "Something went wrong. Check your password.",
|
||||
"@somethingWentWrongPasswordCheck": {},
|
||||
"sortBy": "Sort by",
|
||||
"@sortBy": {},
|
||||
"sortName": "Name",
|
||||
"@sortName": {},
|
||||
"sortOrder": "Sort order",
|
||||
"@sortOrder": {},
|
||||
"start": "Start",
|
||||
"@start": {},
|
||||
"studio": "{count, plural, other{Studios} one{Studio}}",
|
||||
"@studio": {
|
||||
"description": "studio",
|
||||
|
|
@ -571,10 +802,15 @@
|
|||
}
|
||||
},
|
||||
"subtitleConfigurator": "Subtitle configurator",
|
||||
"@subtitleConfigurator": {},
|
||||
"subtitleConfiguratorPlaceHolder": "This is placeholder text, \n nothing to see here.",
|
||||
"@subtitleConfiguratorPlaceHolder": {},
|
||||
"subtitles": "Subtitles",
|
||||
"@subtitles": {},
|
||||
"switchUser": "Switch user",
|
||||
"@switchUser": {},
|
||||
"sync": "Sync",
|
||||
"@sync": {},
|
||||
"syncDeleteItemDesc": "Delete all synced data for?\n{item}",
|
||||
"@syncDeleteItemDesc": {
|
||||
"description": "Sync delete item pop-up window",
|
||||
|
|
@ -585,12 +821,19 @@
|
|||
}
|
||||
},
|
||||
"syncDeleteItemTitle": "Delete synced item",
|
||||
"@syncDeleteItemTitle": {},
|
||||
"syncDeletePopupPermanent": "This action is permanent and will remove all localy synced files",
|
||||
"@syncDeletePopupPermanent": {},
|
||||
"syncDetails": "Sync details",
|
||||
"@syncDetails": {},
|
||||
"syncOpenParent": "Open parent",
|
||||
"@syncOpenParent": {},
|
||||
"syncRemoveDataDesc": "Delete synced video data? This is permanent and you will need to re-sync the files",
|
||||
"@syncRemoveDataDesc": {},
|
||||
"syncRemoveDataTitle": "Remove synced data?",
|
||||
"@syncRemoveDataTitle": {},
|
||||
"syncedItems": "Synced items",
|
||||
"@syncedItems": {},
|
||||
"tag": "{count, plural, one{Tag} other{Tags}}",
|
||||
"@tag": {
|
||||
"description": "tag",
|
||||
|
|
@ -602,10 +845,15 @@
|
|||
}
|
||||
},
|
||||
"theme": "Theme",
|
||||
"@theme": {},
|
||||
"themeColor": "Theme color",
|
||||
"@themeColor": {},
|
||||
"themeModeDark": "Dark",
|
||||
"@themeModeDark": {},
|
||||
"themeModeLight": "Light",
|
||||
"@themeModeLight": {},
|
||||
"themeModeSystem": "System",
|
||||
"@themeModeSystem": {},
|
||||
"timeAndAnnotation": "{minutes} and {seconds}",
|
||||
"@timeAndAnnotation": {
|
||||
"description": "timeAndAnnotation",
|
||||
|
|
@ -619,6 +867,7 @@
|
|||
}
|
||||
},
|
||||
"timeOut": "Time-out",
|
||||
"@timeOut": {},
|
||||
"totalSize": "Total size: {size}",
|
||||
"@totalSize": {
|
||||
"placeholders": {
|
||||
|
|
@ -638,24 +887,43 @@
|
|||
}
|
||||
},
|
||||
"unPlayed": "Unplayed",
|
||||
"@unPlayed": {},
|
||||
"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": {},
|
||||
"useDefaults": "Use defaults",
|
||||
"@useDefaults": {},
|
||||
"userName": "Username",
|
||||
"@userName": {},
|
||||
"video": "Video",
|
||||
"@video": {},
|
||||
"videoScaling": "Video scaling",
|
||||
"@videoScaling": {},
|
||||
"videoScalingContain": "Contain",
|
||||
"@videoScalingContain": {},
|
||||
"videoScalingCover": "Cover",
|
||||
"@videoScalingCover": {},
|
||||
"videoScalingFill": "Fill",
|
||||
"@videoScalingFill": {},
|
||||
"videoScalingFillScreenDesc": "Fill the navigation and statusbar",
|
||||
"@videoScalingFillScreenDesc": {},
|
||||
"videoScalingFillScreenNotif": "Fill-screen overwrites video fit, in horizontal rotation",
|
||||
"@videoScalingFillScreenNotif": {},
|
||||
"videoScalingFillScreenTitle": "Fill screen",
|
||||
"@videoScalingFillScreenTitle": {},
|
||||
"videoScalingFitHeight": "Fit Height",
|
||||
"@videoScalingFitHeight": {},
|
||||
"videoScalingFitWidth": "Fit Width",
|
||||
"@videoScalingFitWidth": {},
|
||||
"videoScalingScaleDown": "ScaleDown",
|
||||
"@videoScalingScaleDown": {},
|
||||
"viewPhotos": "View photos",
|
||||
"@viewPhotos": {},
|
||||
"watchOn": "Watch on",
|
||||
"@watchOn": {},
|
||||
"writer": "{count, plural, other{Writer} two{Writers}}",
|
||||
"@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/material.dart';
|
||||
|
||||
import 'package:fladder/util/custom_color_themes.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.g.dart';
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
|
|||
Duration? nextUpDateCutoff,
|
||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||
ColorThemes? themeColor,
|
||||
@Default(HomeBanner.carousel) HomeBanner homeBanner,
|
||||
@Default(false) bool amoledBlack,
|
||||
@Default(false) bool blurPlaceHolders,
|
||||
@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 {
|
||||
final double x;
|
||||
final double y;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ mixin _$ClientSettingsModel {
|
|||
Duration? get nextUpDateCutoff => throw _privateConstructorUsedError;
|
||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||
ColorThemes? get themeColor => throw _privateConstructorUsedError;
|
||||
HomeBanner get homeBanner => throw _privateConstructorUsedError;
|
||||
bool get amoledBlack => throw _privateConstructorUsedError;
|
||||
bool get blurPlaceHolders => throw _privateConstructorUsedError;
|
||||
bool get blurUpcomingEpisodes => throw _privateConstructorUsedError;
|
||||
|
|
@ -38,8 +39,12 @@ mixin _$ClientSettingsModel {
|
|||
bool get mouseDragSupport => throw _privateConstructorUsedError;
|
||||
int? get libraryPageSize => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this ClientSettingsModel to a JSON map.
|
||||
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 =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
|
@ -58,6 +63,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
|||
Duration? nextUpDateCutoff,
|
||||
ThemeMode themeMode,
|
||||
ColorThemes? themeColor,
|
||||
HomeBanner homeBanner,
|
||||
bool amoledBlack,
|
||||
bool blurPlaceHolders,
|
||||
bool blurUpcomingEpisodes,
|
||||
|
|
@ -79,6 +85,8 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
|||
// ignore: unused_field
|
||||
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')
|
||||
@override
|
||||
$Res call({
|
||||
|
|
@ -89,6 +97,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
|||
Object? nextUpDateCutoff = freezed,
|
||||
Object? themeMode = null,
|
||||
Object? themeColor = freezed,
|
||||
Object? homeBanner = null,
|
||||
Object? amoledBlack = null,
|
||||
Object? blurPlaceHolders = null,
|
||||
Object? blurUpcomingEpisodes = null,
|
||||
|
|
@ -128,6 +137,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
|||
? _value.themeColor
|
||||
: themeColor // ignore: cast_nullable_to_non_nullable
|
||||
as ColorThemes?,
|
||||
homeBanner: null == homeBanner
|
||||
? _value.homeBanner
|
||||
: homeBanner // ignore: cast_nullable_to_non_nullable
|
||||
as HomeBanner,
|
||||
amoledBlack: null == amoledBlack
|
||||
? _value.amoledBlack
|
||||
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -184,6 +197,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
|||
Duration? nextUpDateCutoff,
|
||||
ThemeMode themeMode,
|
||||
ColorThemes? themeColor,
|
||||
HomeBanner homeBanner,
|
||||
bool amoledBlack,
|
||||
bool blurPlaceHolders,
|
||||
bool blurUpcomingEpisodes,
|
||||
|
|
@ -203,6 +217,8 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
|||
$Res Function(_$ClientSettingsModelImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of ClientSettingsModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
|
|
@ -213,6 +229,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
|||
Object? nextUpDateCutoff = freezed,
|
||||
Object? themeMode = null,
|
||||
Object? themeColor = freezed,
|
||||
Object? homeBanner = null,
|
||||
Object? amoledBlack = null,
|
||||
Object? blurPlaceHolders = null,
|
||||
Object? blurUpcomingEpisodes = null,
|
||||
|
|
@ -252,6 +269,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
|||
? _value.themeColor
|
||||
: themeColor // ignore: cast_nullable_to_non_nullable
|
||||
as ColorThemes?,
|
||||
homeBanner: null == homeBanner
|
||||
? _value.homeBanner
|
||||
: homeBanner // ignore: cast_nullable_to_non_nullable
|
||||
as HomeBanner,
|
||||
amoledBlack: null == amoledBlack
|
||||
? _value.amoledBlack
|
||||
: amoledBlack // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -304,6 +325,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
this.nextUpDateCutoff,
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.themeColor,
|
||||
this.homeBanner = HomeBanner.carousel,
|
||||
this.amoledBlack = false,
|
||||
this.blurPlaceHolders = false,
|
||||
this.blurUpcomingEpisodes = false,
|
||||
|
|
@ -338,6 +360,9 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
final ColorThemes? themeColor;
|
||||
@override
|
||||
@JsonKey()
|
||||
final HomeBanner homeBanner;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool amoledBlack;
|
||||
@override
|
||||
@JsonKey()
|
||||
|
|
@ -365,7 +390,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
|
||||
@override
|
||||
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
|
||||
|
|
@ -380,6 +405,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
|
||||
..add(DiagnosticsProperty('themeMode', themeMode))
|
||||
..add(DiagnosticsProperty('themeColor', themeColor))
|
||||
..add(DiagnosticsProperty('homeBanner', homeBanner))
|
||||
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
|
||||
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
|
||||
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
|
||||
|
|
@ -408,6 +434,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
other.themeMode == themeMode) &&
|
||||
(identical(other.themeColor, themeColor) ||
|
||||
other.themeColor == themeColor) &&
|
||||
(identical(other.homeBanner, homeBanner) ||
|
||||
other.homeBanner == homeBanner) &&
|
||||
(identical(other.amoledBlack, amoledBlack) ||
|
||||
other.amoledBlack == amoledBlack) &&
|
||||
(identical(other.blurPlaceHolders, blurPlaceHolders) ||
|
||||
|
|
@ -428,7 +456,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
other.libraryPageSize == libraryPageSize));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
|
|
@ -439,6 +467,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
nextUpDateCutoff,
|
||||
themeMode,
|
||||
themeColor,
|
||||
homeBanner,
|
||||
amoledBlack,
|
||||
blurPlaceHolders,
|
||||
blurUpcomingEpisodes,
|
||||
|
|
@ -449,7 +478,9 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
mouseDragSupport,
|
||||
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
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
||||
|
|
@ -473,6 +504,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
|||
final Duration? nextUpDateCutoff,
|
||||
final ThemeMode themeMode,
|
||||
final ColorThemes? themeColor,
|
||||
final HomeBanner homeBanner,
|
||||
final bool amoledBlack,
|
||||
final bool blurPlaceHolders,
|
||||
final bool blurUpcomingEpisodes,
|
||||
|
|
@ -502,6 +534,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
|||
@override
|
||||
ColorThemes? get themeColor;
|
||||
@override
|
||||
HomeBanner get homeBanner;
|
||||
@override
|
||||
bool get amoledBlack;
|
||||
@override
|
||||
bool get blurPlaceHolders;
|
||||
|
|
@ -520,8 +554,11 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
|||
bool get mouseDragSupport;
|
||||
@override
|
||||
int? get libraryPageSize;
|
||||
|
||||
/// Create a copy of ClientSettingsModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
|||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
ThemeMode.system,
|
||||
themeColor: $enumDecodeNullable(_$ColorThemesEnumMap, json['themeColor']),
|
||||
homeBanner:
|
||||
$enumDecodeNullable(_$HomeBannerEnumMap, json['homeBanner']) ??
|
||||
HomeBanner.carousel,
|
||||
amoledBlack: json['amoledBlack'] as bool? ?? false,
|
||||
blurPlaceHolders: json['blurPlaceHolders'] as bool? ?? false,
|
||||
blurUpcomingEpisodes: json['blurUpcomingEpisodes'] as bool? ?? false,
|
||||
|
|
@ -47,6 +50,7 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
|||
'nextUpDateCutoff': instance.nextUpDateCutoff?.inMicroseconds,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'themeColor': _$ColorThemesEnumMap[instance.themeColor],
|
||||
'homeBanner': _$HomeBannerEnumMap[instance.homeBanner]!,
|
||||
'amoledBlack': instance.amoledBlack,
|
||||
'blurPlaceHolders': instance.blurPlaceHolders,
|
||||
'blurUpcomingEpisodes': instance.blurUpcomingEpisodes,
|
||||
|
|
@ -81,3 +85,8 @@ const _$ColorThemesEnumMap = {
|
|||
ColorThemes.deepPurple: 'deepPurple',
|
||||
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/views_provider.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/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
|
|
@ -104,20 +104,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
SliverToBoxAdapter(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, AdaptiveLayout.layoutOf(context) == LayoutState.phone ? -14 : 0),
|
||||
child: ConstrainedBox(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TopPostersRow(posters: homeCarouselItems),
|
||||
),
|
||||
),
|
||||
} 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_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/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
|
|
@ -186,6 +187,28 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
.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(
|
||||
label: Text(context.localized.settingsHomeNextUpTitle),
|
||||
subLabel: Text(context.localized.settingsHomeNextUpDesc),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
|
@ -45,7 +44,7 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
leading: context.router.backButton(),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
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(
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.headlineLarge),
|
||||
|
|
@ -75,8 +74,7 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(top: AdaptiveLayout.of(context).isDesktop || kIsWeb ? 0 : null),
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate(items),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import 'package:async/async.dart';
|
||||
import 'package:flutter/material.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/screens/shared/media/components/media_play_button.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/item_base_model/play_item_helpers.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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class CarouselBanner extends ConsumerStatefulWidget {
|
||||
final PageController? controller;
|
||||
final List<ItemBaseModel> items;
|
||||
final double maxHeight;
|
||||
const CarouselBanner({
|
||||
this.controller,
|
||||
required this.items,
|
||||
this.maxHeight = 250,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -29,350 +28,120 @@ class CarouselBanner extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
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
|
||||
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: [
|
||||
Flexible(
|
||||
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;
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: widget.maxHeight),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxExtent = (constraints.maxHeight * 2.1).clamp(250.0, MediaQuery.sizeOf(context).shortestSide * 0.75);
|
||||
final border = BorderRadius.circular(18);
|
||||
return FladderCarousel(
|
||||
shape: RoundedRectangleBorder(borderRadius: border),
|
||||
onTap: (index) => widget.items[index].navigateTo(context),
|
||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
||||
? null
|
||||
: (index) {
|
||||
final poster = widget.items[index];
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: poster.generateActions(context, ref).listTileItems(scrollContext, useIcons: true),
|
||||
),
|
||||
),
|
||||
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: 3,
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
||||
shadows: shadows,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentItem.label(context) != null && currentItem is! MovieModel)
|
||||
Flexible(
|
||||
child: Text(
|
||||
currentItem.label(context)!,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context).textTheme.titleLarge?.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: 3,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
shadows: shadows,
|
||||
color: Colors.white.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 6)),
|
||||
),
|
||||
);
|
||||
},
|
||||
onSecondaryTap: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? null
|
||||
: (details) async {
|
||||
Offset localPosition = details.$2.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
final poster = widget.items[details.$1];
|
||||
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
itemExtent: maxExtent,
|
||||
children: [
|
||||
...widget.items.mapIndexed(
|
||||
(index, e) => LayoutBuilder(builder: (context, constraints) {
|
||||
final opacity = (constraints.maxWidth / maxExtent);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FladderImage(image: e.bannerImage),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: opacity.clamp(0, 1),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer.withOpacity(0.75),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
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(
|
||||
alignment: Alignment.bottomRight,
|
||||
alignment: Alignment.bottomLeft,
|
||||
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),
|
||||
),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
e.title,
|
||||
maxLines: 2,
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
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