diff --git a/lib/models/collection_types.dart b/lib/models/collection_types.dart index c9e3ce6..8d2dd39 100644 --- a/lib/models/collection_types.dart +++ b/lib/models/collection_types.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; @@ -5,6 +7,7 @@ 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'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/library_filter_model.dart'; extension CollectionTypeExtension on CollectionType { IconData get iconOutlined { @@ -30,11 +33,6 @@ extension CollectionTypeExtension on CollectionType { } } - bool get searchRecursive => switch (this) { - CollectionType.homevideos || CollectionType.photos => false, - _ => true, - }; - IconData getIconType(bool outlined) { switch (this) { case CollectionType.music: @@ -58,6 +56,16 @@ extension CollectionTypeExtension on CollectionType { } } + LibraryFilterModel get defaultFilters { + log(name); + return switch (this) { + CollectionType.homevideos || CollectionType.photos => const LibraryFilterModel(recursive: false), + _ => const LibraryFilterModel( + recursive: true, + ) + }; + } + double? get aspectRatio => switch (this) { CollectionType.music || CollectionType.homevideos || diff --git a/lib/models/item_base_model.dart b/lib/models/item_base_model.dart index 164eb21..06cc0a2 100644 --- a/lib/models/item_base_model.dart +++ b/lib/models/item_base_model.dart @@ -22,12 +22,14 @@ import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/playlist_model.dart'; +import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/details_screens/book_detail_screen.dart'; import 'package:fladder/screens/details_screens/details_screens.dart'; import 'package:fladder/screens/details_screens/episode_detail_screen.dart'; import 'package:fladder/screens/details_screens/season_detail_screen.dart'; import 'package:fladder/screens/library_search/library_search_screen.dart'; +import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -140,15 +142,14 @@ class ItemBaseModel with ItemBaseModelMappable { case SeasonModel _: return SeasonDetailScreen(item: this); case FolderModel _: - case PhotoAlbumModel _: case BoxSetModel _: case PlaylistModel _: + case PhotoAlbumModel _: return LibrarySearchScreen(folderId: [id]); case PhotoModel _: final photo = this as PhotoModel; - return LibrarySearchScreen( - folderId: [photo.albumId ?? photo.parentId ?? ""], - photoToView: photo, + return PhotoViewerScreen( + items: [photo], ); case BookModel book: return BookDetailScreen(item: book); @@ -163,7 +164,37 @@ class ItemBaseModel with ItemBaseModelMappable { } } - Future navigateTo(BuildContext context) async => context.router.push(DetailsRoute(id: id, item: this)); + Future navigateTo(BuildContext context, {WidgetRef? ref}) async { + switch (this) { + case FolderModel _: + case BoxSetModel _: + case PlaylistModel _: + context.router.push(LibrarySearchRoute(folderId: [id], recursive: true)); + break; + case PhotoAlbumModel _: + context.router.push(LibrarySearchRoute(folderId: [id], recursive: false)); + break; + case PhotoModel _: + final photo = this as PhotoModel; + context.router.push( + PhotoViewerRoute( + items: [photo], + loadingItems: ref?.read(jellyApiProvider).itemsGetAlbumPhotos(albumId: photo.albumId), + selected: photo.id, + ), + ); + break; + case BookModel _: + case MovieModel _: + case EpisodeModel _: + case SeriesModel _: + case SeasonModel _: + case PersonModel _: + default: + context.router.push(DetailsRoute(id: id, item: this)); + break; + } + } factory ItemBaseModel.fromBaseDto(dto.BaseItemDto item, Ref ref) { return switch (item.type) { diff --git a/lib/models/library_filter_model.dart b/lib/models/library_filter_model.dart index e9e27de..1e434b4 100644 --- a/lib/models/library_filter_model.dart +++ b/lib/models/library_filter_model.dart @@ -50,9 +50,9 @@ abstract class LibraryFilterModel with _$LibraryFilterModel { Map types, @Default(SortingOptions.sortName) SortingOptions sortingOption, @Default(SortingOrder.ascending) SortingOrder sortOrder, - @Default(false) bool favourites, + @Default(false) bool? favourites, @Default(true) bool hideEmptyShows, - @Default(true) bool recursive, + @Default(true) bool? recursive, @Default(GroupBy.none) GroupBy groupBy, }) = _LibraryFilterModel; @@ -64,8 +64,8 @@ abstract class LibraryFilterModel with _$LibraryFilterModel { officialRatings.hasEnabled || hideEmptyShows || itemFilters.hasEnabled || - !recursive || - favourites; + recursive == false || + favourites == true; } LibraryFilterModel loadModel(LibraryFilterModel model) { diff --git a/lib/models/library_filter_model.freezed.dart b/lib/models/library_filter_model.freezed.dart index f73fdc5..4c6352e 100644 --- a/lib/models/library_filter_model.freezed.dart +++ b/lib/models/library_filter_model.freezed.dart @@ -24,9 +24,9 @@ mixin _$LibraryFilterModel implements DiagnosticableTreeMixin { Map get types; SortingOptions get sortingOption; SortingOrder get sortOrder; - bool get favourites; + bool? get favourites; bool get hideEmptyShows; - bool get recursive; + bool? get recursive; GroupBy get groupBy; /// Create a copy of LibraryFilterModel @@ -81,9 +81,9 @@ abstract mixin class $LibraryFilterModelCopyWith<$Res> { Map types, SortingOptions sortingOption, SortingOrder sortOrder, - bool favourites, + bool? favourites, bool hideEmptyShows, - bool recursive, + bool? recursive, GroupBy groupBy}); } @@ -109,9 +109,9 @@ class _$LibraryFilterModelCopyWithImpl<$Res> Object? types = null, Object? sortingOption = null, Object? sortOrder = null, - Object? favourites = null, + Object? favourites = freezed, Object? hideEmptyShows = null, - Object? recursive = null, + Object? recursive = freezed, Object? groupBy = null, }) { return _then(_self.copyWith( @@ -151,18 +151,18 @@ class _$LibraryFilterModelCopyWithImpl<$Res> ? _self.sortOrder : sortOrder // ignore: cast_nullable_to_non_nullable as SortingOrder, - favourites: null == favourites + favourites: freezed == favourites ? _self.favourites : favourites // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, hideEmptyShows: null == hideEmptyShows ? _self.hideEmptyShows : hideEmptyShows // ignore: cast_nullable_to_non_nullable as bool, - recursive: null == recursive + recursive: freezed == recursive ? _self.recursive : recursive // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, groupBy: null == groupBy ? _self.groupBy : groupBy // ignore: cast_nullable_to_non_nullable @@ -274,9 +274,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel { Map types, SortingOptions sortingOption, SortingOrder sortOrder, - bool favourites, + bool? favourites, bool hideEmptyShows, - bool recursive, + bool? recursive, GroupBy groupBy)? $default, { required TResult orElse(), @@ -328,9 +328,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel { Map types, SortingOptions sortingOption, SortingOrder sortOrder, - bool favourites, + bool? favourites, bool hideEmptyShows, - bool recursive, + bool? recursive, GroupBy groupBy) $default, ) { @@ -380,9 +380,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel { Map types, SortingOptions sortingOption, SortingOrder sortOrder, - bool favourites, + bool? favourites, bool hideEmptyShows, - bool recursive, + bool? recursive, GroupBy groupBy)? $default, ) { @@ -529,13 +529,13 @@ class _LibraryFilterModel extends LibraryFilterModel final SortingOrder sortOrder; @override @JsonKey() - final bool favourites; + final bool? favourites; @override @JsonKey() final bool hideEmptyShows; @override @JsonKey() - final bool recursive; + final bool? recursive; @override @JsonKey() final GroupBy groupBy; @@ -598,9 +598,9 @@ abstract mixin class _$LibraryFilterModelCopyWith<$Res> Map types, SortingOptions sortingOption, SortingOrder sortOrder, - bool favourites, + bool? favourites, bool hideEmptyShows, - bool recursive, + bool? recursive, GroupBy groupBy}); } @@ -626,9 +626,9 @@ class __$LibraryFilterModelCopyWithImpl<$Res> Object? types = null, Object? sortingOption = null, Object? sortOrder = null, - Object? favourites = null, + Object? favourites = freezed, Object? hideEmptyShows = null, - Object? recursive = null, + Object? recursive = freezed, Object? groupBy = null, }) { return _then(_LibraryFilterModel( @@ -668,18 +668,18 @@ class __$LibraryFilterModelCopyWithImpl<$Res> ? _self.sortOrder : sortOrder // ignore: cast_nullable_to_non_nullable as SortingOrder, - favourites: null == favourites + favourites: freezed == favourites ? _self.favourites : favourites // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, hideEmptyShows: null == hideEmptyShows ? _self.hideEmptyShows : hideEmptyShows // ignore: cast_nullable_to_non_nullable as bool, - recursive: null == recursive + recursive: freezed == recursive ? _self.recursive : recursive // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, groupBy: null == groupBy ? _self.groupBy : groupBy // ignore: cast_nullable_to_non_nullable diff --git a/lib/providers/library_screen_provider.g.dart b/lib/providers/library_screen_provider.g.dart index 7017b4e..a07b81a 100644 --- a/lib/providers/library_screen_provider.g.dart +++ b/lib/providers/library_screen_provider.g.dart @@ -6,7 +6,7 @@ part of 'library_screen_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c'; +String _$libraryScreenHash() => r'792c4e47e5cd03635f42a4da4e24698c7584bbdb'; /// See also [LibraryScreen]. @ProviderFor(LibraryScreen) diff --git a/lib/providers/library_search_provider.dart b/lib/providers/library_search_provider.dart index 05947cb..0f1ae4a 100644 --- a/lib/providers/library_search_provider.dart +++ b/lib/providers/library_search_provider.dart @@ -70,6 +70,7 @@ class LibrarySearchNotifier extends StateNotifier { await loadViews(viewModelId, filters); } } + await loadFilters(); if (!wasInitialized) { @@ -78,6 +79,7 @@ class LibrarySearchNotifier extends StateNotifier { filters: state.filters.copyWith( types: state.filters.types.replaceMap(filters.types, enabledOnly: true), genres: state.filters.genres.replaceMap(filters.genres, enabledOnly: true), + recursive: filters.recursive, ), ); } @@ -158,7 +160,7 @@ class LibrarySearchNotifier extends StateNotifier { } else if (state.views.hasEnabled) { await handleViewLoading(); } else { - if (state.searchQuery.isEmpty && !state.filters.favourites) { + if (state.searchQuery.isEmpty && state.filters.favourites == false) { state = state.copyWith(posters: []); } else { final response = await _loadLibrary(recursive: true); @@ -236,7 +238,6 @@ class LibrarySearchNotifier extends StateNotifier { genres: {for (var element in genres) element.name: false}.replaceMap(tempFilters.genres), studios: {for (var element in studios) element: false}.replaceMap(tempFilters.studios), tags: {for (var element in tags) element: false}.replaceMap(tempFilters.tags), - recursive: state.views.included.firstOrNull?.collectionType.searchRecursive ?? true, ), ); state = tempState; @@ -295,7 +296,7 @@ class LibrarySearchNotifier extends StateNotifier { }.toList(), filters: [ ...state.filters.itemFilters.included, - if (state.filters.favourites) ItemFilter.isfavorite, + if (state.filters.favourites == true) ItemFilter.isfavorite, ], includeItemTypes: state.filters.types.included.map((e) => e.dtoKind).toList(), ); @@ -353,9 +354,9 @@ class LibrarySearchNotifier extends StateNotifier { } void toggleFavourite() => - state = state.copyWith(filters: state.filters.copyWith(favourites: !state.filters.favourites)); + state = state.copyWith(filters: state.filters.copyWith(favourites: state.filters.favourites == false)); void toggleRecursive() => - state = state.copyWith(filters: state.filters.copyWith(recursive: !state.filters.recursive)); + state = state.copyWith(filters: state.filters.copyWith(recursive: state.filters.recursive == false)); void toggleType(FladderItemType type) => state = state.copyWith(filters: state.filters.copyWith(types: state.filters.types.toggleKey(type))); void toggleView(ViewModel view) => state = state.copyWith(views: state.views.toggleKey(view)); @@ -569,7 +570,7 @@ class LibrarySearchNotifier extends StateNotifier { } else if (state.views.hasEnabled) { await handleViewLoading(); } else { - if (state.searchQuery.isEmpty && !state.filters.favourites) { + if (state.searchQuery.isEmpty && state.filters.favourites == false) { itemsToPlay = []; } else { final response = await _loadLibrary(recursive: true, shuffle: shuffle); @@ -612,7 +613,7 @@ class LibrarySearchNotifier extends StateNotifier { List albumItems = []; if (!state.filters.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) && - state.filters.recursive) { + state.filters.recursive == true) { for (var album in itemsToPlay.where( (element) => element is PhotoAlbumModel || element is FolderModel, )) { @@ -634,7 +635,7 @@ class LibrarySearchNotifier extends StateNotifier { }.toList(), filters: [ ...state.filters.itemFilters.included, - if (state.filters.favourites) ItemFilter.isfavorite, + if (state.filters.favourites == true) ItemFilter.isfavorite, ], sortBy: shuffle ? [ItemSortBy.random] : null, ); @@ -665,7 +666,7 @@ class LibrarySearchNotifier extends StateNotifier { allItems = await showLoadingOverlay(context, callBack: fetchGallery(shuffle: shuffle)); if (allItems.isNotEmpty) { final newItemList = shuffle ? allItems.shuffled() : allItems; - await context.navigateTo(PhotoViewerRoute( + await context.pushRoute(PhotoViewerRoute( items: newItemList, selected: selected?.id, )); diff --git a/lib/providers/service_provider.dart b/lib/providers/service_provider.dart index 938fb9d..22823ba 100644 --- a/lib/providers/service_provider.dart +++ b/lib/providers/service_provider.dart @@ -16,6 +16,7 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/media_segments_model.dart'; +import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/image_provider.dart'; @@ -344,6 +345,20 @@ class JellyService { ); } + Future> itemsGetAlbumPhotos({ + String? albumId, + }) async { + final response = await itemsGet( + parentId: albumId, + enableUserData: true, + fields: [ + ItemFields.parentid, + ItemFields.datecreated, + ], + ); + return response.body?.items.whereType().toList() ?? []; + } + Future>> personsGet({ String? searchTerm, int? limit, diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart index 9a0a63e..9dfdb02 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'9d866549ed7632e855ba30de2765368960889cff'; + r'2571c078d018150bf93c6f35735f58f16cbca3dd'; /// See also [BackgroundDownloader]. @ProviderFor(BackgroundDownloader) diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index 5f24c99..86ca13c 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,14 +19,7 @@ class AutoRouter extends RootStackRouter { List get guards => [...super.guards, AuthGuard(ref: ref)]; @override - RouteType get defaultRouteType => kIsWeb || - { - TargetPlatform.windows, - TargetPlatform.linux, - TargetPlatform.macOS, - }.contains(defaultTargetPlatform) - ? const RouteType.cupertino() - : const RouteType.adaptive(); + RouteType get defaultRouteType => const RouteType.adaptive(); @override List get routes => [ @@ -40,26 +31,43 @@ class AutoRouter extends RootStackRouter { _homeRoute.copyWith( children: [ ...homeRoutes, - AutoRoute(page: DetailsRoute.page, path: 'details', usesPathAsKey: true), - AutoRoute(page: LibrarySearchRoute.page, path: 'library', usesPathAsKey: true), - CustomRoute( + ...detailsRoutes, + AutoRoute( page: SettingsRoute.page, path: settingsPageRoute, children: _settingsChildren, - transitionsBuilder: TransitionsBuilders.fadeIn, ), ], ), AutoRoute(page: LockRoute.page, path: '/locked'), - AutoRoute(page: PhotoViewerRoute.page, path: "/album"), ]; } +final AutoRoute _homeRoute = AutoRoute(page: HomeRoute.page, path: '/'); final List homeRoutes = [ - _dashboardRoute, - _favouritesRoute, - _syncedRoute, - _librariesRoute, + AutoRoute( + page: DashboardRoute.page, + initial: true, + path: 'dashboard', + ), + AutoRoute( + page: FavouritesRoute.page, + path: 'favourites', + ), + AutoRoute( + page: SyncedRoute.page, + path: 'synced', + ), + AutoRoute( + page: LibraryRoute.page, + path: 'libraries', + ), +]; + +final List detailsRoutes = [ + AutoRoute(page: DetailsRoute.page, path: 'details'), + AutoRoute(page: PhotoViewerRoute.page, path: "album"), + AutoRoute(page: LibrarySearchRoute.page, path: 'library'), ]; final List _defaultRoutes = [ @@ -67,40 +75,12 @@ final List _defaultRoutes = [ AutoRoute(page: LoginRoute.page, path: '/login'), ]; -final AutoRoute _homeRoute = AutoRoute(page: HomeRoute.page, path: '/'); -final AutoRoute _dashboardRoute = CustomRoute( - page: DashboardRoute.page, - transitionsBuilder: TransitionsBuilders.fadeIn, - initial: true, - maintainState: false, - path: 'dashboard', -); -final AutoRoute _favouritesRoute = CustomRoute( - page: FavouritesRoute.page, - transitionsBuilder: TransitionsBuilders.fadeIn, - maintainState: false, - path: 'favourites', -); -final AutoRoute _syncedRoute = CustomRoute( - page: SyncedRoute.page, - transitionsBuilder: TransitionsBuilders.fadeIn, - maintainState: false, - 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'), - CustomRoute(page: SecuritySettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'security'), - CustomRoute(page: PlayerSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'player'), - CustomRoute(page: AboutSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'about'), + AutoRoute(page: SettingsSelectionRoute.page, path: 'list'), + AutoRoute(page: ClientSettingsRoute.page, path: 'client'), + AutoRoute(page: SecuritySettingsRoute.page, path: 'security'), + AutoRoute(page: PlayerSettingsRoute.page, path: 'player'), + AutoRoute(page: AboutSettingsRoute.page, path: 'about'), ]; class LockScreenGuard extends AutoRouteGuard { diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 11dd40a..3ee4e9c 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -12,9 +12,9 @@ import 'dart:async' as _i24; import 'package:auto_route/auto_route.dart' as _i18; -import 'package:collection/collection.dart' as _i23; +import 'package:collection/collection.dart' as _i22; import 'package:fladder/models/item_base_model.dart' as _i19; -import 'package:fladder/models/items/photos_model.dart' as _i22; +import 'package:fladder/models/items/photos_model.dart' as _i23; import 'package:fladder/models/library_search/library_search_options.dart' as _i21; import 'package:fladder/routes/nested_details_screen.dart' as _i4; @@ -200,8 +200,7 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { _i21.SortingOptions? sortingOptions, Map<_i19.FladderItemType, bool>? types, Map? genres, - bool recursive = true, - _i22.PhotoModel? photoToView, + bool? recursive, _i20.Key? key, List<_i18.PageRouteInfo>? children, }) : super( @@ -215,7 +214,6 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { types: types, genres: genres, recursive: recursive, - photoToView: photoToView, key: key, ), rawQueryParams: { @@ -246,7 +244,7 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { sortingOptions: queryParams.get('sortOptions'), types: queryParams.get('itemTypes'), genres: queryParams.get('genres'), - recursive: queryParams.getBool('recursive', true), + recursive: queryParams.optBool('recursive'), ), ); return _i8.LibrarySearchScreen( @@ -258,7 +256,6 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { types: args.types, genres: args.genres, recursive: args.recursive, - photoToView: args.photoToView, key: args.key, ); }, @@ -274,8 +271,7 @@ class LibrarySearchRouteArgs { this.sortingOptions, this.types, this.genres, - this.recursive = true, - this.photoToView, + this.recursive, this.key, }); @@ -293,15 +289,13 @@ class LibrarySearchRouteArgs { final Map? genres; - final bool recursive; - - final _i22.PhotoModel? photoToView; + final bool? recursive; final _i20.Key? key; @override String toString() { - return 'LibrarySearchRouteArgs{viewModelId: $viewModelId, folderId: $folderId, favourites: $favourites, sortOrder: $sortOrder, sortingOptions: $sortingOptions, types: $types, genres: $genres, recursive: $recursive, photoToView: $photoToView, key: $key}'; + return 'LibrarySearchRouteArgs{viewModelId: $viewModelId, folderId: $folderId, favourites: $favourites, sortOrder: $sortOrder, sortingOptions: $sortingOptions, types: $types, genres: $genres, recursive: $recursive, key: $key}'; } @override @@ -309,28 +303,26 @@ class LibrarySearchRouteArgs { if (identical(this, other)) return true; if (other is! LibrarySearchRouteArgs) return false; return viewModelId == other.viewModelId && - const _i23.ListEquality().equals(folderId, other.folderId) && + const _i22.ListEquality().equals(folderId, other.folderId) && favourites == other.favourites && sortOrder == other.sortOrder && sortingOptions == other.sortingOptions && - const _i23.MapEquality().equals(types, other.types) && - const _i23.MapEquality().equals(genres, other.genres) && + const _i22.MapEquality().equals(types, other.types) && + const _i22.MapEquality().equals(genres, other.genres) && recursive == other.recursive && - photoToView == other.photoToView && key == other.key; } @override int get hashCode => viewModelId.hashCode ^ - const _i23.ListEquality().hash(folderId) ^ + const _i22.ListEquality().hash(folderId) ^ favourites.hashCode ^ sortOrder.hashCode ^ sortingOptions.hashCode ^ - const _i23.MapEquality().hash(types) ^ - const _i23.MapEquality().hash(genres) ^ + const _i22.MapEquality().hash(types) ^ + const _i22.MapEquality().hash(genres) ^ recursive.hashCode ^ - photoToView.hashCode ^ key.hashCode; } @@ -370,9 +362,9 @@ class LoginRoute extends _i18.PageRouteInfo { /// [_i11.PhotoViewerScreen] class PhotoViewerRoute extends _i18.PageRouteInfo { PhotoViewerRoute({ - List<_i22.PhotoModel>? items, + List<_i23.PhotoModel>? items, String? selected, - _i24.Future>? loadingItems, + _i24.Future>? loadingItems, _i25.Key? key, List<_i18.PageRouteInfo>? children, }) : super( @@ -415,11 +407,11 @@ class PhotoViewerRouteArgs { this.key, }); - final List<_i22.PhotoModel>? items; + final List<_i23.PhotoModel>? items; final String? selected; - final _i24.Future>? loadingItems; + final _i24.Future>? loadingItems; final _i25.Key? key; @@ -432,7 +424,7 @@ class PhotoViewerRouteArgs { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! PhotoViewerRouteArgs) return false; - return const _i23.ListEquality().equals(items, other.items) && + return const _i22.ListEquality().equals(items, other.items) && selected == other.selected && loadingItems == other.loadingItems && key == other.key; @@ -440,7 +432,7 @@ class PhotoViewerRouteArgs { @override int get hashCode => - const _i23.ListEquality().hash(items) ^ + const _i22.ListEquality().hash(items) ^ selected.hashCode ^ loadingItems.hashCode ^ key.hashCode; @@ -561,56 +553,16 @@ class SplashRouteArgs { /// generated route for /// [_i17.SyncedScreen] -class SyncedRoute extends _i18.PageRouteInfo { - SyncedRoute({ - _i25.ScrollController? navigationScrollController, - _i20.Key? key, - List<_i18.PageRouteInfo>? children, - }) : super( - SyncedRoute.name, - args: SyncedRouteArgs( - navigationScrollController: navigationScrollController, - key: key, - ), - initialChildren: children, - ); +class SyncedRoute extends _i18.PageRouteInfo { + const SyncedRoute({List<_i18.PageRouteInfo>? children}) + : super(SyncedRoute.name, initialChildren: children); static const String name = 'SyncedRoute'; static _i18.PageInfo page = _i18.PageInfo( name, builder: (data) { - final args = data.argsAs( - orElse: () => const SyncedRouteArgs(), - ); - return _i17.SyncedScreen( - navigationScrollController: args.navigationScrollController, - key: args.key, - ); + return const _i17.SyncedScreen(); }, ); } - -class SyncedRouteArgs { - const SyncedRouteArgs({this.navigationScrollController, this.key}); - - final _i25.ScrollController? navigationScrollController; - - final _i20.Key? key; - - @override - String toString() { - return 'SyncedRouteArgs{navigationScrollController: $navigationScrollController, key: $key}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! SyncedRouteArgs) return false; - return navigationScrollController == other.navigationScrollController && - key == other.key; - } - - @override - int get hashCode => navigationScrollController.hashCode ^ key.hashCode; -} diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index d8e202d..a6bffc9 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -18,6 +18,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/dashboard/home_banner_widget.dart'; +import 'package:fladder/screens/home_screen.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'; @@ -97,7 +98,7 @@ class _DashboardScreenState extends ConsumerState { child: PinchPosterZoom( scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference), child: CustomScrollView( - controller: AdaptiveLayout.scrollOf(context), + controller: AdaptiveLayout.scrollOf(context, HomeTabs.dashboard), physics: const AlwaysScrollableScrollPhysics(), slivers: [ const DefaultSliverTopBadding(), @@ -196,6 +197,7 @@ class _DashboardScreenState extends ConsumerState { _ => SortingOptions.dateAdded, }, sortOrder: SortingOrder.descending, + recursive: true, ), ), posters: view.recentlyAdded, diff --git a/lib/screens/details_screens/folder_detail_screen.dart b/lib/screens/details_screens/folder_detail_screen.dart index a6b7e08..32b9511 100644 --- a/lib/screens/details_screens/folder_detail_screen.dart +++ b/lib/screens/details_screens/folder_detail_screen.dart @@ -35,7 +35,7 @@ class FolderDetailScreen extends ConsumerWidget { switch (item) { case PhotoModel photoModel: final photoItems = details?.items.whereType().toList(); - await context.navigateTo(PhotoViewerRoute( + await context.pushRoute(PhotoViewerRoute( items: photoItems, selected: photoModel.id, )); diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index 3ae7155..ea5634f 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -7,6 +7,7 @@ import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/providers/favourites_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/home_screen.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'; @@ -35,7 +36,7 @@ class FavouritesScreen extends ConsumerWidget { scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), - controller: AdaptiveLayout.scrollOf(context), + controller: AdaptiveLayout.scrollOf(context, HomeTabs.favorites), slivers: [ if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index f0aba63..cd8e4ce 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -44,7 +44,7 @@ enum HomeTabs { 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()), + HomeTabs.sync => context.router.navigate(const SyncedRoute()), }; String label(BuildContext context) => switch (this) { @@ -101,7 +101,7 @@ class HomeScreen extends ConsumerWidget { label: context.localized.navigationSync, icon: Icon(e.icon), selectedIcon: Icon(e.selectedIcon), - route: SyncedRoute(), + route: const SyncedRoute(), action: () => e.navigate(context), ); } diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index 0aff850..a2b2a38 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -11,6 +11,7 @@ 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/home_screen.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'; @@ -62,7 +63,7 @@ class _LibraryScreenState extends ConsumerState with SingleTicker onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), child: SizedBox.expand( child: CustomScrollView( - controller: AdaptiveLayout.scrollOf(context), + controller: AdaptiveLayout.scrollOf(context, HomeTabs.library), physics: const AlwaysScrollableScrollPhysics(), slivers: [ const DefaultSliverTopBadding(), diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart index 5dd9ec4..f73ccf9 100644 --- a/lib/screens/library_search/library_search_screen.dart +++ b/lib/screens/library_search/library_search_screen.dart @@ -8,7 +8,6 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; @@ -55,8 +54,7 @@ class LibrarySearchScreen extends ConsumerStatefulWidget { final SortingOptions? sortingOptions; final Map? types; final Map? genres; - final bool recursive; - final PhotoModel? photoToView; + final bool? recursive; const LibrarySearchScreen({ @QueryParam("parentId") this.viewModelId, @QueryParam("folderId") this.folderId, @@ -65,8 +63,7 @@ class LibrarySearchScreen extends ConsumerStatefulWidget { @QueryParam("sortOptions") this.sortingOptions, @QueryParam("itemTypes") this.types, @QueryParam("genres") this.genres, - @QueryParam("recursive") this.recursive = true, - this.photoToView, + @QueryParam("recursive") this.recursive, super.key, }); @@ -109,10 +106,6 @@ class _LibrarySearchScreenState extends ConsumerState { SystemUiMode.edgeToEdge, overlays: [], ); - - if (context.mounted && widget.photoToView != null) { - libraryProvider.viewGallery(context, selected: widget.photoToView); - } scrollController.addListener(() { scrollPosition(); }); @@ -227,7 +220,7 @@ class _LibrarySearchScreenState extends ConsumerState { widget.folderId, widget.viewModelId, defaultFilter.copyWith( - favourites: widget.favourites ?? defaultFilter.favourites, + favourites: widget.favourites, sortOrder: widget.sortOrder ?? defaultFilter.sortOrder, sortingOption: widget.sortingOptions ?? defaultFilter.sortingOption, types: widget.types ?? {}, diff --git a/lib/screens/library_search/widgets/library_filter_chips.dart b/lib/screens/library_search/widgets/library_filter_chips.dart index 43d497e..fbe6cc9 100644 --- a/lib/screens/library_search/widgets/library_filter_chips.dart +++ b/lib/screens/library_search/widgets/library_filter_chips.dart @@ -59,8 +59,8 @@ class _LibraryFilterChipsState extends ConsumerState { onClear: () => libraryProvider.setTypes(librarySearchResults.filters.types.setAll(false)), ), ExpressiveButton( - isSelected: favourites, - icon: favourites ? const Icon(IconsaxPlusBold.heart) : null, + isSelected: favourites == true, + icon: favourites == true ? const Icon(IconsaxPlusBold.heart) : null, label: Text(context.localized.favorites), onPressed: () { libraryProvider.toggleFavourite(); @@ -68,8 +68,8 @@ class _LibraryFilterChipsState extends ConsumerState { }, ), ExpressiveButton( - isSelected: recursive, - icon: recursive ? const Icon(IconsaxPlusBold.tick_circle) : null, + isSelected: recursive == true, + icon: recursive == true ? const Icon(IconsaxPlusBold.tick_circle) : null, label: Text(context.localized.recursive), onPressed: () { libraryProvider.toggleRecursive(); diff --git a/lib/screens/metadata/info_screen.dart b/lib/screens/metadata/info_screen.dart index b73761c..a5105cb 100644 --- a/lib/screens/metadata/info_screen.dart +++ b/lib/screens/metadata/info_screen.dart @@ -61,18 +61,23 @@ class ItemInfoScreenState extends ConsumerState { Container( color: Theme.of(context).colorScheme.surface, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( mainAxisSize: MainAxisSize.max, - spacing: 6, + spacing: 12, children: [ - Text( - widget.item.name, - style: Theme.of(context).textTheme.titleLarge, + Expanded( + child: Text( + widget.item.name, + softWrap: false, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, + ), ), - const Spacer(), IconButton( onPressed: () => context.copyToClipboard(info.model.toString()), icon: const Icon(Icons.copy_all_rounded)), diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart index 4578891..f1ec2df 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -23,6 +24,7 @@ import 'package:fladder/util/throttler.dart'; import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; +import 'package:fladder/widgets/shared/selectable_icon_button.dart'; class PhotoViewerControls extends ConsumerStatefulWidget { final EdgeInsets padding; @@ -309,13 +311,19 @@ class _PhotoViewerControllsState extends ConsumerState with children: [ ElevatedIconButton( onPressed: widget.openOptions, - icon: IconsaxPlusLinear.more_2, + icon: IconsaxPlusLinear.more_square, ), const Spacer(), - ElevatedIconButton( + SelectableIconButton( onPressed: markAsFavourite, - color: widget.photo.userData.isFavourite ? Colors.red : null, + selected: false, icon: widget.photo.userData.isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, + backgroundColor: Theme.of(context) + .colorScheme + .onPrimary + .harmonizeWith(Colors.red) + .withValues(alpha: 0.25), + iconColor: widget.photo.userData.isFavourite ? Colors.red : null, ), ProgressFloatingButton( controller: timerController, @@ -341,8 +349,11 @@ class _PhotoViewerControllsState extends ConsumerState with ); } - void markAsFavourite() { - ref.read(userProvider.notifier).setAsFavorite(!widget.photo.userData.isFavourite, widget.photo.id); + Future markAsFavourite() async { + final response = + await ref.read(userProvider.notifier).setAsFavorite(!widget.photo.userData.isFavourite, widget.photo.id); + + if (response?.isSuccessful == false) return; widget.onPhotoChanged(widget.photo .copyWith(userData: widget.photo.userData.copyWith(isFavourite: !widget.photo.userData.isFavourite))); diff --git a/lib/screens/photo_viewer/photo_viewer_screen.dart b/lib/screens/photo_viewer/photo_viewer_screen.dart index a07068a..0195835 100644 --- a/lib/screens/photo_viewer/photo_viewer_screen.dart +++ b/lib/screens/photo_viewer/photo_viewer_screen.dart @@ -90,7 +90,13 @@ class _PhotoViewerScreenState extends ConsumerState with Widg if (context.mounted) { setState(() { - photos = {...photos, ...newItems}.toList(); + if (photos.length == 1 && newItems.contains(photos.first)) { + photos = newItems; + currentPage = photos.indexWhere((value) => value.id == widget.selected).clamp(0, photos.length - 1); + controller.jumpToPage(currentPage); + } else { + photos = {...photos, ...newItems}.toList(); + } loadingItems = false; }); } @@ -165,14 +171,14 @@ class _PhotoViewerScreenState extends ConsumerState with Widg ? Center( child: Text(context.localized.noItemsToShow), ) - : buildViewer(), + : buildViewer(context), ), ), ), ); } - Widget buildViewer() { + Widget buildViewer(BuildContext context) { final currentPhoto = photos[currentPage]; final imageHash = currentPhoto.images?.primary?.hash; return Stack( @@ -498,8 +504,8 @@ class _PhotoViewerScreenState extends ConsumerState with Widg }, ); - void markAsFavourite(PhotoModel photo, {bool? value}) { - ref.read(userProvider.notifier).setAsFavorite(value ?? !photo.userData.isFavourite, photo.id); + Future markAsFavourite(PhotoModel photo, {bool? value}) async { + await ref.read(userProvider.notifier).setAsFavorite(value ?? !photo.userData.isFavourite, photo.id); setState(() { int index = photos.indexOf(photo); diff --git a/lib/screens/shared/media/components/item_logo.dart b/lib/screens/shared/media/components/item_logo.dart index 0b2b343..a32fe37 100644 --- a/lib/screens/shared/media/components/item_logo.dart +++ b/lib/screens/shared/media/components/item_logo.dart @@ -24,7 +24,7 @@ class ItemLogo extends StatelessWidget { child: Text( item.parentBaseModel.name, textAlign: TextAlign.start, - maxLines: 3, + maxLines: 2, overflow: TextOverflow.fade, style: textStyle ?? Theme.of(context).textTheme.headlineLarge?.copyWith( diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index ab7f847..e724df3 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -77,7 +77,7 @@ class _PosterImageState extends ConsumerState { } Future navigateToDetails() async { - await widget.poster.navigateTo(context); + await widget.poster.navigateTo(context, ref: ref); } final posterRadius = FladderTheme.smallShape.borderRadius; diff --git a/lib/screens/shared/media/poster_row.dart b/lib/screens/shared/media/poster_row.dart index a6f38f2..90b25a2 100644 --- a/lib/screens/shared/media/poster_row.dart +++ b/lib/screens/shared/media/poster_row.dart @@ -42,6 +42,7 @@ class _PosterRowState extends ConsumerState { contentPadding: widget.contentPadding, label: widget.label, onLabelClick: widget.onLabelClick, + dominantRatio: dominantRatio, items: widget.posters, itemBuilder: (context, index) { final poster = widget.posters[index]; diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index 0f08e2f..92596d6 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -8,6 +8,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/routes/auto_router.gr.dart'; +import 'package:fladder/screens/home_screen.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'; @@ -20,9 +21,7 @@ import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @RoutePage() class SyncedScreen extends ConsumerStatefulWidget { - final ScrollController? navigationScrollController; - - const SyncedScreen({this.navigationScrollController, super.key}); + const SyncedScreen({super.key}); @override ConsumerState createState() => _SyncedScreenState(); @@ -43,7 +42,7 @@ class _SyncedScreenState extends ConsumerState { scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), - controller: widget.navigationScrollController, + controller: AdaptiveLayout.scrollOf(context, HomeTabs.sync), slivers: [ if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( diff --git a/lib/util/adaptive_layout/adaptive_layout.dart b/lib/util/adaptive_layout/adaptive_layout.dart index 29fc6f0..2ed0751 100644 --- a/lib/util/adaptive_layout/adaptive_layout.dart +++ b/lib/util/adaptive_layout/adaptive_layout.dart @@ -5,6 +5,7 @@ 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/home_screen.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'; @@ -81,9 +82,9 @@ class AdaptiveLayout extends InheritedWidget { return result!.data.posterDefaults; } - static ScrollController scrollOf(BuildContext context) { + static ScrollController scrollOf(BuildContext context, HomeTabs tab) { final AdaptiveLayout? result = maybeOf(context); - return result!.data.controller; + return result?.data.controller[tab] ?? ScrollController(); } static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) { @@ -130,7 +131,10 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { late ViewSize viewSize = ViewSize.tablet; late LayoutMode layoutMode = LayoutMode.single; late TargetPlatform currentPlatform = defaultTargetPlatform; - late ScrollController controller = ScrollController(); + + final Map scrollControllers = { + for (var item in HomeTabs.values) item: ScrollController(), + }; @override void didChangeDependencies() { @@ -177,11 +181,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { 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 posterDefaults = const PosterDefaults(size: 350, ratio: 0.55); final currentLayout = widget.adaptiveLayout ?? AdaptiveLayoutModel( @@ -191,7 +191,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { platform: currentPlatform, isDesktop: isDesktop, sideBarWidth: 0, - controller: controller, + controller: scrollControllers, posterDefaults: posterDefaults, ); @@ -207,7 +207,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { inputDevice: input, platform: currentPlatform, isDesktop: isDesktop, - controller: controller, + controller: scrollControllers, posterDefaults: posterDefaults, ), child: Builder( diff --git a/lib/util/adaptive_layout/adaptive_layout_model.dart b/lib/util/adaptive_layout/adaptive_layout_model.dart index 28149a9..3c2e593 100644 --- a/lib/util/adaptive_layout/adaptive_layout_model.dart +++ b/lib/util/adaptive_layout/adaptive_layout_model.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:fladder/screens/home_screen.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/poster_defaults.dart'; @@ -46,7 +47,7 @@ class AdaptiveLayoutModel { final TargetPlatform platform; final bool isDesktop; final PosterDefaults posterDefaults; - final ScrollController controller; + final Map controller; final double sideBarWidth; const AdaptiveLayoutModel({ @@ -67,7 +68,7 @@ class AdaptiveLayoutModel { TargetPlatform? platform, bool? isDesktop, PosterDefaults? posterDefaults, - ScrollController? controller, + Map? controller, double? sideBarWidth, }) { return AdaptiveLayoutModel( diff --git a/lib/util/fladder_image.dart b/lib/util/fladder_image.dart index 23ed50c..94d30a0 100644 --- a/lib/util/fladder_image.dart +++ b/lib/util/fladder_image.dart @@ -36,6 +36,7 @@ class FladderImage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final useBluredPlaceHolder = ref.watch(clientSettingsProvider.select((value) => value.blurPlaceHolders)); final newImage = image; + final imageProvider = image?.imageProvider; if (newImage == null) { return placeHolder ?? Container(); } else { @@ -44,13 +45,15 @@ class FladderImage extends ConsumerWidget { fit: stackFit, children: [ if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly) - BlurHash( - hash: newImage.hash, - optimizationMode: BlurHashOptimizationMode.approximation, - color: Colors.transparent, - imageFit: blurFit ?? fit, + Image( + image: BlurHashImage( + newImage.hash, + decodingHeight: 24, + decodingWidth: 24, + ), + fit: blurFit ?? fit, ), - if (!blurOnly) + if (!blurOnly && imageProvider != null) FadeInImage( placeholder: MemoryImage(kTransparentImage), fit: fit, @@ -58,7 +61,7 @@ class FladderImage extends ConsumerWidget { excludeFromSemantics: true, alignment: alignment ?? Alignment.center, imageErrorBuilder: imageErrorBuilder, - image: newImage.imageProvider, + image: imageProvider, ) ], ); 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 2230ebd..a7bf1f8 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -8,7 +8,6 @@ import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/collections/add_to_collection.dart'; @@ -142,7 +141,7 @@ extension ItemBaseModelExtensions on ItemBaseModel { else if (!exclude.contains(ItemActions.showAlbum) && galleryItem) ItemActionButton( icon: Icon(FladderItemType.photoAlbum.icon), - action: () => (this as PhotoModel).navigateToAlbum(context), + action: () => parentBaseModel.navigateTo(context), label: Text(context.localized.showAlbum), ), if (!exclude.contains(ItemActions.playFromStart)) diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index 8de88c5..08c3233 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -171,7 +171,7 @@ extension PhotoAlbumExtension on PhotoAlbumModel? { return; } - await context.navigateTo(PhotoViewerRoute( + await context.pushRoute(PhotoViewerRoute( items: photos.toList(), )); diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index 74d2e8d..6a86fc1 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -7,6 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:overflow_view/overflow_view.dart'; import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -24,6 +25,10 @@ import 'package:fladder/widgets/shared/custom_tooltip.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; +const _fullScreenRoutes = { + PhotoViewerRoute.name, +}; + class SideNavigationBar extends ConsumerStatefulWidget { final int currentIndex; final List destinations; @@ -60,6 +65,8 @@ class _SideNavigationBarState extends ConsumerState { final shouldExpand = fullyExpanded; final isDesktop = AdaptiveLayout.of(context).isDesktop; + final fullScreenChildRoute = _fullScreenRoutes.contains(context.router.current.name); + return Stack( children: [ AdaptiveLayoutBuilder( @@ -69,231 +76,244 @@ class _SideNavigationBarState extends ConsumerState { ), child: (context) => widget.child, ), - Container( - color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), - width: shouldExpand ? expandedWidth : collapsedWidth, - child: MouseRegion( - child: Padding( - key: const Key('navigation_rail'), - padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null), - child: Column( - spacing: 2, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (expandedSideBar) ...[ - Expanded(child: Text(context.localized.navigation)), - ], - Opacity( - opacity: largeBar && expandedSideBar ? 0.65 : 1.0, - child: IconButton( - onPressed: !largeBar - ? () => widget.scaffoldKey.currentState?.openDrawer() - : () => setState(() => expandedSideBar = !expandedSideBar), - icon: Icon( - largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, - ), - ), - ) - ], - ), - ), - if (largeBar) ...[ - AnimatedFadeSize( - duration: const Duration(milliseconds: 250), - child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, - ), - ], - Expanded( - child: Column( - mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - ...widget.destinations.mapIndexed( - (index, destination) => CustomTooltip( - tooltipContent: expandedSideBar - ? null - : Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - destination.label, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - ), - position: TooltipPosition.right, - child: destination.toNavigationButton( - widget.currentIndex == index, - true, - shouldExpand, - ), - ), + IgnorePointer( + ignoring: fullScreenChildRoute, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + opacity: !fullScreenChildRoute ? 1 : 0, + child: Container( + color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), + width: shouldExpand ? expandedWidth : collapsedWidth, + child: MouseRegion( + child: Padding( + key: const Key('navigation_rail'), + padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null), + child: Column( + spacing: 2, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (expandedSideBar) ...[ + Expanded(child: Text(context.localized.navigation)), + ], + Opacity( + opacity: largeBar && expandedSideBar ? 0.65 : 1.0, + child: IconButton( + onPressed: !largeBar + ? () => widget.scaffoldKey.currentState?.openDrawer() + : () => setState(() => expandedSideBar = !expandedSideBar), + icon: Icon( + largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, + ), + ), + ) + ], ), - if (views.isNotEmpty && largeBar) ...[ - const Divider( - indent: 32, - endIndent: 32, - ), - Flexible( - child: OverflowView.flexible( - direction: Axis.vertical, - spacing: 4, - children: views.map( - (view) { - final selected = context.router.currentUrl.contains(view.id); - final actions = [ - ItemActionButton( - label: Text(context.localized.scanLibrary), - icon: const Icon(IconsaxPlusLinear.refresh), - action: () => showRefreshPopup(context, view.id, view.name), - ) - ]; - return CustomTooltip( - tooltipContent: expandedSideBar - ? null - : Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - view.name, - style: Theme.of(context).textTheme.titleSmall, - ), - ), + ), + if (largeBar) ...[ + AnimatedFadeSize( + duration: const Duration(milliseconds: 250), + child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, + ), + ], + Expanded( + child: Column( + mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + ...widget.destinations.mapIndexed( + (index, destination) => CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + destination.label, + style: Theme.of(context).textTheme.titleSmall, ), - position: TooltipPosition.right, - child: view.toNavigationButton( - selected, - 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), ), ), - customIcon: usePostersForLibrary - ? ClipRRect( - borderRadius: FladderTheme.smallShape.borderRadius, - child: SizedBox.square( - dimension: 50, - child: FladderImage( - image: view.imageData?.primary, - placeHolder: Card( - child: Icon( - selected - ? view.collectionType.icon - : view.collectionType.iconOutlined, - ), + position: TooltipPosition.right, + child: 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 selected = context.router.currentUrl.contains(view.id); + final actions = [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + view.name, + style: Theme.of(context).textTheme.titleSmall, ), ), ), - ) - : null, - trailing: actions, - ), - ); - }, - ).toList(), - builder: (context, remaining) { - return CustomTooltip( - tooltipContent: expandedSideBar - ? null - : Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - context.localized.moreOptions, - style: Theme.of(context).textTheme.titleSmall, + position: TooltipPosition.right, + child: view.toNavigationButton( + selected, + true, + shouldExpand, + () => context.pushRoute( + LibrarySearchRoute( + viewModelId: view.id, + ).withFilter(view.collectionType.defaultFilters), + ), + onLongPress: () => showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), ), ), + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 50, + child: FladderImage( + image: view.imageData?.primary, + placeHolder: Card( + child: Icon( + selected + ? view.collectionType.icon + : view.collectionType.iconOutlined, + ), + ), + ), + ), + ) + : null, + trailing: actions, ), - position: TooltipPosition.right, - child: PopupMenuButton( - iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), - padding: EdgeInsets.zero, - tooltip: "", - icon: NavigationButton( - label: context.localized.other, - selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), - icon: const Icon(IconsaxPlusLinear.arrow_square_down), - expanded: shouldExpand, - customIcon: usePostersForLibrary - ? ClipRRect( - borderRadius: FladderTheme.smallShape.borderRadius, - child: const SizedBox.square( - dimension: 50, - child: Card( - child: Icon(IconsaxPlusLinear.arrow_square_down), + ); + }, + ).toList(), + builder: (context, remaining) { + return CustomTooltip( + tooltipContent: expandedSideBar + ? null + : Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + context.localized.moreOptions, + style: Theme.of(context).textTheme.titleSmall, ), ), - ) - : null, - horizontal: true, - ), - itemBuilder: (context) => views - .sublist(views.length - remaining) - .map( - (e) => PopupMenuItem( - onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), - child: Row( - spacing: 8, - children: [ - usePostersForLibrary - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: ClipRRect( - borderRadius: FladderTheme.smallShape.borderRadius, - child: SizedBox.square( - dimension: 45, - child: FladderImage( - image: e.imageData?.primary, - placeHolder: Card( - child: Icon( - e.collectionType.iconOutlined, + ), + position: TooltipPosition.right, + child: PopupMenuButton( + iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), + padding: EdgeInsets.zero, + tooltip: "", + icon: NavigationButton( + label: context.localized.other, + selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), + icon: const Icon(IconsaxPlusLinear.arrow_square_down), + expanded: shouldExpand, + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: const SizedBox.square( + dimension: 50, + child: Card( + child: Icon(IconsaxPlusLinear.arrow_square_down), + ), + ), + ) + : null, + horizontal: true, + ), + itemBuilder: (context) => views + .sublist(views.length - remaining) + .map( + (e) => PopupMenuItem( + onTap: () => context.pushRoute(LibrarySearchRoute( + viewModelId: e.id, + ).withFilter(e.collectionType.defaultFilters)), + child: Row( + spacing: 8, + children: [ + usePostersForLibrary + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 45, + child: FladderImage( + image: e.imageData?.primary, + placeHolder: Card( + child: Icon( + e.collectionType.iconOutlined, + ), + ), ), ), ), - ), - ), - ) - : Icon(e.collectionType.iconOutlined), - Text(e.name), - ], - ), - ), - ) - .toList(), - ), - ); - }, - ), - ), - ], - ], - ), + ) + : 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 SettingsUserIcon(), + onPressed: () { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { + context.router.push(const SettingsRoute()); + } else { + context.router.push(const ClientSettingsRoute()); + } + }, + ), + ], ), - NavigationButton( - label: context.localized.settings, - selected: widget.currentLocation.contains(const SettingsRoute().routeName), - selectedIcon: const Icon(IconsaxPlusBold.setting_3), - horizontal: true, - expanded: shouldExpand, - icon: const SettingsUserIcon(), - onPressed: () { - if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { - context.router.push(const SettingsRoute()); - } else { - context.router.push(const ClientSettingsRoute()); - } - }, - ), - ], + ), ), ), ), diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index 87685bb..12c89df 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -8,6 +8,7 @@ import 'package:fladder/providers/connectivity_provider.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/home_screen.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/adaptive_layout.dart'; @@ -77,6 +78,9 @@ class _NavigationScaffoldState extends ConsumerState { final calculatedBottomViewPadding = showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding; + final currentTab = + HomeTabs.values.elementAtOrNull(currentIndex.clamp(0, HomeTabs.values.length - 1)) ?? HomeTabs.dashboard; + return PopScope( canPop: currentIndex == 0, onPopInvokedWithResult: (didPop, result) { @@ -124,7 +128,7 @@ class _NavigationScaffoldState extends ConsumerState { hiddenHeight: calculatedBottomViewPadding, duration: const Duration(milliseconds: 250), child: HideOnScroll( - controller: AdaptiveLayout.scrollOf(context), + controller: AdaptiveLayout.scrollOf(context, currentTab), forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), child: NestedBottomAppBar( child: SizedBox( diff --git a/lib/widgets/shared/hide_on_scroll.dart b/lib/widgets/shared/hide_on_scroll.dart index 76f8889..015ddc4 100644 --- a/lib/widgets/shared/hide_on_scroll.dart +++ b/lib/widgets/shared/hide_on_scroll.dart @@ -29,7 +29,7 @@ class HideOnScroll extends ConsumerStatefulWidget { } class _HideOnScrollState extends ConsumerState { - late final ScrollController scrollController = widget.controller ?? ScrollController(); + late ScrollController scrollController = widget.controller ?? ScrollController(); bool isVisible = true; @override @@ -47,6 +47,16 @@ class _HideOnScrollState extends ConsumerState { super.dispose(); } + @override + void didUpdateWidget(covariant HideOnScroll oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + scrollController.removeListener(_onScroll); + scrollController = widget.controller ?? ScrollController(); + scrollController.addListener(_onScroll); + } + } + void _onScroll() { if (!widget.canHide) { if (!isVisible) { diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 0130734..2661749 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +21,7 @@ class HorizontalList extends ConsumerStatefulWidget { final Widget Function(BuildContext context, int index) itemBuilder; final bool scrollToEnd; final EdgeInsets contentPadding; + final double? dominantRatio; final double? height; final bool shrinkWrap; const HorizontalList({ @@ -33,6 +36,7 @@ class HorizontalList extends ConsumerStatefulWidget { this.contentPadding = const EdgeInsets.symmetric(horizontal: 16), this.subtext, this.shrinkWrap = false, + this.dominantRatio, super.key, }); @@ -201,9 +205,11 @@ class _HorizontalListState extends ConsumerState { ), const SizedBox(height: 8), SizedBox( - height: (widget.height ?? - AdaptiveLayout.poster(context).size * - ref.watch(clientSettingsProvider.select((value) => value.posterSize))), + height: widget.height ?? + ((AdaptiveLayout.poster(context).size * + ref.watch(clientSettingsProvider.select((value) => value.posterSize))) / + pow((widget.dominantRatio ?? 1.0), 0.55)) * + 0.72, child: ListView.separated( controller: _scrollController, scrollDirection: Axis.horizontal, diff --git a/lib/widgets/shared/progress_floating_button.dart b/lib/widgets/shared/progress_floating_button.dart index 306a09c..77df98a 100644 --- a/lib/widgets/shared/progress_floating_button.dart +++ b/lib/widgets/shared/progress_floating_button.dart @@ -158,7 +158,7 @@ class _ProgressFloatingButtonState extends ConsumerState } : null, child: FloatingActionButton( - heroTag: null, + heroTag: "Progress_Floating_Button", onPressed: isActive ? timer.cancel : timer.play, child: Stack( fit: StackFit.expand, diff --git a/lib/widgets/shared/selectable_icon_button.dart b/lib/widgets/shared/selectable_icon_button.dart index bc99013..09635e7 100644 --- a/lib/widgets/shared/selectable_icon_button.dart +++ b/lib/widgets/shared/selectable_icon_button.dart @@ -14,12 +14,16 @@ class SelectableIconButton extends ConsumerStatefulWidget { final IconData icon; final IconData? selectedIcon; final bool selected; + final Color? backgroundColor; + final Color? iconColor; const SelectableIconButton({ required this.onPressed, required this.selected, required this.icon, this.selectedIcon, this.label, + this.backgroundColor, + this.iconColor, super.key, }); @@ -37,9 +41,14 @@ class _SelectableIconButtonState extends ConsumerState { message: widget.label ?? "", child: ElevatedButton( style: ButtonStyle( - backgroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.primary) : null, - iconColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : null, - foregroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : null, + elevation: WidgetStatePropertyAll( + widget.backgroundColor != null ? (widget.backgroundColor!.a < 1 ? 0 : null) : null), + backgroundColor: WidgetStatePropertyAll( + widget.backgroundColor ?? (widget.selected ? Theme.of(context).colorScheme.primary : null)), + iconColor: WidgetStatePropertyAll( + widget.iconColor ?? (widget.selected ? Theme.of(context).colorScheme.onPrimary : null)), + foregroundColor: WidgetStatePropertyAll( + widget.iconColor ?? (widget.selected ? Theme.of(context).colorScheme.onPrimary : null)), padding: const WidgetStatePropertyAll(EdgeInsets.zero), ), onPressed: loading