chore: Lots of bug fixes and navigation improvements

This commit is contained in:
PartyDonut 2025-09-01 20:21:36 +02:00
parent 9bb5e81812
commit 92d5391b93
35 changed files with 513 additions and 455 deletions

View file

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.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.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/library_filter_model.dart';
extension CollectionTypeExtension on CollectionType { extension CollectionTypeExtension on CollectionType {
IconData get iconOutlined { 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) { IconData getIconType(bool outlined) {
switch (this) { switch (this) {
case CollectionType.music: 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) { double? get aspectRatio => switch (this) {
CollectionType.music || CollectionType.music ||
CollectionType.homevideos || CollectionType.homevideos ||

View file

@ -22,12 +22,14 @@ import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/playlist_model.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/routes/auto_router.gr.dart';
import 'package:fladder/screens/details_screens/book_detail_screen.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/details_screens.dart';
import 'package:fladder/screens/details_screens/episode_detail_screen.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/details_screens/season_detail_screen.dart';
import 'package:fladder/screens/library_search/library_search_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/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/util/string_extensions.dart';
@ -140,15 +142,14 @@ class ItemBaseModel with ItemBaseModelMappable {
case SeasonModel _: case SeasonModel _:
return SeasonDetailScreen(item: this); return SeasonDetailScreen(item: this);
case FolderModel _: case FolderModel _:
case PhotoAlbumModel _:
case BoxSetModel _: case BoxSetModel _:
case PlaylistModel _: case PlaylistModel _:
case PhotoAlbumModel _:
return LibrarySearchScreen(folderId: [id]); return LibrarySearchScreen(folderId: [id]);
case PhotoModel _: case PhotoModel _:
final photo = this as PhotoModel; final photo = this as PhotoModel;
return LibrarySearchScreen( return PhotoViewerScreen(
folderId: [photo.albumId ?? photo.parentId ?? ""], items: [photo],
photoToView: photo,
); );
case BookModel book: case BookModel book:
return BookDetailScreen(item: book); return BookDetailScreen(item: book);
@ -163,7 +164,37 @@ class ItemBaseModel with ItemBaseModelMappable {
} }
} }
Future<void> navigateTo(BuildContext context) async => context.router.push(DetailsRoute(id: id, item: this)); Future<void> 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) { factory ItemBaseModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return switch (item.type) { return switch (item.type) {

View file

@ -50,9 +50,9 @@ abstract class LibraryFilterModel with _$LibraryFilterModel {
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
@Default(SortingOptions.sortName) SortingOptions sortingOption, @Default(SortingOptions.sortName) SortingOptions sortingOption,
@Default(SortingOrder.ascending) SortingOrder sortOrder, @Default(SortingOrder.ascending) SortingOrder sortOrder,
@Default(false) bool favourites, @Default(false) bool? favourites,
@Default(true) bool hideEmptyShows, @Default(true) bool hideEmptyShows,
@Default(true) bool recursive, @Default(true) bool? recursive,
@Default(GroupBy.none) GroupBy groupBy, @Default(GroupBy.none) GroupBy groupBy,
}) = _LibraryFilterModel; }) = _LibraryFilterModel;
@ -64,8 +64,8 @@ abstract class LibraryFilterModel with _$LibraryFilterModel {
officialRatings.hasEnabled || officialRatings.hasEnabled ||
hideEmptyShows || hideEmptyShows ||
itemFilters.hasEnabled || itemFilters.hasEnabled ||
!recursive || recursive == false ||
favourites; favourites == true;
} }
LibraryFilterModel loadModel(LibraryFilterModel model) { LibraryFilterModel loadModel(LibraryFilterModel model) {

View file

@ -24,9 +24,9 @@ mixin _$LibraryFilterModel implements DiagnosticableTreeMixin {
Map<FladderItemType, bool> get types; Map<FladderItemType, bool> get types;
SortingOptions get sortingOption; SortingOptions get sortingOption;
SortingOrder get sortOrder; SortingOrder get sortOrder;
bool get favourites; bool? get favourites;
bool get hideEmptyShows; bool get hideEmptyShows;
bool get recursive; bool? get recursive;
GroupBy get groupBy; GroupBy get groupBy;
/// Create a copy of LibraryFilterModel /// Create a copy of LibraryFilterModel
@ -81,9 +81,9 @@ abstract mixin class $LibraryFilterModelCopyWith<$Res> {
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
SortingOptions sortingOption, SortingOptions sortingOption,
SortingOrder sortOrder, SortingOrder sortOrder,
bool favourites, bool? favourites,
bool hideEmptyShows, bool hideEmptyShows,
bool recursive, bool? recursive,
GroupBy groupBy}); GroupBy groupBy});
} }
@ -109,9 +109,9 @@ class _$LibraryFilterModelCopyWithImpl<$Res>
Object? types = null, Object? types = null,
Object? sortingOption = null, Object? sortingOption = null,
Object? sortOrder = null, Object? sortOrder = null,
Object? favourites = null, Object? favourites = freezed,
Object? hideEmptyShows = null, Object? hideEmptyShows = null,
Object? recursive = null, Object? recursive = freezed,
Object? groupBy = null, Object? groupBy = null,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
@ -151,18 +151,18 @@ class _$LibraryFilterModelCopyWithImpl<$Res>
? _self.sortOrder ? _self.sortOrder
: sortOrder // ignore: cast_nullable_to_non_nullable : sortOrder // ignore: cast_nullable_to_non_nullable
as SortingOrder, as SortingOrder,
favourites: null == favourites favourites: freezed == favourites
? _self.favourites ? _self.favourites
: favourites // ignore: cast_nullable_to_non_nullable : favourites // ignore: cast_nullable_to_non_nullable
as bool, as bool?,
hideEmptyShows: null == hideEmptyShows hideEmptyShows: null == hideEmptyShows
? _self.hideEmptyShows ? _self.hideEmptyShows
: hideEmptyShows // ignore: cast_nullable_to_non_nullable : hideEmptyShows // ignore: cast_nullable_to_non_nullable
as bool, as bool,
recursive: null == recursive recursive: freezed == recursive
? _self.recursive ? _self.recursive
: recursive // ignore: cast_nullable_to_non_nullable : recursive // ignore: cast_nullable_to_non_nullable
as bool, as bool?,
groupBy: null == groupBy groupBy: null == groupBy
? _self.groupBy ? _self.groupBy
: groupBy // ignore: cast_nullable_to_non_nullable : groupBy // ignore: cast_nullable_to_non_nullable
@ -274,9 +274,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel {
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
SortingOptions sortingOption, SortingOptions sortingOption,
SortingOrder sortOrder, SortingOrder sortOrder,
bool favourites, bool? favourites,
bool hideEmptyShows, bool hideEmptyShows,
bool recursive, bool? recursive,
GroupBy groupBy)? GroupBy groupBy)?
$default, { $default, {
required TResult orElse(), required TResult orElse(),
@ -328,9 +328,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel {
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
SortingOptions sortingOption, SortingOptions sortingOption,
SortingOrder sortOrder, SortingOrder sortOrder,
bool favourites, bool? favourites,
bool hideEmptyShows, bool hideEmptyShows,
bool recursive, bool? recursive,
GroupBy groupBy) GroupBy groupBy)
$default, $default,
) { ) {
@ -380,9 +380,9 @@ extension LibraryFilterModelPatterns on LibraryFilterModel {
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
SortingOptions sortingOption, SortingOptions sortingOption,
SortingOrder sortOrder, SortingOrder sortOrder,
bool favourites, bool? favourites,
bool hideEmptyShows, bool hideEmptyShows,
bool recursive, bool? recursive,
GroupBy groupBy)? GroupBy groupBy)?
$default, $default,
) { ) {
@ -529,13 +529,13 @@ class _LibraryFilterModel extends LibraryFilterModel
final SortingOrder sortOrder; final SortingOrder sortOrder;
@override @override
@JsonKey() @JsonKey()
final bool favourites; final bool? favourites;
@override @override
@JsonKey() @JsonKey()
final bool hideEmptyShows; final bool hideEmptyShows;
@override @override
@JsonKey() @JsonKey()
final bool recursive; final bool? recursive;
@override @override
@JsonKey() @JsonKey()
final GroupBy groupBy; final GroupBy groupBy;
@ -598,9 +598,9 @@ abstract mixin class _$LibraryFilterModelCopyWith<$Res>
Map<FladderItemType, bool> types, Map<FladderItemType, bool> types,
SortingOptions sortingOption, SortingOptions sortingOption,
SortingOrder sortOrder, SortingOrder sortOrder,
bool favourites, bool? favourites,
bool hideEmptyShows, bool hideEmptyShows,
bool recursive, bool? recursive,
GroupBy groupBy}); GroupBy groupBy});
} }
@ -626,9 +626,9 @@ class __$LibraryFilterModelCopyWithImpl<$Res>
Object? types = null, Object? types = null,
Object? sortingOption = null, Object? sortingOption = null,
Object? sortOrder = null, Object? sortOrder = null,
Object? favourites = null, Object? favourites = freezed,
Object? hideEmptyShows = null, Object? hideEmptyShows = null,
Object? recursive = null, Object? recursive = freezed,
Object? groupBy = null, Object? groupBy = null,
}) { }) {
return _then(_LibraryFilterModel( return _then(_LibraryFilterModel(
@ -668,18 +668,18 @@ class __$LibraryFilterModelCopyWithImpl<$Res>
? _self.sortOrder ? _self.sortOrder
: sortOrder // ignore: cast_nullable_to_non_nullable : sortOrder // ignore: cast_nullable_to_non_nullable
as SortingOrder, as SortingOrder,
favourites: null == favourites favourites: freezed == favourites
? _self.favourites ? _self.favourites
: favourites // ignore: cast_nullable_to_non_nullable : favourites // ignore: cast_nullable_to_non_nullable
as bool, as bool?,
hideEmptyShows: null == hideEmptyShows hideEmptyShows: null == hideEmptyShows
? _self.hideEmptyShows ? _self.hideEmptyShows
: hideEmptyShows // ignore: cast_nullable_to_non_nullable : hideEmptyShows // ignore: cast_nullable_to_non_nullable
as bool, as bool,
recursive: null == recursive recursive: freezed == recursive
? _self.recursive ? _self.recursive
: recursive // ignore: cast_nullable_to_non_nullable : recursive // ignore: cast_nullable_to_non_nullable
as bool, as bool?,
groupBy: null == groupBy groupBy: null == groupBy
? _self.groupBy ? _self.groupBy
: groupBy // ignore: cast_nullable_to_non_nullable : groupBy // ignore: cast_nullable_to_non_nullable

View file

@ -6,7 +6,7 @@ part of 'library_screen_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c'; String _$libraryScreenHash() => r'792c4e47e5cd03635f42a4da4e24698c7584bbdb';
/// See also [LibraryScreen]. /// See also [LibraryScreen].
@ProviderFor(LibraryScreen) @ProviderFor(LibraryScreen)

View file

@ -70,6 +70,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
await loadViews(viewModelId, filters); await loadViews(viewModelId, filters);
} }
} }
await loadFilters(); await loadFilters();
if (!wasInitialized) { if (!wasInitialized) {
@ -78,6 +79,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
filters: state.filters.copyWith( filters: state.filters.copyWith(
types: state.filters.types.replaceMap(filters.types, enabledOnly: true), types: state.filters.types.replaceMap(filters.types, enabledOnly: true),
genres: state.filters.genres.replaceMap(filters.genres, enabledOnly: true), genres: state.filters.genres.replaceMap(filters.genres, enabledOnly: true),
recursive: filters.recursive,
), ),
); );
} }
@ -158,7 +160,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
} else if (state.views.hasEnabled) { } else if (state.views.hasEnabled) {
await handleViewLoading(); await handleViewLoading();
} else { } else {
if (state.searchQuery.isEmpty && !state.filters.favourites) { if (state.searchQuery.isEmpty && state.filters.favourites == false) {
state = state.copyWith(posters: []); state = state.copyWith(posters: []);
} else { } else {
final response = await _loadLibrary(recursive: true); final response = await _loadLibrary(recursive: true);
@ -236,7 +238,6 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
genres: {for (var element in genres) element.name: false}.replaceMap(tempFilters.genres), genres: {for (var element in genres) element.name: false}.replaceMap(tempFilters.genres),
studios: {for (var element in studios) element: false}.replaceMap(tempFilters.studios), studios: {for (var element in studios) element: false}.replaceMap(tempFilters.studios),
tags: {for (var element in tags) element: false}.replaceMap(tempFilters.tags), tags: {for (var element in tags) element: false}.replaceMap(tempFilters.tags),
recursive: state.views.included.firstOrNull?.collectionType.searchRecursive ?? true,
), ),
); );
state = tempState; state = tempState;
@ -295,7 +296,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
}.toList(), }.toList(),
filters: [ filters: [
...state.filters.itemFilters.included, ...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(), includeItemTypes: state.filters.types.included.map((e) => e.dtoKind).toList(),
); );
@ -353,9 +354,9 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
} }
void toggleFavourite() => 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() => 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) => void toggleType(FladderItemType type) =>
state = state.copyWith(filters: state.filters.copyWith(types: state.filters.types.toggleKey(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)); void toggleView(ViewModel view) => state = state.copyWith(views: state.views.toggleKey(view));
@ -569,7 +570,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
} else if (state.views.hasEnabled) { } else if (state.views.hasEnabled) {
await handleViewLoading(); await handleViewLoading();
} else { } else {
if (state.searchQuery.isEmpty && !state.filters.favourites) { if (state.searchQuery.isEmpty && state.filters.favourites == false) {
itemsToPlay = []; itemsToPlay = [];
} else { } else {
final response = await _loadLibrary(recursive: true, shuffle: shuffle); final response = await _loadLibrary(recursive: true, shuffle: shuffle);
@ -612,7 +613,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
List<PhotoModel> albumItems = []; List<PhotoModel> albumItems = [];
if (!state.filters.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) && if (!state.filters.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) &&
state.filters.recursive) { state.filters.recursive == true) {
for (var album in itemsToPlay.where( for (var album in itemsToPlay.where(
(element) => element is PhotoAlbumModel || element is FolderModel, (element) => element is PhotoAlbumModel || element is FolderModel,
)) { )) {
@ -634,7 +635,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
}.toList(), }.toList(),
filters: [ filters: [
...state.filters.itemFilters.included, ...state.filters.itemFilters.included,
if (state.filters.favourites) ItemFilter.isfavorite, if (state.filters.favourites == true) ItemFilter.isfavorite,
], ],
sortBy: shuffle ? [ItemSortBy.random] : null, sortBy: shuffle ? [ItemSortBy.random] : null,
); );
@ -665,7 +666,7 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
allItems = await showLoadingOverlay(context, callBack: fetchGallery(shuffle: shuffle)); allItems = await showLoadingOverlay(context, callBack: fetchGallery(shuffle: shuffle));
if (allItems.isNotEmpty) { if (allItems.isNotEmpty) {
final newItemList = shuffle ? allItems.shuffled() : allItems; final newItemList = shuffle ? allItems.shuffled() : allItems;
await context.navigateTo(PhotoViewerRoute( await context.pushRoute(PhotoViewerRoute(
items: newItemList, items: newItemList,
selected: selected?.id, selected: selected?.id,
)); ));

View file

@ -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/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.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/models/items/trick_play_model.dart';
import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/providers/image_provider.dart'; import 'package:fladder/providers/image_provider.dart';
@ -344,6 +345,20 @@ class JellyService {
); );
} }
Future<List<PhotoModel>> itemsGetAlbumPhotos({
String? albumId,
}) async {
final response = await itemsGet(
parentId: albumId,
enableUserData: true,
fields: [
ItemFields.parentid,
ItemFields.datecreated,
],
);
return response.body?.items.whereType<PhotoModel>().toList() ?? [];
}
Future<Response<List<ItemBaseModel>>> personsGet({ Future<Response<List<ItemBaseModel>>> personsGet({
String? searchTerm, String? searchTerm,
int? limit, int? limit,

View file

@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
// ************************************************************************** // **************************************************************************
String _$backgroundDownloaderHash() => String _$backgroundDownloaderHash() =>
r'9d866549ed7632e855ba30de2765368960889cff'; r'2571c078d018150bf93c6f35735f58f16cbca3dd';
/// See also [BackgroundDownloader]. /// See also [BackgroundDownloader].
@ProviderFor(BackgroundDownloader) @ProviderFor(BackgroundDownloader)

View file

@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -21,14 +19,7 @@ class AutoRouter extends RootStackRouter {
List<AutoRouteGuard> get guards => [...super.guards, AuthGuard(ref: ref)]; List<AutoRouteGuard> get guards => [...super.guards, AuthGuard(ref: ref)];
@override @override
RouteType get defaultRouteType => kIsWeb || RouteType get defaultRouteType => const RouteType.adaptive();
{
TargetPlatform.windows,
TargetPlatform.linux,
TargetPlatform.macOS,
}.contains(defaultTargetPlatform)
? const RouteType.cupertino()
: const RouteType.adaptive();
@override @override
List<AutoRoute> get routes => [ List<AutoRoute> get routes => [
@ -40,26 +31,43 @@ class AutoRouter extends RootStackRouter {
_homeRoute.copyWith( _homeRoute.copyWith(
children: [ children: [
...homeRoutes, ...homeRoutes,
AutoRoute(page: DetailsRoute.page, path: 'details', usesPathAsKey: true), ...detailsRoutes,
AutoRoute(page: LibrarySearchRoute.page, path: 'library', usesPathAsKey: true), AutoRoute(
CustomRoute(
page: SettingsRoute.page, page: SettingsRoute.page,
path: settingsPageRoute, path: settingsPageRoute,
children: _settingsChildren, children: _settingsChildren,
transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
], ],
), ),
AutoRoute(page: LockRoute.page, path: '/locked'), AutoRoute(page: LockRoute.page, path: '/locked'),
AutoRoute(page: PhotoViewerRoute.page, path: "/album"),
]; ];
} }
final AutoRoute _homeRoute = AutoRoute(page: HomeRoute.page, path: '/');
final List<AutoRoute> homeRoutes = [ final List<AutoRoute> homeRoutes = [
_dashboardRoute, AutoRoute(
_favouritesRoute, page: DashboardRoute.page,
_syncedRoute, initial: true,
_librariesRoute, path: 'dashboard',
),
AutoRoute(
page: FavouritesRoute.page,
path: 'favourites',
),
AutoRoute(
page: SyncedRoute.page,
path: 'synced',
),
AutoRoute(
page: LibraryRoute.page,
path: 'libraries',
),
];
final List<AutoRoute> detailsRoutes = [
AutoRoute(page: DetailsRoute.page, path: 'details'),
AutoRoute(page: PhotoViewerRoute.page, path: "album"),
AutoRoute(page: LibrarySearchRoute.page, path: 'library'),
]; ];
final List<AutoRoute> _defaultRoutes = [ final List<AutoRoute> _defaultRoutes = [
@ -67,40 +75,12 @@ final List<AutoRoute> _defaultRoutes = [
AutoRoute(page: LoginRoute.page, path: '/login'), 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<AutoRoute> _settingsChildren = [ final List<AutoRoute> _settingsChildren = [
CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'), AutoRoute(page: SettingsSelectionRoute.page, path: 'list'),
CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), AutoRoute(page: ClientSettingsRoute.page, path: 'client'),
CustomRoute(page: SecuritySettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'security'), AutoRoute(page: SecuritySettingsRoute.page, path: 'security'),
CustomRoute(page: PlayerSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'player'), AutoRoute(page: PlayerSettingsRoute.page, path: 'player'),
CustomRoute(page: AboutSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'about'), AutoRoute(page: AboutSettingsRoute.page, path: 'about'),
]; ];
class LockScreenGuard extends AutoRouteGuard { class LockScreenGuard extends AutoRouteGuard {

View file

@ -12,9 +12,9 @@
import 'dart:async' as _i24; import 'dart:async' as _i24;
import 'package:auto_route/auto_route.dart' as _i18; 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/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' import 'package:fladder/models/library_search/library_search_options.dart'
as _i21; as _i21;
import 'package:fladder/routes/nested_details_screen.dart' as _i4; import 'package:fladder/routes/nested_details_screen.dart' as _i4;
@ -200,8 +200,7 @@ class LibrarySearchRoute extends _i18.PageRouteInfo<LibrarySearchRouteArgs> {
_i21.SortingOptions? sortingOptions, _i21.SortingOptions? sortingOptions,
Map<_i19.FladderItemType, bool>? types, Map<_i19.FladderItemType, bool>? types,
Map<String, bool>? genres, Map<String, bool>? genres,
bool recursive = true, bool? recursive,
_i22.PhotoModel? photoToView,
_i20.Key? key, _i20.Key? key,
List<_i18.PageRouteInfo>? children, List<_i18.PageRouteInfo>? children,
}) : super( }) : super(
@ -215,7 +214,6 @@ class LibrarySearchRoute extends _i18.PageRouteInfo<LibrarySearchRouteArgs> {
types: types, types: types,
genres: genres, genres: genres,
recursive: recursive, recursive: recursive,
photoToView: photoToView,
key: key, key: key,
), ),
rawQueryParams: { rawQueryParams: {
@ -246,7 +244,7 @@ class LibrarySearchRoute extends _i18.PageRouteInfo<LibrarySearchRouteArgs> {
sortingOptions: queryParams.get('sortOptions'), sortingOptions: queryParams.get('sortOptions'),
types: queryParams.get('itemTypes'), types: queryParams.get('itemTypes'),
genres: queryParams.get('genres'), genres: queryParams.get('genres'),
recursive: queryParams.getBool('recursive', true), recursive: queryParams.optBool('recursive'),
), ),
); );
return _i8.LibrarySearchScreen( return _i8.LibrarySearchScreen(
@ -258,7 +256,6 @@ class LibrarySearchRoute extends _i18.PageRouteInfo<LibrarySearchRouteArgs> {
types: args.types, types: args.types,
genres: args.genres, genres: args.genres,
recursive: args.recursive, recursive: args.recursive,
photoToView: args.photoToView,
key: args.key, key: args.key,
); );
}, },
@ -274,8 +271,7 @@ class LibrarySearchRouteArgs {
this.sortingOptions, this.sortingOptions,
this.types, this.types,
this.genres, this.genres,
this.recursive = true, this.recursive,
this.photoToView,
this.key, this.key,
}); });
@ -293,15 +289,13 @@ class LibrarySearchRouteArgs {
final Map<String, bool>? genres; final Map<String, bool>? genres;
final bool recursive; final bool? recursive;
final _i22.PhotoModel? photoToView;
final _i20.Key? key; final _i20.Key? key;
@override @override
String toString() { 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 @override
@ -309,28 +303,26 @@ class LibrarySearchRouteArgs {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! LibrarySearchRouteArgs) return false; if (other is! LibrarySearchRouteArgs) return false;
return viewModelId == other.viewModelId && return viewModelId == other.viewModelId &&
const _i23.ListEquality().equals(folderId, other.folderId) && const _i22.ListEquality().equals(folderId, other.folderId) &&
favourites == other.favourites && favourites == other.favourites &&
sortOrder == other.sortOrder && sortOrder == other.sortOrder &&
sortingOptions == other.sortingOptions && sortingOptions == other.sortingOptions &&
const _i23.MapEquality().equals(types, other.types) && const _i22.MapEquality().equals(types, other.types) &&
const _i23.MapEquality().equals(genres, other.genres) && const _i22.MapEquality().equals(genres, other.genres) &&
recursive == other.recursive && recursive == other.recursive &&
photoToView == other.photoToView &&
key == other.key; key == other.key;
} }
@override @override
int get hashCode => int get hashCode =>
viewModelId.hashCode ^ viewModelId.hashCode ^
const _i23.ListEquality().hash(folderId) ^ const _i22.ListEquality().hash(folderId) ^
favourites.hashCode ^ favourites.hashCode ^
sortOrder.hashCode ^ sortOrder.hashCode ^
sortingOptions.hashCode ^ sortingOptions.hashCode ^
const _i23.MapEquality().hash(types) ^ const _i22.MapEquality().hash(types) ^
const _i23.MapEquality().hash(genres) ^ const _i22.MapEquality().hash(genres) ^
recursive.hashCode ^ recursive.hashCode ^
photoToView.hashCode ^
key.hashCode; key.hashCode;
} }
@ -370,9 +362,9 @@ class LoginRoute extends _i18.PageRouteInfo<void> {
/// [_i11.PhotoViewerScreen] /// [_i11.PhotoViewerScreen]
class PhotoViewerRoute extends _i18.PageRouteInfo<PhotoViewerRouteArgs> { class PhotoViewerRoute extends _i18.PageRouteInfo<PhotoViewerRouteArgs> {
PhotoViewerRoute({ PhotoViewerRoute({
List<_i22.PhotoModel>? items, List<_i23.PhotoModel>? items,
String? selected, String? selected,
_i24.Future<List<_i22.PhotoModel>>? loadingItems, _i24.Future<List<_i23.PhotoModel>>? loadingItems,
_i25.Key? key, _i25.Key? key,
List<_i18.PageRouteInfo>? children, List<_i18.PageRouteInfo>? children,
}) : super( }) : super(
@ -415,11 +407,11 @@ class PhotoViewerRouteArgs {
this.key, this.key,
}); });
final List<_i22.PhotoModel>? items; final List<_i23.PhotoModel>? items;
final String? selected; final String? selected;
final _i24.Future<List<_i22.PhotoModel>>? loadingItems; final _i24.Future<List<_i23.PhotoModel>>? loadingItems;
final _i25.Key? key; final _i25.Key? key;
@ -432,7 +424,7 @@ class PhotoViewerRouteArgs {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! PhotoViewerRouteArgs) return false; 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 && selected == other.selected &&
loadingItems == other.loadingItems && loadingItems == other.loadingItems &&
key == other.key; key == other.key;
@ -440,7 +432,7 @@ class PhotoViewerRouteArgs {
@override @override
int get hashCode => int get hashCode =>
const _i23.ListEquality().hash(items) ^ const _i22.ListEquality().hash(items) ^
selected.hashCode ^ selected.hashCode ^
loadingItems.hashCode ^ loadingItems.hashCode ^
key.hashCode; key.hashCode;
@ -561,56 +553,16 @@ class SplashRouteArgs {
/// generated route for /// generated route for
/// [_i17.SyncedScreen] /// [_i17.SyncedScreen]
class SyncedRoute extends _i18.PageRouteInfo<SyncedRouteArgs> { class SyncedRoute extends _i18.PageRouteInfo<void> {
SyncedRoute({ const SyncedRoute({List<_i18.PageRouteInfo>? children})
_i25.ScrollController? navigationScrollController, : super(SyncedRoute.name, initialChildren: children);
_i20.Key? key,
List<_i18.PageRouteInfo>? children,
}) : super(
SyncedRoute.name,
args: SyncedRouteArgs(
navigationScrollController: navigationScrollController,
key: key,
),
initialChildren: children,
);
static const String name = 'SyncedRoute'; static const String name = 'SyncedRoute';
static _i18.PageInfo page = _i18.PageInfo( static _i18.PageInfo page = _i18.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<SyncedRouteArgs>( return const _i17.SyncedScreen();
orElse: () => const SyncedRouteArgs(),
);
return _i17.SyncedScreen(
navigationScrollController: args.navigationScrollController,
key: args.key,
);
}, },
); );
} }
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;
}

View file

@ -18,6 +18,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/dashboard/home_banner_widget.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/media/poster_row.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
@ -97,7 +98,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: PinchPosterZoom( child: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference), scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: CustomScrollView( child: CustomScrollView(
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context, HomeTabs.dashboard),
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
const DefaultSliverTopBadding(), const DefaultSliverTopBadding(),
@ -196,6 +197,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
_ => SortingOptions.dateAdded, _ => SortingOptions.dateAdded,
}, },
sortOrder: SortingOrder.descending, sortOrder: SortingOrder.descending,
recursive: true,
), ),
), ),
posters: view.recentlyAdded, posters: view.recentlyAdded,

View file

@ -35,7 +35,7 @@ class FolderDetailScreen extends ConsumerWidget {
switch (item) { switch (item) {
case PhotoModel photoModel: case PhotoModel photoModel:
final photoItems = details?.items.whereType<PhotoModel>().toList(); final photoItems = details?.items.whereType<PhotoModel>().toList();
await context.navigateTo(PhotoViewerRoute( await context.pushRoute(PhotoViewerRoute(
items: photoItems, items: photoItems,
selected: photoModel.id, selected: photoModel.id,
)); ));

View file

@ -7,6 +7,7 @@ import 'package:fladder/models/library_filter_model.dart';
import 'package:fladder/providers/favourites_provider.dart'; import 'package:fladder/providers/favourites_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/routes/auto_router.gr.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/media/poster_row.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
@ -35,7 +36,7 @@ class FavouritesScreen extends ConsumerWidget {
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: CustomScrollView( child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context, HomeTabs.favorites),
slivers: [ slivers: [
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar( NestedSliverAppBar(

View file

@ -44,7 +44,7 @@ enum HomeTabs {
HomeTabs.dashboard => context.router.navigate(const DashboardRoute()), HomeTabs.dashboard => context.router.navigate(const DashboardRoute()),
HomeTabs.library => context.router.navigate(const LibraryRoute()), HomeTabs.library => context.router.navigate(const LibraryRoute()),
HomeTabs.favorites => context.router.navigate(const FavouritesRoute()), 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) { String label(BuildContext context) => switch (this) {
@ -101,7 +101,7 @@ class HomeScreen extends ConsumerWidget {
label: context.localized.navigationSync, label: context.localized.navigationSync,
icon: Icon(e.icon), icon: Icon(e.icon),
selectedIcon: Icon(e.selectedIcon), selectedIcon: Icon(e.selectedIcon),
route: SyncedRoute(), route: const SyncedRoute(),
action: () => e.navigate(context), action: () => e.navigate(context),
); );
} }

View file

@ -11,6 +11,7 @@ import 'package:fladder/models/recommended_model.dart';
import 'package:fladder/models/view_model.dart'; import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/library_screen_provider.dart'; import 'package:fladder/providers/library_screen_provider.dart';
import 'package:fladder/routes/auto_router.gr.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/metadata/refresh_metadata.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart';
@ -62,7 +63,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTicker
onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
child: SizedBox.expand( child: SizedBox.expand(
child: CustomScrollView( child: CustomScrollView(
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context, HomeTabs.library),
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
const DefaultSliverTopBadding(), const DefaultSliverTopBadding(),

View file

@ -8,7 +8,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/boxset_model.dart';
import 'package:fladder/models/item_base_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_filter_model.dart';
import 'package:fladder/models/library_search/library_search_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/library_search/library_search_options.dart';
@ -55,8 +54,7 @@ class LibrarySearchScreen extends ConsumerStatefulWidget {
final SortingOptions? sortingOptions; final SortingOptions? sortingOptions;
final Map<FladderItemType, bool>? types; final Map<FladderItemType, bool>? types;
final Map<String, bool>? genres; final Map<String, bool>? genres;
final bool recursive; final bool? recursive;
final PhotoModel? photoToView;
const LibrarySearchScreen({ const LibrarySearchScreen({
@QueryParam("parentId") this.viewModelId, @QueryParam("parentId") this.viewModelId,
@QueryParam("folderId") this.folderId, @QueryParam("folderId") this.folderId,
@ -65,8 +63,7 @@ class LibrarySearchScreen extends ConsumerStatefulWidget {
@QueryParam("sortOptions") this.sortingOptions, @QueryParam("sortOptions") this.sortingOptions,
@QueryParam("itemTypes") this.types, @QueryParam("itemTypes") this.types,
@QueryParam("genres") this.genres, @QueryParam("genres") this.genres,
@QueryParam("recursive") this.recursive = true, @QueryParam("recursive") this.recursive,
this.photoToView,
super.key, super.key,
}); });
@ -109,10 +106,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
SystemUiMode.edgeToEdge, SystemUiMode.edgeToEdge,
overlays: [], overlays: [],
); );
if (context.mounted && widget.photoToView != null) {
libraryProvider.viewGallery(context, selected: widget.photoToView);
}
scrollController.addListener(() { scrollController.addListener(() {
scrollPosition(); scrollPosition();
}); });
@ -227,7 +220,7 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
widget.folderId, widget.folderId,
widget.viewModelId, widget.viewModelId,
defaultFilter.copyWith( defaultFilter.copyWith(
favourites: widget.favourites ?? defaultFilter.favourites, favourites: widget.favourites,
sortOrder: widget.sortOrder ?? defaultFilter.sortOrder, sortOrder: widget.sortOrder ?? defaultFilter.sortOrder,
sortingOption: widget.sortingOptions ?? defaultFilter.sortingOption, sortingOption: widget.sortingOptions ?? defaultFilter.sortingOption,
types: widget.types ?? {}, types: widget.types ?? {},

View file

@ -59,8 +59,8 @@ class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
onClear: () => libraryProvider.setTypes(librarySearchResults.filters.types.setAll(false)), onClear: () => libraryProvider.setTypes(librarySearchResults.filters.types.setAll(false)),
), ),
ExpressiveButton( ExpressiveButton(
isSelected: favourites, isSelected: favourites == true,
icon: favourites ? const Icon(IconsaxPlusBold.heart) : null, icon: favourites == true ? const Icon(IconsaxPlusBold.heart) : null,
label: Text(context.localized.favorites), label: Text(context.localized.favorites),
onPressed: () { onPressed: () {
libraryProvider.toggleFavourite(); libraryProvider.toggleFavourite();
@ -68,8 +68,8 @@ class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
}, },
), ),
ExpressiveButton( ExpressiveButton(
isSelected: recursive, isSelected: recursive == true,
icon: recursive ? const Icon(IconsaxPlusBold.tick_circle) : null, icon: recursive == true ? const Icon(IconsaxPlusBold.tick_circle) : null,
label: Text(context.localized.recursive), label: Text(context.localized.recursive),
onPressed: () { onPressed: () {
libraryProvider.toggleRecursive(); libraryProvider.toggleRecursive();

View file

@ -61,18 +61,23 @@ class ItemInfoScreenState extends ConsumerState<ItemInfoScreen> {
Container( Container(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
spacing: 6, spacing: 12,
children: [ children: [
Text( Expanded(
widget.item.name, child: Text(
style: Theme.of(context).textTheme.titleLarge, widget.item.name,
softWrap: false,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge,
),
), ),
const Spacer(),
IconButton( IconButton(
onPressed: () => context.copyToClipboard(info.model.toString()), onPressed: () => context.copyToClipboard(info.model.toString()),
icon: const Icon(Icons.copy_all_rounded)), icon: const Icon(Icons.copy_all_rounded)),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/full_screen_helpers/full_screen_wrapper.dart';
import 'package:fladder/widgets/shared/elevated_icon.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart';
import 'package:fladder/widgets/shared/progress_floating_button.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
class PhotoViewerControls extends ConsumerStatefulWidget { class PhotoViewerControls extends ConsumerStatefulWidget {
final EdgeInsets padding; final EdgeInsets padding;
@ -309,13 +311,19 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
children: [ children: [
ElevatedIconButton( ElevatedIconButton(
onPressed: widget.openOptions, onPressed: widget.openOptions,
icon: IconsaxPlusLinear.more_2, icon: IconsaxPlusLinear.more_square,
), ),
const Spacer(), const Spacer(),
ElevatedIconButton( SelectableIconButton(
onPressed: markAsFavourite, onPressed: markAsFavourite,
color: widget.photo.userData.isFavourite ? Colors.red : null, selected: false,
icon: widget.photo.userData.isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, 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( ProgressFloatingButton(
controller: timerController, controller: timerController,
@ -341,8 +349,11 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
); );
} }
void markAsFavourite() { Future<void> markAsFavourite() async {
ref.read(userProvider.notifier).setAsFavorite(!widget.photo.userData.isFavourite, widget.photo.id); final response =
await ref.read(userProvider.notifier).setAsFavorite(!widget.photo.userData.isFavourite, widget.photo.id);
if (response?.isSuccessful == false) return;
widget.onPhotoChanged(widget.photo widget.onPhotoChanged(widget.photo
.copyWith(userData: widget.photo.userData.copyWith(isFavourite: !widget.photo.userData.isFavourite))); .copyWith(userData: widget.photo.userData.copyWith(isFavourite: !widget.photo.userData.isFavourite)));

View file

@ -90,7 +90,13 @@ class _PhotoViewerScreenState extends ConsumerState<PhotoViewerScreen> with Widg
if (context.mounted) { if (context.mounted) {
setState(() { 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; loadingItems = false;
}); });
} }
@ -165,14 +171,14 @@ class _PhotoViewerScreenState extends ConsumerState<PhotoViewerScreen> with Widg
? Center( ? Center(
child: Text(context.localized.noItemsToShow), child: Text(context.localized.noItemsToShow),
) )
: buildViewer(), : buildViewer(context),
), ),
), ),
), ),
); );
} }
Widget buildViewer() { Widget buildViewer(BuildContext context) {
final currentPhoto = photos[currentPage]; final currentPhoto = photos[currentPage];
final imageHash = currentPhoto.images?.primary?.hash; final imageHash = currentPhoto.images?.primary?.hash;
return Stack( return Stack(
@ -498,8 +504,8 @@ class _PhotoViewerScreenState extends ConsumerState<PhotoViewerScreen> with Widg
}, },
); );
void markAsFavourite(PhotoModel photo, {bool? value}) { Future<void> markAsFavourite(PhotoModel photo, {bool? value}) async {
ref.read(userProvider.notifier).setAsFavorite(value ?? !photo.userData.isFavourite, photo.id); await ref.read(userProvider.notifier).setAsFavorite(value ?? !photo.userData.isFavourite, photo.id);
setState(() { setState(() {
int index = photos.indexOf(photo); int index = photos.indexOf(photo);

View file

@ -24,7 +24,7 @@ class ItemLogo extends StatelessWidget {
child: Text( child: Text(
item.parentBaseModel.name, item.parentBaseModel.name,
textAlign: TextAlign.start, textAlign: TextAlign.start,
maxLines: 3, maxLines: 2,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
style: textStyle ?? style: textStyle ??
Theme.of(context).textTheme.headlineLarge?.copyWith( Theme.of(context).textTheme.headlineLarge?.copyWith(

View file

@ -77,7 +77,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
} }
Future<void> navigateToDetails() async { Future<void> navigateToDetails() async {
await widget.poster.navigateTo(context); await widget.poster.navigateTo(context, ref: ref);
} }
final posterRadius = FladderTheme.smallShape.borderRadius; final posterRadius = FladderTheme.smallShape.borderRadius;

View file

@ -42,6 +42,7 @@ class _PosterRowState extends ConsumerState<PosterRow> {
contentPadding: widget.contentPadding, contentPadding: widget.contentPadding,
label: widget.label, label: widget.label,
onLabelClick: widget.onLabelClick, onLabelClick: widget.onLabelClick,
dominantRatio: dominantRatio,
items: widget.posters, items: widget.posters,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final poster = widget.posters[index]; final poster = widget.posters[index];

View file

@ -8,6 +8,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/routes/auto_router.gr.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_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
import 'package:fladder/screens/syncing/sync_list_item.dart'; import 'package:fladder/screens/syncing/sync_list_item.dart';
@ -20,9 +21,7 @@ import 'package:fladder/widgets/shared/pull_to_refresh.dart';
@RoutePage() @RoutePage()
class SyncedScreen extends ConsumerStatefulWidget { class SyncedScreen extends ConsumerStatefulWidget {
final ScrollController? navigationScrollController; const SyncedScreen({super.key});
const SyncedScreen({this.navigationScrollController, super.key});
@override @override
ConsumerState<ConsumerStatefulWidget> createState() => _SyncedScreenState(); ConsumerState<ConsumerStatefulWidget> createState() => _SyncedScreenState();
@ -43,7 +42,7 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: CustomScrollView( child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
controller: widget.navigationScrollController, controller: AdaptiveLayout.scrollOf(context, HomeTabs.sync),
slivers: [ slivers: [
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar( NestedSliverAppBar(

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/home_settings_provider.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/adaptive_layout/adaptive_layout_model.dart';
import 'package:fladder/util/debug_banner.dart'; import 'package:fladder/util/debug_banner.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -81,9 +82,9 @@ class AdaptiveLayout extends InheritedWidget {
return result!.data.posterDefaults; return result!.data.posterDefaults;
} }
static ScrollController scrollOf(BuildContext context) { static ScrollController scrollOf(BuildContext context, HomeTabs tab) {
final AdaptiveLayout? result = maybeOf(context); final AdaptiveLayout? result = maybeOf(context);
return result!.data.controller; return result?.data.controller[tab] ?? ScrollController();
} }
static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) { static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) {
@ -130,7 +131,10 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
late ViewSize viewSize = ViewSize.tablet; late ViewSize viewSize = ViewSize.tablet;
late LayoutMode layoutMode = LayoutMode.single; late LayoutMode layoutMode = LayoutMode.single;
late TargetPlatform currentPlatform = defaultTargetPlatform; late TargetPlatform currentPlatform = defaultTargetPlatform;
late ScrollController controller = ScrollController();
final Map<HomeTabs, ScrollController> scrollControllers = {
for (var item in HomeTabs.values) item: ScrollController(),
};
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -177,11 +181,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
final selectedLayoutMode = selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values); final selectedLayoutMode = selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values);
final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch; final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch;
final posterDefaults = switch (selectedViewSize) { final posterDefaults = const PosterDefaults(size: 350, ratio: 0.55);
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 ?? final currentLayout = widget.adaptiveLayout ??
AdaptiveLayoutModel( AdaptiveLayoutModel(
@ -191,7 +191,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
platform: currentPlatform, platform: currentPlatform,
isDesktop: isDesktop, isDesktop: isDesktop,
sideBarWidth: 0, sideBarWidth: 0,
controller: controller, controller: scrollControllers,
posterDefaults: posterDefaults, posterDefaults: posterDefaults,
); );
@ -207,7 +207,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
inputDevice: input, inputDevice: input,
platform: currentPlatform, platform: currentPlatform,
isDesktop: isDesktop, isDesktop: isDesktop,
controller: controller, controller: scrollControllers,
posterDefaults: posterDefaults, posterDefaults: posterDefaults,
), ),
child: Builder( child: Builder(

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; 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/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/poster_defaults.dart'; import 'package:fladder/util/poster_defaults.dart';
@ -46,7 +47,7 @@ class AdaptiveLayoutModel {
final TargetPlatform platform; final TargetPlatform platform;
final bool isDesktop; final bool isDesktop;
final PosterDefaults posterDefaults; final PosterDefaults posterDefaults;
final ScrollController controller; final Map<HomeTabs, ScrollController> controller;
final double sideBarWidth; final double sideBarWidth;
const AdaptiveLayoutModel({ const AdaptiveLayoutModel({
@ -67,7 +68,7 @@ class AdaptiveLayoutModel {
TargetPlatform? platform, TargetPlatform? platform,
bool? isDesktop, bool? isDesktop,
PosterDefaults? posterDefaults, PosterDefaults? posterDefaults,
ScrollController? controller, Map<HomeTabs, ScrollController>? controller,
double? sideBarWidth, double? sideBarWidth,
}) { }) {
return AdaptiveLayoutModel( return AdaptiveLayoutModel(

View file

@ -36,6 +36,7 @@ class FladderImage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final useBluredPlaceHolder = ref.watch(clientSettingsProvider.select((value) => value.blurPlaceHolders)); final useBluredPlaceHolder = ref.watch(clientSettingsProvider.select((value) => value.blurPlaceHolders));
final newImage = image; final newImage = image;
final imageProvider = image?.imageProvider;
if (newImage == null) { if (newImage == null) {
return placeHolder ?? Container(); return placeHolder ?? Container();
} else { } else {
@ -44,13 +45,15 @@ class FladderImage extends ConsumerWidget {
fit: stackFit, fit: stackFit,
children: [ children: [
if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly) if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly)
BlurHash( Image(
hash: newImage.hash, image: BlurHashImage(
optimizationMode: BlurHashOptimizationMode.approximation, newImage.hash,
color: Colors.transparent, decodingHeight: 24,
imageFit: blurFit ?? fit, decodingWidth: 24,
),
fit: blurFit ?? fit,
), ),
if (!blurOnly) if (!blurOnly && imageProvider != null)
FadeInImage( FadeInImage(
placeholder: MemoryImage(kTransparentImage), placeholder: MemoryImage(kTransparentImage),
fit: fit, fit: fit,
@ -58,7 +61,7 @@ class FladderImage extends ConsumerWidget {
excludeFromSemantics: true, excludeFromSemantics: true,
alignment: alignment ?? Alignment.center, alignment: alignment ?? Alignment.center,
imageErrorBuilder: imageErrorBuilder, imageErrorBuilder: imageErrorBuilder,
image: newImage.imageProvider, image: imageProvider,
) )
], ],
); );

View file

@ -8,7 +8,6 @@ import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.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/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/collections/add_to_collection.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) else if (!exclude.contains(ItemActions.showAlbum) && galleryItem)
ItemActionButton( ItemActionButton(
icon: Icon(FladderItemType.photoAlbum.icon), icon: Icon(FladderItemType.photoAlbum.icon),
action: () => (this as PhotoModel).navigateToAlbum(context), action: () => parentBaseModel.navigateTo(context),
label: Text(context.localized.showAlbum), label: Text(context.localized.showAlbum),
), ),
if (!exclude.contains(ItemActions.playFromStart)) if (!exclude.contains(ItemActions.playFromStart))

View file

@ -171,7 +171,7 @@ extension PhotoAlbumExtension on PhotoAlbumModel? {
return; return;
} }
await context.navigateTo(PhotoViewerRoute( await context.pushRoute(PhotoViewerRoute(
items: photos.toList(), items: photos.toList(),
)); ));

View file

@ -7,6 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:overflow_view/overflow_view.dart'; import 'package:overflow_view/overflow_view.dart';
import 'package:fladder/models/collection_types.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/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
@ -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/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
const _fullScreenRoutes = {
PhotoViewerRoute.name,
};
class SideNavigationBar extends ConsumerStatefulWidget { class SideNavigationBar extends ConsumerStatefulWidget {
final int currentIndex; final int currentIndex;
final List<DestinationModel> destinations; final List<DestinationModel> destinations;
@ -60,6 +65,8 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
final shouldExpand = fullyExpanded; final shouldExpand = fullyExpanded;
final isDesktop = AdaptiveLayout.of(context).isDesktop; final isDesktop = AdaptiveLayout.of(context).isDesktop;
final fullScreenChildRoute = _fullScreenRoutes.contains(context.router.current.name);
return Stack( return Stack(
children: [ children: [
AdaptiveLayoutBuilder( AdaptiveLayoutBuilder(
@ -69,231 +76,244 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
), ),
child: (context) => widget.child, child: (context) => widget.child,
), ),
Container( IgnorePointer(
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), ignoring: fullScreenChildRoute,
width: shouldExpand ? expandedWidth : collapsedWidth, child: AnimatedOpacity(
child: MouseRegion( duration: const Duration(milliseconds: 250),
child: Padding( opacity: !fullScreenChildRoute ? 1 : 0,
key: const Key('navigation_rail'), child: Container(
padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null), color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
child: Column( width: shouldExpand ? expandedWidth : collapsedWidth,
spacing: 2, child: MouseRegion(
children: [ child: Padding(
Padding( key: const Key('navigation_rail'),
padding: const EdgeInsets.symmetric(horizontal: 14), padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null),
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.center, spacing: 2,
children: [ children: [
if (expandedSideBar) ...[ Padding(
Expanded(child: Text(context.localized.navigation)), padding: const EdgeInsets.symmetric(horizontal: 14),
], child: Row(
Opacity( mainAxisAlignment: MainAxisAlignment.center,
opacity: largeBar && expandedSideBar ? 0.65 : 1.0, children: [
child: IconButton( if (expandedSideBar) ...[
onPressed: !largeBar Expanded(child: Text(context.localized.navigation)),
? () => widget.scaffoldKey.currentState?.openDrawer() ],
: () => setState(() => expandedSideBar = !expandedSideBar), Opacity(
icon: Icon( opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, 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,
),
),
), ),
if (views.isNotEmpty && largeBar) ...[ ),
const Divider( if (largeBar) ...[
indent: 32, AnimatedFadeSize(
endIndent: 32, duration: const Duration(milliseconds: 250),
), child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
Flexible( ),
child: OverflowView.flexible( ],
direction: Axis.vertical, Expanded(
spacing: 4, child: Column(
children: views.map( mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
(view) { children: [
final selected = context.router.currentUrl.contains(view.id); ...widget.destinations.mapIndexed(
final actions = [ (index, destination) => CustomTooltip(
ItemActionButton( tooltipContent: expandedSideBar
label: Text(context.localized.scanLibrary), ? null
icon: const Icon(IconsaxPlusLinear.refresh), : Card(
action: () => showRefreshPopup(context, view.id, view.name), child: Padding(
) padding: const EdgeInsets.all(12),
]; child: Text(
return CustomTooltip( destination.label,
tooltipContent: expandedSideBar style: Theme.of(context).textTheme.titleSmall,
? null
: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
view.name,
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 position: TooltipPosition.right,
? ClipRRect( child: destination.toNavigationButton(
borderRadius: FladderTheme.smallShape.borderRadius, widget.currentIndex == index,
child: SizedBox.square( true,
dimension: 50, shouldExpand,
child: FladderImage( ),
image: view.imageData?.primary, ),
placeHolder: Card( ),
child: Icon( if (views.isNotEmpty && largeBar) ...[
selected const Divider(
? view.collectionType.icon indent: 32,
: view.collectionType.iconOutlined, 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,
), ),
), ),
), ),
) position: TooltipPosition.right,
: null, child: view.toNavigationButton(
trailing: actions, selected,
), true,
); shouldExpand,
}, () => context.pushRoute(
).toList(), LibrarySearchRoute(
builder: (context, remaining) { viewModelId: view.id,
return CustomTooltip( ).withFilter(view.collectionType.defaultFilters),
tooltipContent: expandedSideBar ),
? null onLongPress: () => showBottomSheetPill(
: Card( context: context,
child: Padding( content: (context, scrollController) => ListView(
padding: const EdgeInsets.all(12), shrinkWrap: true,
child: Text( controller: scrollController,
context.localized.moreOptions, children: actions.listTileItems(context, useIcons: true),
style: Theme.of(context).textTheme.titleSmall,
), ),
), ),
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), ).toList(),
padding: EdgeInsets.zero, builder: (context, remaining) {
tooltip: "", return CustomTooltip(
icon: NavigationButton( tooltipContent: expandedSideBar
label: context.localized.other, ? null
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), : Card(
icon: const Icon(IconsaxPlusLinear.arrow_square_down), child: Padding(
expanded: shouldExpand, padding: const EdgeInsets.all(12),
customIcon: usePostersForLibrary child: Text(
? ClipRRect( context.localized.moreOptions,
borderRadius: FladderTheme.smallShape.borderRadius, style: Theme.of(context).textTheme.titleSmall,
child: const SizedBox.square(
dimension: 50,
child: Card(
child: Icon(IconsaxPlusLinear.arrow_square_down),
), ),
), ),
) ),
: null, position: TooltipPosition.right,
horizontal: true, child: PopupMenuButton(
), iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
itemBuilder: (context) => views padding: EdgeInsets.zero,
.sublist(views.length - remaining) tooltip: "",
.map( icon: NavigationButton(
(e) => PopupMenuItem( label: context.localized.other,
onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
child: Row( icon: const Icon(IconsaxPlusLinear.arrow_square_down),
spacing: 8, expanded: shouldExpand,
children: [ customIcon: usePostersForLibrary
usePostersForLibrary ? ClipRRect(
? Padding( borderRadius: FladderTheme.smallShape.borderRadius,
padding: const EdgeInsets.symmetric(vertical: 4), child: const SizedBox.square(
child: ClipRRect( dimension: 50,
borderRadius: FladderTheme.smallShape.borderRadius, child: Card(
child: SizedBox.square( child: Icon(IconsaxPlusLinear.arrow_square_down),
dimension: 45, ),
child: FladderImage( ),
image: e.imageData?.primary, )
placeHolder: Card( : null,
child: Icon( horizontal: true,
e.collectionType.iconOutlined, ),
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),
: Icon(e.collectionType.iconOutlined), ],
Text(e.name), ),
], ),
), )
), .toList(),
) ),
.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());
}
},
),
],
), ),
), ),
), ),

View file

@ -8,6 +8,7 @@ import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/routes/auto_router.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/animated_fade_size.dart';
import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -77,6 +78,9 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
final calculatedBottomViewPadding = final calculatedBottomViewPadding =
showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding; showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding;
final currentTab =
HomeTabs.values.elementAtOrNull(currentIndex.clamp(0, HomeTabs.values.length - 1)) ?? HomeTabs.dashboard;
return PopScope( return PopScope(
canPop: currentIndex == 0, canPop: currentIndex == 0,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
@ -124,7 +128,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
hiddenHeight: calculatedBottomViewPadding, hiddenHeight: calculatedBottomViewPadding,
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
child: HideOnScroll( child: HideOnScroll(
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context, currentTab),
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
child: NestedBottomAppBar( child: NestedBottomAppBar(
child: SizedBox( child: SizedBox(

View file

@ -29,7 +29,7 @@ class HideOnScroll extends ConsumerStatefulWidget {
} }
class _HideOnScrollState extends ConsumerState<HideOnScroll> { class _HideOnScrollState extends ConsumerState<HideOnScroll> {
late final ScrollController scrollController = widget.controller ?? ScrollController(); late ScrollController scrollController = widget.controller ?? ScrollController();
bool isVisible = true; bool isVisible = true;
@override @override
@ -47,6 +47,16 @@ class _HideOnScrollState extends ConsumerState<HideOnScroll> {
super.dispose(); 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() { void _onScroll() {
if (!widget.canHide) { if (!widget.canHide) {
if (!isVisible) { if (!isVisible) {

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -19,6 +21,7 @@ class HorizontalList<T> extends ConsumerStatefulWidget {
final Widget Function(BuildContext context, int index) itemBuilder; final Widget Function(BuildContext context, int index) itemBuilder;
final bool scrollToEnd; final bool scrollToEnd;
final EdgeInsets contentPadding; final EdgeInsets contentPadding;
final double? dominantRatio;
final double? height; final double? height;
final bool shrinkWrap; final bool shrinkWrap;
const HorizontalList({ const HorizontalList({
@ -33,6 +36,7 @@ class HorizontalList<T> extends ConsumerStatefulWidget {
this.contentPadding = const EdgeInsets.symmetric(horizontal: 16), this.contentPadding = const EdgeInsets.symmetric(horizontal: 16),
this.subtext, this.subtext,
this.shrinkWrap = false, this.shrinkWrap = false,
this.dominantRatio,
super.key, super.key,
}); });
@ -201,9 +205,11 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( SizedBox(
height: (widget.height ?? height: widget.height ??
AdaptiveLayout.poster(context).size * ((AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))), ref.watch(clientSettingsProvider.select((value) => value.posterSize))) /
pow((widget.dominantRatio ?? 1.0), 0.55)) *
0.72,
child: ListView.separated( child: ListView.separated(
controller: _scrollController, controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,

View file

@ -158,7 +158,7 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
} }
: null, : null,
child: FloatingActionButton( child: FloatingActionButton(
heroTag: null, heroTag: "Progress_Floating_Button",
onPressed: isActive ? timer.cancel : timer.play, onPressed: isActive ? timer.cancel : timer.play,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,

View file

@ -14,12 +14,16 @@ class SelectableIconButton extends ConsumerStatefulWidget {
final IconData icon; final IconData icon;
final IconData? selectedIcon; final IconData? selectedIcon;
final bool selected; final bool selected;
final Color? backgroundColor;
final Color? iconColor;
const SelectableIconButton({ const SelectableIconButton({
required this.onPressed, required this.onPressed,
required this.selected, required this.selected,
required this.icon, required this.icon,
this.selectedIcon, this.selectedIcon,
this.label, this.label,
this.backgroundColor,
this.iconColor,
super.key, super.key,
}); });
@ -37,9 +41,14 @@ class _SelectableIconButtonState extends ConsumerState<SelectableIconButton> {
message: widget.label ?? "", message: widget.label ?? "",
child: ElevatedButton( child: ElevatedButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.primary) : null, elevation: WidgetStatePropertyAll(
iconColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : null, widget.backgroundColor != null ? (widget.backgroundColor!.a < 1 ? 0 : null) : null),
foregroundColor: widget.selected ? WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary) : 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), padding: const WidgetStatePropertyAll(EdgeInsets.zero),
), ),
onPressed: loading onPressed: loading