diff --git a/README.md b/README.md index ec7fa57..695357d 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,14 @@
Mobile Fladder + Fladder Fladder Fladder Fladder + Fladder Fladder Fladder + Fladder Fladder
@@ -65,8 +68,14 @@ Tablet Fladder Fladder - Fladder - Fladder + Fladder + Fladder + Fladder + Fladder + Fladder + Fladder + Fladder + Fladder Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder) diff --git a/assets/marketing/screenshots/Mobile/Dashboard.png b/assets/marketing/screenshots/Mobile/Dashboard.png index 9ad9c74..a7d46bd 100644 Binary files a/assets/marketing/screenshots/Mobile/Dashboard.png and b/assets/marketing/screenshots/Mobile/Dashboard.png differ diff --git a/assets/marketing/screenshots/Mobile/Dashboard_2.png b/assets/marketing/screenshots/Mobile/Dashboard_2.png deleted file mode 100644 index 0fe0459..0000000 Binary files a/assets/marketing/screenshots/Mobile/Dashboard_2.png and /dev/null differ diff --git a/assets/marketing/screenshots/Mobile/Details.png b/assets/marketing/screenshots/Mobile/Details.png index c56ccfd..f41046b 100644 Binary files a/assets/marketing/screenshots/Mobile/Details.png and b/assets/marketing/screenshots/Mobile/Details.png differ diff --git a/assets/marketing/screenshots/Mobile/Details_2.png b/assets/marketing/screenshots/Mobile/Details_2.png index e551f3e..26f2828 100644 Binary files a/assets/marketing/screenshots/Mobile/Details_2.png and b/assets/marketing/screenshots/Mobile/Details_2.png differ diff --git a/assets/marketing/screenshots/Mobile/Favourites.png b/assets/marketing/screenshots/Mobile/Favourites.png index 585f1bb..20763f6 100644 Binary files a/assets/marketing/screenshots/Mobile/Favourites.png and b/assets/marketing/screenshots/Mobile/Favourites.png differ diff --git a/assets/marketing/screenshots/Mobile/Library.png b/assets/marketing/screenshots/Mobile/Library.png index e990197..0a27852 100644 Binary files a/assets/marketing/screenshots/Mobile/Library.png and b/assets/marketing/screenshots/Mobile/Library.png differ diff --git a/assets/marketing/screenshots/Mobile/Library_Search.png b/assets/marketing/screenshots/Mobile/Library_Search.png new file mode 100644 index 0000000..4a09053 Binary files /dev/null and b/assets/marketing/screenshots/Mobile/Library_Search.png differ diff --git a/assets/marketing/screenshots/Mobile/Player.png b/assets/marketing/screenshots/Mobile/Player.png index 151a834..20687e6 100644 Binary files a/assets/marketing/screenshots/Mobile/Player.png and b/assets/marketing/screenshots/Mobile/Player.png differ diff --git a/assets/marketing/screenshots/Mobile/Resume_Tab.png b/assets/marketing/screenshots/Mobile/Resume_Tab.png index 2b918b1..03a797b 100644 Binary files a/assets/marketing/screenshots/Mobile/Resume_Tab.png and b/assets/marketing/screenshots/Mobile/Resume_Tab.png differ diff --git a/assets/marketing/screenshots/Mobile/Settings.png b/assets/marketing/screenshots/Mobile/Settings.png index 666031d..efd44c1 100644 Binary files a/assets/marketing/screenshots/Mobile/Settings.png and b/assets/marketing/screenshots/Mobile/Settings.png differ diff --git a/assets/marketing/screenshots/Mobile/Sync.png b/assets/marketing/screenshots/Mobile/Sync.png index 04f22fe..b17e1a7 100644 Binary files a/assets/marketing/screenshots/Mobile/Sync.png and b/assets/marketing/screenshots/Mobile/Sync.png differ diff --git a/assets/marketing/screenshots/Tablet/Dashboard.png b/assets/marketing/screenshots/Tablet/Dashboard.png index ff361cd..107c853 100644 Binary files a/assets/marketing/screenshots/Tablet/Dashboard.png and b/assets/marketing/screenshots/Tablet/Dashboard.png differ diff --git a/assets/marketing/screenshots/Tablet/Details.png b/assets/marketing/screenshots/Tablet/Details.png index 4944efe..9d5e01f 100644 Binary files a/assets/marketing/screenshots/Tablet/Details.png and b/assets/marketing/screenshots/Tablet/Details.png differ diff --git a/assets/marketing/screenshots/Tablet/Details_2.png b/assets/marketing/screenshots/Tablet/Details_2.png new file mode 100644 index 0000000..197d44f Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Details_2.png differ diff --git a/assets/marketing/screenshots/Tablet/Favourites.png b/assets/marketing/screenshots/Tablet/Favourites.png new file mode 100644 index 0000000..6c4ff94 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Favourites.png differ diff --git a/assets/marketing/screenshots/Tablet/Library.png b/assets/marketing/screenshots/Tablet/Library.png new file mode 100644 index 0000000..569373c Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library.png differ diff --git a/assets/marketing/screenshots/Tablet/Library_Search.png b/assets/marketing/screenshots/Tablet/Library_Search.png new file mode 100644 index 0000000..2119aac Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library_Search.png differ diff --git a/assets/marketing/screenshots/Tablet/Player.png b/assets/marketing/screenshots/Tablet/Player.png new file mode 100644 index 0000000..72d96a2 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Player.png differ diff --git a/assets/marketing/screenshots/Tablet/Resume_Tab.png b/assets/marketing/screenshots/Tablet/Resume_Tab.png new file mode 100644 index 0000000..48750a7 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Resume_Tab.png differ diff --git a/assets/marketing/screenshots/Tablet/Settings.png b/assets/marketing/screenshots/Tablet/Settings.png index 3ed3277..b9e2a64 100644 Binary files a/assets/marketing/screenshots/Tablet/Settings.png and b/assets/marketing/screenshots/Tablet/Settings.png differ diff --git a/assets/marketing/screenshots/Tablet/Sync.png b/assets/marketing/screenshots/Tablet/Sync.png index 8a2b677..e5ae951 100644 Binary files a/assets/marketing/screenshots/Tablet/Sync.png and b/assets/marketing/screenshots/Tablet/Sync.png differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 194f281..62361c7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1211,5 +1211,14 @@ "rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.", "@rememberSubtitleSelectionsDesc": {}, "rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.", - "@rememberAudioSelectionsDesc": {} + "@rememberAudioSelectionsDesc": {}, + "similarToRecentlyPlayed": "Similar to recently played", + "similarToLikedItem": "Similar to liked item", + "hasDirectorFromRecentlyPlayed": "Has director from recently played", + "hasActorFromRecentlyPlayed": "Has actor from recently played", + "hasLikedDirector": "Has liked director", + "hasLikedActor": "Has liked actor", + "latest": "Latest", + "recommended": "Recommended" + } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e01ef0f..35b4992 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,6 @@ import 'package:universal_html/html.dart' as html; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/account_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; @@ -31,7 +30,7 @@ import 'package:fladder/routes/auto_router.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/theme.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/application_info.dart'; import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -108,13 +107,7 @@ void main() async { )) ], child: AdaptiveLayoutBuilder( - fallBack: ViewSize.tablet, - layoutPoints: [ - LayoutPoints(start: 0, end: 599, type: ViewSize.phone), - LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet), - LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop), - ], - child: const Main(), + child: (context) => const Main(), ), ), ); @@ -304,6 +297,7 @@ class _MainState extends ConsumerState
with WindowListener, WidgetsBinding colorScheme: darkTheme.colorScheme.copyWith( surface: amoledOverwrite, surfaceContainerHighest: amoledOverwrite, + surfaceContainerLow: amoledOverwrite, ), ), themeMode: themeMode, diff --git a/lib/models/account_model.dart b/lib/models/account_model.dart index e0fdb30..0e2a990 100644 --- a/lib/models/account_model.dart +++ b/lib/models/account_model.dart @@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/credentials_model.dart'; import 'package:fladder/models/library_filters_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; part 'account_model.freezed.dart'; diff --git a/lib/models/collection_types.dart b/lib/models/collection_types.dart index 10867df..282f430 100644 --- a/lib/models/collection_types.dart +++ b/lib/models/collection_types.dart @@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType { Set get itemKinds { switch (this) { + case CollectionType.music: + return {FladderItemType.musicAlbum}; case CollectionType.movies: return {FladderItemType.movie}; case CollectionType.tvshows: @@ -30,6 +32,8 @@ extension CollectionTypeExtension on CollectionType { IconData getIconType(bool outlined) { switch (this) { + case CollectionType.music: + return outlined ? IconsaxPlusLinear.music_square : IconsaxPlusBold.music_square; case CollectionType.movies: return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal; case CollectionType.tvshows: @@ -48,4 +52,16 @@ extension CollectionTypeExtension on CollectionType { return IconsaxPlusLinear.information; } } + + double? get aspectRatio => switch (this) { + CollectionType.music || + CollectionType.homevideos || + CollectionType.boxsets || + CollectionType.photos || + CollectionType.livetv || + CollectionType.playlists => + 0.8, + CollectionType.folders => 1.3, + _ => null, + }; } diff --git a/lib/models/item_base_model.dart b/lib/models/item_base_model.dart index ee16017..54b42d1 100644 --- a/lib/models/item_base_model.dart +++ b/lib/models/item_base_model.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:dart_mappable/dart_mappable.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; @@ -304,6 +304,15 @@ enum FladderItemType { const FladderItemType({required this.icon, required this.selectedicon}); + double get aspectRatio => switch (this) { + FladderItemType.video => 0.8, + FladderItemType.photo => 0.8, + FladderItemType.photoAlbum => 0.8, + FladderItemType.musicAlbum => 0.8, + FladderItemType.baseType => 0.8, + _ => 0.55, + }; + static Set get playable => { FladderItemType.series, FladderItemType.episode, @@ -317,27 +326,25 @@ enum FladderItemType { FladderItemType.video, }; - String label(BuildContext context) { - return switch (this) { - FladderItemType.baseType => context.localized.mediaTypeBase, - FladderItemType.audio => context.localized.audio, - FladderItemType.collectionFolder => context.localized.collectionFolder, - FladderItemType.musicAlbum => context.localized.musicAlbum, - FladderItemType.musicVideo => context.localized.video, - FladderItemType.video => context.localized.video, - FladderItemType.movie => context.localized.mediaTypeMovie, - FladderItemType.series => context.localized.mediaTypeSeries, - FladderItemType.season => context.localized.mediaTypeSeason, - FladderItemType.episode => context.localized.mediaTypeEpisode, - FladderItemType.photo => context.localized.mediaTypePhoto, - FladderItemType.person => context.localized.mediaTypePerson, - FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum, - FladderItemType.folder => context.localized.mediaTypeFolder, - FladderItemType.boxset => context.localized.mediaTypeBoxset, - FladderItemType.playlist => context.localized.mediaTypePlaylist, - FladderItemType.book => context.localized.mediaTypeBook, - }; - } + String label(BuildContext context) => switch (this) { + FladderItemType.baseType => context.localized.mediaTypeBase, + FladderItemType.audio => context.localized.audio, + FladderItemType.collectionFolder => context.localized.collectionFolder, + FladderItemType.musicAlbum => context.localized.musicAlbum, + FladderItemType.musicVideo => context.localized.video, + FladderItemType.video => context.localized.video, + FladderItemType.movie => context.localized.mediaTypeMovie, + FladderItemType.series => context.localized.mediaTypeSeries, + FladderItemType.season => context.localized.mediaTypeSeason, + FladderItemType.episode => context.localized.mediaTypeEpisode, + FladderItemType.photo => context.localized.mediaTypePhoto, + FladderItemType.person => context.localized.mediaTypePerson, + FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum, + FladderItemType.folder => context.localized.mediaTypeFolder, + FladderItemType.boxset => context.localized.mediaTypeBoxset, + FladderItemType.playlist => context.localized.mediaTypePlaylist, + FladderItemType.book => context.localized.mediaTypeBook, + }; BaseItemKind get dtoKind => switch (this) { FladderItemType.baseType => BaseItemKind.userrootfolder, diff --git a/lib/models/items/episode_model.dart b/lib/models/items/episode_model.dart index 56ec3d5..f21a7a3 100644 --- a/lib/models/items/episode_model.dart +++ b/lib/models/items/episode_model.dart @@ -199,20 +199,20 @@ extension EpisodeListExtensions on List { } EpisodeModel? get nextUp { - final episodes = whereNot((element) => element.season <= 0).toList(); + final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList(); + if (episodes.isEmpty) return null; - final lastProgress = episodes - .lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available); - final lastPlayed = - episodes.lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available); + final lastWatchedIndex = [ + episodes.lastIndexWhere((e) => e.userData.progress != 0), + episodes.lastIndexWhere((e) => e.userData.played), + ].reduce((a, b) => a > b ? a : b); - if (lastProgress == -1 && lastPlayed == -1) { - return episodes.firstWhereOrNull((element) => element.status == EpisodeStatus.available); - } else { - return episodes - .getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, episodes.length) - .firstWhereOrNull((element) => element.status == EpisodeStatus.available); + if (lastWatchedIndex >= 0 && lastWatchedIndex + 1 < episodes.length) { + final next = episodes.sublist(lastWatchedIndex + 1).firstWhereOrNull((e) => e.status == EpisodeStatus.available); + if (next != null) return next; } + + return episodes.firstOrNull; } bool get allPlayed { diff --git a/lib/models/items/series_model.dart b/lib/models/items/series_model.dart index c1f16f7..1a22ef9 100644 --- a/lib/models/items/series_model.dart +++ b/lib/models/items/series_model.dart @@ -1,5 +1,6 @@ -import 'package:fladder/screens/details_screens/series_detail_screen.dart'; import 'package:flutter/widgets.dart'; + +import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; @@ -9,8 +10,7 @@ import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/overview_model.dart'; import 'package:fladder/models/items/season_model.dart'; - -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:fladder/screens/details_screens/series_detail_screen.dart'; part 'series_model.mapper.dart'; diff --git a/lib/models/library_search/library_search_options.dart b/lib/models/library_search/library_search_options.dart index adcd681..dbefed0 100644 --- a/lib/models/library_search/library_search_options.dart +++ b/lib/models/library_search/library_search_options.dart @@ -1,9 +1,9 @@ -import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/util/localization_helper.dart'; +import 'package:flutter/material.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; -import 'package:flutter/material.dart'; +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/util/localization_helper.dart'; enum SortingOptions { name([ItemSortBy.name]), diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index 8eb8fcb..27bdfd4 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -198,17 +198,15 @@ class PlaybackModelHelper { final streamModel = firstItemToPlay.streamModel; final audioStreamIndex = selectAudioStream( - ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)), - oldModel?.mediaStreams?.currentAudioStream, - streamModel?.audioStreams, - streamModel?.defaultAudioStreamIndex - ); + ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)), + oldModel?.mediaStreams?.currentAudioStream, + streamModel?.audioStreams, + streamModel?.defaultAudioStreamIndex); final subStreamIndex = selectSubStream( - ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)), - oldModel?.mediaStreams?.currentSubStream, - streamModel?.subStreams, - streamModel?.defaultSubStreamIndex - ); + ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)), + oldModel?.mediaStreams?.currentSubStream, + streamModel?.subStreams, + streamModel?.defaultSubStreamIndex); final Response response = await api.itemsItemIdPlaybackInfoPost( itemId: firstItemToPlay.id, @@ -345,14 +343,12 @@ class PlaybackModelHelper { ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)), playbackModel.mediaStreams?.currentAudioStream, playbackModel.audioStreams, - playbackModel.mediaStreams?.defaultAudioStreamIndex - ); + playbackModel.mediaStreams?.defaultAudioStreamIndex); final subIndex = selectSubStream( ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)), playbackModel.mediaStreams?.currentSubStream, playbackModel.subStreams, - playbackModel.mediaStreams?.defaultSubStreamIndex - ); + playbackModel.mediaStreams?.defaultSubStreamIndex); Response response = await api.itemsItemIdPlaybackInfoPost( itemId: item.id, diff --git a/lib/models/recommended_model.dart b/lib/models/recommended_model.dart index 16f65dd..733a245 100644 --- a/lib/models/recommended_model.dart +++ b/lib/models/recommended_model.dart @@ -1,20 +1,66 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/util/localization_helper.dart'; + +sealed class NameSwitch { + const NameSwitch(); + + String label(BuildContext context); +} + +class NextUp extends NameSwitch { + const NextUp(); + + @override + String label(BuildContext context) => context.localized.nextUp; +} + +class Latest extends NameSwitch { + const Latest(); + + @override + String label(BuildContext context) => context.localized.latest; +} + +class Other extends NameSwitch { + final String customLabel; + + const Other(this.customLabel); + + @override + String label(BuildContext context) => customLabel; +} + +extension RecommendationTypeExtenstion on RecommendationType { + String label(BuildContext context) => switch (this) { + RecommendationType.similartorecentlyplayed => context.localized.similarToRecentlyPlayed, + RecommendationType.similartolikeditem => context.localized.similarToLikedItem, + RecommendationType.hasdirectorfromrecentlyplayed => context.localized.hasDirectorFromRecentlyPlayed, + RecommendationType.hasactorfromrecentlyplayed => context.localized.hasActorFromRecentlyPlayed, + RecommendationType.haslikeddirector => context.localized.hasLikedDirector, + RecommendationType.haslikedactor => context.localized.hasLikedActor, + _ => "", + }; +} class RecommendedModel { - final String name; + final NameSwitch name; final List posters; - final String type; + final RecommendationType? type; RecommendedModel({ required this.name, required this.posters, - required this.type, + this.type, }); RecommendedModel copyWith({ - String? name, + NameSwitch? name, List? posters, - String? type, + RecommendationType? type, }) { return RecommendedModel( name: name ?? this.name, @@ -22,4 +68,12 @@ class RecommendedModel { type: type ?? this.type, ); } + + factory RecommendedModel.fromBaseDto(RecommendationDto e, Ref ref) { + return RecommendedModel( + name: Other(e.baselineItemName ?? ""), + posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], + type: e.recommendationType, + ); + } } diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index 688f3ed..163ad28 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -33,7 +33,7 @@ class ClientSettingsModel with _$ClientSettingsModel { @Default(true) bool requireWifi, @Default(false) bool showAllCollectionTypes, @Default(2) int maxConcurrentDownloads, - @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, + @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant, int? libraryPageSize, }) = _ClientSettingsModel; diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index 50b2f0a..4cc3063 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -375,7 +375,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel this.requireWifi = true, this.showAllCollectionTypes = false, this.maxConcurrentDownloads = 2, - this.schemeVariant = DynamicSchemeVariant.tonalSpot, + this.schemeVariant = DynamicSchemeVariant.rainbow, this.libraryPageSize}) : super._(); diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index c2a34fa..ae036e7 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -40,7 +40,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson( (json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2, schemeVariant: $enumDecodeNullable( _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? - DynamicSchemeVariant.tonalSpot, + DynamicSchemeVariant.rainbow, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), ); diff --git a/lib/models/settings/home_settings_model.dart b/lib/models/settings/home_settings_model.dart index b6102b7..3c38c7b 100644 --- a/lib/models/settings/home_settings_model.dart +++ b/lib/models/settings/home_settings_model.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; part 'home_settings_model.freezed.dart'; @@ -36,42 +37,6 @@ T selectAvailableOrSmaller(T value, Set availableOptions, List allOptio return availableOptions.first; } -enum ViewSize { - phone, - tablet, - desktop; - - const ViewSize(); - - String label(BuildContext context) => switch (this) { - ViewSize.phone => context.localized.phone, - ViewSize.tablet => context.localized.tablet, - ViewSize.desktop => context.localized.desktop, - }; - - bool operator >(ViewSize other) => index > other.index; - bool operator >=(ViewSize other) => index >= other.index; - bool operator <(ViewSize other) => index < other.index; - bool operator <=(ViewSize other) => index <= other.index; -} - -enum LayoutMode { - single, - dual; - - const LayoutMode(); - - String label(BuildContext context) => switch (this) { - LayoutMode.single => context.localized.layoutModeSingle, - LayoutMode.dual => context.localized.layoutModeDual, - }; - - bool operator >(ViewSize other) => index > other.index; - bool operator >=(ViewSize other) => index >= other.index; - bool operator <(ViewSize other) => index < other.index; - bool operator <=(ViewSize other) => index <= other.index; -} - enum HomeBanner { hide, carousel, diff --git a/lib/models/view_model.dart b/lib/models/view_model.dart index 31e4b0b..53c8d2b 100644 --- a/lib/models/view_model.dart +++ b/lib/models/view_model.dart @@ -1,9 +1,17 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; +import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; class ViewModel { final String name; @@ -16,7 +24,9 @@ class ViewModel { final CollectionType collectionType; final dto.PlayAccess playAccess; final List recentlyAdded; + final ImagesData? imageData; final int childCount; + final String? path; ViewModel({ required this.name, required this.id, @@ -28,7 +38,9 @@ class ViewModel { required this.collectionType, required this.playAccess, required this.recentlyAdded, + required this.imageData, required this.childCount, + required this.path, }); ViewModel copyWith({ @@ -42,7 +54,9 @@ class ViewModel { CollectionType? collectionType, dto.PlayAccess? playAccess, List? recentlyAdded, + ImagesData? imageData, int? childCount, + String? path, }) { return ViewModel( name: name ?? this.name, @@ -55,7 +69,9 @@ class ViewModel { collectionType: collectionType ?? this.collectionType, playAccess: playAccess ?? this.playAccess, recentlyAdded: recentlyAdded ?? this.recentlyAdded, + imageData: imageData ?? this.imageData, childCount: childCount ?? this.childCount, + path: path ?? this.path, ); } @@ -69,11 +85,13 @@ class ViewModel { canDownload: item.canDownload ?? false, parentId: item.parentId ?? "", recentlyAdded: [], + imageData: ImagesData.fromBaseItem(item, ref), collectionType: CollectionType.values .firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ?? CollectionType.folders, playAccess: item.playAccess ?? PlayAccess.none, childCount: item.childCount ?? 0, + path: "", ); } @@ -88,6 +106,27 @@ class ViewModel { return id.hashCode ^ serverId.hashCode; } + NavigationButton toNavigationButton( + bool selected, + bool horizontal, + bool expanded, + FutureOr Function() action, { + FutureOr Function()? onLongPress, + List? trailing, + }) { + return NavigationButton( + label: name, + selected: selected, + onPressed: action, + onLongPress: onLongPress, + horizontal: horizontal, + expanded: expanded, + trailing: trailing ?? [], + selectedIcon: Icon(collectionType.icon), + icon: Icon(collectionType.iconOutlined), + ); + } + @override String toString() { return 'ViewModel(name: $name, id: $id, serverId: $serverId, dateCreated: $dateCreated, canDelete: $canDelete, canDownload: $canDownload, parentId: $parentId, collectionType: $collectionType, playAccess: $playAccess, recentlyAdded: $recentlyAdded, childCount: $childCount)'; diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart index 9527f56..1c8ca7d 100644 --- a/lib/providers/dashboard_provider.dart +++ b/lib/providers/dashboard_provider.dart @@ -1,3 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/home_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -6,7 +8,6 @@ import 'package:fladder/providers/service_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/util/list_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; final dashboardProvider = StateNotifierProvider((ref) { return DashboardNotifier(ref); @@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.video], enableTotalRecordCount: false, @@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.audio], enableTotalRecordCount: false, @@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.book], enableTotalRecordCount: false, @@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier { final nextResponse = await api.showsNextUpGet( limit: 16, - nextUpDateCutoff: DateTime.now() - .subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), + nextUpDateCutoff: DateTime.now().subtract( + ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), fields: [ ItemFields.parentid, ItemFields.mediastreams, ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], ); diff --git a/lib/providers/favourites_provider.dart b/lib/providers/favourites_provider.dart index d948396..907bf46 100644 --- a/lib/providers/favourites_provider.dart +++ b/lib/providers/favourites_provider.dart @@ -1,4 +1,6 @@ import 'package:chopper/chopper.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/favourites_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -6,7 +8,6 @@ import 'package:fladder/models/view_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; final favouritesProvider = StateNotifierProvider((ref) { return FavouritesNotifier(ref); @@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier { isFavorite: true, limit: 10, sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], + sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], ); final response2 = await api.itemsGet( parentId: viewModel?.id, @@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier { limit: 10, includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder], sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], + sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], ); return [...?response.body?.items, ...?response2.body?.items]; } diff --git a/lib/providers/library_provider.dart b/lib/providers/library_provider.dart deleted file mode 100644 index c32489b..0000000 --- a/lib/providers/library_provider.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:chopper/chopper.dart'; -import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/items/photos_model.dart'; -import 'package:fladder/models/library_model.dart'; -import 'package:fladder/models/recommended_model.dart'; -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/api_provider.dart'; -import 'package:fladder/providers/service_provider.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; - -bool _useFolders(ViewModel model) { - switch (model.collectionType) { - case CollectionType.boxsets: - case CollectionType.homevideos: - case CollectionType.folders: - return true; - default: - return false; - } -} - -final libraryProvider = StateNotifierProvider.autoDispose.family((ref, id) { - return LibraryNotifier(ref); -}); - -class LibraryNotifier extends StateNotifier { - LibraryNotifier(this.ref) : super(null); - - final Ref ref; - - late final JellyService api = ref.read(jellyApiProvider); - - set loading(bool value) { - state = state?.copyWith(loading: value); - } - - bool get loading => state?.loading ?? true; - - Future setupLibrary(ViewModel viewModel) async { - state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie); - } - - Future loadLibrary(ViewModel viewModel) async { - final response = await api.itemsGet( - parentId: viewModel.id, - sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear], - isMissing: false, - excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [], - fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid], - ); - state = state?.copyWith(posters: response.body?.items); - loading = false; - return response; - } - - Future loadRecommendations(ViewModel viewModel) async { - loading = true; - //Clear recommendations because of all the copying - state = state?.copyWith(recommendations: []); - final latest = await api.usersUserIdItemsLatestGet( - parentId: viewModel.id, - limit: 14, - isPlayed: false, - imageTypeLimit: 1, - includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null, - ); - state = state?.copyWith( - recommendations: [ - ...?state?.recommendations, - RecommendedModel( - name: "Latest", - posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], - type: "Latest", - ), - ], - ); - if (viewModel.collectionType == CollectionType.movies) { - final response = await api.moviesRecommendationsGet( - parentId: viewModel.id, - categoryLimit: 6, - itemLimit: 8, - fields: [ItemFields.mediasourcecount], - ); - state = state?.copyWith(recommendations: [ - ...?state?.recommendations, - ...response.body?.map( - (e) => RecommendedModel( - name: e.baselineItemName ?? "", - posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], - type: e.recommendationType.toString(), - ), - ) ?? - [], - ]); - loading = false; - } else { - final nextUp = await api.showsNextUpGet( - parentId: viewModel.id, - limit: 14, - imageTypeLimit: 1, - fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio], - ); - state = state?.copyWith(recommendations: [ - ...?state?.recommendations, - ...[ - RecommendedModel( - name: "Next up", - posters: nextUp.body?.items - ?.map( - (e) => ItemBaseModel.fromBaseDto( - e, - ref, - ), - ) - .toList() ?? - [], - type: "Latest series") - ], - ]); - loading = false; - } - } - - Future loadFavourites(ViewModel viewModel) async { - loading = true; - final response = await api.itemsGet( - parentId: viewModel.id, - isFavorite: true, - recursive: true, - ); - - state = state?.copyWith(favourites: response.body?.items); - loading = false; - return response; - } - - Future loadTimeline(ViewModel viewModel) async { - loading = true; - final response = await api.itemsGet( - parentId: viewModel.id, - recursive: true, - fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated], - sortBy: [ItemSortBy.datecreated], - sortOrder: [SortOrder.descending], - includeItemTypes: [ - BaseItemKind.photo, - BaseItemKind.video, - ], - ); - state = state?.copyWith( - timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(), - ); - loading = false; - return response; - } - - Future loadGenres(ViewModel viewModel) async { - final genres = await api.genresGet( - sortBy: [ItemSortBy.sortname], - sortOrder: [SortOrder.ascending], - includeItemTypes: viewModel.collectionType == CollectionType.movies - ? [BaseItemKind.movie] - : [ - BaseItemKind.series, - ], - parentId: viewModel.id, - ); - state = state?.copyWith( - genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList()); - return null; - } -} diff --git a/lib/providers/library_screen_provider.dart b/lib/providers/library_screen_provider.dart new file mode 100644 index 0000000..811ed72 --- /dev/null +++ b/lib/providers/library_screen_provider.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; + +import 'package:chopper/chopper.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; +import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/recommended_model.dart'; +import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/api_provider.dart'; +import 'package:fladder/providers/service_provider.dart'; +import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; + +part 'library_screen_provider.freezed.dart'; +part 'library_screen_provider.g.dart'; + +enum LibraryViewType { + recommended, + favourites, + genres; + + const LibraryViewType(); + + String label(BuildContext context) => switch (this) { + LibraryViewType.recommended => context.localized.recommended, + LibraryViewType.favourites => context.localized.favorites, + LibraryViewType.genres => context.localized.genre(2), + }; + + IconData get icon => switch (this) { + LibraryViewType.recommended => IconsaxPlusLinear.star, + LibraryViewType.favourites => IconsaxPlusLinear.heart, + LibraryViewType.genres => IconsaxPlusLinear.hierarchy_3, + }; + + IconData get iconSelected => switch (this) { + LibraryViewType.recommended => IconsaxPlusBold.star, + LibraryViewType.favourites => IconsaxPlusBold.heart, + LibraryViewType.genres => IconsaxPlusBold.hierarchy_3, + }; +} + +@Freezed(fromJson: false, toJson: false) +class LibraryScreenModel with _$LibraryScreenModel { + factory LibraryScreenModel({ + @Default([]) List views, + ViewModel? selectedViewModel, + @Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set viewType, + @Default([]) List recommendations, + @Default([]) List genres, + @Default([]) List favourites, + }) = _LibraryScreenModel; +} + +@Riverpod(keepAlive: true) +class LibraryScreen extends _$LibraryScreen { + late final JellyService api = ref.read(jellyApiProvider); + + @override + LibraryScreenModel build() => LibraryScreenModel(); + + Future fetchAllLibraries() async { + final views = await ref.read(viewsProvider.notifier).fetchViews(); + state = state.copyWith(views: views?.views ?? []); + if (state.views.isEmpty) return; + final viewModel = state.selectedViewModel ?? state.views.firstOrNull; + if (viewModel == null) return; + selectLibrary(viewModel); + await loadLibrary(viewModel); + } + + Future selectLibrary(ViewModel viewModel) async { + state = state.copyWith(selectedViewModel: viewModel); + } + + Future setViewType(Set type) async { + state = state.copyWith(viewType: type); + } + + Future loadLibrary(ViewModel viewModel) async { + await loadRecommendations(viewModel); + await loadGenres(viewModel); + await loadFavourites(viewModel); + return null; + } + + Future loadRecommendations(ViewModel viewModel) async { + List newRecommendations = []; + final latest = await api.usersUserIdItemsLatestGet( + parentId: viewModel.id, + limit: 14, + isPlayed: false, + imageTypeLimit: 1, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + ); + newRecommendations = [ + ...newRecommendations, + RecommendedModel( + name: const Latest(), + posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], + type: null, + ), + ]; + if (viewModel.collectionType == CollectionType.movies) { + final response = await api.moviesRecommendationsGet( + parentId: viewModel.id, + categoryLimit: 6, + itemLimit: 14, + fields: [ItemFields.mediasourcecount], + ); + newRecommendations = [ + ...newRecommendations, + ...(response.body?.map( + (e) => RecommendedModel.fromBaseDto(e, ref), + ) ?? + []) + ]; + } else { + final nextUp = await api.showsNextUpGet( + parentId: viewModel.id, + limit: 14, + imageTypeLimit: 1, + fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio], + ); + newRecommendations = [ + ...newRecommendations, + RecommendedModel( + name: const NextUp(), + posters: nextUp.body?.items + ?.map( + (e) => ItemBaseModel.fromBaseDto( + e, + ref, + ), + ) + .toList() ?? + [], + type: null, + ) + ]; + } + + state = state.copyWith( + recommendations: newRecommendations, + ); + } + + Future loadFavourites(ViewModel viewModel) async { + final response = await api.itemsGet( + parentId: viewModel.id, + isFavorite: true, + recursive: true, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + enableImageTypes: [ImageType.primary], + fields: [ + ItemFields.primaryimageaspectratio, + ItemFields.mediasourcecount, + ], + enableTotalRecordCount: false, + ); + + state = state.copyWith(favourites: response.body?.items ?? []); + return response; + } + + Future loadGenres(ViewModel viewModel) async { + final genres = await api.genresGet( + sortBy: [ItemSortBy.sortname], + sortOrder: [SortOrder.ascending], + includeItemTypes: + viewModel.collectionType == CollectionType.movies ? [BaseItemKind.movie] : [BaseItemKind.series], + parentId: viewModel.id, + ); + + final filteredGenres = (genres.body?.items?.map( + (item) => GenreItems(id: item.id ?? "", name: item.name ?? ""), + ) ?? + []) + .toList(); + + if (filteredGenres.isEmpty) return null; + + final results = await Future.wait(filteredGenres.map((genre) async { + final response = await api.itemsGet( + parentId: viewModel.id, + genreIds: [genre.id], + limit: 9, + recursive: true, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + enableImageTypes: [ImageType.primary], + fields: [ + ItemFields.primaryimageaspectratio, + ItemFields.mediasourcecount, + ], + sortBy: [ItemSortBy.random], + enableTotalRecordCount: false, + imageTypeLimit: 1, + sortOrder: [SortOrder.ascending], + ); + + final items = response.body?.items; + if (items != null && items.isNotEmpty) { + return RecommendedModel(name: Other(genre.name), posters: items); + } + return null; + })); + + state = state.copyWith( + genres: results.whereType().toList(), + ); + + return null; + } +} diff --git a/lib/providers/library_screen_provider.freezed.dart b/lib/providers/library_screen_provider.freezed.dart new file mode 100644 index 0000000..b2a6d84 --- /dev/null +++ b/lib/providers/library_screen_provider.freezed.dart @@ -0,0 +1,301 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'library_screen_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LibraryScreenModel { + List get views => throw _privateConstructorUsedError; + ViewModel? get selectedViewModel => throw _privateConstructorUsedError; + Set get viewType => throw _privateConstructorUsedError; + List get recommendations => + throw _privateConstructorUsedError; + List get genres => throw _privateConstructorUsedError; + List get favourites => throw _privateConstructorUsedError; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LibraryScreenModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LibraryScreenModelCopyWith<$Res> { + factory $LibraryScreenModelCopyWith( + LibraryScreenModel value, $Res Function(LibraryScreenModel) then) = + _$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>; + @useResult + $Res call( + {List views, + ViewModel? selectedViewModel, + Set viewType, + List recommendations, + List genres, + List favourites}); +} + +/// @nodoc +class _$LibraryScreenModelCopyWithImpl<$Res, $Val extends LibraryScreenModel> + implements $LibraryScreenModelCopyWith<$Res> { + _$LibraryScreenModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? views = null, + Object? selectedViewModel = freezed, + Object? viewType = null, + Object? recommendations = null, + Object? genres = null, + Object? favourites = null, + }) { + return _then(_value.copyWith( + views: null == views + ? _value.views + : views // ignore: cast_nullable_to_non_nullable + as List, + selectedViewModel: freezed == selectedViewModel + ? _value.selectedViewModel + : selectedViewModel // ignore: cast_nullable_to_non_nullable + as ViewModel?, + viewType: null == viewType + ? _value.viewType + : viewType // ignore: cast_nullable_to_non_nullable + as Set, + recommendations: null == recommendations + ? _value.recommendations + : recommendations // ignore: cast_nullable_to_non_nullable + as List, + genres: null == genres + ? _value.genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + favourites: null == favourites + ? _value.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LibraryScreenModelImplCopyWith<$Res> + implements $LibraryScreenModelCopyWith<$Res> { + factory _$$LibraryScreenModelImplCopyWith(_$LibraryScreenModelImpl value, + $Res Function(_$LibraryScreenModelImpl) then) = + __$$LibraryScreenModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List views, + ViewModel? selectedViewModel, + Set viewType, + List recommendations, + List genres, + List favourites}); +} + +/// @nodoc +class __$$LibraryScreenModelImplCopyWithImpl<$Res> + extends _$LibraryScreenModelCopyWithImpl<$Res, _$LibraryScreenModelImpl> + implements _$$LibraryScreenModelImplCopyWith<$Res> { + __$$LibraryScreenModelImplCopyWithImpl(_$LibraryScreenModelImpl _value, + $Res Function(_$LibraryScreenModelImpl) _then) + : super(_value, _then); + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? views = null, + Object? selectedViewModel = freezed, + Object? viewType = null, + Object? recommendations = null, + Object? genres = null, + Object? favourites = null, + }) { + return _then(_$LibraryScreenModelImpl( + views: null == views + ? _value._views + : views // ignore: cast_nullable_to_non_nullable + as List, + selectedViewModel: freezed == selectedViewModel + ? _value.selectedViewModel + : selectedViewModel // ignore: cast_nullable_to_non_nullable + as ViewModel?, + viewType: null == viewType + ? _value._viewType + : viewType // ignore: cast_nullable_to_non_nullable + as Set, + recommendations: null == recommendations + ? _value._recommendations + : recommendations // ignore: cast_nullable_to_non_nullable + as List, + genres: null == genres + ? _value._genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + favourites: null == favourites + ? _value._favourites + : favourites // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$LibraryScreenModelImpl implements _LibraryScreenModel { + _$LibraryScreenModelImpl( + {final List views = const [], + this.selectedViewModel, + final Set viewType = const { + LibraryViewType.recommended, + LibraryViewType.favourites + }, + final List recommendations = const [], + final List genres = const [], + final List favourites = const []}) + : _views = views, + _viewType = viewType, + _recommendations = recommendations, + _genres = genres, + _favourites = favourites; + + final List _views; + @override + @JsonKey() + List get views { + if (_views is EqualUnmodifiableListView) return _views; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_views); + } + + @override + final ViewModel? selectedViewModel; + final Set _viewType; + @override + @JsonKey() + Set get viewType { + if (_viewType is EqualUnmodifiableSetView) return _viewType; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_viewType); + } + + final List _recommendations; + @override + @JsonKey() + List get recommendations { + if (_recommendations is EqualUnmodifiableListView) return _recommendations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_recommendations); + } + + final List _genres; + @override + @JsonKey() + List get genres { + if (_genres is EqualUnmodifiableListView) return _genres; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_genres); + } + + final List _favourites; + @override + @JsonKey() + List get favourites { + if (_favourites is EqualUnmodifiableListView) return _favourites; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_favourites); + } + + @override + String toString() { + return 'LibraryScreenModel(views: $views, selectedViewModel: $selectedViewModel, viewType: $viewType, recommendations: $recommendations, genres: $genres, favourites: $favourites)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LibraryScreenModelImpl && + const DeepCollectionEquality().equals(other._views, _views) && + (identical(other.selectedViewModel, selectedViewModel) || + other.selectedViewModel == selectedViewModel) && + const DeepCollectionEquality().equals(other._viewType, _viewType) && + const DeepCollectionEquality() + .equals(other._recommendations, _recommendations) && + const DeepCollectionEquality().equals(other._genres, _genres) && + const DeepCollectionEquality() + .equals(other._favourites, _favourites)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_views), + selectedViewModel, + const DeepCollectionEquality().hash(_viewType), + const DeepCollectionEquality().hash(_recommendations), + const DeepCollectionEquality().hash(_genres), + const DeepCollectionEquality().hash(_favourites)); + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith => + __$$LibraryScreenModelImplCopyWithImpl<_$LibraryScreenModelImpl>( + this, _$identity); +} + +abstract class _LibraryScreenModel implements LibraryScreenModel { + factory _LibraryScreenModel( + {final List views, + final ViewModel? selectedViewModel, + final Set viewType, + final List recommendations, + final List genres, + final List favourites}) = _$LibraryScreenModelImpl; + + @override + List get views; + @override + ViewModel? get selectedViewModel; + @override + Set get viewType; + @override + List get recommendations; + @override + List get genres; + @override + List get favourites; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/providers/library_screen_provider.g.dart b/lib/providers/library_screen_provider.g.dart new file mode 100644 index 0000000..7017b4e --- /dev/null +++ b/lib/providers/library_screen_provider.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_screen_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c'; + +/// See also [LibraryScreen]. +@ProviderFor(LibraryScreen) +final libraryScreenProvider = + NotifierProvider.internal( + LibraryScreen.new, + name: r'libraryScreenProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$libraryScreenHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LibraryScreen = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/library_search_provider.dart b/lib/providers/library_search_provider.dart index 64b626d..a6360aa 100644 --- a/lib/providers/library_search_provider.dart +++ b/lib/providers/library_search_provider.dart @@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier { .toSet() .toList(); var tempState = state.copyWith(); - final genres = mappedList - .expand((element) => element?.genres ?? []) - .nonNulls - .sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase())); + final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel)))) + .expand((element) => element) + .toSet() + .toList(); final tags = mappedList .expand((element) => element?.tags ?? []) .sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); tempState = tempState.copyWith( types: state.types.setAll(false).setKeys(enabledCollections, true), - genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres), + genres: {for (var element in genres) element.name: false}.replaceMap(tempState.genres), studios: {for (var element in studios) element: false}.replaceMap(tempState.studios), tags: {for (var element in tags) element: false}.replaceMap(tempState.tags), ); @@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier { return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? []; } + Future> _loadGenres(ViewModel viewModel) async { + final response = await api.genresGet(parentId: viewModel.id); + return response.body?.items?.map((e) => GenreItems(id: e.id ?? "", name: e.name ?? "")).toList() ?? []; + } + Future _loadLibrary( {ViewModel? viewModel, bool? recursive, diff --git a/lib/providers/settings/home_settings_provider.dart b/lib/providers/settings/home_settings_provider.dart index 0d20f50..335c2ce 100644 --- a/lib/providers/settings/home_settings_provider.dart +++ b/lib/providers/settings/home_settings_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/shared_provider.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; final homeSettingsProvider = StateNotifierProvider((ref) { return HomeSettingsNotifier(ref); diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart index 45e6dd2..285aa71 100644 --- a/lib/providers/sync/background_download_provider.g.dart +++ b/lib/providers/sync/background_download_provider.g.dart @@ -7,7 +7,7 @@ part of 'background_download_provider.dart'; // ************************************************************************** String _$backgroundDownloaderHash() => - r'df72b6338a8e80178935985ba17c43bf720f4522'; + r'dc27f708fc2f1695d37afcb99f8814bc024037af'; /// See also [BackgroundDownloader]. @ProviderFor(BackgroundDownloader) diff --git a/lib/providers/user_provider.g.dart b/lib/providers/user_provider.g.dart index a699761..07e79b3 100644 --- a/lib/providers/user_provider.g.dart +++ b/lib/providers/user_provider.g.dart @@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef; -String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900'; +String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a'; /// See also [User]. @ProviderFor(User) diff --git a/lib/providers/views_provider.dart b/lib/providers/views_provider.dart index 1a0ad02..c04e5a0 100644 --- a/lib/providers/views_provider.dart +++ b/lib/providers/views_provider.dart @@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier { late final JellyService api = ref.read(jellyApiProvider); - Future fetchViews() async { - if (state.loading) return; + Future fetchViews() async { + if (state.loading) return null; final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes)); final response = await api.usersUserIdViewsGet( includeExternalContent: showAllCollections, @@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], ); return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList()); @@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier { .where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true)) .toList(), loading: false); + return state; } void clear() { diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index 2f44c78..c04beaf 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -51,6 +51,7 @@ final List homeRoutes = [ _dashboardRoute, _favouritesRoute, _syncedRoute, + _librariesRoute, ]; final List _defaultRoutes = [ @@ -79,6 +80,13 @@ final AutoRoute _syncedRoute = CustomRoute( path: 'synced', ); +final AutoRoute _librariesRoute = CustomRoute( + page: LibraryRoute.page, + transitionsBuilder: TransitionsBuilders.fadeIn, + maintainState: false, + path: 'libraries', +); + final List _settingsChildren = [ CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'), CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 3b79be3..5ebe950 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -8,35 +8,36 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i16; -import 'package:fladder/models/item_base_model.dart' as _i17; -import 'package:fladder/models/items/photos_model.dart' as _i20; +import 'package:auto_route/auto_route.dart' as _i17; +import 'package:fladder/models/item_base_model.dart' as _i18; +import 'package:fladder/models/items/photos_model.dart' as _i21; import 'package:fladder/models/library_search/library_search_options.dart' - as _i19; + as _i20; import 'package:fladder/routes/nested_details_screen.dart' as _i4; import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3; import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5; import 'package:fladder/screens/home_screen.dart' as _i6; +import 'package:fladder/screens/library/library_screen.dart' as _i7; import 'package:fladder/screens/library_search/library_search_screen.dart' - as _i7; -import 'package:fladder/screens/login/lock_screen.dart' as _i8; -import 'package:fladder/screens/login/login_screen.dart' as _i9; + as _i8; +import 'package:fladder/screens/login/lock_screen.dart' as _i9; +import 'package:fladder/screens/login/login_screen.dart' as _i10; import 'package:fladder/screens/settings/about_settings_page.dart' as _i1; import 'package:fladder/screens/settings/client_settings_page.dart' as _i2; -import 'package:fladder/screens/settings/player_settings_page.dart' as _i10; -import 'package:fladder/screens/settings/security_settings_page.dart' as _i11; -import 'package:fladder/screens/settings/settings_screen.dart' as _i12; +import 'package:fladder/screens/settings/player_settings_page.dart' as _i11; +import 'package:fladder/screens/settings/security_settings_page.dart' as _i12; +import 'package:fladder/screens/settings/settings_screen.dart' as _i13; import 'package:fladder/screens/settings/settings_selection_screen.dart' - as _i13; -import 'package:fladder/screens/splash_screen.dart' as _i14; -import 'package:fladder/screens/syncing/synced_screen.dart' as _i15; -import 'package:flutter/foundation.dart' as _i18; -import 'package:flutter/material.dart' as _i21; + as _i14; +import 'package:fladder/screens/splash_screen.dart' as _i15; +import 'package:fladder/screens/syncing/synced_screen.dart' as _i16; +import 'package:flutter/foundation.dart' as _i19; +import 'package:flutter/material.dart' as _i22; /// generated route for /// [_i1.AboutSettingsPage] -class AboutSettingsRoute extends _i16.PageRouteInfo { - const AboutSettingsRoute({List<_i16.PageRouteInfo>? children}) +class AboutSettingsRoute extends _i17.PageRouteInfo { + const AboutSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( AboutSettingsRoute.name, initialChildren: children, @@ -44,7 +45,7 @@ class AboutSettingsRoute extends _i16.PageRouteInfo { static const String name = 'AboutSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i1.AboutSettingsPage(); @@ -54,8 +55,8 @@ class AboutSettingsRoute extends _i16.PageRouteInfo { /// generated route for /// [_i2.ClientSettingsPage] -class ClientSettingsRoute extends _i16.PageRouteInfo { - const ClientSettingsRoute({List<_i16.PageRouteInfo>? children}) +class ClientSettingsRoute extends _i17.PageRouteInfo { + const ClientSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( ClientSettingsRoute.name, initialChildren: children, @@ -63,7 +64,7 @@ class ClientSettingsRoute extends _i16.PageRouteInfo { static const String name = 'ClientSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i2.ClientSettingsPage(); @@ -73,8 +74,8 @@ class ClientSettingsRoute extends _i16.PageRouteInfo { /// generated route for /// [_i3.DashboardScreen] -class DashboardRoute extends _i16.PageRouteInfo { - const DashboardRoute({List<_i16.PageRouteInfo>? children}) +class DashboardRoute extends _i17.PageRouteInfo { + const DashboardRoute({List<_i17.PageRouteInfo>? children}) : super( DashboardRoute.name, initialChildren: children, @@ -82,7 +83,7 @@ class DashboardRoute extends _i16.PageRouteInfo { static const String name = 'DashboardRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i3.DashboardScreen(); @@ -92,12 +93,12 @@ class DashboardRoute extends _i16.PageRouteInfo { /// generated route for /// [_i4.DetailsScreen] -class DetailsRoute extends _i16.PageRouteInfo { +class DetailsRoute extends _i17.PageRouteInfo { DetailsRoute({ String id = '', - _i17.ItemBaseModel? item, - _i18.Key? key, - List<_i16.PageRouteInfo>? children, + _i18.ItemBaseModel? item, + _i19.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( DetailsRoute.name, args: DetailsRouteArgs( @@ -111,7 +112,7 @@ class DetailsRoute extends _i16.PageRouteInfo { static const String name = 'DetailsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -139,9 +140,9 @@ class DetailsRouteArgs { final String id; - final _i17.ItemBaseModel? item; + final _i18.ItemBaseModel? item; - final _i18.Key? key; + final _i19.Key? key; @override String toString() { @@ -151,8 +152,8 @@ class DetailsRouteArgs { /// generated route for /// [_i5.FavouritesScreen] -class FavouritesRoute extends _i16.PageRouteInfo { - const FavouritesRoute({List<_i16.PageRouteInfo>? children}) +class FavouritesRoute extends _i17.PageRouteInfo { + const FavouritesRoute({List<_i17.PageRouteInfo>? children}) : super( FavouritesRoute.name, initialChildren: children, @@ -160,7 +161,7 @@ class FavouritesRoute extends _i16.PageRouteInfo { static const String name = 'FavouritesRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i5.FavouritesScreen(); @@ -170,8 +171,8 @@ class FavouritesRoute extends _i16.PageRouteInfo { /// generated route for /// [_i6.HomeScreen] -class HomeRoute extends _i16.PageRouteInfo { - const HomeRoute({List<_i16.PageRouteInfo>? children}) +class HomeRoute extends _i17.PageRouteInfo { + const HomeRoute({List<_i17.PageRouteInfo>? children}) : super( HomeRoute.name, initialChildren: children, @@ -179,7 +180,7 @@ class HomeRoute extends _i16.PageRouteInfo { static const String name = 'HomeRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i6.HomeScreen(); @@ -188,17 +189,36 @@ class HomeRoute extends _i16.PageRouteInfo { } /// generated route for -/// [_i7.LibrarySearchScreen] -class LibrarySearchRoute extends _i16.PageRouteInfo { +/// [_i7.LibraryScreen] +class LibraryRoute extends _i17.PageRouteInfo { + const LibraryRoute({List<_i17.PageRouteInfo>? children}) + : super( + LibraryRoute.name, + initialChildren: children, + ); + + static const String name = 'LibraryRoute'; + + static _i17.PageInfo page = _i17.PageInfo( + name, + builder: (data) { + return const _i7.LibraryScreen(); + }, + ); +} + +/// generated route for +/// [_i8.LibrarySearchScreen] +class LibrarySearchRoute extends _i17.PageRouteInfo { LibrarySearchRoute({ String? viewModelId, List? folderId, bool? favourites, - _i19.SortingOrder? sortOrder, - _i19.SortingOptions? sortingOptions, - _i20.PhotoModel? photoToView, - _i18.Key? key, - List<_i16.PageRouteInfo>? children, + _i20.SortingOrder? sortOrder, + _i20.SortingOptions? sortingOptions, + _i21.PhotoModel? photoToView, + _i19.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( LibrarySearchRoute.name, args: LibrarySearchRouteArgs( @@ -222,7 +242,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo { static const String name = 'LibrarySearchRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -234,7 +254,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo { sortOrder: queryParams.get('sortOrder'), sortingOptions: queryParams.get('sortOptions'), )); - return _i7.LibrarySearchScreen( + return _i8.LibrarySearchScreen( viewModelId: args.viewModelId, folderId: args.folderId, favourites: args.favourites, @@ -264,13 +284,13 @@ class LibrarySearchRouteArgs { final bool? favourites; - final _i19.SortingOrder? sortOrder; + final _i20.SortingOrder? sortOrder; - final _i19.SortingOptions? sortingOptions; + final _i20.SortingOptions? sortingOptions; - final _i20.PhotoModel? photoToView; + final _i21.PhotoModel? photoToView; - final _i18.Key? key; + final _i19.Key? key; @override String toString() { @@ -279,9 +299,9 @@ class LibrarySearchRouteArgs { } /// generated route for -/// [_i8.LockScreen] -class LockRoute extends _i16.PageRouteInfo { - const LockRoute({List<_i16.PageRouteInfo>? children}) +/// [_i9.LockScreen] +class LockRoute extends _i17.PageRouteInfo { + const LockRoute({List<_i17.PageRouteInfo>? children}) : super( LockRoute.name, initialChildren: children, @@ -289,18 +309,18 @@ class LockRoute extends _i16.PageRouteInfo { static const String name = 'LockRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i8.LockScreen(); + return const _i9.LockScreen(); }, ); } /// generated route for -/// [_i9.LoginScreen] -class LoginRoute extends _i16.PageRouteInfo { - const LoginRoute({List<_i16.PageRouteInfo>? children}) +/// [_i10.LoginScreen] +class LoginRoute extends _i17.PageRouteInfo { + const LoginRoute({List<_i17.PageRouteInfo>? children}) : super( LoginRoute.name, initialChildren: children, @@ -308,18 +328,18 @@ class LoginRoute extends _i16.PageRouteInfo { static const String name = 'LoginRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i9.LoginScreen(); + return const _i10.LoginScreen(); }, ); } /// generated route for -/// [_i10.PlayerSettingsPage] -class PlayerSettingsRoute extends _i16.PageRouteInfo { - const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i11.PlayerSettingsPage] +class PlayerSettingsRoute extends _i17.PageRouteInfo { + const PlayerSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( PlayerSettingsRoute.name, initialChildren: children, @@ -327,18 +347,18 @@ class PlayerSettingsRoute extends _i16.PageRouteInfo { static const String name = 'PlayerSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i10.PlayerSettingsPage(); + return const _i11.PlayerSettingsPage(); }, ); } /// generated route for -/// [_i11.SecuritySettingsPage] -class SecuritySettingsRoute extends _i16.PageRouteInfo { - const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i12.SecuritySettingsPage] +class SecuritySettingsRoute extends _i17.PageRouteInfo { + const SecuritySettingsRoute({List<_i17.PageRouteInfo>? children}) : super( SecuritySettingsRoute.name, initialChildren: children, @@ -346,18 +366,18 @@ class SecuritySettingsRoute extends _i16.PageRouteInfo { static const String name = 'SecuritySettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i11.SecuritySettingsPage(); + return const _i12.SecuritySettingsPage(); }, ); } /// generated route for -/// [_i12.SettingsScreen] -class SettingsRoute extends _i16.PageRouteInfo { - const SettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i13.SettingsScreen] +class SettingsRoute extends _i17.PageRouteInfo { + const SettingsRoute({List<_i17.PageRouteInfo>? children}) : super( SettingsRoute.name, initialChildren: children, @@ -365,18 +385,18 @@ class SettingsRoute extends _i16.PageRouteInfo { static const String name = 'SettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i12.SettingsScreen(); + return const _i13.SettingsScreen(); }, ); } /// generated route for -/// [_i13.SettingsSelectionScreen] -class SettingsSelectionRoute extends _i16.PageRouteInfo { - const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children}) +/// [_i14.SettingsSelectionScreen] +class SettingsSelectionRoute extends _i17.PageRouteInfo { + const SettingsSelectionRoute({List<_i17.PageRouteInfo>? children}) : super( SettingsSelectionRoute.name, initialChildren: children, @@ -384,21 +404,21 @@ class SettingsSelectionRoute extends _i16.PageRouteInfo { static const String name = 'SettingsSelectionRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i13.SettingsSelectionScreen(); + return const _i14.SettingsSelectionScreen(); }, ); } /// generated route for -/// [_i14.SplashScreen] -class SplashRoute extends _i16.PageRouteInfo { +/// [_i15.SplashScreen] +class SplashRoute extends _i17.PageRouteInfo { SplashRoute({ dynamic Function(bool)? loggedIn, - _i21.Key? key, - List<_i16.PageRouteInfo>? children, + _i22.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( SplashRoute.name, args: SplashRouteArgs( @@ -410,12 +430,12 @@ class SplashRoute extends _i16.PageRouteInfo { static const String name = 'SplashRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SplashRouteArgs()); - return _i14.SplashScreen( + return _i15.SplashScreen( loggedIn: args.loggedIn, key: args.key, ); @@ -431,7 +451,7 @@ class SplashRouteArgs { final dynamic Function(bool)? loggedIn; - final _i21.Key? key; + final _i22.Key? key; @override String toString() { @@ -440,12 +460,12 @@ class SplashRouteArgs { } /// generated route for -/// [_i15.SyncedScreen] -class SyncedRoute extends _i16.PageRouteInfo { +/// [_i16.SyncedScreen] +class SyncedRoute extends _i17.PageRouteInfo { SyncedRoute({ - _i21.ScrollController? navigationScrollController, - _i21.Key? key, - List<_i16.PageRouteInfo>? children, + _i22.ScrollController? navigationScrollController, + _i22.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( SyncedRoute.name, args: SyncedRouteArgs( @@ -457,12 +477,12 @@ class SyncedRoute extends _i16.PageRouteInfo { static const String name = 'SyncedRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SyncedRouteArgs()); - return _i15.SyncedScreen( + return _i16.SyncedScreen( navigationScrollController: args.navigationScrollController, key: args.key, ); @@ -476,9 +496,9 @@ class SyncedRouteArgs { this.key, }); - final _i21.ScrollController? navigationScrollController; + final _i22.ScrollController? navigationScrollController; - final _i21.Key? key; + final _i22.Key? key; @override String toString() { diff --git a/lib/screens/book_viewer/book_viewer_chapters.dart b/lib/screens/book_viewer/book_viewer_chapters.dart index 57c7a83..dede4b3 100644 --- a/lib/screens/book_viewer/book_viewer_chapters.dart +++ b/lib/screens/book_viewer/book_viewer_chapters.dart @@ -1,7 +1,7 @@ import 'package:fladder/models/book_model.dart'; import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart index b33bb83..205b257 100644 --- a/lib/screens/book_viewer/book_viewer_controls.dart +++ b/lib/screens/book_viewer/book_viewer_controls.dart @@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart'; import 'package:fladder/screens/book_viewer/book_viewer_settings.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; diff --git a/lib/screens/book_viewer/book_viewer_settings.dart b/lib/screens/book_viewer/book_viewer_settings.dart index 738b4ec..38b6301 100644 --- a/lib/screens/book_viewer/book_viewer_settings.dart +++ b/lib/screens/book_viewer/book_viewer_settings.dart @@ -1,5 +1,5 @@ import 'package:fladder/providers/settings/book_viewer_settings_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 02320a5..af0ea5d 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; +import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/dashboard_provider.dart'; @@ -19,10 +20,11 @@ import 'package:fladder/screens/dashboard/home_banner_widget.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'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final padding = AdaptiveLayout.adaptivePadding(context); + final dashboardData = ref.watch(dashboardProvider); final views = ref.watch(viewsProvider); final homeSettings = ref.watch(homeSettingsProvider); @@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState { return MediaQuery.removeViewInsets( context: context, child: NestedScaffold( + background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]), body: PullToRefresh( refreshKey: _refreshIndicatorKey, displacement: 80 + MediaQuery.of(context).viewPadding.top, @@ -104,7 +109,13 @@ class _DashboardScreenState extends ConsumerState { SliverToBoxAdapter( child: Transform.translate( offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0), - child: HomeBannerWidget(posters: homeCarouselItems), + child: Padding( + padding: AdaptiveLayout.adaptivePadding( + context, + horizontalPadding: 0, + ), + child: HomeBannerWidget(posters: homeCarouselItems), + ), ), ), }, @@ -122,6 +133,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueWatching, posters: resumeVideo, ), @@ -130,6 +142,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueListening, posters: resumeAudio, ), @@ -138,6 +151,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueReading, posters: resumeBooks, ), @@ -146,6 +160,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.nextUp, posters: dashboardData.nextUp, ), @@ -153,6 +168,7 @@ class _DashboardScreenState extends ConsumerState { if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinue, posters: [...allResume, ...dashboardData.nextUp], ), @@ -161,7 +177,9 @@ class _DashboardScreenState extends ConsumerState { .where((element) => element.recentlyAdded.isNotEmpty) .map((view) => SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardRecentlyAdded(view.name), + collectionAspectRatio: view.collectionType.aspectRatio, onLabelClick: () => context.router.push(LibrarySearchRoute( viewModelId: view.id, sortingOptions: switch (view.collectionType) { diff --git a/lib/screens/dashboard/home_banner_widget.dart b/lib/screens/dashboard/home_banner_widget.dart index c793db7..db10520 100644 --- a/lib/screens/dashboard/home_banner_widget.dart +++ b/lib/screens/dashboard/home_banner_widget.dart @@ -27,9 +27,12 @@ class HomeBannerWidget extends ConsumerWidget { const SizedBox(height: 24) ], ), - HomeBanner.banner => MediaBanner( - items: posters, - maxHeight: maxHeight, + HomeBanner.banner => Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: MediaBanner( + items: posters, + maxHeight: maxHeight, + ), ), _ => const SizedBox.shrink(), }; diff --git a/lib/screens/details_screens/components/overview_header.dart b/lib/screens/details_screens/components/overview_header.dart index 90959be..7e9a2a1 100644 --- a/lib/screens/details_screens/components/overview_header.dart +++ b/lib/screens/details_screens/components/overview_header.dart @@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/chip_button.dart'; import 'package:fladder/screens/shared/media/components/media_header.dart'; import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/details_screens/empty_item.dart b/lib/screens/details_screens/empty_item.dart index 795a198..7bcea6d 100644 --- a/lib/screens/details_screens/empty_item.dart +++ b/lib/screens/details_screens/empty_item.dart @@ -37,36 +37,42 @@ class EmptyItem extends ConsumerWidget { } }, ), - content: (padding) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 350), - child: AspectRatio( - aspectRatio: 0.67, - child: Card( - elevation: 6, - color: Theme.of(context).colorScheme.secondaryContainer, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.0, - color: Colors.white.withValues(alpha: 0.10), + content: (padding) => Center( + child: Padding( + padding: padding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 350), + child: AspectRatio( + aspectRatio: 0.67, + child: Card( + elevation: 6, + color: Theme.of(context).colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.0, + color: Colors.white.withValues(alpha: 0.10), + ), + borderRadius: FladderTheme.defaultShape.borderRadius, + ), + child: FladderImage( + image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull, + placeHolder: PosterPlaceholder(item: item), + ), ), - borderRadius: FladderTheme.defaultShape.borderRadius, - ), - child: FladderImage( - image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull, - placeHolder: PosterPlaceholder(item: item), ), ), - ), + Text( + item.title, + style: Theme.of(context).textTheme.titleLarge, + ), + Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."), + ].addInBetween(const SizedBox(height: 32)), ), - Text( - item.title, - style: Theme.of(context).textTheme.titleLarge, - ), - Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."), - ].addInBetween(const SizedBox(height: 32)), + ), ), ); } diff --git a/lib/screens/details_screens/episode_detail_screen.dart b/lib/screens/details_screens/episode_detail_screen.dart index 967d4bd..fcaaa5d 100644 --- a/lib/screens/details_screens/episode_detail_screen.dart +++ b/lib/screens/details_screens/episode_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/episode_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; @@ -18,7 +17,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.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'; diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart index 7c64fe9..d76c65f 100644 --- a/lib/screens/details_screens/movie_detail_screen.dart +++ b/lib/screens/details_screens/movie_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/movies_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; @@ -17,7 +16,7 @@ import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.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'; diff --git a/lib/screens/details_screens/person_detail_screen.dart b/lib/screens/details_screens/person_detail_screen.dart index 13ff8a5..175b54b 100644 --- a/lib/screens/details_screens/person_detail_screen.dart +++ b/lib/screens/details_screens/person_detail_screen.dart @@ -1,4 +1,3 @@ -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -11,7 +10,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/detail_scaffold.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState { spacing: 32, children: [ Container( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), diff --git a/lib/screens/details_screens/series_detail_screen.dart b/lib/screens/details_screens/series_detail_screen.dart index 32b91eb..b2a497f 100644 --- a/lib/screens/details_screens/series_detail_screen.dart +++ b/lib/screens/details_screens/series_detail_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/series_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/series_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/overview_header.dart'; @@ -19,7 +18,7 @@ import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/media/season_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.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'; diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index fe78f14..afee5db 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -1,19 +1,20 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/routes/auto_router.gr.dart'; -import 'package:fladder/screens/shared/nested_scaffold.dart'; -import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; -import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/favourites_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/routes/auto_router.gr.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'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; +import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; +import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @RoutePage() @@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final favourites = ref.watch(favouritesProvider); + final padding = AdaptiveLayout.adaptivePadding(context); return PullToRefresh( onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(), child: NestedScaffold( + background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()), body: PinchPosterZoom( scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), child: CustomScrollView( @@ -52,25 +55,19 @@ class FavouritesScreen extends ConsumerWidget { ), ...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map( (e) => SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: PosterGrid( - stickyHeader: true, - name: e.key.label(context), - posters: e.value, - ), + child: PosterRow( + contentPadding: padding, + label: e.key.label(context), + posters: e.value, ), ), ), if (favourites.people.isNotEmpty) SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: PosterGrid( - stickyHeader: true, - name: "People", - posters: favourites.people, - ), + child: PosterRow( + contentPadding: padding, + label: context.localized.actor(favourites.people.length), + posters: favourites.people, ), ), const DefautlSliverBottomPadding(), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 61c0d55..3fb9fff 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart'; enum HomeTabs { dashboard, + library, favorites, - sync; + sync, + ; + + const HomeTabs(); + + IconData get icon => switch (this) { + HomeTabs.dashboard => IconsaxPlusLinear.home_1, + HomeTabs.library => IconsaxPlusLinear.book, + HomeTabs.favorites => IconsaxPlusLinear.heart, + HomeTabs.sync => IconsaxPlusLinear.cloud, + }; + + IconData get selectedIcon => switch (this) { + HomeTabs.dashboard => IconsaxPlusBold.home_1, + HomeTabs.library => IconsaxPlusBold.book, + HomeTabs.favorites => IconsaxPlusBold.heart, + HomeTabs.sync => IconsaxPlusBold.cloud, + }; + + Future navigate(BuildContext context) => switch (this) { + HomeTabs.dashboard => context.router.navigate(const DashboardRoute()), + HomeTabs.library => context.router.navigate(const LibraryRoute()), + HomeTabs.favorites => context.router.navigate(const FavouritesRoute()), + HomeTabs.sync => context.router.navigate(SyncedRoute()), + }; + + String label(BuildContext context) => switch (this) { + HomeTabs.dashboard => context.localized.dashboard, + HomeTabs.library => context.localized.library(0), + HomeTabs.favorites => context.localized.favorites, + HomeTabs.sync => context.localized.sync, + }; } @RoutePage() @@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget { case HomeTabs.dashboard: return DestinationModel( label: context.localized.navigationDashboard, - icon: const Icon(IconsaxPlusLinear.home), - selectedIcon: const Icon(IconsaxPlusBold.home), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: const DashboardRoute(), - action: () => context.router.navigate(const DashboardRoute()), + action: () => e.navigate(context), floatingActionButton: AdaptiveFab( context: context, title: context.localized.search, @@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget { case HomeTabs.favorites: return DestinationModel( label: context.localized.navigationFavorites, - icon: const Icon(IconsaxPlusLinear.heart), - selectedIcon: const Icon(IconsaxPlusBold.heart), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: const FavouritesRoute(), floatingActionButton: AdaptiveFab( context: context, @@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget { onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), child: const Icon(IconsaxPlusLinear.heart_search), ), - action: () => context.router.navigate(const FavouritesRoute()), + action: () => e.navigate(context), ); case HomeTabs.sync: if (canDownload) { return DestinationModel( label: context.localized.navigationSync, - icon: const Icon(IconsaxPlusLinear.cloud), - selectedIcon: const Icon(IconsaxPlusBold.cloud), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: SyncedRoute(), - action: () => context.router.navigate(SyncedRoute()), + action: () => e.navigate(context), ); } - return null; + case HomeTabs.library: + return DestinationModel( + label: context.localized.library(0), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), + route: const LibraryRoute(), + action: () => e.navigate(context), + ); } }) .nonNulls diff --git a/lib/screens/library/components/library_tabs.dart b/lib/screens/library/components/library_tabs.dart deleted file mode 100644 index e27c1f8..0000000 --- a/lib/screens/library/components/library_tabs.dart +++ /dev/null @@ -1,83 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; -import 'package:fladder/screens/library/tabs/favourites_tab.dart'; -import 'package:fladder/screens/library/tabs/library_tab.dart'; -import 'package:fladder/screens/library/tabs/timeline_tab.dart'; -import 'package:flutter/material.dart'; - -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/screens/library/tabs/recommendations_tab.dart'; - -class LibraryTabs { - final String name; - final Icon icon; - final Widget page; - final FloatingActionButton? floatingActionButton; - LibraryTabs({ - required this.name, - required this.icon, - required this.page, - this.floatingActionButton, - }); - - static List getLibraryForType(ViewModel viewModel, CollectionType type) { - LibraryTabs recommendTab() { - return LibraryTabs( - name: "Recommended", - icon: const Icon(Icons.recommend_rounded), - page: RecommendationsTab(viewModel: viewModel), - ); - } - - LibraryTabs timelineTab() { - return LibraryTabs( - name: "Timeline", - icon: const Icon(Icons.timeline), - page: TimelineTab(viewModel: viewModel), - ); - } - - LibraryTabs favouritesTab() { - return LibraryTabs( - name: "Favourites", - icon: const Icon(Icons.favorite_rounded), - page: FavouritesTab(viewModel: viewModel), - ); - } - - LibraryTabs libraryTab() { - return LibraryTabs( - name: "Library", - icon: const Icon(Icons.book_rounded), - page: LibraryTab(viewModel: viewModel), - ); - } - - switch (type) { - case CollectionType.tvshows: - case CollectionType.movies: - return [ - libraryTab(), - recommendTab(), - favouritesTab(), - ]; - case CollectionType.books: - case CollectionType.homevideos: - return [ - libraryTab(), - timelineTab(), - recommendTab(), - favouritesTab(), - ]; - case CollectionType.boxsets: - case CollectionType.playlists: - case CollectionType.folders: - return [ - libraryTab(), - ]; - default: - return []; - } - } -} diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index 896c9f6..b051b07 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -1,14 +1,33 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/library/components/library_tabs.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/models/recommended_model.dart'; +import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/library_screen_provider.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/metadata/refresh_metadata.dart'; +import 'package:fladder/screens/shared/flat_button.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'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/fladder_image.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; +import 'package:fladder/widgets/shared/button_group.dart'; +import 'package:fladder/widgets/shared/horizontal_list.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; +import 'package:fladder/widgets/shared/pull_to_refresh.dart'; + +@RoutePage() class LibraryScreen extends ConsumerStatefulWidget { - final ViewModel viewModel; const LibraryScreen({ - required this.viewModel, super.key, }); @@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget { } class _LibraryScreenState extends ConsumerState with SingleTickerProviderStateMixin { - late final List tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType); - late final TabController tabController = TabController(length: tabs.length, vsync: this); - - @override - void initState() { - super.initState(); - Future.microtask(() { - ref.read(libraryProvider(widget.viewModel.id).notifier).setupLibrary(widget.viewModel); - }); - - tabController.addListener(() { - if (tabController.previousIndex != tabController.index) { - setState(() {}); - } - }); - } - + final GlobalKey? refreshKey = GlobalKey(); @override Widget build(BuildContext context) { - final PreferredSizeWidget tabBar = TabBar( - isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false, - indicatorWeight: 3, - controller: tabController, - tabs: tabs - .map((e) => Tab( - text: e.name, - icon: e.icon, - )) - .toList(), - ); - - return Padding( - padding: AdaptiveLayout.of(context).isDesktop - ? EdgeInsets.only(top: MediaQuery.of(context).padding.top) - : EdgeInsets.zero, - child: ClipRRect( - borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0), - child: Card( - margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero, - elevation: 2, - child: Scaffold( - backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, - floatingActionButton: tabs[tabController.index].floatingActionButton, - floatingActionButtonLocation: FloatingActionButtonLocation.endContained, - appBar: AppBar( - centerTitle: true, - backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, - title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, - toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40, - bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, - ), - extendBody: true, - body: Padding( - padding: !AdaptiveLayout.of(context).isDesktop - ? EdgeInsets.only( - left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right) - : EdgeInsets.zero, - child: TabBarView( - controller: tabController, - children: tabs - .map((e) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: e.page, - )) - .toList(), - ), - ), + final libraryScreenState = ref.watch(libraryScreenProvider); + final views = libraryScreenState.views; + final recommendations = libraryScreenState.recommendations; + final favourites = libraryScreenState.favourites; + final selectedView = libraryScreenState.selectedViewModel; + final viewTypes = libraryScreenState.viewType; + final genres = libraryScreenState.genres; + final padding = AdaptiveLayout.adaptivePadding(context); + return NestedScaffold( + background: BackgroundImage( + items: [ + ...recommendations.expand((e) => e.posters), + ...favourites, + ], + ), + body: PullToRefresh( + refreshOnStart: true, + refreshKey: refreshKey, + onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), + child: SizedBox.expand( + child: CustomScrollView( + controller: AdaptiveLayout.scrollOf(context), + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + const DefaultSliverTopBadding(), + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) + NestedSliverAppBar( + route: LibrarySearchRoute(), + parent: context, + ), + if (views.isNotEmpty) + SliverToBoxAdapter( + child: LibraryRow( + padding: padding, + views: views, + selectedView: libraryScreenState.selectedViewModel, + onSelected: (view) { + ref.read(libraryScreenProvider.notifier).selectLibrary(view); + refreshKey?.currentState?.show(); + }, + ), + ), + if (selectedView != null) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 16), + child: SizedBox( + height: 40, + child: ListView( + padding: padding, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + FilledButton.tonalIcon( + onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)), + label: Text("${context.localized.search} ${selectedView.name}..."), + icon: const Icon(IconsaxPlusLinear.search_normal), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: VerticalDivider(), + ), + ExpressiveButtonGroup( + multiSelection: true, + options: LibraryViewType.values + .map((element) => ButtonGroupOption( + value: element, + icon: Icon(element.icon), + selected: Icon(element.iconSelected), + child: Text( + element.label(context), + ))) + .toList(), + selectedValues: viewTypes, + onSelected: (value) { + ref.read(libraryScreenProvider.notifier).setViewType(value); + }, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: VerticalDivider(), + ), + ElevatedButton.icon( + onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name), + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + ), + ], + ), + ), + ), + ), + if (viewTypes.isEmpty) + SliverFillRemaining( + child: Center(child: Text(context.localized.noResults)), + ), + if (viewTypes.contains(LibraryViewType.recommended)) ...[ + if (recommendations.isNotEmpty) + ...recommendations.where((element) => element.posters.isNotEmpty).map( + (element) => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: element.posters, + label: element.type != null + ? "${element.type?.label(context)} - ${element.name.label(context)}" + : element.name.label(context), + ), + ), + ), + ), + ], + if (viewTypes.contains(LibraryViewType.favourites)) + if (favourites.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: favourites, + label: context.localized.favorites, + ), + ), + ), + if (viewTypes.contains(LibraryViewType.genres)) ...[ + if (genres.isNotEmpty) + ...genres.where((element) => element.posters.isNotEmpty).map( + (element) => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: element.posters, + label: element.type != null + ? "${element.type?.label(context)} - ${element.name.label(context)}" + : element.name.label(context), + ), + ), + ), + ) + ], + const DefautlSliverBottomPadding(), + ], ), ), ), ); } } + +class LibraryRow extends ConsumerWidget { + const LibraryRow({ + super.key, + required this.views, + this.selectedView, + required this.padding, + this.onSelected, + }); + + final List views; + final ViewModel? selectedView; + final EdgeInsets padding; + final FutureOr Function(ViewModel selected)? onSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return HorizontalList( + label: context.localized.library(views.length), + items: views, + startIndex: selectedView != null ? views.indexOf(selectedView!) : null, + height: 165, + contentPadding: padding, + itemBuilder: (context, index) { + final view = views[index]; + final isSelected = selectedView == view; + final List viewActions = [ + ItemActionButton( + label: Text(context.localized.search), + icon: const Icon(IconsaxPlusLinear.search_normal), + action: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + ), + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return FlatButton( + onTap: isSelected ? null : () => onSelected?.call(view), + onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + onSecondaryTapDown: (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu( + context: context, + position: position, + items: viewActions.popupMenuItems(useIcons: true), + ); + }, + child: Card( + color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null, + shadowColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + SizedBox( + width: 200, + child: Card( + child: AspectRatio( + aspectRatio: 1.60, + child: FladderImage( + image: view.imageData?.primary, + fit: BoxFit.cover, + placeHolder: Center( + child: Text( + view.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + spacing: 8, + children: [ + if (isSelected) + Container( + height: 12, + width: 12, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + ), + Text( + view.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screens/library/tabs/favourites_tab.dart b/lib/screens/library/tabs/favourites_tab.dart deleted file mode 100644 index ff633b7..0000000 --- a/lib/screens/library/tabs/favourites_tab.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class FavouritesTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - const FavouritesTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _FavouritesTabState(); -} - -class _FavouritesTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - final favourites = ref.watch(libraryProvider(widget.viewModel.id))?.favourites ?? []; - super.build(context); - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadFavourites(widget.viewModel); - }, - child: favourites.isNotEmpty - ? ListView( - children: [ - PosterGrid(posters: favourites), - ], - ) - : const Center(child: Text("No favourites, add some using the heart icon.")), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/library_tab.dart b/lib/screens/library/tabs/library_tab.dart deleted file mode 100644 index 8ec1163..0000000 --- a/lib/screens/library/tabs/library_tab.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/util/grouping.dart'; -import 'package:fladder/util/keyed_list_view.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class LibraryTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - const LibraryTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _LibraryTabState(); -} - -class _LibraryTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - final library = ref.watch(libraryProvider(widget.viewModel.id).select((value) => value?.posters)) ?? []; - final items = groupByName(library); - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadLibrary(widget.viewModel); - }, - child: KeyedListView( - map: items, - itemBuilder: (context, index) { - final currentIndex = items.entries.elementAt(index); - return PosterGrid(name: currentIndex.key, posters: currentIndex.value); - }, - ), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/recommendations_tab.dart b/lib/screens/library/tabs/recommendations_tab.dart deleted file mode 100644 index 2de9ca7..0000000 --- a/lib/screens/library/tabs/recommendations_tab.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/util/list_padding.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class RecommendationsTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - - const RecommendationsTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _RecommendationsTabState(); -} - -class _RecommendationsTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - final recommendations = ref.watch(libraryProvider(widget.viewModel.id) - .select((value) => value?.recommendations.where((element) => element.posters.isNotEmpty))) ?? - []; - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadRecommendations(widget.viewModel); - }, - child: recommendations.isNotEmpty - ? ListView( - children: recommendations - .map( - (e) => PosterGrid(name: e.name, posters: e.posters), - ) - .toList() - .addPadding( - const EdgeInsets.only( - bottom: 32, - ), - ), - ) - : const Center( - child: Text("No recommendations, add more movies and or shows to receive more recomendations")), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/timeline_tab.dart b/lib/screens/library/tabs/timeline_tab.dart deleted file mode 100644 index d4abedf..0000000 --- a/lib/screens/library/tabs/timeline_tab.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:fladder/models/items/photos_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/util/fladder_image.dart'; -import 'package:fladder/util/sticky_header_text.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:intl/intl.dart'; -import 'package:page_transition/page_transition.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:sticky_headers/sticky_headers.dart'; - -class TimelineTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - - const TimelineTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _TimelineTabState(); -} - -class _TimelineTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - final itemScrollController = ItemScrollController(); - double get posterCount { - if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { - return 200; - } - return 125; - } - - @override - Widget build(BuildContext context) { - super.build(context); - final timeLine = ref.watch(libraryProvider(widget.viewModel.id))?.timelinePhotos ?? []; - final items = groupedItems(timeLine); - - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadTimeline(widget.viewModel); - }, - child: ScrollablePositionedList.builder( - itemScrollController: itemScrollController, - itemCount: items.length, - itemBuilder: (context, index) { - final item = items.entries.elementAt(index); - return Padding( - padding: const EdgeInsets.only(bottom: 64.0), - child: StickyHeader( - header: StickyHeaderText( - label: item.key.year != DateTime.now().year - ? DateFormat('E dd MMM. y').format(item.key) - : DateFormat('E dd MMM.').format(item.key)), - content: StaggeredGrid.count( - crossAxisCount: MediaQuery.of(context).size.width ~/ posterCount, - mainAxisSpacing: 0, - crossAxisSpacing: 0, - axisDirection: AxisDirection.down, - children: item.value - .map( - (e) => Hero( - tag: e.id, - child: AspectRatio( - aspectRatio: e.primaryRatio ?? 0.0, - child: Card( - margin: const EdgeInsets.all(4), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - clipBehavior: Clip.antiAlias, - child: Stack( - children: [ - FladderImage(image: e.thumbnail?.primary), - FlatButton( - onLongPress: () {}, - onTap: () async { - final position = await Navigator.of(context, rootNavigator: true).push( - PageTransition( - child: PhotoViewerScreen( - items: timeLine, - indexOfSelected: timeLine.indexOf(e), - ), - type: PageTransitionType.fade), - ); - getParentPosition(items, timeLine, position); - }, - ) - ], - ), - ), - ), - ), - ) - .toList(), - ), - ), - ); - }, - ), - ); - } - - void getParentPosition(Map> items, List timeLine, int position) { - items.forEach( - (key, value) { - if (value.contains(timeLine[position])) { - itemScrollController.scrollTo( - index: items.keys.toList().indexOf(key), duration: const Duration(milliseconds: 250)); - } - }, - ); - } - - Map> groupedItems(List items) { - Map> groupedItems = {}; - for (int i = 0; i < items.length; i++) { - DateTime curretDate = items[i].dateTaken ?? DateTime.now(); - DateTime key = DateTime(curretDate.year, curretDate.month, curretDate.day); - if (!groupedItems.containsKey(key)) { - groupedItems[key] = [items[i]]; - } else { - groupedItems[key]?.add(items[i]); - } - } - return groupedItems; - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart index 2f3dcbe..3a44996 100644 --- a/lib/screens/library_search/library_search_screen.dart +++ b/lib/screens/library_search/library_search_screen.dart @@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; -import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playlist_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/collections/add_to_collection.dart'; import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart'; import 'package:fladder/screens/library_search/widgets/library_play_options_.dart'; @@ -26,9 +23,9 @@ import 'package:fladder/screens/library_search/widgets/library_views.dart'; import 'package:fladder/screens/library_search/widgets/suggestion_search_bar.dart'; import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/shared/nested_scaffold.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/debouncer.dart'; import 'package:fladder/util/fab_extended_anim.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -37,8 +34,7 @@ import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/router_extension.dart'; -import 'package:fladder/util/sliver_list_padding.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/fladder_scrollbar.dart'; import 'package:fladder/widgets/shared/hide_on_scroll.dart'; @@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState { final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null; final librarySearchResults = ref.watch(providerKey); final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows); - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final libraryViewType = ref.watch(libraryViewTypeProvider); ref.listen( @@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState { libraryProvider.toggleSelectMode(); } }, - child: Scaffold( - extendBody: true, - extendBodyBehindAppBar: true, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: - playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ), - _ => HideOnScroll( + child: NestedScaffold( + background: BackgroundImage(items: librarySearchResults.activePosters), + body: Padding( + padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth), + child: Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + floatingActionButton: HideOnScroll( controller: scrollController, visibleBuilder: (visible) => Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -206,29 +196,26 @@ class _LibrarySearchScreenState extends ConsumerState { ].addInBetween(const SizedBox(height: 10)), ), ), - }, - bottomNavigationBar: HideOnScroll( - controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, - child: IgnorePointer( - ignoring: librarySearchResults.fetchingItems, - child: _LibrarySearchBottomBar( - uniqueKey: uniqueKey, - refreshKey: refreshKey, - scrollController: scrollController, - libraryProvider: libraryProvider, - postersList: postersList, + bottomNavigationBar: HideOnScroll( + controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, + child: IgnorePointer( + ignoring: librarySearchResults.fetchingItems, + child: _LibrarySearchBottomBar( + uniqueKey: uniqueKey, + refreshKey: refreshKey, + scrollController: scrollController, + libraryProvider: libraryProvider, + postersList: postersList, + ), + ), ), - ), - ), - body: Stack( - children: [ - Positioned.fill( - child: Card( - elevation: 1, - child: PinchPosterZoom( - scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference), - child: MediaQuery.removeViewInsets( - context: context, + body: Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: PinchPosterZoom( + scaleDifference: (difference) => + ref.read(clientSettingsProvider.notifier).addPosterSize(difference), child: ClipRRect( borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop ? BorderRadius.circular(15) @@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState { onTapUp: (details) async { if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) { double left = details.globalPosition.dx; - double top = details.globalPosition.dy + 20; + double top = details.globalPosition.dy; await showMenu( context: context, position: RelativeRect.fromLTRB(left, top, 40, 100), @@ -463,16 +450,10 @@ class _LibrarySearchScreenState extends ConsumerState { padding: const EdgeInsets.all(8), scrollDirection: Axis.horizontal, child: LibraryFilterChips( - controller: scrollController, - libraryProvider: libraryProvider, - librarySearchResults: librarySearchResults, - uniqueKey: uniqueKey, - postersList: postersList, - libraryViewType: libraryViewType, + key: uniqueKey, ), ), ), - const Row(), ], ), ), @@ -500,13 +481,12 @@ class _LibrarySearchScreenState extends ConsumerState { ), ) else - SliverToBoxAdapter( + SliverFillRemaining( child: Center( child: Text(context.localized.noItemsToShow), ), ), - const DefautlSliverBottomPadding(), - const SliverPadding(padding: EdgeInsets.only(bottom: 80)) + SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20)) ], ), ), @@ -514,36 +494,36 @@ class _LibrarySearchScreenState extends ConsumerState { ), ), ), - ), - ), - if (librarySearchResults.fetchingItems) ...[ - Container( - color: Colors.black.withValues(alpha: 0.1), - ), - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), + if (librarySearchResults.fetchingItems) ...[ + Container( + color: Colors.black.withValues(alpha: 0.1), ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator.adaptive(), - Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), - IconButton( - onPressed: () => libraryProvider.cancelFetch(), - icon: const Icon(IconsaxPlusLinear.close_square), - ) - ].addInBetween(const SizedBox(width: 16)), + Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator.adaptive(), + Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), + IconButton( + onPressed: () => libraryProvider.cancelFetch(), + icon: const Icon(IconsaxPlusLinear.close_square), + ) + ].addInBetween(const SizedBox(width: 16)), + ), + ), ), - ), - ), - ) - ], - ], + ) + ], + ], + ), + ), ), ), ); @@ -663,173 +643,156 @@ class _LibrarySearchBottomBar extends ConsumerWidget { icon: const Icon(IconsaxPlusLinear.save_add), ), ]; - return NestedBottomAppBar( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Row( + + return Padding( + padding: EdgeInsets.only(left: MediaQuery.paddingOf(context).left), + child: NestedBottomAppBar( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, children: [ - ScrollStatePosition( - controller: scrollController, - positionBuilder: (state) => AnimatedFadeSize( - child: state != ScrollState.top - ? Tooltip( - message: context.localized.scrollToTop, - child: FlatButton( - clipBehavior: Clip.antiAlias, - elevation: 0, - borderRadiusGeometry: BorderRadius.circular(6), - onTap: () => scrollController.animateTo(0, - duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), + Row( + spacing: 6, + children: [ + ScrollStatePosition( + controller: scrollController, + positionBuilder: (state) => AnimatedFadeSize( + child: state != ScrollState.top + ? Tooltip( + message: context.localized.scrollToTop, + child: IconButton.filled( + onPressed: () => scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic), + icon: const Icon( + IconsaxPlusLinear.arrow_up, + ), ), - padding: const EdgeInsets.all(6), - child: Icon( - IconsaxPlusLinear.arrow_up, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ), - ), - ) - : const SizedBox(), - ), - ), - const SizedBox(width: 6), - if (!librarySearchResults.selecteMode) ...{ - const SizedBox(width: 6), - IconButton( - tooltip: context.localized.sortBy, - onPressed: () async { - final newOptions = await openSortByDialogue( - context, - libraryProvider: libraryProvider, - uniqueKey: uniqueKey, - options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder), - ); - if (newOptions != null) { - if (newOptions.$1 != null) { - libraryProvider.setSortBy(newOptions.$1!); - } - if (newOptions.$2 != null) { - libraryProvider.setSortOrder(newOptions.$2!); - } - } - }, - icon: const Icon(IconsaxPlusLinear.sort), - ), - if (librarySearchResults.hasActiveFilters) ...{ - const SizedBox(width: 6), - IconButton( - tooltip: context.localized.disableFilters, - onPressed: disableFilters(librarySearchResults, libraryProvider), - icon: const Icon(IconsaxPlusLinear.filter_remove), - ), - }, - }, - const SizedBox(width: 6), - IconButton( - onPressed: () => libraryProvider.toggleSelectMode(), - color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null, - icon: const Icon(IconsaxPlusLinear.category_2), - ), - const SizedBox(width: 6), - AnimatedFadeSize( - child: librarySearchResults.selecteMode - ? Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16)), - child: Row( - children: [ - Tooltip( - message: context.localized.selectAll, - child: IconButton( - onPressed: () => libraryProvider.selectAll(true), - icon: const Icon(IconsaxPlusLinear.box_add), - ), - ), - const SizedBox(width: 6), - Tooltip( - message: context.localized.clearSelection, - child: IconButton( - onPressed: () => libraryProvider.selectAll(false), - icon: const Icon(IconsaxPlusLinear.box_remove), - ), - ), - const SizedBox(width: 6), - if (librarySearchResults.selectedPosters.isNotEmpty) ...{ - if (AdaptiveLayout.of(context).isDesktop) - PopupMenuButton( - itemBuilder: (context) => actions.popupMenuItems(useIcons: true), - ) - else - IconButton( - onPressed: () { - showBottomSheetPill( - context: context, - content: (context, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: actions.listTileItems(context, useIcons: true), - ), - ); - }, - icon: const Icon(IconsaxPlusLinear.more)) - }, - ], - ), - ) - : const SizedBox(), - ), - const Spacer(), - if (librarySearchResults.activePosters.isNotEmpty) - IconButton( - tooltip: context.localized.random, - onPressed: () => libraryProvider.openRandom(context), - icon: Card( - color: Theme.of(context).colorScheme.secondary, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Icon( - IconsaxPlusBold.arrow_up_1, - color: Theme.of(context).colorScheme.onSecondary, - ), + ) + : const SizedBox(), ), ), - ), - if (librarySearchResults.activePosters.isNotEmpty) - IconButton( - tooltip: context.localized.shuffleVideos, - onPressed: () async { - if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) { - libraryProvider.viewGallery(context, shuffle: true); - return; - } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) { - libraryProvider.playLibraryItems(context, ref, shuffle: true); - return; - } - - await showLibraryPlayOptions( - context, - context.localized.libraryShuffleAndPlayItems, - playVideos: librarySearchResults.showPlayButtons - ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true) - : null, - viewGallery: librarySearchResults.showGalleryButtons - ? () => libraryProvider.viewGallery(context, shuffle: true) - : null, - ); + if (!librarySearchResults.selecteMode) ...{ + IconButton( + tooltip: context.localized.sortBy, + onPressed: () async { + final newOptions = await openSortByDialogue( + context, + libraryProvider: libraryProvider, + uniqueKey: uniqueKey, + options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder), + ); + if (newOptions != null) { + if (newOptions.$1 != null) { + libraryProvider.setSortBy(newOptions.$1!); + } + if (newOptions.$2 != null) { + libraryProvider.setSortOrder(newOptions.$2!); + } + } + }, + icon: const Icon(IconsaxPlusLinear.sort), + ), + if (librarySearchResults.hasActiveFilters) ...{ + IconButton( + tooltip: context.localized.disableFilters, + onPressed: disableFilters(librarySearchResults, libraryProvider), + icon: const Icon(IconsaxPlusLinear.filter_remove), + ), + }, }, - icon: const Icon(IconsaxPlusLinear.shuffle), - ), + IconButton( + onPressed: () => libraryProvider.toggleSelectMode(), + color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null, + icon: const Icon(IconsaxPlusLinear.category_2), + ), + AnimatedFadeSize( + child: librarySearchResults.selecteMode + ? Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16)), + child: Row( + spacing: 6, + children: [ + Tooltip( + message: context.localized.selectAll, + child: IconButton( + onPressed: () => libraryProvider.selectAll(true), + icon: const Icon(IconsaxPlusLinear.box_add), + ), + ), + Tooltip( + message: context.localized.clearSelection, + child: IconButton( + onPressed: () => libraryProvider.selectAll(false), + icon: const Icon(IconsaxPlusLinear.box_remove), + ), + ), + if (librarySearchResults.selectedPosters.isNotEmpty) ...{ + if (AdaptiveLayout.of(context).isDesktop) + PopupMenuButton( + itemBuilder: (context) => actions.popupMenuItems(useIcons: true), + ) + else + IconButton( + onPressed: () { + showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), + ), + ); + }, + icon: const Icon(IconsaxPlusLinear.more)) + }, + ], + ), + ) + : const SizedBox(), + ), + const Spacer(), + if (librarySearchResults.activePosters.isNotEmpty) + IconButton.filledTonal( + tooltip: context.localized.random, + onPressed: () => libraryProvider.openRandom(context), + icon: const Icon( + IconsaxPlusBold.arrow_up_1, + ), + ), + if (librarySearchResults.activePosters.isNotEmpty) + IconButton( + tooltip: context.localized.shuffleVideos, + onPressed: () async { + if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) { + libraryProvider.viewGallery(context, shuffle: true); + return; + } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) { + libraryProvider.playLibraryItems(context, ref, shuffle: true); + return; + } + + await showLibraryPlayOptions( + context, + context.localized.libraryShuffleAndPlayItems, + playVideos: librarySearchResults.showPlayButtons + ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true) + : null, + viewGallery: librarySearchResults.showGalleryButtons + ? () => libraryProvider.viewGallery(context, shuffle: true) + : null, + ); + }, + icon: const Icon(IconsaxPlusLinear.shuffle), + ), + ], + ), ], ), - if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 8), - ], + ), ), ); } diff --git a/lib/screens/library_search/widgets/library_filter_chips.dart b/lib/screens/library_search/widgets/library_filter_chips.dart index a158673..94cf3bb 100644 --- a/lib/screens/library_search/widgets/library_filter_chips.dart +++ b/lib/screens/library_search/widgets/library_filter_chips.dart @@ -1,221 +1,193 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/providers/library_search_provider.dart'; -import 'package:fladder/screens/library_search/widgets/library_views.dart'; import 'package:fladder/screens/shared/chips/category_chip.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/refresh_state.dart'; -import 'package:fladder/widgets/shared/scroll_position.dart'; -class LibraryFilterChips extends ConsumerWidget { - final Key uniqueKey; - final ScrollController controller; - final LibrarySearchModel librarySearchResults; - final LibrarySearchNotifier libraryProvider; - final List postersList; - final LibraryViewTypes libraryViewType; - const LibraryFilterChips({ - required this.uniqueKey, - required this.controller, - required this.librarySearchResults, - required this.libraryProvider, - required this.postersList, - required this.libraryViewType, - super.key, - }); +class LibraryFilterChips extends ConsumerStatefulWidget { + const LibraryFilterChips({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return ScrollStatePosition( - controller: controller, - positionBuilder: (state) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: libraryFilterChips( - context, - ref, - uniqueKey, - librarySearchResults: librarySearchResults, - libraryProvider: libraryProvider, - postersList: postersList, - libraryViewType: libraryViewType, - ).addPadding(const EdgeInsets.symmetric(horizontal: 8)), - ); - }, - ); - } + ConsumerState createState() => _LibraryFilterChipsState(); } -List libraryFilterChips( - BuildContext context, - WidgetRef ref, - Key uniqueKey, { - required LibrarySearchModel librarySearchResults, - required LibrarySearchNotifier libraryProvider, - required List postersList, - required LibraryViewTypes libraryViewType, -}) { - Future openGroupDialogue() { - return showDialog( +class _LibraryFilterChipsState extends ConsumerState { + @override + Widget build(BuildContext context) { + final uniqueKey = widget.key ?? UniqueKey(); + final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier); + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); + final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites)); + final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive)); + final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows)); + final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey)); + + return Row( + spacing: 8, + children: [ + if (librarySearchResults.folderOverwrite.isEmpty) + CategoryChip( + label: Text(context.localized.library(2)), + items: librarySearchResults.views, + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setViews(value), + onCancel: () => libraryProvider.setViews(librarySearchResults.views), + onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), + ), + CategoryChip( + label: Text(context.localized.type(librarySearchResults.types.length)), + items: librarySearchResults.types, + labelBuilder: (item) => Row( + children: [ + Icon(item.icon), + const SizedBox(width: 12), + Text(item.label(context)), + ], + ), + onSave: (value) => libraryProvider.setTypes(value), + onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)), + ), + FilterChip( + label: Text(context.localized.favorites), + avatar: Icon( + favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, + color: Theme.of(context).colorScheme.onSurface, + ), + selected: favourites, + showCheckmark: false, + onSelected: (_) { + libraryProvider.toggleFavourite(); + context.refreshData(); + }, + ), + FilterChip( + label: Text(context.localized.recursive), + selected: recursive, + onSelected: (_) { + libraryProvider.toggleRecursive(); + context.refreshData(); + }, + ), + if (librarySearchResults.genres.isNotEmpty) + CategoryChip( + label: Text(context.localized.genre(librarySearchResults.genres.length)), + activeIcon: IconsaxPlusBold.hierarchy_2, + items: librarySearchResults.genres, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setGenres(value), + onCancel: () => libraryProvider.setGenres(librarySearchResults.genres), + onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)), + ), + if (librarySearchResults.studios.isNotEmpty) + CategoryChip( + label: Text(context.localized.studio(librarySearchResults.studios.length)), + activeIcon: IconsaxPlusBold.airdrop, + items: librarySearchResults.studios, + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setStudios(value), + onCancel: () => libraryProvider.setStudios(librarySearchResults.studios), + onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)), + ), + if (librarySearchResults.tags.isNotEmpty) + CategoryChip( + label: Text(context.localized.label(librarySearchResults.tags.length)), + activeIcon: Icons.label_rounded, + items: librarySearchResults.tags, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setTags(value), + onCancel: () => libraryProvider.setTags(librarySearchResults.tags), + onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)), + ), + FilterChip( + label: Text(context.localized.group), + selected: groupBy != GroupBy.none, + onSelected: (_) { + _openGroupDialogue(context, ref, libraryProvider, uniqueKey); + }, + ), + CategoryChip( + label: Text(context.localized.filter(librarySearchResults.filters.length)), + items: librarySearchResults.filters, + labelBuilder: (item) => Text(item.label(context)), + onSave: (value) => libraryProvider.setFilters(value), + onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)), + ), + if (librarySearchResults.types[FladderItemType.series] == true) + FilterChip( + avatar: Icon( + hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + selected: hideEmpty, + showCheckmark: false, + label: Text(context.localized.hideEmpty), + onSelected: libraryProvider.setHideEmpty, + ), + if (librarySearchResults.officialRatings.isNotEmpty) + CategoryChip( + label: Text(context.localized.rating(librarySearchResults.officialRatings.length)), + activeIcon: Icons.star_rate_rounded, + items: librarySearchResults.officialRatings, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setRatings(value), + onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings), + onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)), + ), + if (librarySearchResults.years.isNotEmpty) + CategoryChip( + label: Text(context.localized.year(librarySearchResults.years.length)), + items: librarySearchResults.years, + labelBuilder: (item) => Text(item.toString()), + onSave: (value) => libraryProvider.setYears(value), + onCancel: () => libraryProvider.setYears(librarySearchResults.years), + onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)), + ), + ], + ); + } + + void _openGroupDialogue( + BuildContext context, + WidgetRef ref, + LibrarySearchNotifier provider, + Key uniqueKey, + ) { + showDialog( context: context, builder: (context) { - return Consumer( - builder: (context, ref, child) { - return AlertDialog( - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.65, - child: ListView( - shrinkWrap: true, - children: [ - Text(context.localized.groupBy), - ...GroupBy.values.map((groupBy) => RadioListTile.adaptive( - value: groupBy, - title: Text(groupBy.value(context)), - groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)), - onChanged: (value) { - libraryProvider.setGroupBy(groupBy); - Navigator.pop(context); - }, - )), - ], + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); + return AlertDialog( + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.65, + child: ListView( + shrinkWrap: true, + children: [ + Text(context.localized.groupBy), + ...GroupBy.values.map( + (group) => RadioListTile.adaptive( + value: group, + groupValue: groupBy, + title: Text(group.value(context)), + onChanged: (_) { + provider.setGroupBy(group); + Navigator.pop(context); + }, + ), ), - ), - ); - }, + ], + ), + ), ); }, ); } - - return [ - if (librarySearchResults.folderOverwrite.isEmpty) - CategoryChip( - label: Text(context.localized.library(2)), - items: librarySearchResults.views, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setViews(value), - onCancel: () => libraryProvider.setViews(librarySearchResults.views), - onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), - ), - CategoryChip( - label: Text(context.localized.type(librarySearchResults.types.length)), - items: librarySearchResults.types, - labelBuilder: (item) => Row( - children: [ - Icon(item.icon), - const SizedBox(width: 12), - Text(item.label(context)), - ], - ), - onSave: (value) => libraryProvider.setTypes(value), - onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)), - ), - FilterChip( - label: Text(context.localized.favorites), - avatar: Icon( - librarySearchResults.favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: librarySearchResults.favourites, - showCheckmark: false, - onSelected: (value) { - libraryProvider.toggleFavourite(); - context.refreshData(); - }, - ), - FilterChip( - label: Text(context.localized.recursive), - selected: librarySearchResults.recursive, - onSelected: (value) { - libraryProvider.toggleRecursive(); - context.refreshData(); - }, - ), - if (librarySearchResults.genres.isNotEmpty) - CategoryChip( - label: Text(context.localized.genre(librarySearchResults.genres.length)), - activeIcon: IconsaxPlusBold.hierarchy_2, - items: librarySearchResults.genres, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setGenres(value), - onCancel: () => libraryProvider.setGenres(librarySearchResults.genres), - onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)), - ), - if (librarySearchResults.studios.isNotEmpty) - CategoryChip( - label: Text(context.localized.studio(librarySearchResults.studios.length)), - activeIcon: IconsaxPlusBold.airdrop, - items: librarySearchResults.studios, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setStudios(value), - onCancel: () => libraryProvider.setStudios(librarySearchResults.studios), - onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)), - ), - if (librarySearchResults.tags.isNotEmpty) - CategoryChip( - label: Text(context.localized.label(librarySearchResults.tags.length)), - activeIcon: Icons.label_rounded, - items: librarySearchResults.tags, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setTags(value), - onCancel: () => libraryProvider.setTags(librarySearchResults.tags), - onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)), - ), - FilterChip( - label: Text(context.localized.group), - selected: librarySearchResults.groupBy != GroupBy.none, - onSelected: (value) { - openGroupDialogue(); - }, - ), - CategoryChip( - label: Text(context.localized.filter(librarySearchResults.filters.length)), - items: librarySearchResults.filters, - labelBuilder: (item) => Text(item.label(context)), - onSave: (value) => libraryProvider.setFilters(value), - onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)), - ), - if (librarySearchResults.types[FladderItemType.series] == true) - FilterChip( - avatar: Icon( - librarySearchResults.hideEmptyShows ? Icons.visibility_off_rounded : Icons.visibility_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: librarySearchResults.hideEmptyShows, - showCheckmark: false, - label: Text(context.localized.hideEmpty), - onSelected: libraryProvider.setHideEmpty, - ), - if (librarySearchResults.officialRatings.isNotEmpty) - CategoryChip( - label: Text(context.localized.rating(librarySearchResults.officialRatings.length)), - activeIcon: Icons.star_rate_rounded, - items: librarySearchResults.officialRatings, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setRatings(value), - onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings), - onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)), - ), - if (librarySearchResults.years.isNotEmpty) - CategoryChip( - label: Text(context.localized.year(librarySearchResults.years.length)), - items: librarySearchResults.years, - labelBuilder: (item) => Text(item.toString()), - onSave: (value) => libraryProvider.setYears(value), - onCancel: () => libraryProvider.setYears(librarySearchResults.years), - onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)), - ), - ]; } diff --git a/lib/screens/library_search/widgets/library_views.dart b/lib/screens/library_search/widgets/library_views.dart index 5a2eee0..0c0713a 100644 --- a/lib/screens/library_search/widgets/library_views.dart +++ b/lib/screens/library_search/widgets/library_views.dart @@ -1,7 +1,16 @@ import 'dart:ui'; +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:intl/intl.dart'; +import 'package:page_transition/page_transition.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; @@ -10,22 +19,15 @@ import 'package:fladder/models/playlist_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; import 'package:fladder/screens/shared/media/poster_list_item.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/string_extensions.dart'; +import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:intl/intl.dart'; -import 'package:page_transition/page_transition.dart'; -import 'package:sliver_tools/sliver_tools.dart'; -import 'package:sticky_headers/sticky_headers/widget.dart'; final libraryViewTypeProvider = StateProvider((ref) { return LibraryViewTypes.grid; @@ -107,179 +109,139 @@ class LibraryViews extends ConsumerWidget { switch (ref.watch(libraryViewTypeProvider)) { case LibraryViewTypes.grid: - if (groupByType != GroupBy.none) { - final groupedItems = groupItemsBy(context, items, groupByType); - return SliverList.builder( - itemCount: groupedItems.length, + Widget createGrid(List items) { + return SliverGrid.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: posterSize.toInt(), + mainAxisSpacing: (8 * decimal) + 8, + crossAxisSpacing: (8 * decimal) + 8, + childAspectRatio: items.getMostCommonType.aspectRatio, + ), + itemCount: items.length, itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false || group == null) { - return Text(context.localized.empty); - } - return PosterGrid( - posters: group!, - name: name, - itemBuilder: (context, index) { - final item = group[index]; - return PosterWidget( - key: Key(item.id), - poster: group[index], - maxLines: 2, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(item), - selected: selected.contains(item), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, + final item = items[index]; + return PosterWidget( + key: Key(item.id), + poster: item, + maxLines: 2, + heroTag: true, + subTitle: item.subTitle(sortingOptions), + excludeActions: excludeActions, + otherActions: otherActions(item), + selected: selected.contains(item), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); + } + + if (groupByType != GroupBy.none) { + final groupedItems = groupItemsBy(context, items, groupByType); + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + sliver: createGrid(group), + ); + }, + ).toList()); } else { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), - sliver: SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: posterSize.toInt(), - mainAxisSpacing: (8 * decimal) + 8, - crossAxisSpacing: (8 * decimal) + 8, - childAspectRatio: AdaptiveLayout.poster(context).ratio, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - return PosterWidget( - key: Key(item.id), - poster: item, - maxLines: 2, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(item), - selected: selected.contains(item), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), + sliver: createGrid(items), ); } case LibraryViewTypes.list: - if (groupByType != GroupBy.none) { - final groupedItems = groupItemsBy(context, items, groupByType); + Widget listBuilder(List items) { return SliverList.builder( - itemCount: groupedItems.length, + itemCount: items.length, itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false) { - return Text(context.localized.empty); - } - return StickyHeader( - header: Text(name, style: Theme.of(context).textTheme.headlineSmall), - content: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - itemCount: group?.length, - itemBuilder: (context, index) { - final poster = group![index]; - return PosterListItem( - key: Key(poster.id), - poster: poster, - subTitle: poster.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(poster), - selected: selected.contains(poster), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), + final poster = items[index]; + return PosterListItem( + poster: poster, + selected: selected.contains(poster), + excludeActions: excludeActions, + otherActions: otherActions(poster), + subTitle: poster.subTitle(sortingOptions), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), + onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); } - return SliverList.builder( - itemCount: items.length, - itemBuilder: (context, index) { - final poster = items[index]; - return PosterListItem( - poster: poster, - selected: selected.contains(poster), - excludeActions: excludeActions, - otherActions: otherActions(poster), - subTitle: poster.subTitle(sortingOptions), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ); + if (groupByType != GroupBy.none) { + final groupedItems = groupItemsBy(context, items, groupByType); + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + sliver: listBuilder(group), + ); + }, + ).toList()); + } + return listBuilder(items); case LibraryViewTypes.masonry: if (groupByType != GroupBy.none) { final groupedItems = groupItemsBy(context, items, groupByType); - return SliverList.builder( - itemCount: groupedItems.length, - itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false) { - return Text(context.localized.empty); - } - return Padding( - padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0), - child: StickyHeader( - header: Text(name, style: Theme.of(context).textTheme.headlineMedium), - overlapHeaders: true, - content: Padding( - padding: const EdgeInsets.only(top: 16.0), - child: MasonryGridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: (8 * decimal) + 8, - crossAxisSpacing: (8 * decimal) + 8, - gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) - .toDouble() * - 20, - ), - itemCount: group!.length, - itemBuilder: (context, index) { - final item = group[index]; - return PosterWidget( - key: Key(item.id), - poster: item, - aspectRatio: item.primaryRatio, - selected: selected.contains(item), - inlineTitle: true, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(group[index]), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), - )), + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + //MasonryGridView because SliverMasonryGrid breaks scrolling + sliver: SliverToBoxAdapter( + child: MasonryGridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: (8 * decimal) + 8, + crossAxisSpacing: (8 * decimal) + 8, + gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) + .toDouble() * + 12, + ), + itemCount: group.length, + itemBuilder: (context, index) { + final item = group[index]; + return PosterWidget( + key: Key(item.id), + poster: item, + aspectRatio: item.primaryRatio, + selected: selected.contains(item), + inlineTitle: true, + heroTag: true, + subTitle: item.subTitle(sortingOptions), + excludeActions: excludeActions, + otherActions: otherActions(group[index]), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), + onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), + ); + }, + ), + ), ); }, - ); + ).toList()); } else { return SliverMasonryGrid.count( mainAxisSpacing: (8 * decimal) + 8, @@ -309,6 +271,36 @@ class LibraryViews extends ConsumerWidget { } } + SliverStickyHeader stickyHeaderBuilder( + BuildContext context, { + required String header, + Widget? sliver, + }) { + return SliverStickyHeader( + header: Container( + height: 50, + alignment: Alignment.centerLeft, + child: Transform.translate( + offset: const Offset(-20, 0), + child: Container( + decoration: BoxDecoration( + color: context.colors.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + header, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + ), + ), + sliver: sliver, + ); + } + Map> groupItemsBy(BuildContext context, List list, GroupBy groupOption) { switch (groupOption) { case GroupBy.dateAdded: diff --git a/lib/screens/library_search/widgets/suggestion_search_bar.dart b/lib/screens/library_search/widgets/suggestion_search_bar.dart index b52b286..8acb64d 100644 --- a/lib/screens/library_search/widgets/suggestion_search_bar.dart +++ b/lib/screens/library_search/widgets/suggestion_search_bar.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:page_transition/page_transition.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -65,6 +65,9 @@ class _SearchBarState extends ConsumerState { }); return Card( elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: FladderTheme.largeShape.borderRadius, + ), shadowColor: Colors.transparent, child: TypeAheadField( focusNode: focusNode, @@ -80,7 +83,7 @@ class _SearchBarState extends ConsumerState { decorationBuilder: (context, child) => DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: FladderTheme.defaultShape.borderRadius, + borderRadius: FladderTheme.largeShape.borderRadius, ), child: child, ), @@ -133,39 +136,45 @@ class _SearchBarState extends ConsumerState { } }, contentPadding: const EdgeInsets.symmetric(horizontal: 8), - title: SizedBox( - height: 50, - child: Row( - children: [ - Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - child: AspectRatio( - aspectRatio: 0.8, - child: FladderImage( - image: suggestion.images?.primary, - fit: BoxFit.cover, + title: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 50, + maxHeight: 65, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: AspectRatio( + aspectRatio: 0.8, + child: FladderImage( + image: suggestion.images?.primary, + fit: BoxFit.cover, + ), ), ), - ), - const SizedBox(width: 8), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - child: Text( - suggestion.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - if (suggestion.overview.yearAired.toString().isNotEmpty) + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Flexible( - child: - Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))), - ], + child: Text( + suggestion.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + )), + if (suggestion.overview.yearAired.toString().isNotEmpty) + Flexible( + child: Opacity( + opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/screens/login/login_screen.dart b/lib/screens/login/login_screen.dart index 3b03518..891d814 100644 --- a/lib/screens/login/login_screen.dart +++ b/lib/screens/login/login_screen.dart @@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/screens/shared/passcode_input.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/auth_service.dart'; import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/login/login_user_grid.dart b/lib/screens/login/login_user_grid.dart index a2f6684..31c2b9b 100644 --- a/lib/screens/login/login_user_grid.dart +++ b/lib/screens/login/login_user_grid.dart @@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget { return Card( elevation: 1, shadowColor: Colors.transparent, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150), diff --git a/lib/screens/login/widgets/login_icon.dart b/lib/screens/login/widgets/login_icon.dart index d0f374e..30114f2 100644 --- a/lib/screens/login/widgets/login_icon.dart +++ b/lib/screens/login/widgets/login_icon.dart @@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget { aspectRatio: 1.0, child: Card( elevation: 1, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: Stack( children: [ diff --git a/lib/screens/metadata/edit_item.dart b/lib/screens/metadata/edit_item.dart index 9ef4a82..c7f8566 100644 --- a/lib/screens/metadata/edit_item.dart +++ b/lib/screens/metadata/edit_item.dart @@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart'; import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart'; import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/metadata/edit_screens/edit_image_content.dart b/lib/screens/metadata/edit_screens/edit_image_content.dart index b5fa60c..6d2abc0 100644 --- a/lib/screens/metadata/edit_screens/edit_image_content.dart +++ b/lib/screens/metadata/edit_screens/edit_image_content.dart @@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/shared/file_picker.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class EditImageContent extends ConsumerStatefulWidget { final ImageType type; diff --git a/lib/screens/metadata/identifty_screen.dart b/lib/screens/metadata/identifty_screen.dart index 9c662a2..1672150 100644 --- a/lib/screens/metadata/identifty_screen.dart +++ b/lib/screens/metadata/identifty_screen.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/items/identify_provider.dart'; @@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState with TickerProv final state = ref.watch(provider); final posters = state.results; final processing = state.processing; - return ActionContent( - showDividers: false, - title: Container( - color: Theme.of(context).colorScheme.surface, - child: Column( + return Card( + child: ActionContent( + showDividers: false, + title: Column( mainAxisSize: MainAxisSize.min, children: [ Row( @@ -89,137 +88,137 @@ class _IdentifyScreenState extends ConsumerState with TickerProv ) ], ), - ), - child: TabBarView( - controller: tabController, - children: [ - inputFields(state), - if (posters.isEmpty) - Center( - child: processing - ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round) - : Text(context.localized.noResults), - ) - else - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(context.localized.replaceAllImages), - const SizedBox(width: 16), - Switch.adaptive( - value: state.replaceAllImages, - onChanged: (value) { - ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value)); - }, - ), - ], - ), - Flexible( - child: ListView( - shrinkWrap: true, - children: posters - .map((result) => ListTile( - title: Row( - children: [ - SizedBox( - width: 75, - child: Card( - child: CachedNetworkImage( - imageUrl: result.imageUrl ?? "", - errorWidget: (context, url, error) => SizedBox( - height: 75, - child: Card( - child: Center( - child: Text(result.name?.getInitials() ?? ""), + child: TabBarView( + controller: tabController, + children: [ + inputFields(state), + if (posters.isEmpty) + Center( + child: processing + ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round) + : Text(context.localized.noResults), + ) + else + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text(context.localized.replaceAllImages), + const SizedBox(width: 16), + Switch.adaptive( + value: state.replaceAllImages, + onChanged: (value) { + ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value)); + }, + ), + ], + ), + Flexible( + child: ListView( + shrinkWrap: true, + children: posters + .map((result) => ListTile( + title: Row( + children: [ + SizedBox( + width: 75, + child: Card( + child: CachedNetworkImage( + imageUrl: result.imageUrl ?? "", + errorWidget: (context, url, error) => SizedBox( + height: 75, + child: Card( + child: Center( + child: Text(result.name?.getInitials() ?? ""), + ), ), ), ), ), ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"), - Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? "")) - ], + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"), + Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? "")) + ], + ), ), - ), - Tooltip( - message: context.localized.openWebLink, - child: IconButton( - onPressed: () { - final providerKeyEntry = result.providerIds?.entries.first; - final providerKey = providerKeyEntry?.key; - final providerValue = providerKeyEntry?.value; + Tooltip( + message: context.localized.openWebLink, + child: IconButton( + onPressed: () { + final providerKeyEntry = result.providerIds?.entries.first; + final providerKey = providerKeyEntry?.key; + final providerValue = providerKeyEntry?.value; - final externalId = state.externalIds - .firstWhereOrNull((element) => element.key == providerKey) - // ignore: deprecated_member_use_from_same_package - ?.urlFormatString; + final externalId = state.externalIds + .firstWhereOrNull((element) => element.key == providerKey) + // ignore: deprecated_member_use_from_same_package + ?.urlFormatString; - final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? ""); + final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? ""); - launchUrl(context, url ?? ""); - }, - icon: const Icon(Icons.launch_rounded)), - ), - Tooltip( - message: "Select result", - child: IconButton( - onPressed: !processing - ? () async { - final response = await ref.read(provider.notifier).setIdentity(result); - if (response?.isSuccessful == true) { - fladderSnackbar(context, - title: context.localized.setIdentityTo(result.name ?? "")); - } else { - fladderSnackbarResponse(context, response, - altTitle: context.localized.somethingWentWrong); + launchUrl(context, url ?? ""); + }, + icon: const Icon(Icons.launch_rounded)), + ), + Tooltip( + message: "Select result", + child: IconButton( + onPressed: !processing + ? () async { + final response = await ref.read(provider.notifier).setIdentity(result); + if (response?.isSuccessful == true) { + fladderSnackbar(context, + title: context.localized.setIdentityTo(result.name ?? "")); + } else { + fladderSnackbarResponse(context, response, + altTitle: context.localized.somethingWentWrong); + } + + Navigator.of(context).pop(); } - - Navigator.of(context).pop(); - } - : null, - icon: const Icon(Icons.save_alt_rounded), - ), - ) - ], - ), - )) - .toList(), + : null, + icon: const Icon(Icons.save_alt_rounded), + ), + ) + ], + ), + )) + .toList(), + ), ), - ), - ], - ) + ], + ) + ], + ), + actions: [ + ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), + const SizedBox(width: 16), + FilledButton( + onPressed: !processing + ? () async { + await ref.read(provider.notifier).remoteSearch(); + tabController.animateTo(1); + } + : null, + child: processing + ? SizedBox( + width: 21, + height: 21, + child: CircularProgressIndicator.adaptive( + backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round), + ) + : Text(context.localized.search), + ), ], ), - actions: [ - ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), - const SizedBox(width: 16), - FilledButton( - onPressed: !processing - ? () async { - await ref.read(provider.notifier).remoteSearch(); - tabController.animateTo(1); - } - : null, - child: processing - ? SizedBox( - width: 21, - height: 21, - child: CircularProgressIndicator.adaptive( - backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round), - ) - : Text(context.localized.search), - ), - ], ); } @@ -248,7 +247,7 @@ class _IdentifyScreenState extends ConsumerState with TickerProv final controller = currentKey == "Name" ? currentController : TextEditingController(text: state.searchString); return FocusedOutlinedTextField( - label: context.localized.userName, + label: context.localized.name, controller: controller, onChanged: (value) { currentController = controller; diff --git a/lib/screens/metadata/info_screen.dart b/lib/screens/metadata/info_screen.dart index 74b4fb1..c5eabed 100644 --- a/lib/screens/metadata/info_screen.dart +++ b/lib/screens/metadata/info_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/information_model.dart'; import 'package:fladder/models/item_base_model.dart'; diff --git a/lib/screens/metadata/refresh_metadata.dart b/lib/screens/metadata/refresh_metadata.dart index 9494cbc..8fe9a08 100644 --- a/lib/screens/metadata/refresh_metadata.dart +++ b/lib/screens/metadata/refresh_metadata.dart @@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart index 60c10c9..91e344d 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/input_fields.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; diff --git a/lib/screens/photo_viewer/photo_viewer_screen.dart b/lib/screens/photo_viewer/photo_viewer_screen.dart index f8b8d6d..a12f9f2 100644 --- a/lib/screens/photo_viewer/photo_viewer_screen.dart +++ b/lib/screens/photo_viewer/photo_viewer_screen.dart @@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart'; import 'package:fladder/screens/photo_viewer/simple_video_player.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/custom_cache_manager.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/settings/about_settings_page.dart b/lib/screens/settings/about_settings_page.dart index ac24bf4..47952a2 100644 --- a/lib/screens/settings/about_settings_page.dart +++ b/lib/screens/settings/about_settings_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/screens/crash_screen/crash_screen.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; @@ -42,75 +42,79 @@ class AboutSettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final applicationInfo = ref.watch(applicationInfoProvider); - return Card( - child: SettingsScaffold( - label: "", - items: [ - const FladderLogo(), - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)), - Text(context.localized.aboutBuild(applicationInfo.buildNumber)), - const SizedBox(height: 16), - Text(context.localized.aboutCreatedBy), - ], + return SettingsScaffold( + label: "", + items: [ + const FladderLogo(), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)), + Text(context.localized.aboutBuild(applicationInfo.buildNumber)), + const SizedBox(height: 16), + Text(context.localized.aboutCreatedBy), + ], + ), + const FractionallySizedBox( + widthFactor: 0.25, + child: Divider( + indent: 16, + endIndent: 16, ), - const Divider(), - Column( - children: [ - Text( - context.localized.aboutSocials, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 6), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: socials - .map( - (e) => IconButton.filledTonal( - onPressed: () => launchUrl(context, e.url), - icon: Column( - children: [ - Icon(e.icon), - Text(e.label), - ], - ), + ), + Column( + children: [ + Text( + context.localized.aboutSocials, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: socials + .map( + (e) => IconButton.filledTonal( + onPressed: () => launchUrl(context, e.url), + icon: Column( + children: [ + Icon(e.icon), + Text(e.label), + ], ), - ) - .toList() - .addInBetween(const SizedBox(width: 16)), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton.tonal( - onPressed: () => showLicensePage( - context: context, - applicationIcon: const FladderIcon(size: 55), - applicationVersion: applicationInfo.versionPlatformBuild, - applicationLegalese: "DonutWare", - ), - child: Text(context.localized.aboutLicenses), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton.tonal( - onPressed: () => showDialog( - context: context, - builder: (context) => const CrashScreen(), - ), - child: Text(context.localized.errorLogs), - ) - ], - ), - ].addInBetween(const SizedBox(height: 16)), - ), + ), + ) + .toList() + .addInBetween(const SizedBox(width: 16)), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonal( + onPressed: () => showLicensePage( + context: context, + applicationIcon: const FladderIcon(size: 55), + applicationVersion: applicationInfo.versionPlatformBuild, + applicationLegalese: "DonutWare", + ), + child: Text(context.localized.aboutLicenses), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonal( + onPressed: () => showDialog( + context: context, + builder: (context) => const CrashScreen(), + ), + child: Text(context.localized.errorLogs), + ) + ], + ), + ].addInBetween(const SizedBox(height: 16)), ); } } diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart index a0f1272..a736658 100644 --- a/lib/screens/settings/client_sections/client_settings_advanced.dart +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -2,79 +2,83 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/option_dialogue.dart'; List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.advanced), - SettingsListTile( - label: Text(context.localized.settingsLayoutSizesTitle), - subLabel: Text(context.localized.settingsLayoutSizesDesc), - onTap: () async { - final newItems = await openMultiSelectOptions( - context, - label: context.localized.settingsLayoutSizesTitle, - items: ViewSize.values, - allowMultiSelection: true, - selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), - itemBuilder: (type, selected, tap) => CheckboxListTile( - contentPadding: EdgeInsets.zero, - value: selected, - onChanged: (value) => tap(), - title: Text(type.label(context)), + [ + SettingsListTile( + label: Text(context.localized.settingsLayoutSizesTitle), + subLabel: Text(context.localized.settingsLayoutSizesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutSizesTitle, + items: ViewSize.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.layoutStates.toList())) + .map((e) => e.label(context)) + .join(', ')), ), - ); - ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet()); - }, - trailing: Card( - color: Theme.of(context).colorScheme.primaryContainer, - shadowColor: Colors.transparent, - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(ref - .watch(homeSettingsProvider.select((value) => value.layoutStates.toList())) - .map((e) => e.label(context)) - .join(', ')), ), ), - ), - SettingsListTile( - label: Text(context.localized.settingsLayoutModesTitle), - subLabel: Text(context.localized.settingsLayoutModesDesc), - onTap: () async { - final newItems = await openMultiSelectOptions( - context, - label: context.localized.settingsLayoutModesTitle, - items: LayoutMode.values, - allowMultiSelection: true, - selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), - itemBuilder: (type, selected, tap) => CheckboxListTile( - contentPadding: EdgeInsets.zero, - value: selected, - onChanged: (value) => tap(), - title: Text(type.label(context)), + SettingsListTile( + label: Text(context.localized.settingsLayoutModesTitle), + subLabel: Text(context.localized.settingsLayoutModesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutModesTitle, + items: LayoutMode.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList())) + .map((e) => e.label(context)) + .join(', ')), ), - ); - ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet()); - }, - trailing: Card( - color: Theme.of(context).colorScheme.primaryContainer, - shadowColor: Colors.transparent, - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(ref - .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList())) - .map((e) => e.label(context)) - .join(', ')), ), ), - ), - ]; + ], + ); } diff --git a/lib/screens/settings/client_sections/client_settings_dashboard.dart b/lib/screens/settings/client_sections/client_settings_dashboard.dart index 8377695..fc31f9c 100644 --- a/lib/screens/settings/client_sections/client_settings_dashboard.dart +++ b/lib/screens/settings/client_sections/client_settings_dashboard.dart @@ -7,89 +7,92 @@ import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; List buildClientSettingsDashboard(BuildContext context, WidgetRef ref) { final clientSettings = ref.watch(clientSettingsProvider); - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.dashboard), - SettingsListTile( - label: Text(context.localized.settingsHomeBannerTitle), - subLabel: Text(context.localized.settingsHomeBannerDescription), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.homeBanner.label(context), - ), - ), - itemBuilder: (context) => HomeBanner.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => - ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)), - ), - ) - .toList(), - ), - ), - if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) + [ SettingsListTile( - label: Text(context.localized.settingsHomeBannerInformationTitle), - subLabel: Text(context.localized.settingsHomeBannerInformationDesc), + label: Text(context.localized.settingsHomeBannerTitle), + subLabel: Text(context.localized.settingsHomeBannerDescription), trailing: EnumBox( current: ref.watch( - homeSettingsProvider.select((value) => value.carouselSettings.label(context)), + homeSettingsProvider.select( + (value) => value.homeBanner.label(context), + ), ), - itemBuilder: (context) => HomeCarouselSettings.values + itemBuilder: (context) => HomeBanner.values .map( (entry) => PopupMenuItem( value: entry, child: Text(entry.label(context)), - onTap: () => ref - .read(homeSettingsProvider.notifier) - .update((context) => context.copyWith(carouselSettings: entry)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)), ), ) .toList(), ), ), - SettingsListTile( - label: Text(context.localized.settingsHomeNextUpTitle), - subLabel: Text(context.localized.settingsHomeNextUpDesc), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.nextUp.label(context), + if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) + SettingsListTile( + label: Text(context.localized.settingsHomeBannerInformationTitle), + subLabel: Text(context.localized.settingsHomeBannerInformationDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select((value) => value.carouselSettings.label(context)), + ), + itemBuilder: (context) => HomeCarouselSettings.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref + .read(homeSettingsProvider.notifier) + .update((context) => context.copyWith(carouselSettings: entry)), + ), + ) + .toList(), ), ), - itemBuilder: (context) => HomeNextUp.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => - ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), - ), - ) - .toList(), + SettingsListTile( + label: Text(context.localized.settingsHomeNextUpTitle), + subLabel: Text(context.localized.settingsHomeNextUpDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select( + (value) => value.nextUp.label(context), + ), + ), + itemBuilder: (context) => HomeNextUp.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), + ), + ) + .toList(), + ), ), - ), - SettingsListTile( - label: Text(context.localized.clientSettingsShowAllCollectionsTitle), - subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), - trailing: Switch( - value: clientSettings.showAllCollectionTypes, - onChanged: (value) => ref + SettingsListTile( + label: Text(context.localized.clientSettingsShowAllCollectionsTitle), + subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), + onTap: () => ref .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: value)), + .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), + trailing: Switch( + value: clientSettings.showAllCollectionTypes, + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: value)), + ), ), - ), - const Divider(), - ]; + ], + ); } diff --git a/lib/screens/settings/client_sections/client_settings_download.dart b/lib/screens/settings/client_sections/client_settings_download.dart index 75303bd..73ddde8 100644 --- a/lib/screens/settings/client_sections/client_settings_download.dart +++ b/lib/screens/settings/client_sections/client_settings_download.dart @@ -11,9 +11,10 @@ import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart'; import 'package:fladder/screens/shared/input_fields.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/size_formatting.dart'; @@ -24,121 +25,122 @@ List buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu return [ if (canSync && !kIsWeb) ...[ - SettingsLabelDivider(label: context.localized.downloadsTitle), - if (AdaptiveLayout.of(context).isDesktop) ...[ - SettingsListTile( - label: Text(context.localized.downloadsPath), - subLabel: Text(currentFolder ?? "-"), - onTap: currentFolder != null - ? () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathEditTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () async { - String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); - } - Navigator.of(context).pop(); - }, - child: Text(context.localized.change), - ) - ], + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [ + if (AdaptiveLayout.of(context).isDesktop) ...[ + SettingsListTile( + label: Text(context.localized.downloadsPath), + subLabel: Text(currentFolder ?? "-"), + onTap: currentFolder != null + ? () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathEditTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + Navigator.of(context).pop(); + }, + child: Text(context.localized.change), + ) + ], + ), + ) + : () async { + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + }, + trailing: currentFolder?.isNotEmpty == true + ? IconButton( + color: Theme.of(context).colorScheme.error, + onPressed: () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathClearTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () { + ref.read(clientSettingsProvider.notifier).setSyncPath(null); + Navigator.of(context).pop(); + }, + child: Text(context.localized.clear), + ) + ], + ), ), + icon: const Icon(IconsaxPlusLinear.folder_minus), ) - : () async { - String? selectedDirectory = await FilePicker.platform - .getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + : null, + ), + ], + FutureBuilder( + future: ref.watch(syncProvider.notifier).directorySize, + builder: (context, snapshot) { + final data = snapshot.data ?? 0; + return SettingsListTile( + label: Text(context.localized.downloadsSyncedData), + subLabel: Text(data.byteFormat ?? ""), + trailing: FilledButton( + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.downloadsClearTitle, + context.localized.downloadsClearDesc, + (context) async { + await ref.read(syncProvider.notifier).clear(); + setState(() {}); + Navigator.of(context).pop(); + }, + context.localized.clear, + (context) => Navigator.of(context).pop(), + context.localized.cancel, + ); + }, + child: Text(context.localized.clear), + ), + ); + }, + ), + SettingsListTile( + label: Text(context.localized.clientSettingsRequireWifiTitle), + subLabel: Text(context.localized.clientSettingsRequireWifiDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), + trailing: Switch( + value: clientSettings.requireWifi, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), + ), + ), + SettingsListTile( + label: Text(context.localized.maxConcurrentDownloadsTitle), + subLabel: Text(context.localized.maxConcurrentDownloadsDesc), + trailing: SizedBox( + width: 100, + child: IntInputField( + controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()), + onSubmitted: (value) { + if (value != null) { + ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith( + maxConcurrentDownloads: value, + ), + ); + + ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value); } }, - trailing: currentFolder?.isNotEmpty == true - ? IconButton( - color: Theme.of(context).colorScheme.error, - onPressed: () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathClearTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () { - ref.read(clientSettingsProvider.notifier).setSyncPath(null); - Navigator.of(context).pop(); - }, - child: Text(context.localized.clear), - ) - ], - ), - ), - icon: const Icon(IconsaxPlusLinear.folder_minus), - ) - : null, + )), ), - ], - FutureBuilder( - future: ref.watch(syncProvider.notifier).directorySize, - builder: (context, snapshot) { - final data = snapshot.data ?? 0; - return SettingsListTile( - label: Text(context.localized.downloadsSyncedData), - subLabel: Text(data.byteFormat ?? ""), - trailing: FilledButton( - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.downloadsClearTitle, - context.localized.downloadsClearDesc, - (context) async { - await ref.read(syncProvider.notifier).clear(); - setState(() {}); - Navigator.of(context).pop(); - }, - context.localized.clear, - (context) => Navigator.of(context).pop(), - context.localized.cancel, - ); - }, - child: Text(context.localized.clear), - ), - ); - }, - ), - SettingsListTile( - label: Text(context.localized.clientSettingsRequireWifiTitle), - subLabel: Text(context.localized.clientSettingsRequireWifiDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), - trailing: Switch( - value: clientSettings.requireWifi, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), - ), - ), - SettingsListTile( - label: Text(context.localized.maxConcurrentDownloadsTitle), - subLabel: Text(context.localized.maxConcurrentDownloadsDesc), - trailing: SizedBox( - width: 100, - child: IntInputField( - controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()), - onSubmitted: (value) { - if (value != null) { - ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith( - maxConcurrentDownloads: value, - ), - ); - - ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value); - } - }, - )), - ), - const Divider(), + ]), + const SizedBox(height: 12), ], ]; } diff --git a/lib/screens/settings/client_sections/client_settings_theme.dart b/lib/screens/settings/client_sections/client_settings_theme.dart index de3b055..cbef7c3 100644 --- a/lib/screens/settings/client_sections/client_settings_theme.dart +++ b/lib/screens/settings/client_sections/client_settings_theme.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/util/color_extensions.dart'; import 'package:fladder/util/custom_color_themes.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart'; List buildClientSettingsTheme(BuildContext context, WidgetRef ref) { final clientSettings = ref.watch(clientSettingsProvider); - return [ - SettingsLabelDivider(label: context.localized.theme), + return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [ SettingsListTile( label: Text(context.localized.mode), subLabel: Text(clientSettings.themeMode.label(context)), @@ -107,6 +107,5 @@ List buildClientSettingsTheme(BuildContext context, WidgetRef ref) { onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value), ), ), - const Divider(), - ]; + ]); } diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart index b5578df..015bfca 100644 --- a/lib/screens/settings/client_sections/client_settings_visual.dart +++ b/lib/screens/settings/client_sections/client_settings_visual.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,8 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/input_fields.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; @@ -22,136 +21,136 @@ List buildClientSettingsVisual( ) { final clientSettings = ref.watch(clientSettingsProvider); Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale; - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.settingsVisual), - SettingsListTile( - label: Text(context.localized.displayLanguage), - trailing: Localizations.override( - context: context, - locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))), - child: Builder(builder: (context) { - String language = "Unknown"; - try { - language = context.localized.nativeName; - } catch (e) { - log(e.toString()); - } - return EnumBox( - current: language, - itemBuilder: (context) { - return [ - ...AppLocalizations.supportedLocales.map( - (entry) => PopupMenuItem( - value: entry, - child: Localizations.override( - context: context, - locale: entry, - child: Builder(builder: (context) { - return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})"); - }), + [ + SettingsListTile( + label: Text(context.localized.displayLanguage), + trailing: Localizations.override( + context: context, + locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))), + child: Builder(builder: (context) { + String language = "Unknown"; + try { + language = context.localized.nativeName; + } catch (_) {} + return EnumBox( + current: language, + itemBuilder: (context) { + return [ + ...AppLocalizations.supportedLocales.map( + (entry) => PopupMenuItem( + value: entry, + child: Localizations.override( + context: context, + locale: entry, + child: Builder(builder: (context) { + return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})"); + }), + ), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((state) => state.copyWith(selectedLocale: entry)), ), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((state) => state.copyWith(selectedLocale: entry)), + ) + ]; + }, + ); + }), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurredPlaceholderTitle), + subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), + trailing: Switch( + value: clientSettings.blurPlaceHolders, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurEpisodesTitle), + subLabel: Text(context.localized.settingsBlurEpisodesDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), + trailing: Switch( + value: clientSettings.blurUpcomingEpisodes, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsEnableOsMediaControls), + onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), + trailing: Switch( + value: clientSettings.enableMediaKeys, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsNextUpCutoffDays), + trailing: SizedBox( + width: 100, + child: IntInputField( + suffix: context.localized.days, + controller: nextUpDaysEditor, + onSubmitted: (value) { + if (value != null) { + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( + nextUpDateCutoff: Duration(days: value), + )); + } + }, + )), + ), + SettingsListTile( + label: Text(context.localized.libraryPageSizeTitle), + subLabel: Text(context.localized.libraryPageSizeDesc), + trailing: SizedBox( + width: 100, + child: IntInputField( + controller: libraryPageSizeController, + placeHolder: "500", + onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(libraryPageSize: value), ), - ) - ]; - }, - ); - }), + )), ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurredPlaceholderTitle), - subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), - trailing: Switch( - value: clientSettings.blurPlaceHolders, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurEpisodesTitle), - subLabel: Text(context.localized.settingsBlurEpisodesDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), - trailing: Switch( - value: clientSettings.blurUpcomingEpisodes, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsEnableOsMediaControls), - onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), - trailing: Switch( - value: clientSettings.enableMediaKeys, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsNextUpCutoffDays), - trailing: SizedBox( - width: 100, - child: IntInputField( - suffix: context.localized.days, - controller: nextUpDaysEditor, - onSubmitted: (value) { - if (value != null) { - ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( - nextUpDateCutoff: Duration(days: value), - )); - } - }, - )), - ), - SettingsListTile( - label: Text(context.localized.libraryPageSizeTitle), - subLabel: Text(context.localized.libraryPageSizeDesc), - trailing: SizedBox( - width: 100, - child: IntInputField( - controller: libraryPageSizeController, - placeHolder: "500", - onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(libraryPageSize: value), - ), - )), - ), - SettingsListTile( - label: Text(AdaptiveLayout.of(context).isDesktop - ? context.localized.settingsShowScaleSlider - : context.localized.settingsPosterPinch), - onTap: () => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), - ), - trailing: Switch( - value: clientSettings.pinchPosterZoom, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: value), + SettingsListTile( + label: Text(AdaptiveLayout.of(context).isDesktop + ? context.localized.settingsShowScaleSlider + : context.localized.settingsPosterPinch), + onTap: () => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), ), + trailing: Switch( + value: clientSettings.pinchPosterZoom, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: value), + ), + ), ), - ), - Column( - children: [ - SettingsListTile( - label: Text(context.localized.settingsPosterSize), - trailing: Text( - clientSettings.posterSize.toString(), - style: Theme.of(context).textTheme.bodyLarge, + Column( + children: [ + SettingsListTile( + label: Text(context.localized.settingsPosterSize), + trailing: Text( + clientSettings.posterSize.toString(), + style: Theme.of(context).textTheme.bodyLarge, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: FladderSlider( - min: 0.5, - max: 1.5, - value: clientSettings.posterSize, - divisions: 20, - onChanged: (value) => - ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FladderSlider( + min: 0.5, + max: 1.5, + value: clientSettings.posterSize, + divisions: 20, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), + ), ), - ), - ], - ), - const Divider(), - ]; + ], + ), + ], + ); } diff --git a/lib/screens/settings/client_settings_page.dart b/lib/screens/settings/client_settings_page.dart index e594717..1282115 100644 --- a/lib/screens/settings/client_settings_page.dart +++ b/lib/screens/settings/client_settings_page.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -16,7 +15,8 @@ import 'package:fladder/screens/settings/client_sections/client_settings_visual. import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/simple_duration_picker.dart'; @@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final clientSettings = ref.watch(clientSettingsProvider); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: "Fladder", - items: [ - ...buildClientSettingsDownload(context, ref, setState), - SettingsLabelDivider(label: context.localized.lockscreen), + return SettingsScaffold( + label: "Fladder", + items: [ + ...buildClientSettingsDownload(context, ref, setState), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [ SettingsListTile( label: Text(context.localized.timeOut), subLabel: Text(timePickerString(context, clientSettings.timeOut)), @@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState { : null); }, ), - const Divider(), - ...buildClientSettingsDashboard(context, ref), - ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), - ...buildClientSettingsTheme(context, ref), - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ - SettingsLabelDivider(label: context.localized.controls), + ]), + const SizedBox(height: 12), + ...buildClientSettingsDashboard(context, ref), + const SizedBox(height: 12), + ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), + const SizedBox(height: 12), + ...buildClientSettingsTheme(context, ref), + const SizedBox(height: 12), + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [ SettingsListTile( label: Text(context.localized.mouseDragSupport), subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled), @@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState { .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), ), ), - const Divider(), - ], - ...buildClientSettingsAdvanced(context, ref), - if (kDebugMode) ...[ - const SizedBox(height: 64), - SettingsListTile( - label: Text( - context.localized.clearAllSettings, - ), - contentColor: Theme.of(context).colorScheme.error, - onTap: () { - showDialog( - context: context, - builder: (context) => Dialog( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.localized.clearAllSettingsQuestion, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Text( - context.localized.unableToReverseAction, - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - FilledButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.localized.cancel), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - await ref.read(sharedPreferencesProvider).clear(); - context.router.push(const LoginRoute()); - }, - child: Text(context.localized.clear), - ) - ], - ), - ], - ), + ]), + const SizedBox(height: 12), + ], + ...buildClientSettingsAdvanced(context, ref), + if (kDebugMode) ...[ + const SizedBox(height: 64), + SettingsListTile( + label: Text( + context.localized.clearAllSettings, + ), + contentColor: Theme.of(context).colorScheme.error, + onTap: () { + showDialog( + context: context, + builder: (context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.localized.clearAllSettingsQuestion, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Text( + context.localized.unableToReverseAction, + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.localized.cancel), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + await ref.read(sharedPreferencesProvider).clear(); + context.router.push(const LoginRoute()); + }, + child: Text(context.localized.clear), + ) + ], + ), + ], ), ), - ); - }, - ), - ], + ), + ); + }, + ), ], - ), + ], ); } } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 93748b7..0a64edb 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -8,20 +8,20 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/media_segments_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; -import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/connectivity_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/settings/widgets/settings_message_box.dart'; import 'package:fladder/screens/settings/widgets/subtitle_editor.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/bitrate_helper.dart'; import 'package:fladder/util/box_fit_extension.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -41,100 +41,105 @@ class _PlayerSettingsPageState extends ConsumerState { Widget build(BuildContext context) { final videoSettings = ref.watch(videoPlayerSettingsProvider); final provider = ref.read(videoPlayerSettingsProvider.notifier); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; final connectionState = ref.watch(connectivityStatusProvider); - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: context.localized.settingsPlayerTitle, - items: [ + return SettingsScaffold( + label: context.localized.settingsPlayerTitle, + items: [ + ...settingsListGroup( + context, SettingsLabelDivider(label: context.localized.video), - if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) + [ + if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) + Column( + children: [ + SettingsListTile( + label: Text(context.localized.videoScalingFillScreenTitle), + subLabel: Text(context.localized.videoScalingFillScreenDesc), + onTap: () => provider.setFillScreen(!videoSettings.fillScreen), + trailing: Switch( + value: videoSettings.fillScreen, + onChanged: (value) => provider.setFillScreen(value), + ), + ), + AnimatedFadeSize( + child: videoSettings.fillScreen + ? SettingsMessageBox( + context.localized.videoScalingFillScreenNotif, + messageType: MessageType.warning, + ) + : Container(), + ), + ], + ), SettingsListTile( label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(context.localized.videoScalingFillScreenDesc), - onTap: () => provider.setFillScreen(!videoSettings.fillScreen), - trailing: Switch( - value: videoSettings.fillScreen, - onChanged: (value) => provider.setFillScreen(value), + subLabel: Text(videoSettings.videoFit.label(context)), + onTap: () => openMultiSelectOptions( + context, + label: context.localized.videoScalingFillScreenTitle, + items: BoxFit.values, + selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))], + onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first), + itemBuilder: (type, selected, tap) => RadioListTile( + groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), + title: Text(type.label(context)), + value: type, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + contentPadding: EdgeInsets.zero, + onChanged: (value) => tap(), + ), ), ), - AnimatedFadeSize( - child: videoSettings.fillScreen - ? SettingsMessageBox( - context.localized.videoScalingFillScreenNotif, - messageType: MessageType.warning, - ) - : Container(), - ), - SettingsListTile( - label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(videoSettings.videoFit.label(context)), - onTap: () => openMultiSelectOptions( - context, - label: context.localized.videoScalingFillScreenTitle, - items: BoxFit.values, - selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))], - onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first), - itemBuilder: (type, selected, tap) => RadioListTile( - groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), - title: Text(type.label(context)), - value: type, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - contentPadding: EdgeInsets.zero, - onChanged: (value) => tap(), + SettingsListTile( + label: _StatusIndicator( + homeInternet: connectionState.homeInternet, + label: Text(context.localized.homeStreamingQualityTitle), + ), + subLabel: Text(context.localized.homeStreamingQualityDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), + ), + itemBuilder: (context) => Bitrate.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry), + ), + ) + .toList(), ), ), - ), - SettingsListTile( - label: _StatusIndicator( - homeInternet: connectionState.homeInternet, - label: Text(context.localized.homeStreamingQualityTitle), - ), - subLabel: Text(context.localized.homeStreamingQualityDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), + SettingsListTile( + label: _StatusIndicator( + homeInternet: !connectionState.homeInternet, + label: Text(context.localized.internetStreamingQualityTitle), ), - itemBuilder: (context) => Bitrate.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry), - ), - ) - .toList(), - ), - ), - SettingsListTile( - label: _StatusIndicator( - homeInternet: !connectionState.homeInternet, - label: Text(context.localized.internetStreamingQualityTitle), - ), - subLabel: Text(context.localized.internetStreamingQualityDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), + subLabel: Text(context.localized.internetStreamingQualityDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), + ), + itemBuilder: (context) => Bitrate.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry), + ), + ) + .toList(), ), - itemBuilder: (context) => Bitrate.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry), - ), - ) - .toList(), ), - ), - const Divider(), - SettingsLabelDivider(label: context.localized.mediaSegmentActions), + ], + ), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( (entry) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState { ), ), ), - SettingsLabelDivider(label: context.localized.playbackTrackSelection), + ]), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [ SettingsListTile( label: Text(context.localized.rememberAudioSelections), subLabel: Text(context.localized.rememberAudioSelectionsDesc), @@ -190,8 +197,9 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), ), ), - const Divider(), - SettingsLabelDivider(label: context.localized.advanced), + ]), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [ if (PlayerOptions.available.length != 1) SettingsListTile( label: Text(context.localized.playerSettingsBackendTitle), @@ -236,7 +244,7 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setHardwareAccel(value), ), ), - if (!kIsWeb) ...[ + if (!kIsWeb) SettingsListTile( label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), @@ -246,15 +254,14 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setUseLibass(value), ), ), - AnimatedFadeSize( - child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid - ? SettingsMessageBox( - context.localized.settingsPlayerMobileWarning, - messageType: MessageType.warning, - ) - : Container(), - ), - ], + AnimatedFadeSize( + child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid + ? SettingsMessageBox( + context.localized.settingsPlayerMobileWarning, + messageType: MessageType.warning, + ) + : Container(), + ), SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), @@ -291,33 +298,37 @@ class _PlayerSettingsPageState extends ConsumerState { "${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}") }, ), - SettingsListTile( - label: Text(context.localized.settingsAutoNextTitle), - subLabel: Text(context.localized.settingsAutoNextDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select( - (value) => value.nextVideoType.label(context), + Column( + children: [ + SettingsListTile( + label: Text(context.localized.settingsAutoNextTitle), + subLabel: Text(context.localized.settingsAutoNextDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select( + (value) => value.nextVideoType.label(context), + ), + ), + itemBuilder: (context) => AutoNextType.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry), + ), + ) + .toList(), ), ), - itemBuilder: (context) => AutoNextType.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry), - ), - ) - .toList(), - ), - ), - AnimatedFadeSize( - child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { - AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), - AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), - _ => const SizedBox.shrink(), - }, + AnimatedFadeSize( + child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { + AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), + AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), + _ => const SizedBox.shrink(), + }, + ), + ], ), if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) SettingsListTile( @@ -325,8 +336,8 @@ class _PlayerSettingsPageState extends ConsumerState { subLabel: Text(context.localized.playerSettingsOrientationDesc), onTap: () => showOrientationOptions(context, ref), ), - ], - ), + ]), + ], ); } } diff --git a/lib/screens/settings/security_settings_page.dart b/lib/screens/settings/security_settings_page.dart index bafaafa..6fbfed5 100644 --- a/lib/screens/settings/security_settings_page.dart +++ b/lib/screens/settings/security_settings_page.dart @@ -1,14 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/authenticate_button_options.dart'; -import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; @RoutePage() class SecuritySettingsPage extends ConsumerStatefulWidget { @@ -22,14 +23,10 @@ class _UserSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(userProvider); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: context.localized.settingsProfileTitle, - items: [ - SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), + return SettingsScaffold( + label: context.localized.settingsProfileTitle, + items: [ + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), [ SettingsListTile( label: Text(context.localized.settingSecurityApplockTitle), subLabel: Text(user?.authMethod.name(context) ?? ""), @@ -37,8 +34,8 @@ class _UserSettingsPageState extends ConsumerState { ref.read(userProvider.notifier).updateUser(newUser); }), ), - ], - ), + ]), + ], ); } } diff --git a/lib/screens/settings/settings_list_tile.dart b/lib/screens/settings/settings_list_tile.dart index c8d3c74..3539456 100644 --- a/lib/screens/settings/settings_list_tile.dart +++ b/lib/screens/settings/settings_list_tile.dart @@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget { ), if (subLabel != null) Opacity( - opacity: 0.75, + opacity: 0.65, child: Material( color: Colors.transparent, textStyle: Theme.of(context).textTheme.labelLarge, diff --git a/lib/screens/settings/settings_scaffold.dart b/lib/screens/settings/settings_scaffold.dart index 0121d67..0b17395 100644 --- a/lib/screens/settings/settings_scaffold.dart +++ b/lib/screens/settings/settings_scaffold.dart @@ -4,10 +4,9 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/user_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/router_extension.dart'; class SettingsScaffold extends ConsumerWidget { @@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget { final padding = MediaQuery.of(context).padding; final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single; return Scaffold( - backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: floatingActionButton, body: Column( @@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget { ), ), SliverPadding( - padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8), - sliver: SliverList( - delegate: SliverChildListDelegate(items), + padding: MediaQuery.paddingOf(context).copyWith(top: 0), + sliver: SliverList.builder( + itemBuilder: (context, index) => items[index], + itemCount: items.length, ), ), if (bottomActions.isEmpty) diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index f9945e0..1f15c3e 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -12,7 +11,7 @@ import 'package:fladder/screens/settings/quick_connect_window.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/shared/fladder_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/theme_extensions.dart'; @@ -97,119 +96,122 @@ class _SettingsScreenState extends ConsumerState { final quickConnectAvailable = ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false)); - return Container( - color: context.colors.surface, - child: SettingsScaffold( - label: context.localized.settings, - scrollController: scrollController, - showBackButtonNested: true, - showUserIcon: true, - items: [ - SettingsListTile( - label: Text(context.localized.settingsClientTitle), - subLabel: Text(context.localized.settingsClientDesc), - selected: containsRoute(const ClientSettingsRoute()), - icon: deviceIcon, - onTap: () => navigateTo(const ClientSettingsRoute()), - ), - if (quickConnectAvailable) + return Padding( + padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth), + child: Container( + color: context.colors.surface, + child: SettingsScaffold( + label: context.localized.settings, + scrollController: scrollController, + showBackButtonNested: true, + showUserIcon: true, + items: [ SettingsListTile( - label: Text(context.localized.settingsQuickConnectTitle), - icon: IconsaxPlusLinear.password_check, - onTap: () => openQuickConnectDialog(context), + label: Text(context.localized.settingsClientTitle), + subLabel: Text(context.localized.settingsClientDesc), + selected: containsRoute(const ClientSettingsRoute()), + icon: deviceIcon, + onTap: () => navigateTo(const ClientSettingsRoute()), ), - SettingsListTile( - label: Text(context.localized.settingsProfileTitle), - subLabel: Text(context.localized.settingsProfileDesc), - selected: containsRoute(const SecuritySettingsRoute()), - icon: IconsaxPlusLinear.security_user, - onTap: () => navigateTo(const SecuritySettingsRoute()), - ), - SettingsListTile( - label: Text(context.localized.settingsPlayerTitle), - subLabel: Text(context.localized.settingsPlayerDesc), - selected: containsRoute(const PlayerSettingsRoute()), - icon: IconsaxPlusLinear.video_play, - onTap: () => navigateTo(const PlayerSettingsRoute()), - ), - SettingsListTile( - label: Text(context.localized.about), - subLabel: const Text("Fladder"), - selected: containsRoute(const AboutSettingsRoute()), - suffix: Opacity( - opacity: 1, - child: FladderIconOutlined( - size: 24, - color: context.colors.onSurfaceVariant, + if (quickConnectAvailable) + SettingsListTile( + label: Text(context.localized.settingsQuickConnectTitle), + icon: IconsaxPlusLinear.password_check, + onTap: () => openQuickConnectDialog(context), ), + SettingsListTile( + label: Text(context.localized.settingsProfileTitle), + subLabel: Text(context.localized.settingsProfileDesc), + selected: containsRoute(const SecuritySettingsRoute()), + icon: IconsaxPlusLinear.security_user, + onTap: () => navigateTo(const SecuritySettingsRoute()), ), - onTap: () => navigateTo(const AboutSettingsRoute()), - ), - ], - floatingActionButton: Padding( - padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - FloatingActionButton( - key: Key(context.localized.switchUser), - tooltip: context.localized.switchUser, - onPressed: () async { - await ref.read(userProvider.notifier).logoutUser(); - context.router.replaceAll([const LoginRoute()]); - }, - child: const Icon( - IconsaxPlusLinear.arrow_swap_horizontal, - ), + SettingsListTile( + label: Text(context.localized.settingsPlayerTitle), + subLabel: Text(context.localized.settingsPlayerDesc), + selected: containsRoute(const PlayerSettingsRoute()), + icon: IconsaxPlusLinear.video_play, + onTap: () => navigateTo(const PlayerSettingsRoute()), + ), + SettingsListTile( + label: Text(context.localized.about), + subLabel: const Text("Fladder"), + selected: containsRoute(const AboutSettingsRoute()), + suffix: Opacity( + opacity: 1, + child: FladderIconOutlined( + size: 24, + color: context.colors.onSurfaceVariant, ), - const SizedBox(width: 16), - FloatingActionButton( - heroTag: context.localized.logout, - key: Key(context.localized.logout), - tooltip: context.localized.logout, - backgroundColor: Theme.of(context).colorScheme.errorContainer, - onPressed: () { - final user = ref.read(userProvider); - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")), - scrollable: true, - content: Text( - context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""), - ), - actions: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: Text(context.localized.cancel), + ), + onTap: () => navigateTo(const AboutSettingsRoute()), + ), + ], + floatingActionButton: Padding( + padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + FloatingActionButton( + key: Key(context.localized.switchUser), + tooltip: context.localized.switchUser, + onPressed: () async { + await ref.read(userProvider.notifier).logoutUser(); + context.router.replaceAll([const LoginRoute()]); + }, + child: const Icon( + IconsaxPlusLinear.arrow_swap_horizontal, + ), + ), + const SizedBox(width: 16), + FloatingActionButton( + heroTag: context.localized.logout, + key: Key(context.localized.logout), + tooltip: context.localized.logout, + backgroundColor: Theme.of(context).colorScheme.errorContainer, + onPressed: () { + final user = ref.read(userProvider); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")), + scrollable: true, + content: Text( + context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""), ), - ElevatedButton( - style: ElevatedButton.styleFrom().copyWith( - iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text(context.localized.cancel), ), - onPressed: () async { - await ref.read(authProvider.notifier).logOutUser(); - if (context.mounted) { - context.router.replaceAll([const LoginRoute()]); - } - }, - child: Text(context.localized.logout), - ), - ], - ), - ); - }, - child: Icon( - IconsaxPlusLinear.logout, - color: Theme.of(context).colorScheme.onErrorContainer, + ElevatedButton( + style: ElevatedButton.styleFrom().copyWith( + iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), + ), + onPressed: () async { + await ref.read(authProvider.notifier).logOutUser(); + if (context.mounted) { + context.router.replaceAll([const LoginRoute()]); + } + }, + child: Text(context.localized.logout), + ), + ], + ), + ); + }, + child: Icon( + IconsaxPlusLinear.logout, + color: Theme.of(context).colorScheme.onErrorContainer, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/settings/widgets/settings_list_group.dart b/lib/screens/settings/widgets/settings_list_group.dart new file mode 100644 index 0000000..ca8bf59 --- /dev/null +++ b/lib/screens/settings/widgets/settings_list_group.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:dynamic_color/dynamic_color.dart'; + +List settingsListGroup(BuildContext context, Widget label, List children) { + final radius = BorderRadius.circular(24); + final radiusSmall = const Radius.circular(6); + final color = Theme.of(context).colorScheme.surfaceContainerLow.harmonizeWith(Colors.red); + return [ + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + color: color, + shape: RoundedRectangleBorder( + borderRadius: radius.copyWith( + bottomLeft: radiusSmall, + bottomRight: radiusSmall, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: label, + ), + ), + ...children.map( + (e) { + return Card( + elevation: 0, + color: color, + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + shape: RoundedRectangleBorder( + borderRadius: radius.copyWith( + topLeft: radiusSmall, + topRight: radiusSmall, + bottomLeft: e != children.last ? radiusSmall : null, + bottomRight: e != children.last ? radiusSmall : null, + )), + child: e, + ); + }, + ) + ]; +} diff --git a/lib/screens/settings/widgets/subtitle_editor.dart b/lib/screens/settings/widgets/subtitle_editor.dart index 9b0ebef..0d80071 100644 --- a/lib/screens/settings/widgets/subtitle_editor.dart +++ b/lib/screens/settings/widgets/subtitle_editor.dart @@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; diff --git a/lib/screens/shared/adaptive_dialog.dart b/lib/screens/shared/adaptive_dialog.dart index c3867fc..1e2439f 100644 --- a/lib/screens/shared/adaptive_dialog.dart +++ b/lib/screens/shared/adaptive_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; Future showDialogAdaptive( {required BuildContext context, required Widget Function(BuildContext context) builder}) { diff --git a/lib/screens/shared/animated_fade_size.dart b/lib/screens/shared/animated_fade_size.dart index 11e860a..eadbf7e 100644 --- a/lib/screens/shared/animated_fade_size.dart +++ b/lib/screens/shared/animated_fade_size.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; class AnimatedFadeSize extends ConsumerWidget { final Duration duration; final Widget child; + final Alignment alignment; const AnimatedFadeSize({ this.duration = const Duration(milliseconds: 125), required this.child, + this.alignment = Alignment.center, super.key, }); @@ -14,6 +17,7 @@ class AnimatedFadeSize extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return AnimatedSize( duration: duration, + alignment: alignment, curve: Curves.easeInOutCubic, child: AnimatedSwitcher( duration: duration, diff --git a/lib/screens/shared/chips/category_chip.dart b/lib/screens/shared/chips/category_chip.dart index 0921c29..0d07d83 100644 --- a/lib/screens/shared/chips/category_chip.dart +++ b/lib/screens/shared/chips/category_chip.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; @@ -100,33 +99,37 @@ class CategoryChip extends StatelessWidget { label: Text(context.localized.clear), ) ].addInBetween(const SizedBox(width: 6)); - Widget header() => Row( + Widget header(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Material( color: Colors.transparent, textStyle: Theme.of(context).textTheme.titleLarge, child: dialogueTitle ?? label, ), - const Spacer(), - FilledButton.tonal( - onPressed: () { - Navigator.of(context).pop(); - newEntry = null; - onCancel?.call(); - }, - child: Text(context.localized.cancel), + Row( + children: [ + FilledButton.tonal( + onPressed: () { + Navigator.of(context).pop(); + newEntry = null; + onCancel?.call(); + }, + child: Text(context.localized.cancel), + ), + if (onClear != null) + ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + newEntry = null; + onClear!(); + }, + icon: const Icon(IconsaxPlusLinear.back_square), + label: Text(context.localized.clear), + ) + ].addInBetween(const SizedBox(width: 6)), ), - if (onClear != null) - ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - newEntry = null; - onClear!(); - }, - icon: const Icon(IconsaxPlusLinear.back_square), - label: Text(context.localized.clear), - ) - ].addInBetween(const SizedBox(width: 6)), + ], ); if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) { @@ -156,7 +159,7 @@ class CategoryChip extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: header(), + child: header(context), ), const Divider(), CategoryChipEditor( diff --git a/lib/screens/shared/default_title_bar.dart b/lib/screens/shared/default_title_bar.dart index 4fa1f2c..9ad3dec 100644 --- a/lib/screens/shared/default_title_bar.dart +++ b/lib/screens/shared/default_title_bar.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class DefaultTitleBar extends ConsumerStatefulWidget { final String? label; diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index 63d6b24..dadf07d 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -1,22 +1,18 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/images_models.dart'; -import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/theme.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/router_extension.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -64,9 +60,9 @@ class _DetailScaffoldState extends ConsumerState { Widget build(BuildContext context) { final padding = EdgeInsets.symmetric(horizontal: MediaQuery.sizeOf(context).width / 25); final backGroundColor = Theme.of(context).colorScheme.surface.withValues(alpha: 0.8); - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final minHeight = 450.0.clamp(0, MediaQuery.sizeOf(context).height).toDouble(); final maxHeight = MediaQuery.sizeOf(context).height - 10; + final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth; return PullToRefresh( onRefresh: () async { await widget.onRefresh?.call(); @@ -78,16 +74,6 @@ class _DetailScaffoldState extends ConsumerState { }, refreshOnStart: true, child: Scaffold( - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.all(8.0), - child: FloatingPlayerBar(), - ), - _ => null, - }, backgroundColor: Theme.of(context).colorScheme.surface, extendBodyBehindAppBar: true, body: Stack( @@ -164,7 +150,6 @@ class _DetailScaffoldState extends ConsumerState { Padding( padding: EdgeInsets.only( bottom: 0, - left: MediaQuery.of(context).padding.left, top: MediaQuery.of(context).padding.top, ), child: ConstrainedBox( @@ -172,7 +157,9 @@ class _DetailScaffoldState extends ConsumerState { minHeight: MediaQuery.sizeOf(context).height, maxWidth: MediaQuery.sizeOf(context).width, ), - child: widget.content(padding), + child: widget.content(padding.copyWith( + left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left, + )), ), ), ], @@ -182,9 +169,11 @@ class _DetailScaffoldState extends ConsumerState { IconTheme( data: IconThemeData(color: Theme.of(context).colorScheme.onSurface), child: Padding( - padding: MediaQuery.paddingOf(context).add( - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), + padding: MediaQuery.paddingOf(context) + .copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left) + .add( + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), child: Row( children: [ IconButton.filledTonal( @@ -255,13 +244,13 @@ class _DetailScaffoldState extends ConsumerState { child: SettingsUserIcon(), ), ), - Tooltip( - message: context.localized.home, - child: IconButton( - onPressed: () => context.router.navigate(const DashboardRoute()), - icon: const Icon(IconsaxPlusLinear.home), - ), - ), + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) + Tooltip( + message: context.localized.home, + child: IconButton( + onPressed: () => context.navigateTo(const DashboardRoute()), + icon: const Icon(IconsaxPlusLinear.home), + )), ], ), ), diff --git a/lib/screens/shared/file_picker.dart b/lib/screens/shared/file_picker.dart index 89f6ad9..a63a0c3 100644 --- a/lib/screens/shared/file_picker.dart +++ b/lib/screens/shared/file_picker.dart @@ -1,7 +1,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:flutter/foundation.dart'; // ignore: depend_on_referenced_packages import 'package:path/path.dart' as p; diff --git a/lib/screens/shared/floating_search_bar.dart b/lib/screens/shared/floating_search_bar.dart index f4fe13d..448ba79 100644 --- a/lib/screens/shared/floating_search_bar.dart +++ b/lib/screens/shared/floating_search_bar.dart @@ -47,7 +47,7 @@ class _FloatingSearchBarState extends ConsumerState { closedColor: Colors.transparent, closedElevation: 0, closedBuilder: (context, openAction) => Card( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shadowColor: Colors.transparent, elevation: 5, margin: EdgeInsets.zero, diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index 2294425..5db17c5 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -7,7 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/banner_play_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/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'; @@ -51,16 +51,15 @@ class _CarouselBannerState extends ConsumerState { final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4) - .copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), + padding: EdgeInsets.only(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), child: Stack( children: [ CarouselView( elevation: 3, shrinkExtent: 0, controller: carouselController, - shape: RoundedRectangleBorder(borderRadius: border), padding: const EdgeInsets.symmetric(horizontal: 6), + shape: RoundedRectangleBorder(borderRadius: border), enableSplash: false, itemExtent: itemExtent, children: [ @@ -146,8 +145,8 @@ class _CarouselBannerState extends ConsumerState { ? null : (details) async { Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320, - localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect position = RelativeRect.fromLTRB( + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); final poster = widget.items[index]; await showMenu( diff --git a/lib/screens/shared/media/chapter_row.dart b/lib/screens/shared/media/chapter_row.dart index e92d7ad..c5badcd 100644 --- a/lib/screens/shared/media/chapter_row.dart +++ b/lib/screens/shared/media/chapter_row.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -67,8 +67,8 @@ class ChapterRow extends ConsumerWidget { FlatButton( onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 80, localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index fabdd75..9736e25 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -7,7 +7,7 @@ import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; import 'package:fladder/util/string_extensions.dart'; diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 882f76b..93773fb 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -11,7 +11,7 @@ import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/components/poster_placeholder.dart'; import 'package:fladder/theme.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/humanize_duration.dart'; @@ -137,7 +137,7 @@ class _PosterImageState extends ConsumerState { border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), borderRadius: FladderTheme.defaultShape.borderRadius, ), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( alignment: Alignment.topCenter, children: [ @@ -225,7 +225,7 @@ class _PosterImageState extends ConsumerState { onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/episode_details_list.dart b/lib/screens/shared/media/episode_details_list.dart index 3dfd80f..f2bc2b6 100644 --- a/lib/screens/shared/media/episode_details_list.dart +++ b/lib/screens/shared/media/episode_details_list.dart @@ -2,7 +2,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index 33d8df1..653a034 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -9,7 +9,7 @@ import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/syncing/sync_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -241,12 +241,13 @@ class EpisodePoster extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { return FlatButton( - onSecondaryTapDown: (details) { + onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy); + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); - showMenu(context: context, position: position, items: actions.popupMenuItems(useIcons: true)); + await showMenu( + context: context, position: position, items: actions.popupMenuItems(useIcons: true)); }, onTap: onTap, onLongPress: onLongPress, diff --git a/lib/screens/shared/media/expanding_overview.dart b/lib/screens/shared/media/expanding_overview.dart index d0508a9..4c2137c 100644 --- a/lib/screens/shared/media/expanding_overview.dart +++ b/lib/screens/shared/media/expanding_overview.dart @@ -1,9 +1,11 @@ -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/sticky_header_text.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/sticky_header_text.dart'; class ExpandingOverview extends ConsumerStatefulWidget { final String text; @@ -68,11 +70,11 @@ class _ExpandingOverviewState extends ConsumerState { child: expanded ? IconButton.filledTonal( onPressed: toggleState, - icon: const Icon(IconsaxPlusLinear.arrow_up_2), + icon: const Icon(IconsaxPlusLinear.arrow_up_1), ) : IconButton.filledTonal( onPressed: toggleState, - icon: const Icon(IconsaxPlusLinear.arrow_down_1), + icon: const Icon(IconsaxPlusLinear.arrow_down), ), ), ), diff --git a/lib/screens/shared/media/external_urls.dart b/lib/screens/shared/media/external_urls.dart index b796bcc..e0d2ff0 100644 --- a/lib/screens/shared/media/external_urls.dart +++ b/lib/screens/shared/media/external_urls.dart @@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart' as urilauncher; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; diff --git a/lib/screens/shared/media/item_detail_list_widget.dart b/lib/screens/shared/media/item_detail_list_widget.dart index 9b5e4f6..525a6b5 100644 --- a/lib/screens/shared/media/item_detail_list_widget.dart +++ b/lib/screens/shared/media/item_detail_list_widget.dart @@ -25,7 +25,7 @@ class _ItemDetailListWidgetState extends ConsumerState { return Card( elevation: widget.elevation, margin: EdgeInsets.zero, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( children: [ FlatButton( diff --git a/lib/screens/shared/media/media_banner.dart b/lib/screens/shared/media/media_banner.dart index 302de4e..ae4929a 100644 --- a/lib/screens/shared/media/media_banner.dart +++ b/lib/screens/shared/media/media_banner.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:async/async.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/banner_play_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/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'; @@ -87,7 +87,7 @@ class _MediaBannerState extends ConsumerState { final double dragOpacity = (1 - dragOffset.abs()).clamp(0, 1); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 6), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/shared/media/people_row.dart b/lib/screens/shared/media/people_row.dart index f7b89d0..c8bf543 100644 --- a/lib/screens/shared/media/people_row.dart +++ b/lib/screens/shared/media/people_row.dart @@ -7,7 +7,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/screens/details_screens/person_detail_screen.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -22,12 +22,14 @@ class PeopleRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Widget placeHolder(String name) { - return Card( - child: FractionallySizedBox( - widthFactor: 0.4, + return Center( + child: SizedBox( + height: 75, + width: 75, child: Card( elevation: 5, - shape: const CircleBorder(), + shadowColor: Colors.transparent, + color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.50), child: Center( child: Text( name.getInitials(), diff --git a/lib/screens/shared/media/poster_grid.dart b/lib/screens/shared/media/poster_grid.dart index cce4cd1..fd5d553 100644 --- a/lib/screens/shared/media/poster_grid.dart +++ b/lib/screens/shared/media/poster_grid.dart @@ -1,11 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sticky_headers/sticky_headers.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/sticky_header_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sticky_headers/sticky_headers.dart'; class PosterGrid extends ConsumerWidget { final String? name; diff --git a/lib/screens/shared/media/poster_list_item.dart b/lib/screens/shared/media/poster_list_item.dart index 51fa691..035082f 100644 --- a/lib/screens/shared/media/poster_list_item.dart +++ b/lib/screens/shared/media/poster_list_item.dart @@ -1,10 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; + import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/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'; @@ -12,8 +15,6 @@ import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/clickable_text.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 PosterListItem extends ConsumerWidget { final ItemBaseModel poster; @@ -64,12 +65,12 @@ class PosterListItem extends ConsumerWidget { color: Theme.of(context).colorScheme.primary.withValues(alpha: selected == true ? 0.25 : 0), borderRadius: BorderRadius.circular(6), ), - child: FlatButton( + child: InkWell( onTap: () => pressedWidget(context), onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = - RelativeRect.fromLTRB(localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/poster_row.dart b/lib/screens/shared/media/poster_row.dart index 41a907e..a6f38f2 100644 --- a/lib/screens/shared/media/poster_row.dart +++ b/lib/screens/shared/media/poster_row.dart @@ -1,18 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; +import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/widgets/shared/horizontal_list.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class PosterRow extends ConsumerStatefulWidget { final List posters; final String label; + final double? collectionAspectRatio; final Function()? onLabelClick; final EdgeInsets contentPadding; const PosterRow({ required this.posters, this.contentPadding = const EdgeInsets.symmetric(horizontal: 16), required this.label, + this.collectionAspectRatio, this.onLabelClick, super.key, }); @@ -32,6 +37,7 @@ class _PosterRowState extends ConsumerState { @override Widget build(BuildContext context) { + final dominantRatio = widget.collectionAspectRatio ?? widget.posters.getMostCommonType.aspectRatio; return HorizontalList( contentPadding: widget.contentPadding, label: widget.label, @@ -41,6 +47,7 @@ class _PosterRowState extends ConsumerState { final poster = widget.posters[index]; return PosterWidget( poster: poster, + aspectRatio: dominantRatio, key: Key(poster.id), ); }, diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index f128ad5..4fd4981 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -4,9 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/poster_image.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.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/widgets/shared/clickable_text.dart'; diff --git a/lib/screens/shared/media/season_row.dart b/lib/screens/shared/media/season_row.dart index 73c817f..375692f 100644 --- a/lib/screens/shared/media/season_row.dart +++ b/lib/screens/shared/media/season_row.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -124,11 +124,11 @@ class SeasonPoster extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { return FlatButton( - onSecondaryTapDown: (details) { + onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy); - showMenu( + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu( context: context, position: position, items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); diff --git a/lib/screens/shared/nested_bottom_appbar.dart b/lib/screens/shared/nested_bottom_appbar.dart index 15c7cbf..da99db5 100644 --- a/lib/screens/shared/nested_bottom_appbar.dart +++ b/lib/screens/shared/nested_bottom_appbar.dart @@ -1,7 +1,5 @@ -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/shared/shapes.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; class NestedBottomAppBar extends ConsumerWidget { @@ -10,27 +8,17 @@ class NestedBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final double bottomPadding = - (AdaptiveLayout.of(context).isDesktop || kIsWeb) ? 12 : MediaQuery.of(context).padding.bottom; - return Card( - color: Theme.of(context).colorScheme.surface, - shape: BottomBarShape(), - elevation: 0, - child: Padding( - padding: const EdgeInsets.only(top: 8), - child: SizedBox( - height: kBottomNavigationBarHeight + 12 + bottomPadding, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12) - .copyWith( - bottom: bottomPadding, - ) - .add(EdgeInsets.only( - left: MediaQuery.of(context).padding.left, - right: MediaQuery.of(context).padding.right, - )), - child: child, - ), + return Padding( + padding: const EdgeInsets.all(8.0).copyWith(bottom: MediaQuery.paddingOf(context).bottom), + child: Card( + color: Theme.of(context).colorScheme.surfaceContainerLow, + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.circular(24), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: child, ), ), ); diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index aed43c7..7971dc4 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -2,38 +2,38 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/video_player_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; - class NestedScaffold extends ConsumerWidget { final Widget body; - const NestedScaffold({required this.body, super.key}); + final Widget? background; + const NestedScaffold({ + required this.body, + this.background, + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); - - return Card( - child: Scaffold( - backgroundColor: Colors.transparent, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) { - ViewSize.phone => null, - _ => switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ), - _ => null, - }, - }, - body: body, - ), + return Stack( + alignment: Alignment.bottomCenter, + children: [ + if (background != null) background!, + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.surface.withValues(alpha: 0.98), + Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), + ], + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + body: body, + ), + ), + ], ); } } diff --git a/lib/screens/shared/nested_sliver_appbar.dart b/lib/screens/shared/nested_sliver_appbar.dart index bd23967..c2a212d 100644 --- a/lib/screens/shared/nested_sliver_appbar.dart +++ b/lib/screens/shared/nested_sliver_appbar.dart @@ -1,11 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/util/list_padding.dart'; + import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/shapes.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class NestedSliverAppBar extends ConsumerWidget { final BuildContext parent; @@ -29,15 +30,14 @@ class NestedSliverAppBar extends ConsumerWidget { padding: const EdgeInsets.only(bottom: 24), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 10, children: [ - IconButton.filledTonal( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.surface), - ), - onPressed: () => Scaffold.of(parent).openDrawer(), - icon: const Icon( - IconsaxPlusBold.menu, - size: 28, + SizedBox( + width: 30, + child: IconButton( + onPressed: () => Scaffold.of(parent).openDrawer(), + icon: const Icon(IconsaxPlusLinear.menu), + padding: EdgeInsets.zero, ), ), Expanded( @@ -62,8 +62,9 @@ class NestedSliverAppBar extends ConsumerWidget { const Icon(IconsaxPlusLinear.search_normal), const SizedBox(width: 16), Transform.translate( - offset: const Offset(0, 2.5), - child: Text(searchTitle ?? "${context.localized.search}...")), + offset: const Offset(0, 1.5), + child: Text(searchTitle ?? "${context.localized.search}..."), + ), ], ), ), @@ -73,7 +74,7 @@ class NestedSliverAppBar extends ConsumerWidget { ), ), const SettingsUserIcon() - ].addInBetween(const SizedBox(width: 16)), + ], ), ), ), diff --git a/lib/screens/shared/user_icon.dart b/lib/screens/shared/user_icon.dart index d8aa2f6..47a9149 100644 --- a/lib/screens/shared/user_icon.dart +++ b/lib/screens/shared/user_icon.dart @@ -49,7 +49,7 @@ class UserIcon extends ConsumerWidget { elevation: 0, surfaceTintColor: Colors.transparent, color: Colors.transparent, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: SizedBox.fromSize( size: size, child: Stack( diff --git a/lib/screens/syncing/sync_item_details.dart b/lib/screens/syncing/sync_item_details.dart index 2cb2180..0b4a276 100644 --- a/lib/screens/syncing/sync_item_details.dart +++ b/lib/screens/syncing/sync_item_details.dart @@ -17,7 +17,7 @@ import 'package:fladder/screens/syncing/sync_child_item.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/size_formatting.dart'; diff --git a/lib/screens/syncing/sync_list_item.dart b/lib/screens/syncing/sync_list_item.dart index 49c38d7..c759c29 100644 --- a/lib/screens/syncing/sync_list_item.dart +++ b/lib/screens/syncing/sync_list_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; @@ -35,7 +35,7 @@ class SyncListItemState extends ConsumerState { syncedItem: syncedItem, child: Card( elevation: 1, - color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.2), + color: Theme.of(context).colorScheme.surfaceDim, shadowColor: Colors.transparent, child: Dismissible( background: Container( diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index b5d3279..fcd7832 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/syncing/sync_list_item.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @@ -31,48 +31,49 @@ class _SyncedScreenState extends ConsumerState { @override Widget build(BuildContext context) { final items = ref.watch(syncProvider.select((value) => value.items)); + final padding = AdaptiveLayout.adaptivePadding(context); + return PullToRefresh( refreshOnStart: true, onRefresh: () => ref.read(syncProvider.notifier).refresh(), child: NestedScaffold( + background: BackgroundImage(images: items.map((value) => value.images).nonNulls.toList()), body: PinchPosterZoom( scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), - child: items.isNotEmpty - ? CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: widget.navigationScrollController, - slivers: [ - if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) - NestedSliverAppBar( - searchTitle: "${context.localized.search} ...", - parent: context, - route: LibrarySearchRoute(), - ) - else - const DefaultSliverTopBadding(), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - context.localized.syncedItems, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverList.builder( - itemBuilder: (context, index) { - final item = items[index]; - return SyncListItem(syncedItem: item); - }, - itemCount: items.length, - ), - ), - const DefautlSliverBottomPadding(), - ], + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: widget.navigationScrollController, + slivers: [ + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) + NestedSliverAppBar( + searchTitle: "${context.localized.search} ...", + parent: context, + route: LibrarySearchRoute(), ) - : Center( + else + const DefaultSliverTopBadding(), + if (items.isNotEmpty) ...[ + SliverToBoxAdapter( + child: Padding( + padding: padding, + child: Text( + context.localized.syncedItems, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SliverPadding( + padding: padding, + sliver: SliverList.builder( + itemBuilder: (context, index) { + final item = items[index]; + return SyncListItem(syncedItem: item); + }, + itemCount: items.length, + ), + ), + ] else ...[ + SliverFillRemaining( child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -87,7 +88,11 @@ class _SyncedScreenState extends ConsumerState { ) ], ), - ), + ) + ], + const DefautlSliverBottomPadding(), + ], + ), ), ), ); diff --git a/lib/screens/video_player/components/video_playback_information.dart b/lib/screens/video_player/components/video_playback_information.dart index 0b575ad..f6b9241 100644 --- a/lib/screens/video_player/components/video_playback_information.dart +++ b/lib/screens/video_player/components/video_playback_information.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/providers/session_info_provider.dart'; @@ -33,112 +33,117 @@ class _VideoPlaybackInformation extends ConsumerWidget { return Dialog( child: Padding( padding: const EdgeInsets.all(12.0), - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Player info", style: Theme.of(context).textTheme.titleMedium), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), - child: Opacity( - opacity: 0.80, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('url: '), - const SizedBox(width: 8), - Flexible( - child: ImageFiltered( - imageFilter: ImageFilter.blur( - sigmaX: 3.0, - sigmaY: 3.0, - ), - child: Text( - playbackModel?.media?.url ?? "No url", - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: SingleChildScrollView( + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Player info", style: Theme.of(context).textTheme.titleMedium), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), + child: Opacity( + opacity: 0.80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('backend: '), + Text(backend?.label(context) ?? context.localized.unknown) + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('url: '), + const SizedBox(width: 8), + Flexible( + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: 3.0, + sigmaY: 3.0, + ), + child: Text( + playbackModel?.media?.url ?? "No url", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ), - ), - IconButton.filled( - onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"), - icon: const Icon(IconsaxPlusLinear.copy), - ) - ], - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + IconButton.filled( + onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"), + icon: const Icon(IconsaxPlusLinear.copy), + ) + ], + ) + ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + ), ), ), - ), - const Divider(), - if (playbackState != null) _PlayerInformation(state: playbackState), - Text("Playback information", style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), - child: Opacity( - opacity: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [const Text('type: '), Text(playbackModel.label ?? "")], - ), - if (sessionInfo.transCodeInfo != null) ...[ - Text("Transcoding", style: Theme.of(context).textTheme.titleMedium), - if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('reason: '), - Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "") - ], - ), - if (sessionInfo.transCodeInfo?.completionPercentage != null) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('transcode progress: '), - Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %") - ], - ), - if (sessionInfo.transCodeInfo?.container != null) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('container: '), - Text(sessionInfo.transCodeInfo!.container.toString()) - ], - ), - ], - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('resolution: '), - Text(playbackModel?.item.streamModel?.resolutionText ?? "") + const Divider(), + if (playbackState != null) _PlayerInformation(state: playbackState), + Text("Playback information", style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), + child: Opacity( + opacity: 0.8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('type: '), Text(playbackModel.label ?? "")], + ), + if (sessionInfo.transCodeInfo != null) ...[ + Text("Transcoding", style: Theme.of(context).textTheme.titleMedium), + if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('reason: '), + Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "") + ], + ), + if (sessionInfo.transCodeInfo?.completionPercentage != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('transcode progress: '), + Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %") + ], + ), + if (sessionInfo.transCodeInfo?.container != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('container: '), + Text(sessionInfo.transCodeInfo!.container.toString()) + ], + ), ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('container: '), - Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "") - ], - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('resolution: '), + Text(playbackModel?.item.streamModel?.resolutionText ?? "") + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('container: '), + Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "") + ], + ) + ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/video_player/components/video_player_chapters.dart b/lib/screens/video_player/components/video_player_chapters.dart index 26035b5..09328ee 100644 --- a/lib/screens/video_player/components/video_player_chapters.dart +++ b/lib/screens/video_player/components/video_player_chapters.dart @@ -50,7 +50,7 @@ class VideoPlayerChapters extends ConsumerWidget { final isCurrent = chapter == currentChapter; return Card( color: Colors.black, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( children: [ Center( diff --git a/lib/screens/video_player/components/video_player_next_wrapper.dart b/lib/screens/video_player/components/video_player_next_wrapper.dart index b66d313..3f9ed40 100644 --- a/lib/screens/video_player/components/video_player_next_wrapper.dart +++ b/lib/screens/video_player/components/video_player_next_wrapper.dart @@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index e9221f6..affbe25 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -22,7 +22,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/device_orientation_extension.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; diff --git a/lib/screens/video_player/components/video_subtitle_controls.dart b/lib/screens/video_player/components/video_subtitle_controls.dart index de66028..e82e716 100644 --- a/lib/screens/video_player/components/video_subtitle_controls.dart +++ b/lib/screens/video_player/components/video_subtitle_controls.dart @@ -199,7 +199,7 @@ class _VideoSubtitleControlsState extends ConsumerState { (e) => FlatButton( onTap: () => provider.setSubColor(e), borderRadiusGeometry: BorderRadius.circular(5), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Container( height: 25, width: 25, @@ -223,7 +223,7 @@ class _VideoSubtitleControlsState extends ConsumerState { onTap: () => provider .setOutlineColor(e == Colors.transparent ? e : e.withValues(alpha: 0.85)), borderRadiusGeometry: BorderRadius.circular(5), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Container( height: 25, width: 25, diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index 56f91a5..59feea0 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -11,7 +11,7 @@ import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/video_player/components/video_player_next_wrapper.dart'; import 'package:fladder/screens/video_player/video_player_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/themes_data.dart'; class VideoPlayer extends ConsumerStatefulWidget { diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 11b6c83..96fa276 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -13,7 +13,6 @@ import 'package:screen_brightness/screen_brightness.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_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/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; @@ -26,7 +25,7 @@ import 'package:fladder/screens/video_player/components/video_player_quality_con import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart'; import 'package:fladder/screens/video_player/components/video_progress_bar.dart'; import 'package:fladder/screens/video_player/components/video_volume_slider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/theme.dart b/lib/theme.dart index f9b521d..8bcd605 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -30,9 +30,9 @@ ColorScheme _insertAdditionalColours(ColorScheme scheme) => scheme.copyWith( ); class FladderTheme { - static RoundedRectangleBorder get smallShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); - static RoundedRectangleBorder get defaultShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)); - static RoundedRectangleBorder get largeShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)); + static RoundedRectangleBorder get smallShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)); + static RoundedRectangleBorder get defaultShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)); + static RoundedRectangleBorder get largeShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(32)); static Color get darkBackgroundColor => const Color.fromARGB(255, 10, 10, 10); static Color get lightBackgroundColor => const Color.fromARGB(237, 255, 255, 255); @@ -54,7 +54,7 @@ class FladderTheme { elevation: 3, clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, - shape: defaultShape, + shape: smallShape, ), progressIndicatorTheme: const ProgressIndicatorThemeData(), floatingActionButtonTheme: FloatingActionButtonThemeData( @@ -123,7 +123,26 @@ class FladderTheme { iconColor: scheme?.onSecondaryContainer, surfaceTintColor: scheme?.onSecondaryContainer, ), - listTileTheme: ListTileThemeData(shape: defaultShape), + listTileTheme: ListTileThemeData( + shape: defaultShape, + ), + dividerTheme: const DividerThemeData( + indent: 6, + endIndent: 6, + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.selected)) { + return scheme?.primaryContainer; + } + return scheme?.surfaceContainer; + }), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 8, horizontal: 12)), + elevation: const WidgetStatePropertyAll(5), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + ), elevatedButtonTheme: ElevatedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), filledButtonTheme: FilledButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), outlinedButtonTheme: OutlinedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), diff --git a/lib/util/adaptive_layout.dart b/lib/util/adaptive_layout.dart deleted file mode 100644 index 3638fa6..0000000 --- a/lib/util/adaptive_layout.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/settings/home_settings_provider.dart'; -import 'package:fladder/util/debug_banner.dart'; -import 'package:fladder/util/poster_defaults.dart'; - -enum InputDevice { - touch, - pointer, -} - -class LayoutPoints { - final double start; - final double end; - final ViewSize type; - LayoutPoints({ - required this.start, - required this.end, - required this.type, - }); - - LayoutPoints copyWith({ - double? start, - double? end, - ViewSize? type, - }) { - return LayoutPoints( - start: start ?? this.start, - end: end ?? this.end, - type: type ?? this.type, - ); - } - - @override - String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)'; - - @override - bool operator ==(covariant LayoutPoints other) { - if (identical(this, other)) return true; - - return other.start == start && other.end == end && other.type == type; - } - - @override - int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode; -} - -class AdaptiveLayout extends InheritedWidget { - final ViewSize viewSize; - final LayoutMode layoutMode; - final InputDevice inputDevice; - final TargetPlatform platform; - final bool isDesktop; - final PosterDefaults posterDefaults; - final ScrollController controller; - - const AdaptiveLayout({ - super.key, - required this.viewSize, - required this.layoutMode, - required this.inputDevice, - required this.platform, - required this.isDesktop, - required this.posterDefaults, - required this.controller, - required super.child, - }); - - static AdaptiveLayout? maybeOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } - - static ViewSize layoutOf(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.viewSize; - } - - static PosterDefaults poster(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.posterDefaults; - } - - static AdaptiveLayout of(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!; - } - - static ScrollController scrollOf(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.controller; - } - - static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.layoutMode; - static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.viewSize; - - static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.inputDevice; - - @override - bool updateShouldNotify(AdaptiveLayout oldWidget) { - return viewSize != oldWidget.viewSize || - layoutMode != oldWidget.layoutMode || - platform != oldWidget.platform || - inputDevice != oldWidget.inputDevice || - isDesktop != oldWidget.isDesktop; - } -} - -const defaultTitleBarHeight = 35.0; - -class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { - final List layoutPoints; - final ViewSize fallBack; - final Widget child; - const AdaptiveLayoutBuilder({required this.layoutPoints, required this.child, required this.fallBack, super.key}); - - @override - ConsumerState createState() => _AdaptiveLayoutBuilderState(); -} - -class _AdaptiveLayoutBuilderState extends ConsumerState { - late ViewSize viewSize = widget.fallBack; - late LayoutMode layoutMode = LayoutMode.single; - late TargetPlatform currentPlatform = defaultTargetPlatform; - late ScrollController controller = ScrollController(); - - @override - void didChangeDependencies() { - calculateLayout(); - calculateSize(); - super.didChangeDependencies(); - } - - bool get isDesktop { - if (kIsWeb) return false; - return [ - TargetPlatform.macOS, - TargetPlatform.windows, - TargetPlatform.linux, - ].contains(currentPlatform); - } - - void calculateLayout() { - ViewSize? newType; - for (var element in widget.layoutPoints) { - if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { - newType = element.type; - } - } - viewSize = newType ?? widget.fallBack; - } - - void calculateSize() { - LayoutMode newSize; - if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { - newSize = LayoutMode.single; - } else { - newSize = LayoutMode.dual; - } - layoutMode = newSize; - } - - @override - Widget build(BuildContext context) { - final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); - final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); - return MediaQuery( - data: MediaQuery.of(context).copyWith( - padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, - viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, - ), - child: AdaptiveLayout( - viewSize: selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values), - controller: controller, - layoutMode: selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values), - inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch, - platform: currentPlatform, - isDesktop: isDesktop, - posterDefaults: switch (viewSize) { - ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55), - ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55), - ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55), - }, - child: DebugBanner(child: widget.child), - ), - ); - } -} - -double? get topPadding { - return switch (defaultTargetPlatform) { - TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35, - _ => null - }; -} diff --git a/lib/util/adaptive_layout/adaptive_layout.dart b/lib/util/adaptive_layout/adaptive_layout.dart new file mode 100644 index 0000000..f5e127e --- /dev/null +++ b/lib/util/adaptive_layout/adaptive_layout.dart @@ -0,0 +1,223 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart'; +import 'package:fladder/util/debug_banner.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/poster_defaults.dart'; + +enum InputDevice { + touch, + pointer, +} + +enum ViewSize { + phone, + tablet, + desktop; + + const ViewSize(); + + String label(BuildContext context) => switch (this) { + ViewSize.phone => context.localized.phone, + ViewSize.tablet => context.localized.tablet, + ViewSize.desktop => context.localized.desktop, + }; + + bool operator >(ViewSize other) => index > other.index; + bool operator >=(ViewSize other) => index >= other.index; + bool operator <(ViewSize other) => index < other.index; + bool operator <=(ViewSize other) => index <= other.index; +} + +enum LayoutMode { + single, + dual; + + const LayoutMode(); + + String label(BuildContext context) => switch (this) { + LayoutMode.single => context.localized.layoutModeSingle, + LayoutMode.dual => context.localized.layoutModeDual, + }; + + bool operator >(ViewSize other) => index > other.index; + bool operator >=(ViewSize other) => index >= other.index; + bool operator <(ViewSize other) => index < other.index; + bool operator <=(ViewSize other) => index <= other.index; +} + +class AdaptiveLayout extends InheritedWidget { + final AdaptiveLayoutModel data; + + const AdaptiveLayout({ + super.key, + required this.data, + required super.child, + }); + + static AdaptiveLayoutModel of(BuildContext context) { + final inherited = context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No AdaptiveLayout found in context'); + return inherited!.data; + } + + static AdaptiveLayout? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + static ViewSize layoutOf(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.viewSize; + } + + static PosterDefaults poster(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.posterDefaults; + } + + static ScrollController scrollOf(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.controller; + } + + static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) { + final viewPadding = MediaQuery.paddingOf(context); + final padding = viewPadding.copyWith( + left: AdaptiveLayout.of(context).sideBarWidth + horizontalPadding + viewPadding.left, + top: 0, + bottom: 0, + right: viewPadding.left + horizontalPadding, + ); + return padding; + } + + static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.data.layoutMode; + static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.data.viewSize; + + static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.data.inputDevice; + + @override + bool updateShouldNotify(AdaptiveLayout oldWidget) => data != oldWidget.data; +} + +const defaultTitleBarHeight = 35.0; + +class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { + final AdaptiveLayoutModel? adaptiveLayout; + final Widget Function(BuildContext context) child; + const AdaptiveLayoutBuilder({ + this.adaptiveLayout, + required this.child, + super.key, + }); + + @override + ConsumerState createState() => _AdaptiveLayoutBuilderState(); +} + +class _AdaptiveLayoutBuilderState extends ConsumerState { + List layoutPoints = [ + LayoutPoints(start: 0, end: 599, type: ViewSize.phone), + LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet), + LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop), + ]; + late ViewSize viewSize = ViewSize.tablet; + late LayoutMode layoutMode = LayoutMode.single; + late TargetPlatform currentPlatform = defaultTargetPlatform; + late ScrollController controller = ScrollController(); + + @override + void didChangeDependencies() { + calculateLayout(); + calculateSize(); + super.didChangeDependencies(); + } + + bool get isDesktop { + if (kIsWeb) return false; + return [ + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.linux, + ].contains(currentPlatform); + } + + void calculateLayout() { + ViewSize? newType; + for (var element in layoutPoints) { + if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { + newType = element.type; + } + } + viewSize = newType ?? ViewSize.tablet; + } + + void calculateSize() { + LayoutMode newSize; + if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { + newSize = LayoutMode.single; + } else { + newSize = LayoutMode.dual; + } + layoutMode = newSize; + } + + @override + Widget build(BuildContext context) { + final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); + final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); + + final selectedViewSize = selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values); + final selectedLayoutMode = selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values); + final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch; + + final posterDefaults = switch (selectedViewSize) { + ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55), + ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55), + ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55), + }; + + final currentLayout = widget.adaptiveLayout ?? + AdaptiveLayoutModel( + viewSize: selectedViewSize, + layoutMode: selectedLayoutMode, + inputDevice: input, + platform: currentPlatform, + isDesktop: isDesktop, + sideBarWidth: 0, + controller: controller, + posterDefaults: posterDefaults, + ); + + return MediaQuery( + data: MediaQuery.of(context).copyWith( + padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, + viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, + ), + child: AdaptiveLayout( + data: currentLayout.copyWith( + viewSize: selectedViewSize, + layoutMode: selectedLayoutMode, + inputDevice: input, + platform: currentPlatform, + isDesktop: isDesktop, + controller: controller, + posterDefaults: posterDefaults, + ), + child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), + ), + ); + } +} + +double? get topPadding { + return switch (defaultTargetPlatform) { + TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35, + _ => null + }; +} diff --git a/lib/util/adaptive_layout/adaptive_layout_model.dart b/lib/util/adaptive_layout/adaptive_layout_model.dart new file mode 100644 index 0000000..28149a9 --- /dev/null +++ b/lib/util/adaptive_layout/adaptive_layout_model.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/poster_defaults.dart'; + +class LayoutPoints { + final double start; + final double end; + final ViewSize type; + LayoutPoints({ + required this.start, + required this.end, + required this.type, + }); + + LayoutPoints copyWith({ + double? start, + double? end, + ViewSize? type, + }) { + return LayoutPoints( + start: start ?? this.start, + end: end ?? this.end, + type: type ?? this.type, + ); + } + + @override + String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)'; + + @override + bool operator ==(covariant LayoutPoints other) { + if (identical(this, other)) return true; + return other.start == start && other.end == end && other.type == type; + } + + @override + int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode; +} + +@immutable +class AdaptiveLayoutModel { + final ViewSize viewSize; + final LayoutMode layoutMode; + final InputDevice inputDevice; + final TargetPlatform platform; + final bool isDesktop; + final PosterDefaults posterDefaults; + final ScrollController controller; + final double sideBarWidth; + + const AdaptiveLayoutModel({ + required this.viewSize, + required this.layoutMode, + required this.inputDevice, + required this.platform, + required this.isDesktop, + required this.posterDefaults, + required this.controller, + required this.sideBarWidth, + }); + + AdaptiveLayoutModel copyWith({ + ViewSize? viewSize, + LayoutMode? layoutMode, + InputDevice? inputDevice, + TargetPlatform? platform, + bool? isDesktop, + PosterDefaults? posterDefaults, + ScrollController? controller, + double? sideBarWidth, + }) { + return AdaptiveLayoutModel( + viewSize: viewSize ?? this.viewSize, + layoutMode: layoutMode ?? this.layoutMode, + inputDevice: inputDevice ?? this.inputDevice, + platform: platform ?? this.platform, + isDesktop: isDesktop ?? this.isDesktop, + posterDefaults: posterDefaults ?? this.posterDefaults, + controller: controller ?? this.controller, + sideBarWidth: sideBarWidth ?? this.sideBarWidth, + ); + } + + @override + bool operator ==(covariant AdaptiveLayoutModel other) { + if (identical(this, other)) return true; + return other.viewSize == viewSize && other.layoutMode == layoutMode && other.sideBarWidth == sideBarWidth; + } + + @override + int get hashCode => viewSize.hashCode ^ layoutMode.hashCode ^ sideBarWidth.hashCode; +} diff --git a/lib/util/fladder_image.dart b/lib/util/fladder_image.dart index 28ae8a5..902607b 100644 --- a/lib/util/fladder_image.dart +++ b/lib/util/fladder_image.dart @@ -13,6 +13,7 @@ class FladderImage extends ConsumerWidget { final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder; final Widget? placeHolder; final BoxFit fit; + final BoxFit? blurFit; final AlignmentGeometry? alignment; final bool disableBlur; final bool blurOnly; @@ -22,6 +23,7 @@ class FladderImage extends ConsumerWidget { this.imageErrorBuilder, this.placeHolder, this.fit = BoxFit.cover, + this.blurFit, this.alignment, this.disableBlur = false, this.blurOnly = false, @@ -41,7 +43,7 @@ class FladderImage extends ConsumerWidget { children: [ if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty) Image( - fit: fit, + fit: blurFit ?? fit, excludeFromSemantics: true, filterQuality: FilterQuality.low, image: BlurHashImage( diff --git a/lib/util/item_base_model/item_base_model_extensions.dart b/lib/util/item_base_model/item_base_model_extensions.dart index c5cdff1..dcdca9c 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -41,6 +41,37 @@ extension ItemBaseModelsBooleans on List { } return groupedItems; } + + FladderItemType get getMostCommonType { + final Map counts = {}; + + for (final item in this) { + final type = item.type; + counts[type] = (counts[type] ?? 0) + 1; + } + + return counts.entries.reduce((a, b) => a.value >= b.value ? a : b).key; + } + + double? getMostCommonAspectRatio({double tolerance = 0.01}) { + final Map> buckets = {}; + + for (final item in this) { + final aspectRatio = item.primaryRatio; + if (aspectRatio == null) continue; + + final bucketKey = (aspectRatio / tolerance).round(); + + buckets.putIfAbsent(bucketKey, () => []).add(aspectRatio); + } + + if (buckets.isEmpty) return null; + + final mostCommonBucket = buckets.entries.reduce((a, b) => a.value.length >= b.value.length ? a : b); + + final average = mostCommonBucket.value.reduce((a, b) => a + b) / mostCommonBucket.value.length; + return average; + } } enum ItemActions { diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index c152ee5..82e682d 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -1,4 +1,3 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -23,7 +22,7 @@ import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; import 'package:fladder/screens/shared/adaptive_dialog.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/video_player/video_player.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; diff --git a/lib/util/keyed_list_view.dart b/lib/util/keyed_list_view.dart deleted file mode 100644 index bf38921..0000000 --- a/lib/util/keyed_list_view.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class KeyedListView extends ConsumerStatefulWidget { - final Map map; - final Widget Function(BuildContext context, int index) itemBuilder; - const KeyedListView({required this.map, required this.itemBuilder, super.key}); - - @override - ConsumerState createState() => _KeyedListViewState(); -} - -class _KeyedListViewState extends ConsumerState { - final ItemScrollController itemScrollController = ItemScrollController(); - final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); - final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create(); - int currentIndex = 0; - - @override - void initState() { - super.initState(); - itemPositionsListener.itemPositions.addListener(() { - if (currentIndex != itemPositionsListener.itemPositions.value.toList()[0].index) { - setState(() { - currentIndex = itemPositionsListener.itemPositions.value.toList()[0].index; - }); - } - }); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Flexible( - child: ScrollablePositionedList.builder( - itemCount: widget.map.length, - itemBuilder: widget.itemBuilder, - itemScrollController: itemScrollController, - scrollOffsetController: scrollOffsetController, - itemPositionsListener: itemPositionsListener, - scrollOffsetListener: scrollOffsetListener, - ), - ), - const SizedBox(width: 8), - SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: widget.map.keys.mapIndexed( - (index, e) { - final atPosition = currentIndex == index; - return Container( - decoration: BoxDecoration( - color: atPosition ? Theme.of(context).colorScheme.secondary : Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), - height: 28, - width: 28, - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - textStyle: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - iconColor: atPosition - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35), - foregroundColor: atPosition - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35), - ), - onPressed: () { - itemScrollController.scrollTo( - index: index, - duration: const Duration(seconds: 1), - opacityAnimationWeights: [20, 20, 60], - curve: Curves.easeOutCubic, - ); - }, - child: Text( - e, - ), - ), - ); - }, - ).toList(), - ), - ), - ], - ); - } -} diff --git a/lib/util/sliver_list_padding.dart b/lib/util/sliver_list_padding.dart index e1b30cf..0a11a7b 100644 --- a/lib/util/sliver_list_padding.dart +++ b/lib/util/sliver_list_padding.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class DefautlSliverBottomPadding extends StatelessWidget { const DefautlSliverBottomPadding({super.key}); @@ -9,8 +8,8 @@ class DefautlSliverBottomPadding extends StatelessWidget { @override Widget build(BuildContext context) { return (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) - ? SliverPadding(padding: EdgeInsets.only(bottom: 35 + MediaQuery.of(context).padding.bottom)) - : SliverPadding(padding: EdgeInsets.only(bottom: 85 + MediaQuery.of(context).padding.bottom)); + ? SliverPadding(padding: EdgeInsets.only(bottom: 60 + MediaQuery.of(context).padding.bottom)) + : SliverPadding(padding: EdgeInsets.only(bottom: 100 + MediaQuery.of(context).padding.bottom)); } } diff --git a/lib/util/sticky_header_text.dart b/lib/util/sticky_header_text.dart index c934057..ae5c07d 100644 --- a/lib/util/sticky_header_text.dart +++ b/lib/util/sticky_header_text.dart @@ -1,7 +1,9 @@ -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/screens/shared/flat_button.dart'; class StickyHeaderText extends ConsumerStatefulWidget { final String label; @@ -21,16 +23,21 @@ class StickyHeaderTextState extends ConsumerState { return FlatButton( onTap: widget.onClick, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, + spacing: 6, children: [ - Text( - widget.label, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + Flexible( + child: Text( + widget.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), ), if (widget.onClick != null) Padding( diff --git a/lib/widgets/navigation_scaffold/components/adaptive_fab.dart b/lib/widgets/navigation_scaffold/components/adaptive_fab.dart index a0ae48c..bd3d744 100644 --- a/lib/widgets/navigation_scaffold/components/adaptive_fab.dart +++ b/lib/widgets/navigation_scaffold/components/adaptive_fab.dart @@ -29,19 +29,15 @@ class AdaptiveFab { duration: const Duration(milliseconds: 250), height: 60, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ElevatedButton( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: FilledButton.tonal( onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - child, - const Spacer(), - Flexible(child: Text(title)), - const Spacer(), - ], - ), + child: Row( + spacing: 24, + children: [ + child, + Flexible(child: Text(title)), + ], ), ), ), diff --git a/lib/widgets/navigation_scaffold/components/background_image.dart b/lib/widgets/navigation_scaffold/components/background_image.dart new file mode 100644 index 0000000..01f8dc0 --- /dev/null +++ b/lib/widgets/navigation_scaffold/components/background_image.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/providers/api_provider.dart'; +import 'package:fladder/util/fladder_image.dart'; + +class BackgroundImage extends ConsumerStatefulWidget { + final List items; + final List images; + const BackgroundImage({this.items = const [], this.images = const [], super.key}); + + @override + ConsumerState createState() => _BackgroundImageState(); +} + +class _BackgroundImageState extends ConsumerState { + ImageData? backgroundImage; + + @override + void didUpdateWidget(covariant BackgroundImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.items.length != widget.items.length || oldWidget.images.length != widget.images.length) { + updateItems(); + } + } + + @override + void initState() { + super.initState(); + updateItems(); + } + + void updateItems() { + WidgetsBinding.instance.addPostFrameCallback((value) async { + if (widget.images.isNotEmpty) { + setState(() { + backgroundImage = widget.images.shuffled().firstOrNull?.primary; + }); + return; + } + final randomItem = widget.items.shuffled().firstOrNull; + if (widget.items.isEmpty) return; + final itemId = switch (randomItem?.type) { + FladderItemType.folder => randomItem?.id, + FladderItemType.series => randomItem?.parentId ?? randomItem?.id, + _ => randomItem?.id, + } ?? + randomItem?.id; + if (itemId == null) return; + final apiProvider = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet( + itemId: itemId, + ); + setState(() { + backgroundImage = apiProvider.body?.parentBaseModel.getPosters?.randomBackDrop ?? + apiProvider.body?.getPosters?.randomBackDrop; + }); + }); + } + + @override + Widget build(BuildContext context) { + return FladderImage( + image: backgroundImage, + fit: BoxFit.cover, + blurOnly: false, + ); + } +} diff --git a/lib/widgets/navigation_scaffold/components/destination_model.dart b/lib/widgets/navigation_scaffold/components/destination_model.dart index 2761745..7be11e9 100644 --- a/lib/widgets/navigation_scaffold/components/destination_model.dart +++ b/lib/widgets/navigation_scaffold/components/destination_model.dart @@ -1,7 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; + import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; -import 'package:flutter/material.dart'; class DestinationModel { final String label; @@ -79,12 +81,13 @@ class DestinationModel { ); } - NavigationButton toNavigationButton(bool selected, bool expanded) { + NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) { return NavigationButton( label: label, selected: selected, onPressed: action, - horizontal: expanded, + horizontal: horizontal, + expanded: expanded, selectedIcon: selectedIcon!, icon: icon!, ); diff --git a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart index 55ee378..5b5477f 100644 --- a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart +++ b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart @@ -1,5 +1,5 @@ import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart b/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart index 563e21a..2783b2d 100644 --- a/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart +++ b/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:auto_route/auto_route.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; bool get _isDesktop { if (kIsWeb) return false; diff --git a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart index 3c73930..b9078b7 100644 --- a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart +++ b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/media_playback_model.dart'; @@ -11,14 +11,15 @@ import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/video_player/video_player.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; const videoPlayerHeroTag = "HeroPlayer"; +const floatingPlayerHeight = 70.0; + class FloatingPlayerBar extends ConsumerStatefulWidget { const FloatingPlayerBar({super.key}); @@ -71,29 +72,29 @@ class _CurrentlyPlayingBarState extends ConsumerState { }, direction: DismissDirection.vertical, child: InkWell( - onLongPress: () { - fladderSnackbar(context, title: "Swipe up/down to open/close the player"); - }, + onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), child: Card( elevation: 5, color: Theme.of(context).colorScheme.primaryContainer, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 50, maxHeight: 85), + child: SizedBox( + height: floatingPlayerHeight, child: LayoutBuilder(builder: (context, constraints) { return Row( children: [ Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - if (playbackInfo.state == VideoPlayerState.minimized) - Card( - child: SizedBox( + child: Padding( + padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + spacing: 7, + children: [ + if (playbackInfo.state == VideoPlayerState.minimized) + Card( child: AspectRatio( aspectRatio: 1.67, child: MouseRegion( @@ -131,72 +132,76 @@ class _CurrentlyPlayingBarState extends ConsumerState { ), ), ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - playbackModel?.title ?? "", - style: Theme.of(context).textTheme.titleLarge, - ), - ), - if (playbackModel?.detailedName(context)?.isNotEmpty == true) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ Flexible( child: Text( - playbackModel?.detailedName(context) ?? "", + playbackModel?.title ?? "", style: Theme.of(context).textTheme.titleMedium, ), ), - ], - ), - ), - if (!progress.isNaN && constraints.maxWidth > 500) - Text( - "${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: IconButton.filledTonal( - onPressed: () => ref.read(videoPlayerProvider).playOrPause(), - icon: playbackInfo.playing - ? const Icon(Icons.pause_rounded) - : const Icon(Icons.play_arrow_rounded), - ), - ), - if (constraints.maxWidth > 500) ...{ - IconButton( - onPressed: () { - final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; - player.setVolume(volume); - }, - icon: Icon( - ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0 - ? IconsaxPlusBold.volume_cross - : IconsaxPlusBold.volume_high, + if (playbackModel?.detailedName(context)?.isNotEmpty == true) + Flexible( + child: Text( + playbackModel?.detailedName(context) ?? "", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), + ), + ), + ), + ], ), ), - }, - Tooltip( - message: context.localized.stop, - waitDuration: const Duration(milliseconds: 500), - child: IconButton( - onPressed: () async => stopPlayer(), - icon: const Icon(IconsaxPlusBold.stop), + if (!progress.isNaN && constraints.maxWidth > 500) + Text( + "${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: IconButton.filledTonal( + onPressed: () => ref.read(videoPlayerProvider).playOrPause(), + icon: playbackInfo.playing + ? const Icon(Icons.pause_rounded) + : const Icon(Icons.play_arrow_rounded), + ), ), - ), - ].addInBetween(const SizedBox(width: 6)), + if (constraints.maxWidth > 500) ...[ + IconButton( + onPressed: () { + final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; + player.setVolume(volume); + }, + icon: Icon( + ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0 + ? IconsaxPlusBold.volume_cross + : IconsaxPlusBold.volume_high, + ), + ), + Tooltip( + message: context.localized.stop, + waitDuration: const Duration(milliseconds: 500), + child: IconButton( + onPressed: () async => stopPlayer(), + icon: const Icon(IconsaxPlusBold.stop), + ), + ), + ], + ], + ), ), ), - ), - LinearProgressIndicator( - minHeight: 6, - backgroundColor: Colors.black.withValues(alpha: 0.25), - color: Theme.of(context).colorScheme.primary, - value: progress.clamp(0, 1), - ), - ], + LinearProgressIndicator( + minHeight: 6, + backgroundColor: Colors.black.withValues(alpha: 0.25), + color: Theme.of(context).colorScheme.primary, + value: progress.clamp(0, 1), + ), + ], + ), ), ), ], diff --git a/lib/widgets/navigation_scaffold/components/navigation_body.dart b/lib/widgets/navigation_scaffold/components/navigation_body.dart index 0e40256..bdc6f78 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_body.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_body.dart @@ -2,21 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.dart'; -import 'package:fladder/routes/auto_router.gr.dart'; -import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/navigation_drawer.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart'; class NavigationBody extends ConsumerStatefulWidget { final BuildContext parentContext; @@ -40,7 +33,7 @@ class NavigationBody extends ConsumerStatefulWidget { } class _NavigationBodyState extends ConsumerState { - bool expandedSideBar = true; + double currentSideBarWidth = 80; @override void initState() { @@ -52,9 +45,9 @@ class _NavigationBodyState extends ConsumerState { @override Widget build(BuildContext context) { - final views = ref.watch(viewsProvider.select((value) => value.views)); final hasOverlay = AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual || homeRoutes.any((element) => element.name.contains(context.router.current.name)); + ref.listen( clientSettingsProvider, (previous, next) { @@ -66,56 +59,28 @@ class _NavigationBodyState extends ConsumerState { }, ); - return switch (AdaptiveLayout.layoutOf(context)) { - ViewSize.phone => MediaQuery.removePadding( - context: widget.parentContext, + Widget paddedChild() => MediaQuery( + data: semiNestedPadding(widget.parentContext, hasOverlay), child: widget.child, - ), - ViewSize.tablet => Row( - children: [ - AnimatedFadeSize( - duration: const Duration(milliseconds: 250), - child: hasOverlay ? navigationRail(context) : const SizedBox(), - ), - Flexible( - child: MediaQuery( - data: semiNestedPadding(context, hasOverlay), - child: widget.child, - ), + ); + + return switch (AdaptiveLayout.layoutOf(context)) { + ViewSize.phone => paddedChild(), + ViewSize.tablet => hasOverlay + ? SideNavigationBar( + currentIndex: widget.currentIndex, + destinations: widget.destinations, + currentLocation: widget.currentLocation, + child: paddedChild(), + scaffoldKey: widget.drawerKey, ) - ], - ), - ViewSize.desktop => Row( - children: [ - AnimatedFadeSize( - duration: const Duration(milliseconds: 125), - child: hasOverlay - ? expandedSideBar - ? MediaQuery.removePadding( - context: widget.parentContext, - child: NestedNavigationDrawer( - isExpanded: expandedSideBar, - actionButton: actionButton(), - toggleExpanded: (value) { - setState(() { - expandedSideBar = value; - }); - }, - views: views, - destinations: widget.destinations, - currentLocation: widget.currentLocation, - ), - ) - : navigationRail(context) - : const SizedBox(), - ), - Flexible( - child: MediaQuery( - data: semiNestedPadding(context, hasOverlay), - child: widget.child, - ), - ), - ], + : paddedChild(), + ViewSize.desktop => SideNavigationBar( + currentIndex: widget.currentIndex, + destinations: widget.destinations, + currentLocation: widget.currentLocation, + child: paddedChild(), + scaffoldKey: widget.drawerKey, ) }; } @@ -126,89 +91,4 @@ class _NavigationBodyState extends ConsumerState { padding: paddingOf.copyWith(left: hasOverlay ? 0 : paddingOf.left), ); } - - AdaptiveFab? actionButton() { - return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) - ? widget.destinations[widget.currentIndex].floatingActionButton - : null; - } - - Widget navigationRail(BuildContext context) { - return Column( - children: [ - if (AdaptiveLayout.of(context).isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ - const SizedBox(height: 4), - Text( - "Fladder", - style: Theme.of(context).textTheme.titleSmall, - ), - }, - if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) - SizedBox(height: MediaQuery.of(context).padding.top), - Flexible( - child: Padding( - key: const Key('navigation_rail'), - padding: - MediaQuery.paddingOf(context).copyWith(right: 0, top: AdaptiveLayout.of(context).isDesktop ? 8 : null), - child: Column( - children: [ - IconButton( - onPressed: () { - if (AdaptiveLayout.layoutOf(context) != ViewSize.desktop) { - widget.drawerKey.currentState?.openDrawer(); - } else { - setState(() { - expandedSideBar = true; - }); - } - }, - icon: const Icon(IconsaxPlusBold.menu), - ), - if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual) ...[ - const SizedBox(height: 8), - AnimatedFadeSize( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: actionButton()?.normal, - ), - ), - ], - const Spacer(), - IconTheme( - data: const IconThemeData(size: 28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...widget.destinations.mapIndexed( - (index, destination) => destination.toNavigationButton(widget.currentIndex == index, false), - ) - ], - ), - ), - const Spacer(), - SizedBox( - height: 48, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: widget.currentLocation.contains(const SettingsRoute().routeName) - ? Card( - color: Theme.of(context).colorScheme.primaryContainer, - child: const Padding( - padding: EdgeInsets.all(10), - child: Icon(IconsaxPlusBold.setting_3), - ), - ) - : const SettingsUserIcon()), - ), - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), - ], - ), - ), - ), - ], - ); - } } diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 305b0f7..004fec5 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -2,14 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/util/widget_extensions.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; class NavigationButton extends ConsumerStatefulWidget { final String? label; final Widget selectedIcon; final Widget icon; final bool horizontal; + final bool expanded; final Function()? onPressed; + final Function()? onLongPress; + final List trailing; final bool selected; final Duration duration; const NavigationButton({ @@ -17,8 +21,11 @@ class NavigationButton extends ConsumerStatefulWidget { required this.selectedIcon, required this.icon, this.horizontal = false, + this.expanded = false, this.onPressed, + this.onLongPress, this.selected = false, + this.trailing = const [], this.duration = const Duration(milliseconds: 125), super.key, }); @@ -28,106 +35,119 @@ class NavigationButton extends ConsumerStatefulWidget { } class _NavigationButtonState extends ConsumerState { + bool showPopupButton = false; @override Widget build(BuildContext context) { - return Tooltip( - waitDuration: const Duration(seconds: 1), - preferBelow: false, - triggerMode: TooltipTriggerMode.longPress, - message: widget.label ?? "", - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + final foreGroundColor = widget.selected + ? widget.expanded + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ElevatedButton( + onHover: (value) => setState(() => showPopupButton = value), + style: ButtonStyle( + elevation: const WidgetStatePropertyAll(0), + padding: const WidgetStatePropertyAll(EdgeInsets.zero), + backgroundColor: WidgetStatePropertyAll( + widget.expanded && widget.selected ? Theme.of(context).colorScheme.primary : Colors.transparent, + ), + iconSize: const WidgetStatePropertyAll(24), + iconColor: WidgetStateProperty.resolveWith((states) { + return foreGroundColor; + }), + foregroundColor: WidgetStateProperty.resolveWith((states) { + return foreGroundColor; + })), + onPressed: widget.onPressed, + onLongPress: widget.onLongPress, child: widget.horizontal - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: getChildren(context), - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: getChildren(context), - ), - ), - ); - } - - List getChildren(BuildContext context) { - return [ - Flexible( - child: ElevatedButton( - style: ButtonStyle( - elevation: const WidgetStatePropertyAll(0), - padding: const WidgetStatePropertyAll(EdgeInsets.zero), - backgroundColor: const WidgetStatePropertyAll(Colors.transparent), - iconSize: const WidgetStatePropertyAll(24), - iconColor: WidgetStateProperty.resolveWith((states) { - return widget.selected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); - }), - foregroundColor: WidgetStateProperty.resolveWith((states) { - return widget.selected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); - })), - onPressed: widget.onPressed, - child: AnimatedContainer( - curve: Curves.fastOutSlowIn, - duration: widget.duration, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: SizedBox( + height: 35, + child: Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 250), + height: widget.selected ? 16 : 0, + margin: const EdgeInsets.only(top: 1.5), + width: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: widget.selected && !widget.expanded ? 1 : 0), + ), + ), AnimatedSwitcher( duration: widget.duration, - child: widget.selected - ? widget.selectedIcon.setKey(Key("${widget.label}+selected")) - : widget.icon.setKey(Key("${widget.label}+normal")), + child: widget.selected ? widget.selectedIcon : widget.icon, ), - if (widget.horizontal && widget.label != null) - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: _Label(widget: widget), - ) + const SizedBox(width: 6), + if (widget.horizontal && widget.expanded) ...[ + if (widget.label != null) + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 80), + child: Text( + widget.label!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (widget.trailing.isNotEmpty) + AnimatedOpacity( + duration: const Duration(milliseconds: 125), + opacity: showPopupButton ? 1 : 0, + child: PopupMenuButton( + tooltip: context.localized.options, + iconColor: foreGroundColor, + iconSize: 18, + itemBuilder: (context) => widget.trailing.popupMenuItems(useIcons: true), + ), + ) + ], ], ), - AnimatedContainer( - duration: const Duration(milliseconds: 250), - margin: EdgeInsets.only(top: widget.selected ? 8 : 0), - height: widget.selected ? 6 : 0, - width: widget.selected ? 14 : 0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0), + ), + ) + : Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 8, + children: [ + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), + if (widget.label != null && widget.horizontal && widget.expanded) + Flexible(child: Text(widget.label!)) + ], ), - ), - ], + AnimatedContainer( + duration: const Duration(milliseconds: 250), + margin: EdgeInsets.only(top: widget.selected ? 4 : 0), + height: widget.selected ? 6 : 0, + width: widget.selected ? 14 : 0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0), + ), + ), + ], + ), ), - ), - ), - ), ), - ]; - } -} - -class _Label extends StatelessWidget { - const _Label({required this.widget}); - - final NavigationButton widget; - - @override - Widget build(BuildContext context) { - return Text( - widget.label!, - maxLines: 1, - overflow: TextOverflow.fade, - style: - Theme.of(context).textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer), ); } } diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index a1adf7d..ba5ec29 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -6,12 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/collection_types.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -54,7 +53,7 @@ class NestedNavigationDrawer extends ConsumerWidget { ), IconButton( onPressed: () => toggleExpanded(false), - icon: const Icon(IconsaxPlusLinear.menu_1), + icon: const Icon(IconsaxPlusLinear.sidebar_left), ), ], ), @@ -71,16 +70,18 @@ class NestedNavigationDrawer extends ConsumerWidget { ), ), ), - ...destinations.map((destination) => DrawerListButton( - label: destination.label, - selected: context.router.current.name == destination.route?.routeName, - selectedIcon: destination.selectedIcon!, - icon: destination.icon!, - onPressed: () { - destination.action!(); - Scaffold.of(context).closeDrawer(); - }, - )), + ...destinations.map( + (destination) => DrawerListButton( + label: destination.label, + selected: context.router.current.name == destination.route?.routeName, + selectedIcon: destination.selectedIcon!, + icon: destination.icon!, + onPressed: () { + destination.action!(); + Scaffold.of(context).closeDrawer(); + }, + ), + ), if (views.isNotEmpty) ...{ const Divider(indent: 28, endIndent: 28), Padding( diff --git a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart index 0befea7..b4fe5d3 100644 --- a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart +++ b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/user_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; class SettingsUserIcon extends ConsumerWidget { @@ -15,13 +15,11 @@ class SettingsUserIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final users = ref.watch(userProvider); + final user = ref.watch(userProvider); return Tooltip( message: context.localized.settings, waitDuration: const Duration(seconds: 1), - child: UserIcon( - user: users, - cornerRadius: 200, + child: FlatButton( onLongPress: () => context.router.push(const LockRoute()), onTap: () { if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { @@ -30,6 +28,10 @@ class SettingsUserIcon extends ConsumerWidget { context.router.push(const ClientSettingsRoute()); } }, + child: UserIcon( + user: user, + cornerRadius: 200, + ), ), ); } diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart new file mode 100644 index 0000000..c5de4ff --- /dev/null +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:overflow_view/overflow_view.dart'; + +import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/metadata/refresh_metadata.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; +import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; + +class SideNavigationBar extends ConsumerStatefulWidget { + final int currentIndex; + final List destinations; + final String currentLocation; + final Widget child; + final GlobalKey scaffoldKey; + const SideNavigationBar({ + required this.currentIndex, + required this.destinations, + required this.currentLocation, + required this.child, + required this.scaffoldKey, + super.key, + }); + + @override + ConsumerState createState() => _SideNavigationBarState(); +} + +class _SideNavigationBarState extends ConsumerState { + bool expandedSideBar = false; + bool showOnHover = false; + Timer? timer; + double currentWidth = 80; + + void startTimer() { + timer?.cancel(); + timer = Timer(const Duration(milliseconds: 650), () { + setState(() { + showOnHover = true; + }); + }); + } + + void stopTimer() { + timer?.cancel(); + timer = Timer(const Duration(milliseconds: 350), () { + setState(() { + showOnHover = false; + }); + }); + } + + @override + Widget build(BuildContext context) { + final views = ref.watch(viewsProvider.select((value) => value.views)); + final expandedWidth = 250.0; + final padding = MediaQuery.paddingOf(context); + + final collapsedWidth = 90.0 + padding.left; + final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; + final fullyExpanded = largeBar ? expandedSideBar : false; + final shouldExpand = showOnHover || fullyExpanded; + final isDesktop = AdaptiveLayout.of(context).isDesktop; + return Stack( + children: [ + AdaptiveLayoutBuilder( + adaptiveLayout: AdaptiveLayout.of(context).copyWith( + sideBarWidth: fullyExpanded ? expandedWidth : collapsedWidth, + ), + child: (context) => widget.child, + ), + AnimatedFadeSize( + alignment: Alignment.topLeft, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), + width: shouldExpand ? expandedWidth : collapsedWidth, + child: MouseRegion( + onEnter: (value) => startTimer(), + onExit: (event) => stopTimer(), + child: Column( + children: [ + if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ + const SizedBox(height: 4), + Text( + "Fladder", + style: Theme.of(context).textTheme.titleSmall, + ), + }, + if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: padding.top), + Expanded( + child: Padding( + key: const Key('navigation_rail'), + padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null), + child: Column( + spacing: 2, + children: [ + Align( + alignment: largeBar && expandedSideBar ? Alignment.centerRight : Alignment.center, + child: Opacity( + opacity: largeBar && expandedSideBar ? 0.65 : 1.0, + child: IconButton( + onPressed: !largeBar + ? () => widget.scaffoldKey.currentState?.openDrawer() + : () => setState(() { + expandedSideBar = !expandedSideBar; + if (!expandedSideBar) { + showOnHover = false; + } + }), + icon: Icon( + largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, + ), + ), + ), + ), + const SizedBox(height: 8), + if (largeBar) ...[ + AnimatedFadeSize( + duration: const Duration(milliseconds: 250), + child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, + ), + ], + Expanded( + child: Column( + spacing: 2, + mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + ...widget.destinations.mapIndexed( + (index, destination) => + destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand), + ), + if (views.isNotEmpty && largeBar) ...[ + const Divider( + indent: 32, + endIndent: 32, + ), + Flexible( + child: OverflowView.flexible( + direction: Axis.vertical, + spacing: 4, + children: views.map( + (view) { + final actions = [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return view.toNavigationButton( + context.router.currentUrl.contains(view.id), + true, + shouldExpand, + () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + onLongPress: () => showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), + ), + ), + trailing: actions, + ); + }, + ).toList(), + builder: (context, remaining) { + return PopupMenuButton( + iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), + padding: EdgeInsets.zero, + icon: NavigationButton( + label: context.localized.other, + selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), + icon: const Icon(IconsaxPlusLinear.arrow_square_down), + expanded: shouldExpand, + horizontal: true, + ), + itemBuilder: (context) => views + .sublist(views.length - remaining) + .map( + (e) => PopupMenuItem( + onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), + child: Row( + spacing: 8, + children: [ + Icon(e.collectionType.iconOutlined), + Text(e.name), + ], + ), + ), + ) + .toList(), + ); + }, + ), + ), + ], + ], + ), + ), + NavigationButton( + label: context.localized.settings, + selected: widget.currentLocation.contains(const SettingsRoute().routeName), + selectedIcon: const Icon(IconsaxPlusBold.setting_3), + horizontal: true, + expanded: shouldExpand, + icon: const SizedBox(height: 32, child: SettingsUserIcon()), + onPressed: () { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { + context.router.push(const SettingsRoute()); + } else { + context.router.push(const ClientSettingsRoute()); + } + }, + ), + ], + ), + ), + ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), + ], + ), + ), + ), + ), + ], + ); + } + + AdaptiveFab actionButton(BuildContext context) { + return ((widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) + ? widget.destinations[widget.currentIndex].floatingActionButton + : null) ?? + AdaptiveFab( + context: context, + title: context.localized.search, + key: const Key("Search"), + onPressed: () => context.router.navigate(LibrarySearchRoute()), + child: const Icon(IconsaxPlusLinear.search_normal_1), + ); + } +} diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index 648a1ed..c6cbb47 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -1,15 +1,16 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; @@ -51,11 +52,15 @@ class _NavigationScaffoldState extends ConsumerState { @override Widget build(BuildContext context) { - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final views = ref.watch(viewsProvider.select((value) => value.views)); + final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); + final showPlayerBar = playerState == VideoPlayerState.minimized; - final isHomeRoutes = homeRoutes.any((element) => element.name.contains(context.router.current.name)); + final isDesktop = AdaptiveLayout.of(context).isDesktop; + final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom; + + final isHomeScreen = currentIndex != -1; return PopScope( canPop: currentIndex == 0, onPopInvokedWithResult: (didPop, result) { @@ -63,67 +68,86 @@ class _NavigationScaffoldState extends ConsumerState { widget.destinations.first.action!(); } }, - child: Scaffold( - key: _key, - appBar: const FladderAppBar(), - extendBodyBehindAppBar: true, - resizeToAvoidBottomInset: false, - extendBody: true, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: - playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeRoutes - ? switch (playerState) { - VideoPlayerState.minimized => AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ) - : null, - _ => currentIndex != -1 + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0), + child: Scaffold( + key: _key, + appBar: const FladderAppBar(), + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + extendBody: true, + floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen ? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal : null, - } - : null, - drawer: NestedNavigationDrawer( - actionButton: null, - toggleExpanded: (value) => _key.currentState?.closeDrawer(), - views: views, - destinations: widget.destinations, - currentLocation: currentLocation, - ), - bottomNavigationBar: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? HideOnScroll( - controller: AdaptiveLayout.scrollOf(context), - forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), - child: NestedBottomAppBar( - child: Transform.translate( - offset: const Offset(0, 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: widget.destinations - .map( - (destination) => destination.toNavigationButton( - widget.currentRouteName == destination.route?.routeName, false), - ) - .toList(), - ), + drawer: homeRoutes.any((element) => element.name.contains(currentLocation)) + ? NestedNavigationDrawer( + actionButton: null, + toggleExpanded: (value) => _key.currentState?.closeDrawer(), + views: views, + destinations: widget.destinations, + currentLocation: currentLocation, + ) + : null, + bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone + ? HideOnScroll( + controller: AdaptiveLayout.scrollOf(context), + forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), + child: NestedBottomAppBar( + child: SizedBox( + height: 65, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.destinations + .map( + (destination) => destination.toNavigationButton( + widget.currentRouteName == destination.route?.routeName, false, false), + ) + .toList(), + ), + ), + ), + ) + : null, + body: widget.nestedChild != null + ? NavigationBody( + child: widget.nestedChild!, + parentContext: context, + currentIndex: currentIndex, + destinations: widget.destinations, + currentLocation: currentLocation, + drawerKey: _key, + ) + : null, + ), + ), + ), + Material( + color: Colors.transparent, + child: AnimatedFadeSize( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.colors.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - ) - : null, - body: widget.nestedChild != null - ? NavigationBody( - child: widget.nestedChild!, - parentContext: context, - currentIndex: currentIndex, - destinations: widget.destinations, - currentLocation: currentLocation, - drawerKey: _key, - ) - : null, + child: showPlayerBar + ? Padding( + padding: EdgeInsets.only(bottom: bottomPadding), + child: const FloatingPlayerBar(), + ) + : const SizedBox.shrink(), + ), + ), + ) + ], ), ); } diff --git a/lib/widgets/shared/alert_content.dart b/lib/widgets/shared/alert_content.dart index ebb1c48..e1f3e65 100644 --- a/lib/widgets/shared/alert_content.dart +++ b/lib/widgets/shared/alert_content.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:fladder/util/list_padding.dart'; - class ActionContent extends StatelessWidget { final Widget? title; final Widget child; @@ -23,6 +21,7 @@ class ActionContent extends StatelessWidget { padding: padding ?? MediaQuery.paddingOf(context).add(const EdgeInsets.symmetric(horizontal: 16)), child: Column( mainAxisSize: MainAxisSize.min, + spacing: 16, children: [ if (title != null) ...[ title!, @@ -42,7 +41,7 @@ class ActionContent extends StatelessWidget { children: actions, ) ], - ].addInBetween(const SizedBox(height: 16)), + ], ), ); } diff --git a/lib/widgets/shared/background_item_image.dart b/lib/widgets/shared/background_item_image.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/widgets/shared/button_group.dart b/lib/widgets/shared/button_group.dart new file mode 100644 index 0000000..d7e54ab --- /dev/null +++ b/lib/widgets/shared/button_group.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +class ExpressiveButtonGroup extends StatelessWidget { + final List> options; + final Set selectedValues; + final ValueChanged> onSelected; + final bool multiSelection; + + const ExpressiveButtonGroup({ + super.key, + required this.options, + required this.selectedValues, + required this.onSelected, + this.multiSelection = false, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + spacing: 2, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(options.length, (index) { + final option = options[index]; + final isSelected = selectedValues.contains(option.value); + final isFirst = index == 0; + final isLast = index == options.length - 1; + + final borderRadius = BorderRadius.horizontal( + left: isSelected || isFirst ? const Radius.circular(20) : const Radius.circular(6), + right: isSelected || isLast ? const Radius.circular(20) : const Radius.circular(6), + ); + + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: borderRadius), + elevation: isSelected ? 3 : 0, + backgroundColor: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: + isSelected ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, + textStyle: Theme.of(context).textTheme.labelLarge, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + onPressed: () { + final newSet = Set.from(selectedValues); + if (multiSelection) { + isSelected ? newSet.remove(option.value) : newSet.add(option.value); + } else { + newSet + ..clear() + ..add(option.value); + } + onSelected(newSet); + }, + label: option.child, + icon: isSelected ? option.selected ?? const Icon(Icons.check_rounded) : option.icon, + ); + }), + ); + } +} + +class ButtonGroupOption { + final T value; + final Icon? icon; + final Icon? selected; + final Widget child; + + const ButtonGroupOption({ + required this.value, + this.icon, + this.selected, + required this.child, + }); +} diff --git a/lib/widgets/shared/enum_selection.dart b/lib/widgets/shared/enum_selection.dart index cb72ef8..d661159 100644 --- a/lib/widgets/shared/enum_selection.dart +++ b/lib/widgets/shared/enum_selection.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; class EnumBox extends StatelessWidget { diff --git a/lib/widgets/shared/fladder_scrollbar.dart b/lib/widgets/shared/fladder_scrollbar.dart index c95d87b..fd9f124 100644 --- a/lib/widgets/shared/fladder_scrollbar.dart +++ b/lib/widgets/shared/fladder_scrollbar.dart @@ -1,5 +1,6 @@ -import 'package:flexible_scrollbar/flexible_scrollbar.dart'; import 'package:flutter/material.dart'; + +import 'package:flexible_scrollbar/flexible_scrollbar.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class FladderScrollbar extends ConsumerWidget { diff --git a/lib/widgets/shared/hide_on_scroll.dart b/lib/widgets/shared/hide_on_scroll.dart index e543aa5..31f4e0c 100644 --- a/lib/widgets/shared/hide_on_scroll.dart +++ b/lib/widgets/shared/hide_on_scroll.dart @@ -3,8 +3,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class HideOnScroll extends ConsumerStatefulWidget { final Widget? child; @@ -28,59 +27,64 @@ class HideOnScroll extends ConsumerStatefulWidget { } class _HideOnScrollState extends ConsumerState { - late final scrollController = widget.controller ?? ScrollController(); + late final ScrollController scrollController = widget.controller ?? ScrollController(); bool isVisible = true; - bool atEdge = false; @override void initState() { super.initState(); - scrollController.addListener(listen); + scrollController.addListener(_onScroll); } @override void dispose() { - scrollController.removeListener(listen); + scrollController.removeListener(_onScroll); + if (widget.controller == null) { + scrollController.dispose(); + } super.dispose(); } - void listen() { - final direction = scrollController.position.userScrollDirection; + void _onScroll() { + final position = scrollController.position; + final direction = position.userScrollDirection; - if (scrollController.offset < scrollController.position.maxScrollExtent) { - if (direction == ScrollDirection.forward) { - if (!isVisible) { - setState(() => isVisible = true); - } - } else if (direction == ScrollDirection.reverse) { - if (isVisible) { - setState(() => isVisible = false); - } - } + bool newVisible; + if (position.atEdge && position.pixels >= position.maxScrollExtent) { + // Always show when scrolled to bottom + newVisible = true; } else { - setState(() { - isVisible = true; - }); + newVisible = direction == ScrollDirection.forward; + } + + if (newVisible != isVisible) { + setState(() => isVisible = newVisible); } } @override Widget build(BuildContext context) { - if (widget.visibleBuilder != null) return widget.visibleBuilder!(isVisible)!; + if (widget.visibleBuilder != null) { + return widget.visibleBuilder!(isVisible) ?? const SizedBox(); + } + if (widget.child == null) return const SizedBox(); + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { return widget.child!; - } else { - return AnimatedAlign( - alignment: const Alignment(0, -1), - heightFactor: widget.forceHide - ? 0 - : isVisible - ? 1.0 - : 0, - duration: widget.duration, - child: Wrap(children: [widget.child!]), - ); } + + return AnimatedAlign( + alignment: const Alignment(0, -1), + heightFactor: widget.forceHide + ? 0 + : isVisible + ? 1.0 + : 0, + duration: widget.duration, + child: Wrap( + children: [widget.child!], + ), + ); } } diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 43b42f7..0130734 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -2,20 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/sticky_header_text.dart'; -class HorizontalList extends ConsumerStatefulWidget { +class HorizontalList extends ConsumerStatefulWidget { final String? label; final List titleActions; final Function()? onLabelClick; final String? subtext; - final List items; + final List items; final int? startIndex; final Widget Function(BuildContext context, int index) itemBuilder; final bool scrollToEnd; @@ -42,19 +41,16 @@ class HorizontalList extends ConsumerStatefulWidget { } class _HorizontalListState extends ConsumerState { - final itemScrollController = ItemScrollController(); - late final scrollOffsetController = ScrollOffsetController(); + final GlobalKey _firstItemKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final contentPadding = 8.0; + double? contentWidth; + double? _firstItemWidth; @override void initState() { super.initState(); - Future.microtask(() async { - if (widget.startIndex != null) { - itemScrollController.jumpTo(index: widget.startIndex!); - scrollOffsetController.animateScroll( - offset: -widget.contentPadding.left, duration: const Duration(milliseconds: 125)); - } - }); + _measureFirstItem(scrollTo: true); } @override @@ -62,19 +58,56 @@ class _HorizontalListState extends ConsumerState { super.dispose(); } + void _measureFirstItem({bool scrollTo = false}) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.startIndex != null) { + final context = _firstItemKey.currentContext; + if (context != null) { + final box = context.findRenderObject() as RenderBox; + _firstItemWidth = box.size.width; + if (scrollTo) { + _scrollToPosition(widget.startIndex!); + } + } + } + }); + } + + void _scrollToPosition(int index) { + final offset = index * _firstItemWidth! + index * contentPadding; + _scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + } + void _scrollToStart() { - itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); } void _scrollToEnd() { - itemScrollController.scrollTo( - index: widget.items.length, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); + _scrollController.animateTo( + (_firstItemWidth ?? 200) * widget.items.length + 200, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + } + + int getFirstVisibleIndex() { + if (widget.startIndex == null) return 0; + if (!_scrollController.hasClients || _firstItemWidth == null) return 0; + return (_scrollController.offset / _firstItemWidth!).floor().clamp(0, widget.items.length - 1); } @override Widget build(BuildContext context) { final hasPointer = AdaptiveLayout.of(context).inputDevice == InputDevice.pointer; - return Column( + final content = Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -97,11 +130,13 @@ class _HorizontalListState extends ConsumerState { ), ), if (widget.subtext != null) - Opacity( - opacity: 0.5, - child: Text( - widget.subtext!, - style: Theme.of(context).textTheme.titleMedium, + Flexible( + child: Opacity( + opacity: 0.5, + child: Text( + widget.subtext!, + style: Theme.of(context).textTheme.titleMedium, + ), ), ), ...widget.titleActions @@ -120,8 +155,8 @@ class _HorizontalListState extends ConsumerState { onLongPress: () => _scrollToStart(), child: IconButton( onPressed: () { - scrollOffsetController.animateScroll( - offset: -(MediaQuery.of(context).size.width / 1.75), + _scrollController.animateTo( + _scrollController.offset + -(MediaQuery.of(context).size.width / 1.75), duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); }, @@ -134,12 +169,8 @@ class _HorizontalListState extends ConsumerState { IconButton( tooltip: "Scroll to current", onPressed: () { - if (widget.startIndex != null) { - itemScrollController.jumpTo(index: widget.startIndex!); - scrollOffsetController.animateScroll( - offset: -widget.contentPadding.left, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOutQuad); + if (_firstItemWidth != null && widget.startIndex != null) { + _scrollToPosition(widget.startIndex!); } }, icon: const Icon( @@ -151,8 +182,8 @@ class _HorizontalListState extends ConsumerState { onLongPress: () => _scrollToEnd(), child: IconButton( onPressed: () { - scrollOffsetController.animateScroll( - offset: (MediaQuery.of(context).size.width / 1.75), + _scrollController.animateTo( + _scrollController.offset + (MediaQuery.of(context).size.width / 1.75), duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); }, @@ -170,23 +201,30 @@ class _HorizontalListState extends ConsumerState { ), const SizedBox(height: 8), SizedBox( - height: widget.height ?? + height: (widget.height ?? AdaptiveLayout.poster(context).size * - ref.watch(clientSettingsProvider.select((value) => value.posterSize)), - child: ScrollablePositionedList.separated( - shrinkWrap: widget.shrinkWrap, - itemScrollController: itemScrollController, - scrollOffsetController: scrollOffsetController, - padding: widget.contentPadding, - itemCount: widget.items.length, + ref.watch(clientSettingsProvider.select((value) => value.posterSize))), + child: ListView.separated( + controller: _scrollController, scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => const SizedBox( - width: 16, - ), - itemBuilder: widget.itemBuilder, + padding: widget.contentPadding, + itemBuilder: (context, index) => index == getFirstVisibleIndex() + ? Container( + key: _firstItemKey, + child: widget.itemBuilder(context, index), + ) + : widget.itemBuilder(context, index), + separatorBuilder: (context, index) => SizedBox(width: contentPadding), + itemCount: widget.items.length, ), ), ], ); + return widget.startIndex == null + ? content + : LayoutBuilder(builder: (context, constraints) { + _measureFirstItem(); + return content; + }); } } diff --git a/lib/widgets/shared/modal_bottom_sheet.dart b/lib/widgets/shared/modal_bottom_sheet.dart index 3a06205..8733092 100644 --- a/lib/widgets/shared/modal_bottom_sheet.dart +++ b/lib/widgets/shared/modal_bottom_sheet.dart @@ -3,8 +3,7 @@ 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/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/fladder_image.dart'; Future showBottomSheetPill({ @@ -18,31 +17,58 @@ Future showBottomSheetPill({ ScrollController scrollController, ) content, }) async { - final screenSize = MediaQuery.sizeOf(context); await showModalBottomSheet( isScrollControlled: true, + backgroundColor: Colors.transparent, useRootNavigator: true, - showDragHandle: true, enableDrag: true, context: context, - constraints: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? BoxConstraints(maxHeight: screenSize.height * 0.9) - : BoxConstraints(maxWidth: screenSize.width * 0.75, maxHeight: screenSize.height * 0.85), builder: (context) { final controller = ScrollController(); - return ListView( - shrinkWrap: true, - controller: controller, - children: [ - if (item != null) ...{ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ItemBottomSheetPreview(item: item), + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.85, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8).add(MediaQuery.paddingOf(context)), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: FladderTheme.largeShape.borderRadius, ), - const Divider(), - }, - content(context, controller), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Container( + height: 8, + width: 35, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: FladderTheme.largeShape.borderRadius, + ), + ), + ), + Flexible( + child: ListView( + shrinkWrap: true, + controller: controller, + children: [ + if (item != null) ...{ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ItemBottomSheetPreview(item: item), + ), + const Divider(), + }, + content(context, ScrollController()), + ], + ), + ), + ], + ), + ), + ), ); }, ); diff --git a/lib/widgets/shared/modal_side_sheet.dart b/lib/widgets/shared/modal_side_sheet.dart index 360a8a0..60601a5 100644 --- a/lib/widgets/shared/modal_side_sheet.dart +++ b/lib/widgets/shared/modal_side_sheet.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:fladder/theme.dart'; + Future showModalSideSheet( BuildContext context, { required Widget content, @@ -30,13 +32,18 @@ Future showModalSideSheet( pageBuilder: (context, animation1, animation2) { return Align( alignment: Alignment.centerRight, - child: Sheet( - header: header, - backButton: backButton, - closeButton: closeButton, - actions: actions, - content: content, - addDivider: addDivider, + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith( + top: MediaQuery.paddingOf(context).top, + ), + child: Sheet( + header: header, + backButton: backButton, + closeButton: closeButton, + actions: actions, + content: content, + addDivider: addDivider, + ), ), ); }, @@ -64,32 +71,40 @@ class Sheet extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; + final mediaQuery = MediaQuery.of(context); + final size = mediaQuery.size; final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final padding = mediaQuery.padding.copyWith(left: 0, top: 0); - return Material( - elevation: 1, - color: colorScheme.surface, - surfaceTintColor: colorScheme.onSurface, - borderRadius: const BorderRadius.horizontal(left: Radius.circular(20)), - child: Padding( - padding: MediaQuery.of(context).padding, - child: Container( - constraints: BoxConstraints( - minWidth: 256, - maxWidth: size.width <= 600 ? size.width : 400, - minHeight: size.height, - maxHeight: size.height, - ), - child: Column( - children: [ - _buildHeader(context), - Expanded( - child: content, - ), - if (actions?.isNotEmpty ?? false) _buildFooter(context) - ], + return MediaQuery( + data: mediaQuery.copyWith( + padding: mediaQuery.padding.copyWith( + left: 0, + )), + child: Material( + elevation: 1, + color: colorScheme.surface, + surfaceTintColor: colorScheme.onSurface, + borderRadius: FladderTheme.largeShape.borderRadius, + child: Padding( + padding: padding, + child: Container( + constraints: BoxConstraints( + minWidth: 256, + maxWidth: size.width <= 600 ? size.width : 400, + minHeight: size.height, + maxHeight: size.height, + ), + child: Column( + children: [ + _buildHeader(context), + Expanded( + child: content, + ), + if (actions?.isNotEmpty ?? false) _buildFooter(context) + ], + ), ), ), ), diff --git a/lib/widgets/shared/pull_to_refresh.dart b/lib/widgets/shared/pull_to_refresh.dart index 3d6a17b..38eb684 100644 --- a/lib/widgets/shared/pull_to_refresh.dart +++ b/lib/widgets/shared/pull_to_refresh.dart @@ -1,4 +1,4 @@ -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/shared/scroll_position.dart b/lib/widgets/shared/scroll_position.dart index fb3bc6d..11924da 100644 --- a/lib/widgets/shared/scroll_position.dart +++ b/lib/widgets/shared/scroll_position.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; enum ScrollState { @@ -22,45 +23,41 @@ class ScrollStatePosition extends ConsumerStatefulWidget { } class _ScrollStatePositionState extends ConsumerState { - late final scrollController = widget.controller ?? ScrollController(); - ScrollState scrollState = ScrollState.top; + late final ScrollController _scrollController = widget.controller ?? ScrollController(); + ScrollState _scrollState = ScrollState.top; @override void initState() { super.initState(); - scrollController.addListener(listen); + _scrollController.addListener(_onScroll); } @override void dispose() { - scrollController.removeListener(listen); + _scrollController.removeListener(_onScroll); + if (widget.controller == null) { + _scrollController.dispose(); + } super.dispose(); } - void listen() { - if (scrollController.offset < scrollController.position.maxScrollExtent) { - if (scrollController.position.atEdge) { - bool isTop = scrollController.position.pixels == 0; - if (isTop) { - setState(() { - scrollState = ScrollState.top; - }); - print('At the top'); - } else { - setState(() { - scrollState = ScrollState.bottom; - }); - } - } else { - setState(() { - scrollState = ScrollState.middle; - }); - } + void _onScroll() { + final position = _scrollController.position; + final newState = () { + if (position.pixels == 0) return ScrollState.top; + if (position.pixels >= position.maxScrollExtent) return ScrollState.bottom; + return ScrollState.middle; + }(); + + if (newState != _scrollState) { + setState(() { + _scrollState = newState; + }); } } @override Widget build(BuildContext context) { - return widget.positionBuilder(scrollState); + return widget.positionBuilder(_scrollState); } } diff --git a/pubspec.lock b/pubspec.lock index 23bd056..26da9be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -688,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_sticky_header: + dependency: "direct main" + description: + name: flutter_sticky_header + sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: "direct main" description: @@ -1194,6 +1202,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + overflow_view: + dependency: "direct main" + description: + name: overflow_view + sha256: e75e834cd93f7abe9d4edc371a17ee92373147b9e973fdc05373ec3d3f163075 + url: "https://pub.dev" + source: hosted + version: "0.4.0" package_config: dependency: transitive description: @@ -1554,14 +1570,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" - source: hosted - version: "0.3.8" share_plus: dependency: "direct main" description: @@ -1799,6 +1807,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + super_sliver_list: + dependency: "direct main" + description: + name: super_sliver_list + sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56 + url: "https://pub.dev" + source: hosted + version: "0.4.1" swagger_dart_code_generator: dependency: "direct dev" description: @@ -1967,6 +1983,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + url: "https://pub.dev" + source: hosted + version: "0.4.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b37fdb..09c2634 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,13 +80,14 @@ dependencies: page_transition: ^2.2.1 sticky_headers: ^0.3.0+2 flutter_staggered_grid_view: ^0.7.0 - scrollable_positioned_list: ^0.3.8 sliver_tools: ^0.2.12 square_progress_indicator: ^0.0.7 flutter_blurhash: ^0.8.2 extended_image: ^9.1.0 flutter_widget_from_html: ^0.15.3 font_awesome_flutter: ^10.8.0 + reorderable_grid: ^1.0.10 + overflow_view: ^0.4.0 # Navigation auto_route: ^9.3.0+1 @@ -122,7 +123,8 @@ dependencies: share_plus: ^10.1.4 archive: ^4.0.2 dart_mappable: ^4.3.0 - reorderable_grid: ^1.0.10 + super_sliver_list: ^0.4.1 + flutter_sticky_header: ^0.7.0 dev_dependencies: flutter_test: