mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
chore: Lots of bug fixes and navigation improvements
This commit is contained in:
parent
9bb5e81812
commit
92d5391b93
35 changed files with 513 additions and 455 deletions
|
|
@ -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 ||
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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 ?? {},
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ extension PhotoAlbumExtension on PhotoAlbumModel? {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.navigateTo(PhotoViewerRoute(
|
await context.pushRoute(PhotoViewerRoute(
|
||||||
items: photos.toList(),
|
items: photos.toList(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue