diff --git a/README.md b/README.md
index ec7fa57..695357d 100644
--- a/README.md
+++ b/README.md
@@ -53,11 +53,14 @@
Mobile
+
+
+
@@ -65,8 +68,14 @@
Tablet
-
-
+
+
+
+
+
+
+
+
Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder)
diff --git a/assets/marketing/screenshots/Mobile/Dashboard.png b/assets/marketing/screenshots/Mobile/Dashboard.png
index 9ad9c74..a7d46bd 100644
Binary files a/assets/marketing/screenshots/Mobile/Dashboard.png and b/assets/marketing/screenshots/Mobile/Dashboard.png differ
diff --git a/assets/marketing/screenshots/Mobile/Dashboard_2.png b/assets/marketing/screenshots/Mobile/Dashboard_2.png
deleted file mode 100644
index 0fe0459..0000000
Binary files a/assets/marketing/screenshots/Mobile/Dashboard_2.png and /dev/null differ
diff --git a/assets/marketing/screenshots/Mobile/Details.png b/assets/marketing/screenshots/Mobile/Details.png
index c56ccfd..f41046b 100644
Binary files a/assets/marketing/screenshots/Mobile/Details.png and b/assets/marketing/screenshots/Mobile/Details.png differ
diff --git a/assets/marketing/screenshots/Mobile/Details_2.png b/assets/marketing/screenshots/Mobile/Details_2.png
index e551f3e..26f2828 100644
Binary files a/assets/marketing/screenshots/Mobile/Details_2.png and b/assets/marketing/screenshots/Mobile/Details_2.png differ
diff --git a/assets/marketing/screenshots/Mobile/Favourites.png b/assets/marketing/screenshots/Mobile/Favourites.png
index 585f1bb..20763f6 100644
Binary files a/assets/marketing/screenshots/Mobile/Favourites.png and b/assets/marketing/screenshots/Mobile/Favourites.png differ
diff --git a/assets/marketing/screenshots/Mobile/Library.png b/assets/marketing/screenshots/Mobile/Library.png
index e990197..0a27852 100644
Binary files a/assets/marketing/screenshots/Mobile/Library.png and b/assets/marketing/screenshots/Mobile/Library.png differ
diff --git a/assets/marketing/screenshots/Mobile/Library_Search.png b/assets/marketing/screenshots/Mobile/Library_Search.png
new file mode 100644
index 0000000..4a09053
Binary files /dev/null and b/assets/marketing/screenshots/Mobile/Library_Search.png differ
diff --git a/assets/marketing/screenshots/Mobile/Player.png b/assets/marketing/screenshots/Mobile/Player.png
index 151a834..20687e6 100644
Binary files a/assets/marketing/screenshots/Mobile/Player.png and b/assets/marketing/screenshots/Mobile/Player.png differ
diff --git a/assets/marketing/screenshots/Mobile/Resume_Tab.png b/assets/marketing/screenshots/Mobile/Resume_Tab.png
index 2b918b1..03a797b 100644
Binary files a/assets/marketing/screenshots/Mobile/Resume_Tab.png and b/assets/marketing/screenshots/Mobile/Resume_Tab.png differ
diff --git a/assets/marketing/screenshots/Mobile/Settings.png b/assets/marketing/screenshots/Mobile/Settings.png
index 666031d..efd44c1 100644
Binary files a/assets/marketing/screenshots/Mobile/Settings.png and b/assets/marketing/screenshots/Mobile/Settings.png differ
diff --git a/assets/marketing/screenshots/Mobile/Sync.png b/assets/marketing/screenshots/Mobile/Sync.png
index 04f22fe..b17e1a7 100644
Binary files a/assets/marketing/screenshots/Mobile/Sync.png and b/assets/marketing/screenshots/Mobile/Sync.png differ
diff --git a/assets/marketing/screenshots/Tablet/Dashboard.png b/assets/marketing/screenshots/Tablet/Dashboard.png
index ff361cd..107c853 100644
Binary files a/assets/marketing/screenshots/Tablet/Dashboard.png and b/assets/marketing/screenshots/Tablet/Dashboard.png differ
diff --git a/assets/marketing/screenshots/Tablet/Details.png b/assets/marketing/screenshots/Tablet/Details.png
index 4944efe..9d5e01f 100644
Binary files a/assets/marketing/screenshots/Tablet/Details.png and b/assets/marketing/screenshots/Tablet/Details.png differ
diff --git a/assets/marketing/screenshots/Tablet/Details_2.png b/assets/marketing/screenshots/Tablet/Details_2.png
new file mode 100644
index 0000000..197d44f
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Details_2.png differ
diff --git a/assets/marketing/screenshots/Tablet/Favourites.png b/assets/marketing/screenshots/Tablet/Favourites.png
new file mode 100644
index 0000000..6c4ff94
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Favourites.png differ
diff --git a/assets/marketing/screenshots/Tablet/Library.png b/assets/marketing/screenshots/Tablet/Library.png
new file mode 100644
index 0000000..569373c
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library.png differ
diff --git a/assets/marketing/screenshots/Tablet/Library_Search.png b/assets/marketing/screenshots/Tablet/Library_Search.png
new file mode 100644
index 0000000..2119aac
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library_Search.png differ
diff --git a/assets/marketing/screenshots/Tablet/Player.png b/assets/marketing/screenshots/Tablet/Player.png
new file mode 100644
index 0000000..72d96a2
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Player.png differ
diff --git a/assets/marketing/screenshots/Tablet/Resume_Tab.png b/assets/marketing/screenshots/Tablet/Resume_Tab.png
new file mode 100644
index 0000000..48750a7
Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Resume_Tab.png differ
diff --git a/assets/marketing/screenshots/Tablet/Settings.png b/assets/marketing/screenshots/Tablet/Settings.png
index 3ed3277..b9e2a64 100644
Binary files a/assets/marketing/screenshots/Tablet/Settings.png and b/assets/marketing/screenshots/Tablet/Settings.png differ
diff --git a/assets/marketing/screenshots/Tablet/Sync.png b/assets/marketing/screenshots/Tablet/Sync.png
index 8a2b677..e5ae951 100644
Binary files a/assets/marketing/screenshots/Tablet/Sync.png and b/assets/marketing/screenshots/Tablet/Sync.png differ
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 194f281..62361c7 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1211,5 +1211,14 @@
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.",
- "@rememberAudioSelectionsDesc": {}
+ "@rememberAudioSelectionsDesc": {},
+ "similarToRecentlyPlayed": "Similar to recently played",
+ "similarToLikedItem": "Similar to liked item",
+ "hasDirectorFromRecentlyPlayed": "Has director from recently played",
+ "hasActorFromRecentlyPlayed": "Has actor from recently played",
+ "hasLikedDirector": "Has liked director",
+ "hasLikedActor": "Has liked actor",
+ "latest": "Latest",
+ "recommended": "Recommended"
+
}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index e01ef0f..35b4992 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -19,7 +19,6 @@ import 'package:universal_html/html.dart' as html;
import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/account_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
@@ -31,7 +30,7 @@ import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/theme.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/application_info.dart';
import 'package:fladder/util/fladder_config.dart';
import 'package:fladder/util/localization_helper.dart';
@@ -108,13 +107,7 @@ void main() async {
))
],
child: AdaptiveLayoutBuilder(
- fallBack: ViewSize.tablet,
- layoutPoints: [
- LayoutPoints(start: 0, end: 599, type: ViewSize.phone),
- LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet),
- LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop),
- ],
- child: const Main(),
+ child: (context) => const Main(),
),
),
);
@@ -304,6 +297,7 @@ class _MainState extends ConsumerState with WindowListener, WidgetsBinding
colorScheme: darkTheme.colorScheme.copyWith(
surface: amoledOverwrite,
surfaceContainerHighest: amoledOverwrite,
+ surfaceContainerLow: amoledOverwrite,
),
),
themeMode: themeMode,
diff --git a/lib/models/account_model.dart b/lib/models/account_model.dart
index e0fdb30..0e2a990 100644
--- a/lib/models/account_model.dart
+++ b/lib/models/account_model.dart
@@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/models/library_filters_model.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
part 'account_model.freezed.dart';
diff --git a/lib/models/collection_types.dart b/lib/models/collection_types.dart
index 10867df..282f430 100644
--- a/lib/models/collection_types.dart
+++ b/lib/models/collection_types.dart
@@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType {
Set get itemKinds {
switch (this) {
+ case CollectionType.music:
+ return {FladderItemType.musicAlbum};
case CollectionType.movies:
return {FladderItemType.movie};
case CollectionType.tvshows:
@@ -30,6 +32,8 @@ extension CollectionTypeExtension on CollectionType {
IconData getIconType(bool outlined) {
switch (this) {
+ case CollectionType.music:
+ return outlined ? IconsaxPlusLinear.music_square : IconsaxPlusBold.music_square;
case CollectionType.movies:
return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal;
case CollectionType.tvshows:
@@ -48,4 +52,16 @@ extension CollectionTypeExtension on CollectionType {
return IconsaxPlusLinear.information;
}
}
+
+ double? get aspectRatio => switch (this) {
+ CollectionType.music ||
+ CollectionType.homevideos ||
+ CollectionType.boxsets ||
+ CollectionType.photos ||
+ CollectionType.livetv ||
+ CollectionType.playlists =>
+ 0.8,
+ CollectionType.folders => 1.3,
+ _ => null,
+ };
}
diff --git a/lib/models/item_base_model.dart b/lib/models/item_base_model.dart
index ee16017..54b42d1 100644
--- a/lib/models/item_base_model.dart
+++ b/lib/models/item_base_model.dart
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:dart_mappable/dart_mappable.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
@@ -304,6 +304,15 @@ enum FladderItemType {
const FladderItemType({required this.icon, required this.selectedicon});
+ double get aspectRatio => switch (this) {
+ FladderItemType.video => 0.8,
+ FladderItemType.photo => 0.8,
+ FladderItemType.photoAlbum => 0.8,
+ FladderItemType.musicAlbum => 0.8,
+ FladderItemType.baseType => 0.8,
+ _ => 0.55,
+ };
+
static Set get playable => {
FladderItemType.series,
FladderItemType.episode,
@@ -317,27 +326,25 @@ enum FladderItemType {
FladderItemType.video,
};
- String label(BuildContext context) {
- return switch (this) {
- FladderItemType.baseType => context.localized.mediaTypeBase,
- FladderItemType.audio => context.localized.audio,
- FladderItemType.collectionFolder => context.localized.collectionFolder,
- FladderItemType.musicAlbum => context.localized.musicAlbum,
- FladderItemType.musicVideo => context.localized.video,
- FladderItemType.video => context.localized.video,
- FladderItemType.movie => context.localized.mediaTypeMovie,
- FladderItemType.series => context.localized.mediaTypeSeries,
- FladderItemType.season => context.localized.mediaTypeSeason,
- FladderItemType.episode => context.localized.mediaTypeEpisode,
- FladderItemType.photo => context.localized.mediaTypePhoto,
- FladderItemType.person => context.localized.mediaTypePerson,
- FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum,
- FladderItemType.folder => context.localized.mediaTypeFolder,
- FladderItemType.boxset => context.localized.mediaTypeBoxset,
- FladderItemType.playlist => context.localized.mediaTypePlaylist,
- FladderItemType.book => context.localized.mediaTypeBook,
- };
- }
+ String label(BuildContext context) => switch (this) {
+ FladderItemType.baseType => context.localized.mediaTypeBase,
+ FladderItemType.audio => context.localized.audio,
+ FladderItemType.collectionFolder => context.localized.collectionFolder,
+ FladderItemType.musicAlbum => context.localized.musicAlbum,
+ FladderItemType.musicVideo => context.localized.video,
+ FladderItemType.video => context.localized.video,
+ FladderItemType.movie => context.localized.mediaTypeMovie,
+ FladderItemType.series => context.localized.mediaTypeSeries,
+ FladderItemType.season => context.localized.mediaTypeSeason,
+ FladderItemType.episode => context.localized.mediaTypeEpisode,
+ FladderItemType.photo => context.localized.mediaTypePhoto,
+ FladderItemType.person => context.localized.mediaTypePerson,
+ FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum,
+ FladderItemType.folder => context.localized.mediaTypeFolder,
+ FladderItemType.boxset => context.localized.mediaTypeBoxset,
+ FladderItemType.playlist => context.localized.mediaTypePlaylist,
+ FladderItemType.book => context.localized.mediaTypeBook,
+ };
BaseItemKind get dtoKind => switch (this) {
FladderItemType.baseType => BaseItemKind.userrootfolder,
diff --git a/lib/models/items/episode_model.dart b/lib/models/items/episode_model.dart
index 56ec3d5..f21a7a3 100644
--- a/lib/models/items/episode_model.dart
+++ b/lib/models/items/episode_model.dart
@@ -199,20 +199,20 @@ extension EpisodeListExtensions on List {
}
EpisodeModel? get nextUp {
- final episodes = whereNot((element) => element.season <= 0).toList();
+ final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList();
+ if (episodes.isEmpty) return null;
- final lastProgress = episodes
- .lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available);
- final lastPlayed =
- episodes.lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available);
+ final lastWatchedIndex = [
+ episodes.lastIndexWhere((e) => e.userData.progress != 0),
+ episodes.lastIndexWhere((e) => e.userData.played),
+ ].reduce((a, b) => a > b ? a : b);
- if (lastProgress == -1 && lastPlayed == -1) {
- return episodes.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
- } else {
- return episodes
- .getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, episodes.length)
- .firstWhereOrNull((element) => element.status == EpisodeStatus.available);
+ if (lastWatchedIndex >= 0 && lastWatchedIndex + 1 < episodes.length) {
+ final next = episodes.sublist(lastWatchedIndex + 1).firstWhereOrNull((e) => e.status == EpisodeStatus.available);
+ if (next != null) return next;
}
+
+ return episodes.firstOrNull;
}
bool get allPlayed {
diff --git a/lib/models/items/series_model.dart b/lib/models/items/series_model.dart
index c1f16f7..1a22ef9 100644
--- a/lib/models/items/series_model.dart
+++ b/lib/models/items/series_model.dart
@@ -1,5 +1,6 @@
-import 'package:fladder/screens/details_screens/series_detail_screen.dart';
import 'package:flutter/widgets.dart';
+
+import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
@@ -9,8 +10,7 @@ import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/season_model.dart';
-
-import 'package:dart_mappable/dart_mappable.dart';
+import 'package:fladder/screens/details_screens/series_detail_screen.dart';
part 'series_model.mapper.dart';
diff --git a/lib/models/library_search/library_search_options.dart b/lib/models/library_search/library_search_options.dart
index adcd681..dbefed0 100644
--- a/lib/models/library_search/library_search_options.dart
+++ b/lib/models/library_search/library_search_options.dart
@@ -1,9 +1,9 @@
-import 'package:fladder/models/item_base_model.dart';
-import 'package:fladder/util/localization_helper.dart';
+import 'package:flutter/material.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
-import 'package:flutter/material.dart';
+import 'package:fladder/models/item_base_model.dart';
+import 'package:fladder/util/localization_helper.dart';
enum SortingOptions {
name([ItemSortBy.name]),
diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart
index 8eb8fcb..27bdfd4 100644
--- a/lib/models/playback/playback_model.dart
+++ b/lib/models/playback/playback_model.dart
@@ -198,17 +198,15 @@ class PlaybackModelHelper {
final streamModel = firstItemToPlay.streamModel;
final audioStreamIndex = selectAudioStream(
- ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
- oldModel?.mediaStreams?.currentAudioStream,
- streamModel?.audioStreams,
- streamModel?.defaultAudioStreamIndex
- );
+ ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
+ oldModel?.mediaStreams?.currentAudioStream,
+ streamModel?.audioStreams,
+ streamModel?.defaultAudioStreamIndex);
final subStreamIndex = selectSubStream(
- ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
- oldModel?.mediaStreams?.currentSubStream,
- streamModel?.subStreams,
- streamModel?.defaultSubStreamIndex
- );
+ ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
+ oldModel?.mediaStreams?.currentSubStream,
+ streamModel?.subStreams,
+ streamModel?.defaultSubStreamIndex);
final Response response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id,
@@ -345,14 +343,12 @@ class PlaybackModelHelper {
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
playbackModel.mediaStreams?.currentAudioStream,
playbackModel.audioStreams,
- playbackModel.mediaStreams?.defaultAudioStreamIndex
- );
+ playbackModel.mediaStreams?.defaultAudioStreamIndex);
final subIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
playbackModel.mediaStreams?.currentSubStream,
playbackModel.subStreams,
- playbackModel.mediaStreams?.defaultSubStreamIndex
- );
+ playbackModel.mediaStreams?.defaultSubStreamIndex);
Response response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id,
diff --git a/lib/models/recommended_model.dart b/lib/models/recommended_model.dart
index 16f65dd..733a245 100644
--- a/lib/models/recommended_model.dart
+++ b/lib/models/recommended_model.dart
@@ -1,20 +1,66 @@
-// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'package:flutter/material.dart';
+
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
+import 'package:fladder/util/localization_helper.dart';
+
+sealed class NameSwitch {
+ const NameSwitch();
+
+ String label(BuildContext context);
+}
+
+class NextUp extends NameSwitch {
+ const NextUp();
+
+ @override
+ String label(BuildContext context) => context.localized.nextUp;
+}
+
+class Latest extends NameSwitch {
+ const Latest();
+
+ @override
+ String label(BuildContext context) => context.localized.latest;
+}
+
+class Other extends NameSwitch {
+ final String customLabel;
+
+ const Other(this.customLabel);
+
+ @override
+ String label(BuildContext context) => customLabel;
+}
+
+extension RecommendationTypeExtenstion on RecommendationType {
+ String label(BuildContext context) => switch (this) {
+ RecommendationType.similartorecentlyplayed => context.localized.similarToRecentlyPlayed,
+ RecommendationType.similartolikeditem => context.localized.similarToLikedItem,
+ RecommendationType.hasdirectorfromrecentlyplayed => context.localized.hasDirectorFromRecentlyPlayed,
+ RecommendationType.hasactorfromrecentlyplayed => context.localized.hasActorFromRecentlyPlayed,
+ RecommendationType.haslikeddirector => context.localized.hasLikedDirector,
+ RecommendationType.haslikedactor => context.localized.hasLikedActor,
+ _ => "",
+ };
+}
class RecommendedModel {
- final String name;
+ final NameSwitch name;
final List posters;
- final String type;
+ final RecommendationType? type;
RecommendedModel({
required this.name,
required this.posters,
- required this.type,
+ this.type,
});
RecommendedModel copyWith({
- String? name,
+ NameSwitch? name,
List? posters,
- String? type,
+ RecommendationType? type,
}) {
return RecommendedModel(
name: name ?? this.name,
@@ -22,4 +68,12 @@ class RecommendedModel {
type: type ?? this.type,
);
}
+
+ factory RecommendedModel.fromBaseDto(RecommendationDto e, Ref ref) {
+ return RecommendedModel(
+ name: Other(e.baselineItemName ?? ""),
+ posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
+ type: e.recommendationType,
+ );
+ }
}
diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart
index 688f3ed..163ad28 100644
--- a/lib/models/settings/client_settings_model.dart
+++ b/lib/models/settings/client_settings_model.dart
@@ -33,7 +33,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
@Default(true) bool requireWifi,
@Default(false) bool showAllCollectionTypes,
@Default(2) int maxConcurrentDownloads,
- @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant,
+ @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
int? libraryPageSize,
}) = _ClientSettingsModel;
diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart
index 50b2f0a..4cc3063 100644
--- a/lib/models/settings/client_settings_model.freezed.dart
+++ b/lib/models/settings/client_settings_model.freezed.dart
@@ -375,7 +375,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
this.requireWifi = true,
this.showAllCollectionTypes = false,
this.maxConcurrentDownloads = 2,
- this.schemeVariant = DynamicSchemeVariant.tonalSpot,
+ this.schemeVariant = DynamicSchemeVariant.rainbow,
this.libraryPageSize})
: super._();
diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart
index c2a34fa..ae036e7 100644
--- a/lib/models/settings/client_settings_model.g.dart
+++ b/lib/models/settings/client_settings_model.g.dart
@@ -40,7 +40,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
(json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2,
schemeVariant: $enumDecodeNullable(
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
- DynamicSchemeVariant.tonalSpot,
+ DynamicSchemeVariant.rainbow,
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
);
diff --git a/lib/models/settings/home_settings_model.dart b/lib/models/settings/home_settings_model.dart
index b6102b7..3c38c7b 100644
--- a/lib/models/settings/home_settings_model.dart
+++ b/lib/models/settings/home_settings_model.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
part 'home_settings_model.freezed.dart';
@@ -36,42 +37,6 @@ T selectAvailableOrSmaller(T value, Set availableOptions, List allOptio
return availableOptions.first;
}
-enum ViewSize {
- phone,
- tablet,
- desktop;
-
- const ViewSize();
-
- String label(BuildContext context) => switch (this) {
- ViewSize.phone => context.localized.phone,
- ViewSize.tablet => context.localized.tablet,
- ViewSize.desktop => context.localized.desktop,
- };
-
- bool operator >(ViewSize other) => index > other.index;
- bool operator >=(ViewSize other) => index >= other.index;
- bool operator <(ViewSize other) => index < other.index;
- bool operator <=(ViewSize other) => index <= other.index;
-}
-
-enum LayoutMode {
- single,
- dual;
-
- const LayoutMode();
-
- String label(BuildContext context) => switch (this) {
- LayoutMode.single => context.localized.layoutModeSingle,
- LayoutMode.dual => context.localized.layoutModeDual,
- };
-
- bool operator >(ViewSize other) => index > other.index;
- bool operator >=(ViewSize other) => index >= other.index;
- bool operator <(ViewSize other) => index < other.index;
- bool operator <=(ViewSize other) => index <= other.index;
-}
-
enum HomeBanner {
hide,
carousel,
diff --git a/lib/models/view_model.dart b/lib/models/view_model.dart
index 31e4b0b..53c8d2b 100644
--- a/lib/models/view_model.dart
+++ b/lib/models/view_model.dart
@@ -1,9 +1,17 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
+import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/item_base_model.dart';
+import 'package:fladder/models/items/images_models.dart';
+import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
+import 'package:fladder/widgets/shared/item_actions.dart';
class ViewModel {
final String name;
@@ -16,7 +24,9 @@ class ViewModel {
final CollectionType collectionType;
final dto.PlayAccess playAccess;
final List recentlyAdded;
+ final ImagesData? imageData;
final int childCount;
+ final String? path;
ViewModel({
required this.name,
required this.id,
@@ -28,7 +38,9 @@ class ViewModel {
required this.collectionType,
required this.playAccess,
required this.recentlyAdded,
+ required this.imageData,
required this.childCount,
+ required this.path,
});
ViewModel copyWith({
@@ -42,7 +54,9 @@ class ViewModel {
CollectionType? collectionType,
dto.PlayAccess? playAccess,
List? recentlyAdded,
+ ImagesData? imageData,
int? childCount,
+ String? path,
}) {
return ViewModel(
name: name ?? this.name,
@@ -55,7 +69,9 @@ class ViewModel {
collectionType: collectionType ?? this.collectionType,
playAccess: playAccess ?? this.playAccess,
recentlyAdded: recentlyAdded ?? this.recentlyAdded,
+ imageData: imageData ?? this.imageData,
childCount: childCount ?? this.childCount,
+ path: path ?? this.path,
);
}
@@ -69,11 +85,13 @@ class ViewModel {
canDownload: item.canDownload ?? false,
parentId: item.parentId ?? "",
recentlyAdded: [],
+ imageData: ImagesData.fromBaseItem(item, ref),
collectionType: CollectionType.values
.firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ??
CollectionType.folders,
playAccess: item.playAccess ?? PlayAccess.none,
childCount: item.childCount ?? 0,
+ path: "",
);
}
@@ -88,6 +106,27 @@ class ViewModel {
return id.hashCode ^ serverId.hashCode;
}
+ NavigationButton toNavigationButton(
+ bool selected,
+ bool horizontal,
+ bool expanded,
+ FutureOr Function() action, {
+ FutureOr Function()? onLongPress,
+ List? trailing,
+ }) {
+ return NavigationButton(
+ label: name,
+ selected: selected,
+ onPressed: action,
+ onLongPress: onLongPress,
+ horizontal: horizontal,
+ expanded: expanded,
+ trailing: trailing ?? [],
+ selectedIcon: Icon(collectionType.icon),
+ icon: Icon(collectionType.iconOutlined),
+ );
+ }
+
@override
String toString() {
return 'ViewModel(name: $name, id: $id, serverId: $serverId, dateCreated: $dateCreated, canDelete: $canDelete, canDownload: $canDownload, parentId: $parentId, collectionType: $collectionType, playAccess: $playAccess, recentlyAdded: $recentlyAdded, childCount: $childCount)';
diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart
index 9527f56..1c8ca7d 100644
--- a/lib/providers/dashboard_provider.dart
+++ b/lib/providers/dashboard_provider.dart
@@ -1,3 +1,5 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/home_model.dart';
import 'package:fladder/models/item_base_model.dart';
@@ -6,7 +8,6 @@ import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/list_extensions.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
final dashboardProvider = StateNotifierProvider((ref) {
return DashboardNotifier(ref);
@@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
+ ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.video],
enableTotalRecordCount: false,
@@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
+ ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.audio],
enableTotalRecordCount: false,
@@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
+ ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.book],
enableTotalRecordCount: false,
@@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier {
final nextResponse = await api.showsNextUpGet(
limit: 16,
- nextUpDateCutoff: DateTime.now()
- .subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
+ nextUpDateCutoff: DateTime.now().subtract(
+ ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
+ ItemFields.primaryimageaspectratio,
],
);
diff --git a/lib/providers/favourites_provider.dart b/lib/providers/favourites_provider.dart
index d948396..907bf46 100644
--- a/lib/providers/favourites_provider.dart
+++ b/lib/providers/favourites_provider.dart
@@ -1,4 +1,6 @@
import 'package:chopper/chopper.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/favourites_model.dart';
import 'package:fladder/models/item_base_model.dart';
@@ -6,7 +8,6 @@ import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
final favouritesProvider = StateNotifierProvider((ref) {
return FavouritesNotifier(ref);
@@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier {
isFavorite: true,
limit: 10,
sortOrder: [SortOrder.ascending],
- sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
+ sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
);
final response2 = await api.itemsGet(
parentId: viewModel?.id,
@@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier {
limit: 10,
includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder],
sortOrder: [SortOrder.ascending],
- sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
+ sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
);
return [...?response.body?.items, ...?response2.body?.items];
}
diff --git a/lib/providers/library_provider.dart b/lib/providers/library_provider.dart
deleted file mode 100644
index c32489b..0000000
--- a/lib/providers/library_provider.dart
+++ /dev/null
@@ -1,174 +0,0 @@
-import 'package:chopper/chopper.dart';
-import 'package:fladder/models/item_base_model.dart';
-import 'package:fladder/models/items/photos_model.dart';
-import 'package:fladder/models/library_model.dart';
-import 'package:fladder/models/recommended_model.dart';
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/api_provider.dart';
-import 'package:fladder/providers/service_provider.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
-
-bool _useFolders(ViewModel model) {
- switch (model.collectionType) {
- case CollectionType.boxsets:
- case CollectionType.homevideos:
- case CollectionType.folders:
- return true;
- default:
- return false;
- }
-}
-
-final libraryProvider = StateNotifierProvider.autoDispose.family((ref, id) {
- return LibraryNotifier(ref);
-});
-
-class LibraryNotifier extends StateNotifier {
- LibraryNotifier(this.ref) : super(null);
-
- final Ref ref;
-
- late final JellyService api = ref.read(jellyApiProvider);
-
- set loading(bool value) {
- state = state?.copyWith(loading: value);
- }
-
- bool get loading => state?.loading ?? true;
-
- Future setupLibrary(ViewModel viewModel) async {
- state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie);
- }
-
- Future loadLibrary(ViewModel viewModel) async {
- final response = await api.itemsGet(
- parentId: viewModel.id,
- sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear],
- isMissing: false,
- excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [],
- fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid],
- );
- state = state?.copyWith(posters: response.body?.items);
- loading = false;
- return response;
- }
-
- Future loadRecommendations(ViewModel viewModel) async {
- loading = true;
- //Clear recommendations because of all the copying
- state = state?.copyWith(recommendations: []);
- final latest = await api.usersUserIdItemsLatestGet(
- parentId: viewModel.id,
- limit: 14,
- isPlayed: false,
- imageTypeLimit: 1,
- includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null,
- );
- state = state?.copyWith(
- recommendations: [
- ...?state?.recommendations,
- RecommendedModel(
- name: "Latest",
- posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
- type: "Latest",
- ),
- ],
- );
- if (viewModel.collectionType == CollectionType.movies) {
- final response = await api.moviesRecommendationsGet(
- parentId: viewModel.id,
- categoryLimit: 6,
- itemLimit: 8,
- fields: [ItemFields.mediasourcecount],
- );
- state = state?.copyWith(recommendations: [
- ...?state?.recommendations,
- ...response.body?.map(
- (e) => RecommendedModel(
- name: e.baselineItemName ?? "",
- posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
- type: e.recommendationType.toString(),
- ),
- ) ??
- [],
- ]);
- loading = false;
- } else {
- final nextUp = await api.showsNextUpGet(
- parentId: viewModel.id,
- limit: 14,
- imageTypeLimit: 1,
- fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
- );
- state = state?.copyWith(recommendations: [
- ...?state?.recommendations,
- ...[
- RecommendedModel(
- name: "Next up",
- posters: nextUp.body?.items
- ?.map(
- (e) => ItemBaseModel.fromBaseDto(
- e,
- ref,
- ),
- )
- .toList() ??
- [],
- type: "Latest series")
- ],
- ]);
- loading = false;
- }
- }
-
- Future loadFavourites(ViewModel viewModel) async {
- loading = true;
- final response = await api.itemsGet(
- parentId: viewModel.id,
- isFavorite: true,
- recursive: true,
- );
-
- state = state?.copyWith(favourites: response.body?.items);
- loading = false;
- return response;
- }
-
- Future loadTimeline(ViewModel viewModel) async {
- loading = true;
- final response = await api.itemsGet(
- parentId: viewModel.id,
- recursive: true,
- fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated],
- sortBy: [ItemSortBy.datecreated],
- sortOrder: [SortOrder.descending],
- includeItemTypes: [
- BaseItemKind.photo,
- BaseItemKind.video,
- ],
- );
- state = state?.copyWith(
- timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(),
- );
- loading = false;
- return response;
- }
-
- Future loadGenres(ViewModel viewModel) async {
- final genres = await api.genresGet(
- sortBy: [ItemSortBy.sortname],
- sortOrder: [SortOrder.ascending],
- includeItemTypes: viewModel.collectionType == CollectionType.movies
- ? [BaseItemKind.movie]
- : [
- BaseItemKind.series,
- ],
- parentId: viewModel.id,
- );
- state = state?.copyWith(
- genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList());
- return null;
- }
-}
diff --git a/lib/providers/library_screen_provider.dart b/lib/providers/library_screen_provider.dart
new file mode 100644
index 0000000..811ed72
--- /dev/null
+++ b/lib/providers/library_screen_provider.dart
@@ -0,0 +1,219 @@
+import 'package:flutter/material.dart';
+
+import 'package:chopper/chopper.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
+import 'package:fladder/models/collection_types.dart';
+import 'package:fladder/models/item_base_model.dart';
+import 'package:fladder/models/items/item_shared_models.dart';
+import 'package:fladder/models/recommended_model.dart';
+import 'package:fladder/models/view_model.dart';
+import 'package:fladder/providers/api_provider.dart';
+import 'package:fladder/providers/service_provider.dart';
+import 'package:fladder/providers/views_provider.dart';
+import 'package:fladder/util/localization_helper.dart';
+
+part 'library_screen_provider.freezed.dart';
+part 'library_screen_provider.g.dart';
+
+enum LibraryViewType {
+ recommended,
+ favourites,
+ genres;
+
+ const LibraryViewType();
+
+ String label(BuildContext context) => switch (this) {
+ LibraryViewType.recommended => context.localized.recommended,
+ LibraryViewType.favourites => context.localized.favorites,
+ LibraryViewType.genres => context.localized.genre(2),
+ };
+
+ IconData get icon => switch (this) {
+ LibraryViewType.recommended => IconsaxPlusLinear.star,
+ LibraryViewType.favourites => IconsaxPlusLinear.heart,
+ LibraryViewType.genres => IconsaxPlusLinear.hierarchy_3,
+ };
+
+ IconData get iconSelected => switch (this) {
+ LibraryViewType.recommended => IconsaxPlusBold.star,
+ LibraryViewType.favourites => IconsaxPlusBold.heart,
+ LibraryViewType.genres => IconsaxPlusBold.hierarchy_3,
+ };
+}
+
+@Freezed(fromJson: false, toJson: false)
+class LibraryScreenModel with _$LibraryScreenModel {
+ factory LibraryScreenModel({
+ @Default([]) List views,
+ ViewModel? selectedViewModel,
+ @Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set viewType,
+ @Default([]) List recommendations,
+ @Default([]) List genres,
+ @Default([]) List favourites,
+ }) = _LibraryScreenModel;
+}
+
+@Riverpod(keepAlive: true)
+class LibraryScreen extends _$LibraryScreen {
+ late final JellyService api = ref.read(jellyApiProvider);
+
+ @override
+ LibraryScreenModel build() => LibraryScreenModel();
+
+ Future fetchAllLibraries() async {
+ final views = await ref.read(viewsProvider.notifier).fetchViews();
+ state = state.copyWith(views: views?.views ?? []);
+ if (state.views.isEmpty) return;
+ final viewModel = state.selectedViewModel ?? state.views.firstOrNull;
+ if (viewModel == null) return;
+ selectLibrary(viewModel);
+ await loadLibrary(viewModel);
+ }
+
+ Future selectLibrary(ViewModel viewModel) async {
+ state = state.copyWith(selectedViewModel: viewModel);
+ }
+
+ Future setViewType(Set type) async {
+ state = state.copyWith(viewType: type);
+ }
+
+ Future loadLibrary(ViewModel viewModel) async {
+ await loadRecommendations(viewModel);
+ await loadGenres(viewModel);
+ await loadFavourites(viewModel);
+ return null;
+ }
+
+ Future loadRecommendations(ViewModel viewModel) async {
+ List newRecommendations = [];
+ final latest = await api.usersUserIdItemsLatestGet(
+ parentId: viewModel.id,
+ limit: 14,
+ isPlayed: false,
+ imageTypeLimit: 1,
+ includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
+ );
+ newRecommendations = [
+ ...newRecommendations,
+ RecommendedModel(
+ name: const Latest(),
+ posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
+ type: null,
+ ),
+ ];
+ if (viewModel.collectionType == CollectionType.movies) {
+ final response = await api.moviesRecommendationsGet(
+ parentId: viewModel.id,
+ categoryLimit: 6,
+ itemLimit: 14,
+ fields: [ItemFields.mediasourcecount],
+ );
+ newRecommendations = [
+ ...newRecommendations,
+ ...(response.body?.map(
+ (e) => RecommendedModel.fromBaseDto(e, ref),
+ ) ??
+ [])
+ ];
+ } else {
+ final nextUp = await api.showsNextUpGet(
+ parentId: viewModel.id,
+ limit: 14,
+ imageTypeLimit: 1,
+ fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
+ );
+ newRecommendations = [
+ ...newRecommendations,
+ RecommendedModel(
+ name: const NextUp(),
+ posters: nextUp.body?.items
+ ?.map(
+ (e) => ItemBaseModel.fromBaseDto(
+ e,
+ ref,
+ ),
+ )
+ .toList() ??
+ [],
+ type: null,
+ )
+ ];
+ }
+
+ state = state.copyWith(
+ recommendations: newRecommendations,
+ );
+ }
+
+ Future loadFavourites(ViewModel viewModel) async {
+ final response = await api.itemsGet(
+ parentId: viewModel.id,
+ isFavorite: true,
+ recursive: true,
+ includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
+ enableImageTypes: [ImageType.primary],
+ fields: [
+ ItemFields.primaryimageaspectratio,
+ ItemFields.mediasourcecount,
+ ],
+ enableTotalRecordCount: false,
+ );
+
+ state = state.copyWith(favourites: response.body?.items ?? []);
+ return response;
+ }
+
+ Future loadGenres(ViewModel viewModel) async {
+ final genres = await api.genresGet(
+ sortBy: [ItemSortBy.sortname],
+ sortOrder: [SortOrder.ascending],
+ includeItemTypes:
+ viewModel.collectionType == CollectionType.movies ? [BaseItemKind.movie] : [BaseItemKind.series],
+ parentId: viewModel.id,
+ );
+
+ final filteredGenres = (genres.body?.items?.map(
+ (item) => GenreItems(id: item.id ?? "", name: item.name ?? ""),
+ ) ??
+ [])
+ .toList();
+
+ if (filteredGenres.isEmpty) return null;
+
+ final results = await Future.wait(filteredGenres.map((genre) async {
+ final response = await api.itemsGet(
+ parentId: viewModel.id,
+ genreIds: [genre.id],
+ limit: 9,
+ recursive: true,
+ includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
+ enableImageTypes: [ImageType.primary],
+ fields: [
+ ItemFields.primaryimageaspectratio,
+ ItemFields.mediasourcecount,
+ ],
+ sortBy: [ItemSortBy.random],
+ enableTotalRecordCount: false,
+ imageTypeLimit: 1,
+ sortOrder: [SortOrder.ascending],
+ );
+
+ final items = response.body?.items;
+ if (items != null && items.isNotEmpty) {
+ return RecommendedModel(name: Other(genre.name), posters: items);
+ }
+ return null;
+ }));
+
+ state = state.copyWith(
+ genres: results.whereType().toList(),
+ );
+
+ return null;
+ }
+}
diff --git a/lib/providers/library_screen_provider.freezed.dart b/lib/providers/library_screen_provider.freezed.dart
new file mode 100644
index 0000000..b2a6d84
--- /dev/null
+++ b/lib/providers/library_screen_provider.freezed.dart
@@ -0,0 +1,301 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'library_screen_provider.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+/// @nodoc
+mixin _$LibraryScreenModel {
+ List get views => throw _privateConstructorUsedError;
+ ViewModel? get selectedViewModel => throw _privateConstructorUsedError;
+ Set get viewType => throw _privateConstructorUsedError;
+ List get recommendations =>
+ throw _privateConstructorUsedError;
+ List get genres => throw _privateConstructorUsedError;
+ List get favourites => throw _privateConstructorUsedError;
+
+ /// Create a copy of LibraryScreenModel
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $LibraryScreenModelCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $LibraryScreenModelCopyWith<$Res> {
+ factory $LibraryScreenModelCopyWith(
+ LibraryScreenModel value, $Res Function(LibraryScreenModel) then) =
+ _$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>;
+ @useResult
+ $Res call(
+ {List views,
+ ViewModel? selectedViewModel,
+ Set viewType,
+ List recommendations,
+ List genres,
+ List favourites});
+}
+
+/// @nodoc
+class _$LibraryScreenModelCopyWithImpl<$Res, $Val extends LibraryScreenModel>
+ implements $LibraryScreenModelCopyWith<$Res> {
+ _$LibraryScreenModelCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of LibraryScreenModel
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? views = null,
+ Object? selectedViewModel = freezed,
+ Object? viewType = null,
+ Object? recommendations = null,
+ Object? genres = null,
+ Object? favourites = null,
+ }) {
+ return _then(_value.copyWith(
+ views: null == views
+ ? _value.views
+ : views // ignore: cast_nullable_to_non_nullable
+ as List,
+ selectedViewModel: freezed == selectedViewModel
+ ? _value.selectedViewModel
+ : selectedViewModel // ignore: cast_nullable_to_non_nullable
+ as ViewModel?,
+ viewType: null == viewType
+ ? _value.viewType
+ : viewType // ignore: cast_nullable_to_non_nullable
+ as Set,
+ recommendations: null == recommendations
+ ? _value.recommendations
+ : recommendations // ignore: cast_nullable_to_non_nullable
+ as List,
+ genres: null == genres
+ ? _value.genres
+ : genres // ignore: cast_nullable_to_non_nullable
+ as List,
+ favourites: null == favourites
+ ? _value.favourites
+ : favourites // ignore: cast_nullable_to_non_nullable
+ as List,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$LibraryScreenModelImplCopyWith<$Res>
+ implements $LibraryScreenModelCopyWith<$Res> {
+ factory _$$LibraryScreenModelImplCopyWith(_$LibraryScreenModelImpl value,
+ $Res Function(_$LibraryScreenModelImpl) then) =
+ __$$LibraryScreenModelImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {List views,
+ ViewModel? selectedViewModel,
+ Set viewType,
+ List recommendations,
+ List genres,
+ List favourites});
+}
+
+/// @nodoc
+class __$$LibraryScreenModelImplCopyWithImpl<$Res>
+ extends _$LibraryScreenModelCopyWithImpl<$Res, _$LibraryScreenModelImpl>
+ implements _$$LibraryScreenModelImplCopyWith<$Res> {
+ __$$LibraryScreenModelImplCopyWithImpl(_$LibraryScreenModelImpl _value,
+ $Res Function(_$LibraryScreenModelImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of LibraryScreenModel
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? views = null,
+ Object? selectedViewModel = freezed,
+ Object? viewType = null,
+ Object? recommendations = null,
+ Object? genres = null,
+ Object? favourites = null,
+ }) {
+ return _then(_$LibraryScreenModelImpl(
+ views: null == views
+ ? _value._views
+ : views // ignore: cast_nullable_to_non_nullable
+ as List,
+ selectedViewModel: freezed == selectedViewModel
+ ? _value.selectedViewModel
+ : selectedViewModel // ignore: cast_nullable_to_non_nullable
+ as ViewModel?,
+ viewType: null == viewType
+ ? _value._viewType
+ : viewType // ignore: cast_nullable_to_non_nullable
+ as Set,
+ recommendations: null == recommendations
+ ? _value._recommendations
+ : recommendations // ignore: cast_nullable_to_non_nullable
+ as List,
+ genres: null == genres
+ ? _value._genres
+ : genres // ignore: cast_nullable_to_non_nullable
+ as List,
+ favourites: null == favourites
+ ? _value._favourites
+ : favourites // ignore: cast_nullable_to_non_nullable
+ as List,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$LibraryScreenModelImpl implements _LibraryScreenModel {
+ _$LibraryScreenModelImpl(
+ {final List views = const [],
+ this.selectedViewModel,
+ final Set viewType = const {
+ LibraryViewType.recommended,
+ LibraryViewType.favourites
+ },
+ final List recommendations = const [],
+ final List genres = const [],
+ final List favourites = const []})
+ : _views = views,
+ _viewType = viewType,
+ _recommendations = recommendations,
+ _genres = genres,
+ _favourites = favourites;
+
+ final List _views;
+ @override
+ @JsonKey()
+ List get views {
+ if (_views is EqualUnmodifiableListView) return _views;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_views);
+ }
+
+ @override
+ final ViewModel? selectedViewModel;
+ final Set _viewType;
+ @override
+ @JsonKey()
+ Set get viewType {
+ if (_viewType is EqualUnmodifiableSetView) return _viewType;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableSetView(_viewType);
+ }
+
+ final List _recommendations;
+ @override
+ @JsonKey()
+ List get recommendations {
+ if (_recommendations is EqualUnmodifiableListView) return _recommendations;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_recommendations);
+ }
+
+ final List _genres;
+ @override
+ @JsonKey()
+ List get genres {
+ if (_genres is EqualUnmodifiableListView) return _genres;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_genres);
+ }
+
+ final List _favourites;
+ @override
+ @JsonKey()
+ List get favourites {
+ if (_favourites is EqualUnmodifiableListView) return _favourites;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_favourites);
+ }
+
+ @override
+ String toString() {
+ return 'LibraryScreenModel(views: $views, selectedViewModel: $selectedViewModel, viewType: $viewType, recommendations: $recommendations, genres: $genres, favourites: $favourites)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$LibraryScreenModelImpl &&
+ const DeepCollectionEquality().equals(other._views, _views) &&
+ (identical(other.selectedViewModel, selectedViewModel) ||
+ other.selectedViewModel == selectedViewModel) &&
+ const DeepCollectionEquality().equals(other._viewType, _viewType) &&
+ const DeepCollectionEquality()
+ .equals(other._recommendations, _recommendations) &&
+ const DeepCollectionEquality().equals(other._genres, _genres) &&
+ const DeepCollectionEquality()
+ .equals(other._favourites, _favourites));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ const DeepCollectionEquality().hash(_views),
+ selectedViewModel,
+ const DeepCollectionEquality().hash(_viewType),
+ const DeepCollectionEquality().hash(_recommendations),
+ const DeepCollectionEquality().hash(_genres),
+ const DeepCollectionEquality().hash(_favourites));
+
+ /// Create a copy of LibraryScreenModel
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
+ __$$LibraryScreenModelImplCopyWithImpl<_$LibraryScreenModelImpl>(
+ this, _$identity);
+}
+
+abstract class _LibraryScreenModel implements LibraryScreenModel {
+ factory _LibraryScreenModel(
+ {final List views,
+ final ViewModel? selectedViewModel,
+ final Set viewType,
+ final List recommendations,
+ final List genres,
+ final List favourites}) = _$LibraryScreenModelImpl;
+
+ @override
+ List get views;
+ @override
+ ViewModel? get selectedViewModel;
+ @override
+ Set get viewType;
+ @override
+ List get recommendations;
+ @override
+ List get genres;
+ @override
+ List get favourites;
+
+ /// Create a copy of LibraryScreenModel
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/providers/library_screen_provider.g.dart b/lib/providers/library_screen_provider.g.dart
new file mode 100644
index 0000000..7017b4e
--- /dev/null
+++ b/lib/providers/library_screen_provider.g.dart
@@ -0,0 +1,26 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'library_screen_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c';
+
+/// See also [LibraryScreen].
+@ProviderFor(LibraryScreen)
+final libraryScreenProvider =
+ NotifierProvider.internal(
+ LibraryScreen.new,
+ name: r'libraryScreenProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$libraryScreenHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$LibraryScreen = Notifier;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/providers/library_search_provider.dart b/lib/providers/library_search_provider.dart
index 64b626d..a6360aa 100644
--- a/lib/providers/library_search_provider.dart
+++ b/lib/providers/library_search_provider.dart
@@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier {
.toSet()
.toList();
var tempState = state.copyWith();
- final genres = mappedList
- .expand((element) => element?.genres ?? [])
- .nonNulls
- .sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
+ final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel))))
+ .expand((element) => element)
+ .toSet()
+ .toList();
final tags = mappedList
.expand((element) => element?.tags ?? [])
.sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
tempState = tempState.copyWith(
types: state.types.setAll(false).setKeys(enabledCollections, true),
- genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres),
+ genres: {for (var element in genres) element.name: false}.replaceMap(tempState.genres),
studios: {for (var element in studios) element: false}.replaceMap(tempState.studios),
tags: {for (var element in tags) element: false}.replaceMap(tempState.tags),
);
@@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier {
return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
}
+ Future> _loadGenres(ViewModel viewModel) async {
+ final response = await api.genresGet(parentId: viewModel.id);
+ return response.body?.items?.map((e) => GenreItems(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
+ }
+
Future _loadLibrary(
{ViewModel? viewModel,
bool? recursive,
diff --git a/lib/providers/settings/home_settings_provider.dart b/lib/providers/settings/home_settings_provider.dart
index 0d20f50..335c2ce 100644
--- a/lib/providers/settings/home_settings_provider.dart
+++ b/lib/providers/settings/home_settings_provider.dart
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/shared_provider.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
final homeSettingsProvider = StateNotifierProvider((ref) {
return HomeSettingsNotifier(ref);
diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart
index 45e6dd2..285aa71 100644
--- a/lib/providers/sync/background_download_provider.g.dart
+++ b/lib/providers/sync/background_download_provider.g.dart
@@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
// **************************************************************************
String _$backgroundDownloaderHash() =>
- r'df72b6338a8e80178935985ba17c43bf720f4522';
+ r'dc27f708fc2f1695d37afcb99f8814bc024037af';
/// See also [BackgroundDownloader].
@ProviderFor(BackgroundDownloader)
diff --git a/lib/providers/user_provider.g.dart b/lib/providers/user_provider.g.dart
index a699761..07e79b3 100644
--- a/lib/providers/user_provider.g.dart
+++ b/lib/providers/user_provider.g.dart
@@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef;
-String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900';
+String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a';
/// See also [User].
@ProviderFor(User)
diff --git a/lib/providers/views_provider.dart b/lib/providers/views_provider.dart
index 1a0ad02..c04e5a0 100644
--- a/lib/providers/views_provider.dart
+++ b/lib/providers/views_provider.dart
@@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier {
late final JellyService api = ref.read(jellyApiProvider);
- Future fetchViews() async {
- if (state.loading) return;
+ Future fetchViews() async {
+ if (state.loading) return null;
final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes));
final response = await api.usersUserIdViewsGet(
includeExternalContent: showAllCollections,
@@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
+ ItemFields.primaryimageaspectratio,
],
);
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
@@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier {
.where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true))
.toList(),
loading: false);
+ return state;
}
void clear() {
diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart
index 2f44c78..c04beaf 100644
--- a/lib/routes/auto_router.dart
+++ b/lib/routes/auto_router.dart
@@ -51,6 +51,7 @@ final List homeRoutes = [
_dashboardRoute,
_favouritesRoute,
_syncedRoute,
+ _librariesRoute,
];
final List _defaultRoutes = [
@@ -79,6 +80,13 @@ final AutoRoute _syncedRoute = CustomRoute(
path: 'synced',
);
+final AutoRoute _librariesRoute = CustomRoute(
+ page: LibraryRoute.page,
+ transitionsBuilder: TransitionsBuilders.fadeIn,
+ maintainState: false,
+ path: 'libraries',
+);
+
final List _settingsChildren = [
CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'),
CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'),
diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart
index 3b79be3..5ebe950 100644
--- a/lib/routes/auto_router.gr.dart
+++ b/lib/routes/auto_router.gr.dart
@@ -8,35 +8,36 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'package:auto_route/auto_route.dart' as _i16;
-import 'package:fladder/models/item_base_model.dart' as _i17;
-import 'package:fladder/models/items/photos_model.dart' as _i20;
+import 'package:auto_route/auto_route.dart' as _i17;
+import 'package:fladder/models/item_base_model.dart' as _i18;
+import 'package:fladder/models/items/photos_model.dart' as _i21;
import 'package:fladder/models/library_search/library_search_options.dart'
- as _i19;
+ as _i20;
import 'package:fladder/routes/nested_details_screen.dart' as _i4;
import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3;
import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5;
import 'package:fladder/screens/home_screen.dart' as _i6;
+import 'package:fladder/screens/library/library_screen.dart' as _i7;
import 'package:fladder/screens/library_search/library_search_screen.dart'
- as _i7;
-import 'package:fladder/screens/login/lock_screen.dart' as _i8;
-import 'package:fladder/screens/login/login_screen.dart' as _i9;
+ as _i8;
+import 'package:fladder/screens/login/lock_screen.dart' as _i9;
+import 'package:fladder/screens/login/login_screen.dart' as _i10;
import 'package:fladder/screens/settings/about_settings_page.dart' as _i1;
import 'package:fladder/screens/settings/client_settings_page.dart' as _i2;
-import 'package:fladder/screens/settings/player_settings_page.dart' as _i10;
-import 'package:fladder/screens/settings/security_settings_page.dart' as _i11;
-import 'package:fladder/screens/settings/settings_screen.dart' as _i12;
+import 'package:fladder/screens/settings/player_settings_page.dart' as _i11;
+import 'package:fladder/screens/settings/security_settings_page.dart' as _i12;
+import 'package:fladder/screens/settings/settings_screen.dart' as _i13;
import 'package:fladder/screens/settings/settings_selection_screen.dart'
- as _i13;
-import 'package:fladder/screens/splash_screen.dart' as _i14;
-import 'package:fladder/screens/syncing/synced_screen.dart' as _i15;
-import 'package:flutter/foundation.dart' as _i18;
-import 'package:flutter/material.dart' as _i21;
+ as _i14;
+import 'package:fladder/screens/splash_screen.dart' as _i15;
+import 'package:fladder/screens/syncing/synced_screen.dart' as _i16;
+import 'package:flutter/foundation.dart' as _i19;
+import 'package:flutter/material.dart' as _i22;
/// generated route for
/// [_i1.AboutSettingsPage]
-class AboutSettingsRoute extends _i16.PageRouteInfo {
- const AboutSettingsRoute({List<_i16.PageRouteInfo>? children})
+class AboutSettingsRoute extends _i17.PageRouteInfo {
+ const AboutSettingsRoute({List<_i17.PageRouteInfo>? children})
: super(
AboutSettingsRoute.name,
initialChildren: children,
@@ -44,7 +45,7 @@ class AboutSettingsRoute extends _i16.PageRouteInfo {
static const String name = 'AboutSettingsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
return const _i1.AboutSettingsPage();
@@ -54,8 +55,8 @@ class AboutSettingsRoute extends _i16.PageRouteInfo {
/// generated route for
/// [_i2.ClientSettingsPage]
-class ClientSettingsRoute extends _i16.PageRouteInfo {
- const ClientSettingsRoute({List<_i16.PageRouteInfo>? children})
+class ClientSettingsRoute extends _i17.PageRouteInfo {
+ const ClientSettingsRoute({List<_i17.PageRouteInfo>? children})
: super(
ClientSettingsRoute.name,
initialChildren: children,
@@ -63,7 +64,7 @@ class ClientSettingsRoute extends _i16.PageRouteInfo {
static const String name = 'ClientSettingsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
return const _i2.ClientSettingsPage();
@@ -73,8 +74,8 @@ class ClientSettingsRoute extends _i16.PageRouteInfo {
/// generated route for
/// [_i3.DashboardScreen]
-class DashboardRoute extends _i16.PageRouteInfo {
- const DashboardRoute({List<_i16.PageRouteInfo>? children})
+class DashboardRoute extends _i17.PageRouteInfo {
+ const DashboardRoute({List<_i17.PageRouteInfo>? children})
: super(
DashboardRoute.name,
initialChildren: children,
@@ -82,7 +83,7 @@ class DashboardRoute extends _i16.PageRouteInfo {
static const String name = 'DashboardRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
return const _i3.DashboardScreen();
@@ -92,12 +93,12 @@ class DashboardRoute extends _i16.PageRouteInfo {
/// generated route for
/// [_i4.DetailsScreen]
-class DetailsRoute extends _i16.PageRouteInfo {
+class DetailsRoute extends _i17.PageRouteInfo {
DetailsRoute({
String id = '',
- _i17.ItemBaseModel? item,
- _i18.Key? key,
- List<_i16.PageRouteInfo>? children,
+ _i18.ItemBaseModel? item,
+ _i19.Key? key,
+ List<_i17.PageRouteInfo>? children,
}) : super(
DetailsRoute.name,
args: DetailsRouteArgs(
@@ -111,7 +112,7 @@ class DetailsRoute extends _i16.PageRouteInfo {
static const String name = 'DetailsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
final queryParams = data.queryParams;
@@ -139,9 +140,9 @@ class DetailsRouteArgs {
final String id;
- final _i17.ItemBaseModel? item;
+ final _i18.ItemBaseModel? item;
- final _i18.Key? key;
+ final _i19.Key? key;
@override
String toString() {
@@ -151,8 +152,8 @@ class DetailsRouteArgs {
/// generated route for
/// [_i5.FavouritesScreen]
-class FavouritesRoute extends _i16.PageRouteInfo {
- const FavouritesRoute({List<_i16.PageRouteInfo>? children})
+class FavouritesRoute extends _i17.PageRouteInfo {
+ const FavouritesRoute({List<_i17.PageRouteInfo>? children})
: super(
FavouritesRoute.name,
initialChildren: children,
@@ -160,7 +161,7 @@ class FavouritesRoute extends _i16.PageRouteInfo {
static const String name = 'FavouritesRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
return const _i5.FavouritesScreen();
@@ -170,8 +171,8 @@ class FavouritesRoute extends _i16.PageRouteInfo {
/// generated route for
/// [_i6.HomeScreen]
-class HomeRoute extends _i16.PageRouteInfo {
- const HomeRoute({List<_i16.PageRouteInfo>? children})
+class HomeRoute extends _i17.PageRouteInfo {
+ const HomeRoute({List<_i17.PageRouteInfo>? children})
: super(
HomeRoute.name,
initialChildren: children,
@@ -179,7 +180,7 @@ class HomeRoute extends _i16.PageRouteInfo {
static const String name = 'HomeRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
return const _i6.HomeScreen();
@@ -188,17 +189,36 @@ class HomeRoute extends _i16.PageRouteInfo {
}
/// generated route for
-/// [_i7.LibrarySearchScreen]
-class LibrarySearchRoute extends _i16.PageRouteInfo {
+/// [_i7.LibraryScreen]
+class LibraryRoute extends _i17.PageRouteInfo {
+ const LibraryRoute({List<_i17.PageRouteInfo>? children})
+ : super(
+ LibraryRoute.name,
+ initialChildren: children,
+ );
+
+ static const String name = 'LibraryRoute';
+
+ static _i17.PageInfo page = _i17.PageInfo(
+ name,
+ builder: (data) {
+ return const _i7.LibraryScreen();
+ },
+ );
+}
+
+/// generated route for
+/// [_i8.LibrarySearchScreen]
+class LibrarySearchRoute extends _i17.PageRouteInfo {
LibrarySearchRoute({
String? viewModelId,
List? folderId,
bool? favourites,
- _i19.SortingOrder? sortOrder,
- _i19.SortingOptions? sortingOptions,
- _i20.PhotoModel? photoToView,
- _i18.Key? key,
- List<_i16.PageRouteInfo>? children,
+ _i20.SortingOrder? sortOrder,
+ _i20.SortingOptions? sortingOptions,
+ _i21.PhotoModel? photoToView,
+ _i19.Key? key,
+ List<_i17.PageRouteInfo>? children,
}) : super(
LibrarySearchRoute.name,
args: LibrarySearchRouteArgs(
@@ -222,7 +242,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo {
static const String name = 'LibrarySearchRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
final queryParams = data.queryParams;
@@ -234,7 +254,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo {
sortOrder: queryParams.get('sortOrder'),
sortingOptions: queryParams.get('sortOptions'),
));
- return _i7.LibrarySearchScreen(
+ return _i8.LibrarySearchScreen(
viewModelId: args.viewModelId,
folderId: args.folderId,
favourites: args.favourites,
@@ -264,13 +284,13 @@ class LibrarySearchRouteArgs {
final bool? favourites;
- final _i19.SortingOrder? sortOrder;
+ final _i20.SortingOrder? sortOrder;
- final _i19.SortingOptions? sortingOptions;
+ final _i20.SortingOptions? sortingOptions;
- final _i20.PhotoModel? photoToView;
+ final _i21.PhotoModel? photoToView;
- final _i18.Key? key;
+ final _i19.Key? key;
@override
String toString() {
@@ -279,9 +299,9 @@ class LibrarySearchRouteArgs {
}
/// generated route for
-/// [_i8.LockScreen]
-class LockRoute extends _i16.PageRouteInfo {
- const LockRoute({List<_i16.PageRouteInfo>? children})
+/// [_i9.LockScreen]
+class LockRoute extends _i17.PageRouteInfo {
+ const LockRoute({List<_i17.PageRouteInfo>? children})
: super(
LockRoute.name,
initialChildren: children,
@@ -289,18 +309,18 @@ class LockRoute extends _i16.PageRouteInfo {
static const String name = 'LockRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i8.LockScreen();
+ return const _i9.LockScreen();
},
);
}
/// generated route for
-/// [_i9.LoginScreen]
-class LoginRoute extends _i16.PageRouteInfo {
- const LoginRoute({List<_i16.PageRouteInfo>? children})
+/// [_i10.LoginScreen]
+class LoginRoute extends _i17.PageRouteInfo {
+ const LoginRoute({List<_i17.PageRouteInfo>? children})
: super(
LoginRoute.name,
initialChildren: children,
@@ -308,18 +328,18 @@ class LoginRoute extends _i16.PageRouteInfo {
static const String name = 'LoginRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i9.LoginScreen();
+ return const _i10.LoginScreen();
},
);
}
/// generated route for
-/// [_i10.PlayerSettingsPage]
-class PlayerSettingsRoute extends _i16.PageRouteInfo {
- const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children})
+/// [_i11.PlayerSettingsPage]
+class PlayerSettingsRoute extends _i17.PageRouteInfo {
+ const PlayerSettingsRoute({List<_i17.PageRouteInfo>? children})
: super(
PlayerSettingsRoute.name,
initialChildren: children,
@@ -327,18 +347,18 @@ class PlayerSettingsRoute extends _i16.PageRouteInfo {
static const String name = 'PlayerSettingsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i10.PlayerSettingsPage();
+ return const _i11.PlayerSettingsPage();
},
);
}
/// generated route for
-/// [_i11.SecuritySettingsPage]
-class SecuritySettingsRoute extends _i16.PageRouteInfo {
- const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children})
+/// [_i12.SecuritySettingsPage]
+class SecuritySettingsRoute extends _i17.PageRouteInfo {
+ const SecuritySettingsRoute({List<_i17.PageRouteInfo>? children})
: super(
SecuritySettingsRoute.name,
initialChildren: children,
@@ -346,18 +366,18 @@ class SecuritySettingsRoute extends _i16.PageRouteInfo {
static const String name = 'SecuritySettingsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i11.SecuritySettingsPage();
+ return const _i12.SecuritySettingsPage();
},
);
}
/// generated route for
-/// [_i12.SettingsScreen]
-class SettingsRoute extends _i16.PageRouteInfo {
- const SettingsRoute({List<_i16.PageRouteInfo>? children})
+/// [_i13.SettingsScreen]
+class SettingsRoute extends _i17.PageRouteInfo {
+ const SettingsRoute({List<_i17.PageRouteInfo>? children})
: super(
SettingsRoute.name,
initialChildren: children,
@@ -365,18 +385,18 @@ class SettingsRoute extends _i16.PageRouteInfo {
static const String name = 'SettingsRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i12.SettingsScreen();
+ return const _i13.SettingsScreen();
},
);
}
/// generated route for
-/// [_i13.SettingsSelectionScreen]
-class SettingsSelectionRoute extends _i16.PageRouteInfo {
- const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children})
+/// [_i14.SettingsSelectionScreen]
+class SettingsSelectionRoute extends _i17.PageRouteInfo {
+ const SettingsSelectionRoute({List<_i17.PageRouteInfo>? children})
: super(
SettingsSelectionRoute.name,
initialChildren: children,
@@ -384,21 +404,21 @@ class SettingsSelectionRoute extends _i16.PageRouteInfo {
static const String name = 'SettingsSelectionRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
- return const _i13.SettingsSelectionScreen();
+ return const _i14.SettingsSelectionScreen();
},
);
}
/// generated route for
-/// [_i14.SplashScreen]
-class SplashRoute extends _i16.PageRouteInfo {
+/// [_i15.SplashScreen]
+class SplashRoute extends _i17.PageRouteInfo {
SplashRoute({
dynamic Function(bool)? loggedIn,
- _i21.Key? key,
- List<_i16.PageRouteInfo>? children,
+ _i22.Key? key,
+ List<_i17.PageRouteInfo>? children,
}) : super(
SplashRoute.name,
args: SplashRouteArgs(
@@ -410,12 +430,12 @@ class SplashRoute extends _i16.PageRouteInfo {
static const String name = 'SplashRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
final args =
data.argsAs(orElse: () => const SplashRouteArgs());
- return _i14.SplashScreen(
+ return _i15.SplashScreen(
loggedIn: args.loggedIn,
key: args.key,
);
@@ -431,7 +451,7 @@ class SplashRouteArgs {
final dynamic Function(bool)? loggedIn;
- final _i21.Key? key;
+ final _i22.Key? key;
@override
String toString() {
@@ -440,12 +460,12 @@ class SplashRouteArgs {
}
/// generated route for
-/// [_i15.SyncedScreen]
-class SyncedRoute extends _i16.PageRouteInfo {
+/// [_i16.SyncedScreen]
+class SyncedRoute extends _i17.PageRouteInfo {
SyncedRoute({
- _i21.ScrollController? navigationScrollController,
- _i21.Key? key,
- List<_i16.PageRouteInfo>? children,
+ _i22.ScrollController? navigationScrollController,
+ _i22.Key? key,
+ List<_i17.PageRouteInfo>? children,
}) : super(
SyncedRoute.name,
args: SyncedRouteArgs(
@@ -457,12 +477,12 @@ class SyncedRoute extends _i16.PageRouteInfo {
static const String name = 'SyncedRoute';
- static _i16.PageInfo page = _i16.PageInfo(
+ static _i17.PageInfo page = _i17.PageInfo(
name,
builder: (data) {
final args =
data.argsAs(orElse: () => const SyncedRouteArgs());
- return _i15.SyncedScreen(
+ return _i16.SyncedScreen(
navigationScrollController: args.navigationScrollController,
key: args.key,
);
@@ -476,9 +496,9 @@ class SyncedRouteArgs {
this.key,
});
- final _i21.ScrollController? navigationScrollController;
+ final _i22.ScrollController? navigationScrollController;
- final _i21.Key? key;
+ final _i22.Key? key;
@override
String toString() {
diff --git a/lib/screens/book_viewer/book_viewer_chapters.dart b/lib/screens/book_viewer/book_viewer_chapters.dart
index 57c7a83..dede4b3 100644
--- a/lib/screens/book_viewer/book_viewer_chapters.dart
+++ b/lib/screens/book_viewer/book_viewer_chapters.dart
@@ -1,7 +1,7 @@
import 'package:fladder/models/book_model.dart';
import 'package:fladder/providers/book_viewer_provider.dart';
import 'package:fladder/providers/items/book_details_provider.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
import 'package:flutter/material.dart';
diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart
index b33bb83..205b257 100644
--- a/lib/screens/book_viewer/book_viewer_controls.dart
+++ b/lib/screens/book_viewer/book_viewer_controls.dart
@@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart';
import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/shared/default_title_bar.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/throttler.dart';
diff --git a/lib/screens/book_viewer/book_viewer_settings.dart b/lib/screens/book_viewer/book_viewer_settings.dart
index 738b4ec..38b6301 100644
--- a/lib/screens/book_viewer/book_viewer_settings.dart
+++ b/lib/screens/book_viewer/book_viewer_settings.dart
@@ -1,5 +1,5 @@
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart
index 02320a5..af0ea5d 100644
--- a/lib/screens/dashboard/dashboard_screen.dart
+++ b/lib/screens/dashboard/dashboard_screen.dart
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
+import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/dashboard_provider.dart';
@@ -19,10 +20,11 @@ import 'package:fladder/screens/dashboard/home_banner_widget.dart';
import 'package:fladder/screens/shared/media/poster_row.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sliver_list_padding.dart';
+import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
import 'package:fladder/widgets/shared/poster_size_slider.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
@@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
+ final padding = AdaptiveLayout.adaptivePadding(context);
+
final dashboardData = ref.watch(dashboardProvider);
final views = ref.watch(viewsProvider);
final homeSettings = ref.watch(homeSettingsProvider);
@@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState {
return MediaQuery.removeViewInsets(
context: context,
child: NestedScaffold(
+ background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
body: PullToRefresh(
refreshKey: _refreshIndicatorKey,
displacement: 80 + MediaQuery.of(context).viewPadding.top,
@@ -104,7 +109,13 @@ class _DashboardScreenState extends ConsumerState {
SliverToBoxAdapter(
child: Transform.translate(
offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0),
- child: HomeBannerWidget(posters: homeCarouselItems),
+ child: Padding(
+ padding: AdaptiveLayout.adaptivePadding(
+ context,
+ horizontalPadding: 0,
+ ),
+ child: HomeBannerWidget(posters: homeCarouselItems),
+ ),
),
),
},
@@ -122,6 +133,7 @@ class _DashboardScreenState extends ConsumerState {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.dashboardContinueWatching,
posters: resumeVideo,
),
@@ -130,6 +142,7 @@ class _DashboardScreenState extends ConsumerState {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.dashboardContinueListening,
posters: resumeAudio,
),
@@ -138,6 +151,7 @@ class _DashboardScreenState extends ConsumerState {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.dashboardContinueReading,
posters: resumeBooks,
),
@@ -146,6 +160,7 @@ class _DashboardScreenState extends ConsumerState {
(homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.nextUp,
posters: dashboardData.nextUp,
),
@@ -153,6 +168,7 @@ class _DashboardScreenState extends ConsumerState {
if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined)
SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.dashboardContinue,
posters: [...allResume, ...dashboardData.nextUp],
),
@@ -161,7 +177,9 @@ class _DashboardScreenState extends ConsumerState {
.where((element) => element.recentlyAdded.isNotEmpty)
.map((view) => SliverToBoxAdapter(
child: PosterRow(
+ contentPadding: padding,
label: context.localized.dashboardRecentlyAdded(view.name),
+ collectionAspectRatio: view.collectionType.aspectRatio,
onLabelClick: () => context.router.push(LibrarySearchRoute(
viewModelId: view.id,
sortingOptions: switch (view.collectionType) {
diff --git a/lib/screens/dashboard/home_banner_widget.dart b/lib/screens/dashboard/home_banner_widget.dart
index c793db7..db10520 100644
--- a/lib/screens/dashboard/home_banner_widget.dart
+++ b/lib/screens/dashboard/home_banner_widget.dart
@@ -27,9 +27,12 @@ class HomeBannerWidget extends ConsumerWidget {
const SizedBox(height: 24)
],
),
- HomeBanner.banner => MediaBanner(
- items: posters,
- maxHeight: maxHeight,
+ HomeBanner.banner => Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: MediaBanner(
+ items: posters,
+ maxHeight: maxHeight,
+ ),
),
_ => const SizedBox.shrink(),
};
diff --git a/lib/screens/details_screens/components/overview_header.dart b/lib/screens/details_screens/components/overview_header.dart
index 90959be..7e9a2a1 100644
--- a/lib/screens/details_screens/components/overview_header.dart
+++ b/lib/screens/details_screens/components/overview_header.dart
@@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/screens/shared/media/components/chip_button.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/humanize_duration.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/details_screens/empty_item.dart b/lib/screens/details_screens/empty_item.dart
index 795a198..7bcea6d 100644
--- a/lib/screens/details_screens/empty_item.dart
+++ b/lib/screens/details_screens/empty_item.dart
@@ -37,36 +37,42 @@ class EmptyItem extends ConsumerWidget {
}
},
),
- content: (padding) => Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- ConstrainedBox(
- constraints: const BoxConstraints(maxHeight: 350),
- child: AspectRatio(
- aspectRatio: 0.67,
- child: Card(
- elevation: 6,
- color: Theme.of(context).colorScheme.secondaryContainer,
- shape: RoundedRectangleBorder(
- side: BorderSide(
- width: 1.0,
- color: Colors.white.withValues(alpha: 0.10),
+ content: (padding) => Center(
+ child: Padding(
+ padding: padding,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 350),
+ child: AspectRatio(
+ aspectRatio: 0.67,
+ child: Card(
+ elevation: 6,
+ color: Theme.of(context).colorScheme.secondaryContainer,
+ shape: RoundedRectangleBorder(
+ side: BorderSide(
+ width: 1.0,
+ color: Colors.white.withValues(alpha: 0.10),
+ ),
+ borderRadius: FladderTheme.defaultShape.borderRadius,
+ ),
+ child: FladderImage(
+ image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull,
+ placeHolder: PosterPlaceholder(item: item),
+ ),
),
- borderRadius: FladderTheme.defaultShape.borderRadius,
- ),
- child: FladderImage(
- image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull,
- placeHolder: PosterPlaceholder(item: item),
),
),
- ),
+ Text(
+ item.title,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."),
+ ].addInBetween(const SizedBox(height: 32)),
),
- Text(
- item.title,
- style: Theme.of(context).textTheme.titleLarge,
- ),
- Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."),
- ].addInBetween(const SizedBox(height: 32)),
+ ),
),
);
}
diff --git a/lib/screens/details_screens/episode_detail_screen.dart b/lib/screens/details_screens/episode_detail_screen.dart
index 967d4bd..fcaaa5d 100644
--- a/lib/screens/details_screens/episode_detail_screen.dart
+++ b/lib/screens/details_screens/episode_detail_screen.dart
@@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/items/episode_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
@@ -18,7 +17,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart
index 7c64fe9..d76c65f 100644
--- a/lib/screens/details_screens/movie_detail_screen.dart
+++ b/lib/screens/details_screens/movie_detail_screen.dart
@@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/items/movies_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
@@ -17,7 +16,7 @@ import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.dart';
import 'package:fladder/screens/shared/media/poster_row.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/details_screens/person_detail_screen.dart b/lib/screens/details_screens/person_detail_screen.dart
index 13ff8a5..175b54b 100644
--- a/lib/screens/details_screens/person_detail_screen.dart
+++ b/lib/screens/details_screens/person_detail_screen.dart
@@ -1,4 +1,3 @@
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@@ -11,7 +10,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/poster_row.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/string_extensions.dart';
@@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState {
spacing: 32,
children: [
Container(
- clipBehavior: Clip.antiAlias,
+ clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
diff --git a/lib/screens/details_screens/series_detail_screen.dart b/lib/screens/details_screens/series_detail_screen.dart
index 32b91eb..b2a497f 100644
--- a/lib/screens/details_screens/series_detail_screen.dart
+++ b/lib/screens/details_screens/series_detail_screen.dart
@@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/series_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/items/series_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/overview_header.dart';
@@ -19,7 +18,7 @@ import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.dart';
import 'package:fladder/screens/shared/media/poster_row.dart';
import 'package:fladder/screens/shared/media/season_row.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart
index fe78f14..afee5db 100644
--- a/lib/screens/favourites/favourites_screen.dart
+++ b/lib/screens/favourites/favourites_screen.dart
@@ -1,19 +1,20 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
-import 'package:fladder/providers/settings/client_settings_provider.dart';
-import 'package:fladder/routes/auto_router.gr.dart';
-import 'package:fladder/screens/shared/nested_scaffold.dart';
-import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
-import 'package:fladder/util/localization_helper.dart';
-import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
-import 'package:fladder/widgets/shared/poster_size_slider.dart';
import 'package:flutter/material.dart';
+
+import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/favourites_provider.dart';
-import 'package:fladder/screens/shared/media/poster_grid.dart';
+import 'package:fladder/providers/settings/client_settings_provider.dart';
+import 'package:fladder/routes/auto_router.gr.dart';
+import 'package:fladder/screens/shared/media/poster_row.dart';
+import 'package:fladder/screens/shared/nested_scaffold.dart';
+import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
+import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sliver_list_padding.dart';
+import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
+import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
+import 'package:fladder/widgets/shared/poster_size_slider.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
@RoutePage()
@@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final favourites = ref.watch(favouritesProvider);
+ final padding = AdaptiveLayout.adaptivePadding(context);
return PullToRefresh(
onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(),
child: NestedScaffold(
+ background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()),
body: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: CustomScrollView(
@@ -52,25 +55,19 @@ class FavouritesScreen extends ConsumerWidget {
),
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
(e) => SliverToBoxAdapter(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: PosterGrid(
- stickyHeader: true,
- name: e.key.label(context),
- posters: e.value,
- ),
+ child: PosterRow(
+ contentPadding: padding,
+ label: e.key.label(context),
+ posters: e.value,
),
),
),
if (favourites.people.isNotEmpty)
SliverToBoxAdapter(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: PosterGrid(
- stickyHeader: true,
- name: "People",
- posters: favourites.people,
- ),
+ child: PosterRow(
+ contentPadding: padding,
+ label: context.localized.actor(favourites.people.length),
+ posters: favourites.people,
),
),
const DefautlSliverBottomPadding(),
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index 61c0d55..3fb9fff 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
@@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart';
enum HomeTabs {
dashboard,
+ library,
favorites,
- sync;
+ sync,
+ ;
+
+ const HomeTabs();
+
+ IconData get icon => switch (this) {
+ HomeTabs.dashboard => IconsaxPlusLinear.home_1,
+ HomeTabs.library => IconsaxPlusLinear.book,
+ HomeTabs.favorites => IconsaxPlusLinear.heart,
+ HomeTabs.sync => IconsaxPlusLinear.cloud,
+ };
+
+ IconData get selectedIcon => switch (this) {
+ HomeTabs.dashboard => IconsaxPlusBold.home_1,
+ HomeTabs.library => IconsaxPlusBold.book,
+ HomeTabs.favorites => IconsaxPlusBold.heart,
+ HomeTabs.sync => IconsaxPlusBold.cloud,
+ };
+
+ Future navigate(BuildContext context) => switch (this) {
+ HomeTabs.dashboard => context.router.navigate(const DashboardRoute()),
+ HomeTabs.library => context.router.navigate(const LibraryRoute()),
+ HomeTabs.favorites => context.router.navigate(const FavouritesRoute()),
+ HomeTabs.sync => context.router.navigate(SyncedRoute()),
+ };
+
+ String label(BuildContext context) => switch (this) {
+ HomeTabs.dashboard => context.localized.dashboard,
+ HomeTabs.library => context.localized.library(0),
+ HomeTabs.favorites => context.localized.favorites,
+ HomeTabs.sync => context.localized.sync,
+ };
}
@RoutePage()
@@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget {
case HomeTabs.dashboard:
return DestinationModel(
label: context.localized.navigationDashboard,
- icon: const Icon(IconsaxPlusLinear.home),
- selectedIcon: const Icon(IconsaxPlusBold.home),
+ icon: Icon(e.icon),
+ selectedIcon: Icon(e.selectedIcon),
route: const DashboardRoute(),
- action: () => context.router.navigate(const DashboardRoute()),
+ action: () => e.navigate(context),
floatingActionButton: AdaptiveFab(
context: context,
title: context.localized.search,
@@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget {
case HomeTabs.favorites:
return DestinationModel(
label: context.localized.navigationFavorites,
- icon: const Icon(IconsaxPlusLinear.heart),
- selectedIcon: const Icon(IconsaxPlusBold.heart),
+ icon: Icon(e.icon),
+ selectedIcon: Icon(e.selectedIcon),
route: const FavouritesRoute(),
floatingActionButton: AdaptiveFab(
context: context,
@@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget {
onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)),
child: const Icon(IconsaxPlusLinear.heart_search),
),
- action: () => context.router.navigate(const FavouritesRoute()),
+ action: () => e.navigate(context),
);
case HomeTabs.sync:
if (canDownload) {
return DestinationModel(
label: context.localized.navigationSync,
- icon: const Icon(IconsaxPlusLinear.cloud),
- selectedIcon: const Icon(IconsaxPlusBold.cloud),
+ icon: Icon(e.icon),
+ selectedIcon: Icon(e.selectedIcon),
route: SyncedRoute(),
- action: () => context.router.navigate(SyncedRoute()),
+ action: () => e.navigate(context),
);
}
- return null;
+ case HomeTabs.library:
+ return DestinationModel(
+ label: context.localized.library(0),
+ icon: Icon(e.icon),
+ selectedIcon: Icon(e.selectedIcon),
+ route: const LibraryRoute(),
+ action: () => e.navigate(context),
+ );
}
})
.nonNulls
diff --git a/lib/screens/library/components/library_tabs.dart b/lib/screens/library/components/library_tabs.dart
deleted file mode 100644
index e27c1f8..0000000
--- a/lib/screens/library/components/library_tabs.dart
+++ /dev/null
@@ -1,83 +0,0 @@
-// ignore_for_file: public_member_api_docs, sort_constructors_first
-
-import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
-import 'package:fladder/screens/library/tabs/favourites_tab.dart';
-import 'package:fladder/screens/library/tabs/library_tab.dart';
-import 'package:fladder/screens/library/tabs/timeline_tab.dart';
-import 'package:flutter/material.dart';
-
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/screens/library/tabs/recommendations_tab.dart';
-
-class LibraryTabs {
- final String name;
- final Icon icon;
- final Widget page;
- final FloatingActionButton? floatingActionButton;
- LibraryTabs({
- required this.name,
- required this.icon,
- required this.page,
- this.floatingActionButton,
- });
-
- static List getLibraryForType(ViewModel viewModel, CollectionType type) {
- LibraryTabs recommendTab() {
- return LibraryTabs(
- name: "Recommended",
- icon: const Icon(Icons.recommend_rounded),
- page: RecommendationsTab(viewModel: viewModel),
- );
- }
-
- LibraryTabs timelineTab() {
- return LibraryTabs(
- name: "Timeline",
- icon: const Icon(Icons.timeline),
- page: TimelineTab(viewModel: viewModel),
- );
- }
-
- LibraryTabs favouritesTab() {
- return LibraryTabs(
- name: "Favourites",
- icon: const Icon(Icons.favorite_rounded),
- page: FavouritesTab(viewModel: viewModel),
- );
- }
-
- LibraryTabs libraryTab() {
- return LibraryTabs(
- name: "Library",
- icon: const Icon(Icons.book_rounded),
- page: LibraryTab(viewModel: viewModel),
- );
- }
-
- switch (type) {
- case CollectionType.tvshows:
- case CollectionType.movies:
- return [
- libraryTab(),
- recommendTab(),
- favouritesTab(),
- ];
- case CollectionType.books:
- case CollectionType.homevideos:
- return [
- libraryTab(),
- timelineTab(),
- recommendTab(),
- favouritesTab(),
- ];
- case CollectionType.boxsets:
- case CollectionType.playlists:
- case CollectionType.folders:
- return [
- libraryTab(),
- ];
- default:
- return [];
- }
- }
-}
diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart
index 896c9f6..b051b07 100644
--- a/lib/screens/library/library_screen.dart
+++ b/lib/screens/library/library_screen.dart
@@ -1,14 +1,33 @@
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/library_provider.dart';
-import 'package:fladder/screens/library/components/library_tabs.dart';
-import 'package:fladder/util/adaptive_layout.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'dart:async';
+import 'package:flutter/material.dart';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
+
+import 'package:fladder/models/recommended_model.dart';
+import 'package:fladder/models/view_model.dart';
+import 'package:fladder/providers/library_screen_provider.dart';
+import 'package:fladder/routes/auto_router.gr.dart';
+import 'package:fladder/screens/metadata/refresh_metadata.dart';
+import 'package:fladder/screens/shared/flat_button.dart';
+import 'package:fladder/screens/shared/media/poster_row.dart';
+import 'package:fladder/screens/shared/nested_scaffold.dart';
+import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
+import 'package:fladder/util/fladder_image.dart';
+import 'package:fladder/util/localization_helper.dart';
+import 'package:fladder/util/sliver_list_padding.dart';
+import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
+import 'package:fladder/widgets/shared/button_group.dart';
+import 'package:fladder/widgets/shared/horizontal_list.dart';
+import 'package:fladder/widgets/shared/item_actions.dart';
+import 'package:fladder/widgets/shared/pull_to_refresh.dart';
+
+@RoutePage()
class LibraryScreen extends ConsumerStatefulWidget {
- final ViewModel viewModel;
const LibraryScreen({
- required this.viewModel,
super.key,
});
@@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget {
}
class _LibraryScreenState extends ConsumerState with SingleTickerProviderStateMixin {
- late final List tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType);
- late final TabController tabController = TabController(length: tabs.length, vsync: this);
-
- @override
- void initState() {
- super.initState();
- Future.microtask(() {
- ref.read(libraryProvider(widget.viewModel.id).notifier).setupLibrary(widget.viewModel);
- });
-
- tabController.addListener(() {
- if (tabController.previousIndex != tabController.index) {
- setState(() {});
- }
- });
- }
-
+ final GlobalKey? refreshKey = GlobalKey();
@override
Widget build(BuildContext context) {
- final PreferredSizeWidget tabBar = TabBar(
- isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false,
- indicatorWeight: 3,
- controller: tabController,
- tabs: tabs
- .map((e) => Tab(
- text: e.name,
- icon: e.icon,
- ))
- .toList(),
- );
-
- return Padding(
- padding: AdaptiveLayout.of(context).isDesktop
- ? EdgeInsets.only(top: MediaQuery.of(context).padding.top)
- : EdgeInsets.zero,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0),
- child: Card(
- margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero,
- elevation: 2,
- child: Scaffold(
- backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
- floatingActionButton: tabs[tabController.index].floatingActionButton,
- floatingActionButtonLocation: FloatingActionButtonLocation.endContained,
- appBar: AppBar(
- centerTitle: true,
- backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
- title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
- toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40,
- bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
- ),
- extendBody: true,
- body: Padding(
- padding: !AdaptiveLayout.of(context).isDesktop
- ? EdgeInsets.only(
- left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right)
- : EdgeInsets.zero,
- child: TabBarView(
- controller: tabController,
- children: tabs
- .map((e) => Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: e.page,
- ))
- .toList(),
- ),
- ),
+ final libraryScreenState = ref.watch(libraryScreenProvider);
+ final views = libraryScreenState.views;
+ final recommendations = libraryScreenState.recommendations;
+ final favourites = libraryScreenState.favourites;
+ final selectedView = libraryScreenState.selectedViewModel;
+ final viewTypes = libraryScreenState.viewType;
+ final genres = libraryScreenState.genres;
+ final padding = AdaptiveLayout.adaptivePadding(context);
+ return NestedScaffold(
+ background: BackgroundImage(
+ items: [
+ ...recommendations.expand((e) => e.posters),
+ ...favourites,
+ ],
+ ),
+ body: PullToRefresh(
+ refreshOnStart: true,
+ refreshKey: refreshKey,
+ onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
+ child: SizedBox.expand(
+ child: CustomScrollView(
+ controller: AdaptiveLayout.scrollOf(context),
+ physics: const AlwaysScrollableScrollPhysics(),
+ slivers: [
+ const DefaultSliverTopBadding(),
+ if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
+ NestedSliverAppBar(
+ route: LibrarySearchRoute(),
+ parent: context,
+ ),
+ if (views.isNotEmpty)
+ SliverToBoxAdapter(
+ child: LibraryRow(
+ padding: padding,
+ views: views,
+ selectedView: libraryScreenState.selectedViewModel,
+ onSelected: (view) {
+ ref.read(libraryScreenProvider.notifier).selectLibrary(view);
+ refreshKey?.currentState?.show();
+ },
+ ),
+ ),
+ if (selectedView != null)
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 24, bottom: 16),
+ child: SizedBox(
+ height: 40,
+ child: ListView(
+ padding: padding,
+ shrinkWrap: true,
+ scrollDirection: Axis.horizontal,
+ children: [
+ FilledButton.tonalIcon(
+ onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)),
+ label: Text("${context.localized.search} ${selectedView.name}..."),
+ icon: const Icon(IconsaxPlusLinear.search_normal),
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 4.0),
+ child: VerticalDivider(),
+ ),
+ ExpressiveButtonGroup(
+ multiSelection: true,
+ options: LibraryViewType.values
+ .map((element) => ButtonGroupOption(
+ value: element,
+ icon: Icon(element.icon),
+ selected: Icon(element.iconSelected),
+ child: Text(
+ element.label(context),
+ )))
+ .toList(),
+ selectedValues: viewTypes,
+ onSelected: (value) {
+ ref.read(libraryScreenProvider.notifier).setViewType(value);
+ },
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 4.0),
+ child: VerticalDivider(),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name),
+ label: Text(context.localized.scanLibrary),
+ icon: const Icon(IconsaxPlusLinear.refresh),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ if (viewTypes.isEmpty)
+ SliverFillRemaining(
+ child: Center(child: Text(context.localized.noResults)),
+ ),
+ if (viewTypes.contains(LibraryViewType.recommended)) ...[
+ if (recommendations.isNotEmpty)
+ ...recommendations.where((element) => element.posters.isNotEmpty).map(
+ (element) => SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: PosterRow(
+ contentPadding: padding,
+ posters: element.posters,
+ label: element.type != null
+ ? "${element.type?.label(context)} - ${element.name.label(context)}"
+ : element.name.label(context),
+ ),
+ ),
+ ),
+ ),
+ ],
+ if (viewTypes.contains(LibraryViewType.favourites))
+ if (favourites.isNotEmpty)
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: PosterRow(
+ contentPadding: padding,
+ posters: favourites,
+ label: context.localized.favorites,
+ ),
+ ),
+ ),
+ if (viewTypes.contains(LibraryViewType.genres)) ...[
+ if (genres.isNotEmpty)
+ ...genres.where((element) => element.posters.isNotEmpty).map(
+ (element) => SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: PosterRow(
+ contentPadding: padding,
+ posters: element.posters,
+ label: element.type != null
+ ? "${element.type?.label(context)} - ${element.name.label(context)}"
+ : element.name.label(context),
+ ),
+ ),
+ ),
+ )
+ ],
+ const DefautlSliverBottomPadding(),
+ ],
),
),
),
);
}
}
+
+class LibraryRow extends ConsumerWidget {
+ const LibraryRow({
+ super.key,
+ required this.views,
+ this.selectedView,
+ required this.padding,
+ this.onSelected,
+ });
+
+ final List views;
+ final ViewModel? selectedView;
+ final EdgeInsets padding;
+ final FutureOr Function(ViewModel selected)? onSelected;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ return HorizontalList(
+ label: context.localized.library(views.length),
+ items: views,
+ startIndex: selectedView != null ? views.indexOf(selectedView!) : null,
+ height: 165,
+ contentPadding: padding,
+ itemBuilder: (context, index) {
+ final view = views[index];
+ final isSelected = selectedView == view;
+ final List viewActions = [
+ ItemActionButton(
+ label: Text(context.localized.search),
+ icon: const Icon(IconsaxPlusLinear.search_normal),
+ action: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
+ ),
+ ItemActionButton(
+ label: Text(context.localized.scanLibrary),
+ icon: const Icon(IconsaxPlusLinear.refresh),
+ action: () => showRefreshPopup(context, view.id, view.name),
+ )
+ ];
+ return FlatButton(
+ onTap: isSelected ? null : () => onSelected?.call(view),
+ onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
+ onSecondaryTapDown: (details) async {
+ Offset localPosition = details.globalPosition;
+ RelativeRect position =
+ RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
+ await showMenu(
+ context: context,
+ position: position,
+ items: viewActions.popupMenuItems(useIcons: true),
+ );
+ },
+ child: Card(
+ color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null,
+ shadowColor: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 4,
+ children: [
+ SizedBox(
+ width: 200,
+ child: Card(
+ child: AspectRatio(
+ aspectRatio: 1.60,
+ child: FladderImage(
+ image: view.imageData?.primary,
+ fit: BoxFit.cover,
+ placeHolder: Center(
+ child: Text(
+ view.name,
+ style: Theme.of(context).textTheme.titleMedium,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.start,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ child: Row(
+ spacing: 8,
+ children: [
+ if (isSelected)
+ Container(
+ height: 12,
+ width: 12,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primary,
+ shape: BoxShape.circle,
+ ),
+ ),
+ Text(
+ view.name,
+ style: Theme.of(context).textTheme.titleMedium,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.start,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/screens/library/tabs/favourites_tab.dart b/lib/screens/library/tabs/favourites_tab.dart
deleted file mode 100644
index ff633b7..0000000
--- a/lib/screens/library/tabs/favourites_tab.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/library_provider.dart';
-import 'package:fladder/screens/shared/media/poster_grid.dart';
-import 'package:fladder/widgets/shared/pull_to_refresh.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-class FavouritesTab extends ConsumerStatefulWidget {
- final ViewModel viewModel;
- const FavouritesTab({required this.viewModel, super.key});
-
- @override
- ConsumerState createState() => _FavouritesTabState();
-}
-
-class _FavouritesTabState extends ConsumerState with AutomaticKeepAliveClientMixin {
- @override
- Widget build(BuildContext context) {
- final favourites = ref.watch(libraryProvider(widget.viewModel.id))?.favourites ?? [];
- super.build(context);
- return PullToRefresh(
- onRefresh: () async {
- await ref.read(libraryProvider(widget.viewModel.id).notifier).loadFavourites(widget.viewModel);
- },
- child: favourites.isNotEmpty
- ? ListView(
- children: [
- PosterGrid(posters: favourites),
- ],
- )
- : const Center(child: Text("No favourites, add some using the heart icon.")),
- );
- }
-
- @override
- bool get wantKeepAlive => true;
-}
diff --git a/lib/screens/library/tabs/library_tab.dart b/lib/screens/library/tabs/library_tab.dart
deleted file mode 100644
index 8ec1163..0000000
--- a/lib/screens/library/tabs/library_tab.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/library_provider.dart';
-import 'package:fladder/screens/shared/media/poster_grid.dart';
-import 'package:fladder/util/grouping.dart';
-import 'package:fladder/util/keyed_list_view.dart';
-import 'package:fladder/widgets/shared/pull_to_refresh.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-class LibraryTab extends ConsumerStatefulWidget {
- final ViewModel viewModel;
- const LibraryTab({required this.viewModel, super.key});
-
- @override
- ConsumerState createState() => _LibraryTabState();
-}
-
-class _LibraryTabState extends ConsumerState with AutomaticKeepAliveClientMixin {
- @override
- Widget build(BuildContext context) {
- super.build(context);
- final library = ref.watch(libraryProvider(widget.viewModel.id).select((value) => value?.posters)) ?? [];
- final items = groupByName(library);
- return PullToRefresh(
- onRefresh: () async {
- await ref.read(libraryProvider(widget.viewModel.id).notifier).loadLibrary(widget.viewModel);
- },
- child: KeyedListView(
- map: items,
- itemBuilder: (context, index) {
- final currentIndex = items.entries.elementAt(index);
- return PosterGrid(name: currentIndex.key, posters: currentIndex.value);
- },
- ),
- );
- }
-
- @override
- bool get wantKeepAlive => true;
-}
diff --git a/lib/screens/library/tabs/recommendations_tab.dart b/lib/screens/library/tabs/recommendations_tab.dart
deleted file mode 100644
index 2de9ca7..0000000
--- a/lib/screens/library/tabs/recommendations_tab.dart
+++ /dev/null
@@ -1,49 +0,0 @@
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/library_provider.dart';
-import 'package:fladder/screens/shared/media/poster_grid.dart';
-import 'package:fladder/util/list_padding.dart';
-import 'package:fladder/widgets/shared/pull_to_refresh.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-class RecommendationsTab extends ConsumerStatefulWidget {
- final ViewModel viewModel;
-
- const RecommendationsTab({required this.viewModel, super.key});
-
- @override
- ConsumerState createState() => _RecommendationsTabState();
-}
-
-class _RecommendationsTabState extends ConsumerState with AutomaticKeepAliveClientMixin {
- @override
- Widget build(BuildContext context) {
- super.build(context);
- final recommendations = ref.watch(libraryProvider(widget.viewModel.id)
- .select((value) => value?.recommendations.where((element) => element.posters.isNotEmpty))) ??
- [];
- return PullToRefresh(
- onRefresh: () async {
- await ref.read(libraryProvider(widget.viewModel.id).notifier).loadRecommendations(widget.viewModel);
- },
- child: recommendations.isNotEmpty
- ? ListView(
- children: recommendations
- .map(
- (e) => PosterGrid(name: e.name, posters: e.posters),
- )
- .toList()
- .addPadding(
- const EdgeInsets.only(
- bottom: 32,
- ),
- ),
- )
- : const Center(
- child: Text("No recommendations, add more movies and or shows to receive more recomendations")),
- );
- }
-
- @override
- bool get wantKeepAlive => true;
-}
diff --git a/lib/screens/library/tabs/timeline_tab.dart b/lib/screens/library/tabs/timeline_tab.dart
deleted file mode 100644
index d4abedf..0000000
--- a/lib/screens/library/tabs/timeline_tab.dart
+++ /dev/null
@@ -1,133 +0,0 @@
-import 'package:fladder/models/items/photos_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
-import 'package:fladder/models/view_model.dart';
-import 'package:fladder/providers/library_provider.dart';
-import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
-import 'package:fladder/screens/shared/flat_button.dart';
-import 'package:fladder/util/adaptive_layout.dart';
-import 'package:fladder/util/fladder_image.dart';
-import 'package:fladder/util/sticky_header_text.dart';
-import 'package:fladder/widgets/shared/pull_to_refresh.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
-import 'package:intl/intl.dart';
-import 'package:page_transition/page_transition.dart';
-import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
-import 'package:sticky_headers/sticky_headers.dart';
-
-class TimelineTab extends ConsumerStatefulWidget {
- final ViewModel viewModel;
-
- const TimelineTab({required this.viewModel, super.key});
-
- @override
- ConsumerState createState() => _TimelineTabState();
-}
-
-class _TimelineTabState extends ConsumerState with AutomaticKeepAliveClientMixin {
- final itemScrollController = ItemScrollController();
- double get posterCount {
- if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) {
- return 200;
- }
- return 125;
- }
-
- @override
- Widget build(BuildContext context) {
- super.build(context);
- final timeLine = ref.watch(libraryProvider(widget.viewModel.id))?.timelinePhotos ?? [];
- final items = groupedItems(timeLine);
-
- return PullToRefresh(
- onRefresh: () async {
- await ref.read(libraryProvider(widget.viewModel.id).notifier).loadTimeline(widget.viewModel);
- },
- child: ScrollablePositionedList.builder(
- itemScrollController: itemScrollController,
- itemCount: items.length,
- itemBuilder: (context, index) {
- final item = items.entries.elementAt(index);
- return Padding(
- padding: const EdgeInsets.only(bottom: 64.0),
- child: StickyHeader(
- header: StickyHeaderText(
- label: item.key.year != DateTime.now().year
- ? DateFormat('E dd MMM. y').format(item.key)
- : DateFormat('E dd MMM.').format(item.key)),
- content: StaggeredGrid.count(
- crossAxisCount: MediaQuery.of(context).size.width ~/ posterCount,
- mainAxisSpacing: 0,
- crossAxisSpacing: 0,
- axisDirection: AxisDirection.down,
- children: item.value
- .map(
- (e) => Hero(
- tag: e.id,
- child: AspectRatio(
- aspectRatio: e.primaryRatio ?? 0.0,
- child: Card(
- margin: const EdgeInsets.all(4),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
- clipBehavior: Clip.antiAlias,
- child: Stack(
- children: [
- FladderImage(image: e.thumbnail?.primary),
- FlatButton(
- onLongPress: () {},
- onTap: () async {
- final position = await Navigator.of(context, rootNavigator: true).push(
- PageTransition(
- child: PhotoViewerScreen(
- items: timeLine,
- indexOfSelected: timeLine.indexOf(e),
- ),
- type: PageTransitionType.fade),
- );
- getParentPosition(items, timeLine, position);
- },
- )
- ],
- ),
- ),
- ),
- ),
- )
- .toList(),
- ),
- ),
- );
- },
- ),
- );
- }
-
- void getParentPosition(Map> items, List timeLine, int position) {
- items.forEach(
- (key, value) {
- if (value.contains(timeLine[position])) {
- itemScrollController.scrollTo(
- index: items.keys.toList().indexOf(key), duration: const Duration(milliseconds: 250));
- }
- },
- );
- }
-
- Map> groupedItems(List items) {
- Map> groupedItems = {};
- for (int i = 0; i < items.length; i++) {
- DateTime curretDate = items[i].dateTaken ?? DateTime.now();
- DateTime key = DateTime(curretDate.year, curretDate.month, curretDate.day);
- if (!groupedItems.containsKey(key)) {
- groupedItems[key] = [items[i]];
- } else {
- groupedItems[key]?.add(items[i]);
- }
- }
- return groupedItems;
- }
-
- @override
- bool get wantKeepAlive => true;
-}
diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart
index 2f3dcbe..3a44996 100644
--- a/lib/screens/library_search/library_search_screen.dart
+++ b/lib/screens/library_search/library_search_screen.dart
@@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/library_search/library_search_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
-import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playlist_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/library_search_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
-import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/collections/add_to_collection.dart';
import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart';
import 'package:fladder/screens/library_search/widgets/library_play_options_.dart';
@@ -26,9 +23,9 @@ import 'package:fladder/screens/library_search/widgets/library_views.dart';
import 'package:fladder/screens/library_search/widgets/suggestion_search_bar.dart';
import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
-import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/screens/shared/nested_scaffold.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/debouncer.dart';
import 'package:fladder/util/fab_extended_anim.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
@@ -37,8 +34,7 @@ import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/router_extension.dart';
-import 'package:fladder/util/sliver_list_padding.dart';
-import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
+import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
import 'package:fladder/widgets/shared/fladder_scrollbar.dart';
import 'package:fladder/widgets/shared/hide_on_scroll.dart';
@@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState {
final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null;
final librarySearchResults = ref.watch(providerKey);
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows);
- final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
final libraryViewType = ref.watch(libraryViewTypeProvider);
ref.listen(
@@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState {
libraryProvider.toggleSelectMode();
}
},
- child: Scaffold(
- extendBody: true,
- extendBodyBehindAppBar: true,
- floatingActionButtonAnimator:
- playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
- floatingActionButtonLocation:
- playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
- floatingActionButton: switch (playerState) {
- VideoPlayerState.minimized => const Padding(
- padding: EdgeInsets.symmetric(horizontal: 8),
- child: FloatingPlayerBar(),
- ),
- _ => HideOnScroll(
+ child: NestedScaffold(
+ background: BackgroundImage(items: librarySearchResults.activePosters),
+ body: Padding(
+ padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
+ child: Scaffold(
+ extendBody: true,
+ extendBodyBehindAppBar: true,
+ floatingActionButton: HideOnScroll(
controller: scrollController,
visibleBuilder: (visible) => Column(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -206,29 +196,26 @@ class _LibrarySearchScreenState extends ConsumerState {
].addInBetween(const SizedBox(height: 10)),
),
),
- },
- bottomNavigationBar: HideOnScroll(
- controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
- child: IgnorePointer(
- ignoring: librarySearchResults.fetchingItems,
- child: _LibrarySearchBottomBar(
- uniqueKey: uniqueKey,
- refreshKey: refreshKey,
- scrollController: scrollController,
- libraryProvider: libraryProvider,
- postersList: postersList,
+ bottomNavigationBar: HideOnScroll(
+ controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
+ child: IgnorePointer(
+ ignoring: librarySearchResults.fetchingItems,
+ child: _LibrarySearchBottomBar(
+ uniqueKey: uniqueKey,
+ refreshKey: refreshKey,
+ scrollController: scrollController,
+ libraryProvider: libraryProvider,
+ postersList: postersList,
+ ),
+ ),
),
- ),
- ),
- body: Stack(
- children: [
- Positioned.fill(
- child: Card(
- elevation: 1,
- child: PinchPosterZoom(
- scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
- child: MediaQuery.removeViewInsets(
- context: context,
+ body: Stack(
+ fit: StackFit.expand,
+ children: [
+ Positioned.fill(
+ child: PinchPosterZoom(
+ scaleDifference: (difference) =>
+ ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: ClipRRect(
borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
? BorderRadius.circular(15)
@@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState {
onTapUp: (details) async {
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
double left = details.globalPosition.dx;
- double top = details.globalPosition.dy + 20;
+ double top = details.globalPosition.dy;
await showMenu(
context: context,
position: RelativeRect.fromLTRB(left, top, 40, 100),
@@ -463,16 +450,10 @@ class _LibrarySearchScreenState extends ConsumerState {
padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal,
child: LibraryFilterChips(
- controller: scrollController,
- libraryProvider: libraryProvider,
- librarySearchResults: librarySearchResults,
- uniqueKey: uniqueKey,
- postersList: postersList,
- libraryViewType: libraryViewType,
+ key: uniqueKey,
),
),
),
- const Row(),
],
),
),
@@ -500,13 +481,12 @@ class _LibrarySearchScreenState extends ConsumerState {
),
)
else
- SliverToBoxAdapter(
+ SliverFillRemaining(
child: Center(
child: Text(context.localized.noItemsToShow),
),
),
- const DefautlSliverBottomPadding(),
- const SliverPadding(padding: EdgeInsets.only(bottom: 80))
+ SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20))
],
),
),
@@ -514,36 +494,36 @@ class _LibrarySearchScreenState extends ConsumerState {
),
),
),
- ),
- ),
- if (librarySearchResults.fetchingItems) ...[
- Container(
- color: Colors.black.withValues(alpha: 0.1),
- ),
- Center(
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer,
- borderRadius: BorderRadius.circular(16),
+ if (librarySearchResults.fetchingItems) ...[
+ Container(
+ color: Colors.black.withValues(alpha: 0.1),
),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const CircularProgressIndicator.adaptive(),
- Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium),
- IconButton(
- onPressed: () => libraryProvider.cancelFetch(),
- icon: const Icon(IconsaxPlusLinear.close_square),
- )
- ].addInBetween(const SizedBox(width: 16)),
+ Center(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const CircularProgressIndicator.adaptive(),
+ Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium),
+ IconButton(
+ onPressed: () => libraryProvider.cancelFetch(),
+ icon: const Icon(IconsaxPlusLinear.close_square),
+ )
+ ].addInBetween(const SizedBox(width: 16)),
+ ),
+ ),
),
- ),
- ),
- )
- ],
- ],
+ )
+ ],
+ ],
+ ),
+ ),
),
),
);
@@ -663,173 +643,156 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
icon: const Icon(IconsaxPlusLinear.save_add),
),
];
- return NestedBottomAppBar(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
+
+ return Padding(
+ padding: EdgeInsets.only(left: MediaQuery.paddingOf(context).left),
+ child: NestedBottomAppBar(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
children: [
- ScrollStatePosition(
- controller: scrollController,
- positionBuilder: (state) => AnimatedFadeSize(
- child: state != ScrollState.top
- ? Tooltip(
- message: context.localized.scrollToTop,
- child: FlatButton(
- clipBehavior: Clip.antiAlias,
- elevation: 0,
- borderRadiusGeometry: BorderRadius.circular(6),
- onTap: () => scrollController.animateTo(0,
- duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer,
- borderRadius: BorderRadius.circular(8),
+ Row(
+ spacing: 6,
+ children: [
+ ScrollStatePosition(
+ controller: scrollController,
+ positionBuilder: (state) => AnimatedFadeSize(
+ child: state != ScrollState.top
+ ? Tooltip(
+ message: context.localized.scrollToTop,
+ child: IconButton.filled(
+ onPressed: () => scrollController.animateTo(0,
+ duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
+ icon: const Icon(
+ IconsaxPlusLinear.arrow_up,
+ ),
),
- padding: const EdgeInsets.all(6),
- child: Icon(
- IconsaxPlusLinear.arrow_up,
- color: Theme.of(context).colorScheme.onPrimaryContainer,
- ),
- ),
- ),
- )
- : const SizedBox(),
- ),
- ),
- const SizedBox(width: 6),
- if (!librarySearchResults.selecteMode) ...{
- const SizedBox(width: 6),
- IconButton(
- tooltip: context.localized.sortBy,
- onPressed: () async {
- final newOptions = await openSortByDialogue(
- context,
- libraryProvider: libraryProvider,
- uniqueKey: uniqueKey,
- options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
- );
- if (newOptions != null) {
- if (newOptions.$1 != null) {
- libraryProvider.setSortBy(newOptions.$1!);
- }
- if (newOptions.$2 != null) {
- libraryProvider.setSortOrder(newOptions.$2!);
- }
- }
- },
- icon: const Icon(IconsaxPlusLinear.sort),
- ),
- if (librarySearchResults.hasActiveFilters) ...{
- const SizedBox(width: 6),
- IconButton(
- tooltip: context.localized.disableFilters,
- onPressed: disableFilters(librarySearchResults, libraryProvider),
- icon: const Icon(IconsaxPlusLinear.filter_remove),
- ),
- },
- },
- const SizedBox(width: 6),
- IconButton(
- onPressed: () => libraryProvider.toggleSelectMode(),
- color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null,
- icon: const Icon(IconsaxPlusLinear.category_2),
- ),
- const SizedBox(width: 6),
- AnimatedFadeSize(
- child: librarySearchResults.selecteMode
- ? Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer,
- borderRadius: BorderRadius.circular(16)),
- child: Row(
- children: [
- Tooltip(
- message: context.localized.selectAll,
- child: IconButton(
- onPressed: () => libraryProvider.selectAll(true),
- icon: const Icon(IconsaxPlusLinear.box_add),
- ),
- ),
- const SizedBox(width: 6),
- Tooltip(
- message: context.localized.clearSelection,
- child: IconButton(
- onPressed: () => libraryProvider.selectAll(false),
- icon: const Icon(IconsaxPlusLinear.box_remove),
- ),
- ),
- const SizedBox(width: 6),
- if (librarySearchResults.selectedPosters.isNotEmpty) ...{
- if (AdaptiveLayout.of(context).isDesktop)
- PopupMenuButton(
- itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
- )
- else
- IconButton(
- onPressed: () {
- showBottomSheetPill(
- context: context,
- content: (context, scrollController) => ListView(
- shrinkWrap: true,
- controller: scrollController,
- children: actions.listTileItems(context, useIcons: true),
- ),
- );
- },
- icon: const Icon(IconsaxPlusLinear.more))
- },
- ],
- ),
- )
- : const SizedBox(),
- ),
- const Spacer(),
- if (librarySearchResults.activePosters.isNotEmpty)
- IconButton(
- tooltip: context.localized.random,
- onPressed: () => libraryProvider.openRandom(context),
- icon: Card(
- color: Theme.of(context).colorScheme.secondary,
- child: Padding(
- padding: const EdgeInsets.all(2.0),
- child: Icon(
- IconsaxPlusBold.arrow_up_1,
- color: Theme.of(context).colorScheme.onSecondary,
- ),
+ )
+ : const SizedBox(),
),
),
- ),
- if (librarySearchResults.activePosters.isNotEmpty)
- IconButton(
- tooltip: context.localized.shuffleVideos,
- onPressed: () async {
- if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) {
- libraryProvider.viewGallery(context, shuffle: true);
- return;
- } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) {
- libraryProvider.playLibraryItems(context, ref, shuffle: true);
- return;
- }
-
- await showLibraryPlayOptions(
- context,
- context.localized.libraryShuffleAndPlayItems,
- playVideos: librarySearchResults.showPlayButtons
- ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true)
- : null,
- viewGallery: librarySearchResults.showGalleryButtons
- ? () => libraryProvider.viewGallery(context, shuffle: true)
- : null,
- );
+ if (!librarySearchResults.selecteMode) ...{
+ IconButton(
+ tooltip: context.localized.sortBy,
+ onPressed: () async {
+ final newOptions = await openSortByDialogue(
+ context,
+ libraryProvider: libraryProvider,
+ uniqueKey: uniqueKey,
+ options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
+ );
+ if (newOptions != null) {
+ if (newOptions.$1 != null) {
+ libraryProvider.setSortBy(newOptions.$1!);
+ }
+ if (newOptions.$2 != null) {
+ libraryProvider.setSortOrder(newOptions.$2!);
+ }
+ }
+ },
+ icon: const Icon(IconsaxPlusLinear.sort),
+ ),
+ if (librarySearchResults.hasActiveFilters) ...{
+ IconButton(
+ tooltip: context.localized.disableFilters,
+ onPressed: disableFilters(librarySearchResults, libraryProvider),
+ icon: const Icon(IconsaxPlusLinear.filter_remove),
+ ),
+ },
},
- icon: const Icon(IconsaxPlusLinear.shuffle),
- ),
+ IconButton(
+ onPressed: () => libraryProvider.toggleSelectMode(),
+ color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null,
+ icon: const Icon(IconsaxPlusLinear.category_2),
+ ),
+ AnimatedFadeSize(
+ child: librarySearchResults.selecteMode
+ ? Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(16)),
+ child: Row(
+ spacing: 6,
+ children: [
+ Tooltip(
+ message: context.localized.selectAll,
+ child: IconButton(
+ onPressed: () => libraryProvider.selectAll(true),
+ icon: const Icon(IconsaxPlusLinear.box_add),
+ ),
+ ),
+ Tooltip(
+ message: context.localized.clearSelection,
+ child: IconButton(
+ onPressed: () => libraryProvider.selectAll(false),
+ icon: const Icon(IconsaxPlusLinear.box_remove),
+ ),
+ ),
+ if (librarySearchResults.selectedPosters.isNotEmpty) ...{
+ if (AdaptiveLayout.of(context).isDesktop)
+ PopupMenuButton(
+ itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
+ )
+ else
+ IconButton(
+ onPressed: () {
+ showBottomSheetPill(
+ context: context,
+ content: (context, scrollController) => ListView(
+ shrinkWrap: true,
+ controller: scrollController,
+ children: actions.listTileItems(context, useIcons: true),
+ ),
+ );
+ },
+ icon: const Icon(IconsaxPlusLinear.more))
+ },
+ ],
+ ),
+ )
+ : const SizedBox(),
+ ),
+ const Spacer(),
+ if (librarySearchResults.activePosters.isNotEmpty)
+ IconButton.filledTonal(
+ tooltip: context.localized.random,
+ onPressed: () => libraryProvider.openRandom(context),
+ icon: const Icon(
+ IconsaxPlusBold.arrow_up_1,
+ ),
+ ),
+ if (librarySearchResults.activePosters.isNotEmpty)
+ IconButton(
+ tooltip: context.localized.shuffleVideos,
+ onPressed: () async {
+ if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) {
+ libraryProvider.viewGallery(context, shuffle: true);
+ return;
+ } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) {
+ libraryProvider.playLibraryItems(context, ref, shuffle: true);
+ return;
+ }
+
+ await showLibraryPlayOptions(
+ context,
+ context.localized.libraryShuffleAndPlayItems,
+ playVideos: librarySearchResults.showPlayButtons
+ ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true)
+ : null,
+ viewGallery: librarySearchResults.showGalleryButtons
+ ? () => libraryProvider.viewGallery(context, shuffle: true)
+ : null,
+ );
+ },
+ icon: const Icon(IconsaxPlusLinear.shuffle),
+ ),
+ ],
+ ),
],
),
- if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 8),
- ],
+ ),
),
);
}
diff --git a/lib/screens/library_search/widgets/library_filter_chips.dart b/lib/screens/library_search/widgets/library_filter_chips.dart
index a158673..94cf3bb 100644
--- a/lib/screens/library_search/widgets/library_filter_chips.dart
+++ b/lib/screens/library_search/widgets/library_filter_chips.dart
@@ -1,221 +1,193 @@
import 'package:flutter/material.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
-import 'package:fladder/models/library_search/library_search_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/providers/library_search_provider.dart';
-import 'package:fladder/screens/library_search/widgets/library_views.dart';
import 'package:fladder/screens/shared/chips/category_chip.dart';
-import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/refresh_state.dart';
-import 'package:fladder/widgets/shared/scroll_position.dart';
-class LibraryFilterChips extends ConsumerWidget {
- final Key uniqueKey;
- final ScrollController controller;
- final LibrarySearchModel librarySearchResults;
- final LibrarySearchNotifier libraryProvider;
- final List postersList;
- final LibraryViewTypes libraryViewType;
- const LibraryFilterChips({
- required this.uniqueKey,
- required this.controller,
- required this.librarySearchResults,
- required this.libraryProvider,
- required this.postersList,
- required this.libraryViewType,
- super.key,
- });
+class LibraryFilterChips extends ConsumerStatefulWidget {
+ const LibraryFilterChips({super.key});
@override
- Widget build(BuildContext context, WidgetRef ref) {
- return ScrollStatePosition(
- controller: controller,
- positionBuilder: (state) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.start,
- mainAxisSize: MainAxisSize.max,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: libraryFilterChips(
- context,
- ref,
- uniqueKey,
- librarySearchResults: librarySearchResults,
- libraryProvider: libraryProvider,
- postersList: postersList,
- libraryViewType: libraryViewType,
- ).addPadding(const EdgeInsets.symmetric(horizontal: 8)),
- );
- },
- );
- }
+ ConsumerState createState() => _LibraryFilterChipsState();
}
-List libraryFilterChips(
- BuildContext context,
- WidgetRef ref,
- Key uniqueKey, {
- required LibrarySearchModel librarySearchResults,
- required LibrarySearchNotifier libraryProvider,
- required List postersList,
- required LibraryViewTypes libraryViewType,
-}) {
- Future openGroupDialogue() {
- return showDialog(
+class _LibraryFilterChipsState extends ConsumerState {
+ @override
+ Widget build(BuildContext context) {
+ final uniqueKey = widget.key ?? UniqueKey();
+ final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier);
+ final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
+ final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites));
+ final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive));
+ final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows));
+ final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey));
+
+ return Row(
+ spacing: 8,
+ children: [
+ if (librarySearchResults.folderOverwrite.isEmpty)
+ CategoryChip(
+ label: Text(context.localized.library(2)),
+ items: librarySearchResults.views,
+ labelBuilder: (item) => Text(item.name),
+ onSave: (value) => libraryProvider.setViews(value),
+ onCancel: () => libraryProvider.setViews(librarySearchResults.views),
+ onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
+ ),
+ CategoryChip(
+ label: Text(context.localized.type(librarySearchResults.types.length)),
+ items: librarySearchResults.types,
+ labelBuilder: (item) => Row(
+ children: [
+ Icon(item.icon),
+ const SizedBox(width: 12),
+ Text(item.label(context)),
+ ],
+ ),
+ onSave: (value) => libraryProvider.setTypes(value),
+ onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)),
+ ),
+ FilterChip(
+ label: Text(context.localized.favorites),
+ avatar: Icon(
+ favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ selected: favourites,
+ showCheckmark: false,
+ onSelected: (_) {
+ libraryProvider.toggleFavourite();
+ context.refreshData();
+ },
+ ),
+ FilterChip(
+ label: Text(context.localized.recursive),
+ selected: recursive,
+ onSelected: (_) {
+ libraryProvider.toggleRecursive();
+ context.refreshData();
+ },
+ ),
+ if (librarySearchResults.genres.isNotEmpty)
+ CategoryChip(
+ label: Text(context.localized.genre(librarySearchResults.genres.length)),
+ activeIcon: IconsaxPlusBold.hierarchy_2,
+ items: librarySearchResults.genres,
+ labelBuilder: (item) => Text(item),
+ onSave: (value) => libraryProvider.setGenres(value),
+ onCancel: () => libraryProvider.setGenres(librarySearchResults.genres),
+ onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)),
+ ),
+ if (librarySearchResults.studios.isNotEmpty)
+ CategoryChip(
+ label: Text(context.localized.studio(librarySearchResults.studios.length)),
+ activeIcon: IconsaxPlusBold.airdrop,
+ items: librarySearchResults.studios,
+ labelBuilder: (item) => Text(item.name),
+ onSave: (value) => libraryProvider.setStudios(value),
+ onCancel: () => libraryProvider.setStudios(librarySearchResults.studios),
+ onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)),
+ ),
+ if (librarySearchResults.tags.isNotEmpty)
+ CategoryChip(
+ label: Text(context.localized.label(librarySearchResults.tags.length)),
+ activeIcon: Icons.label_rounded,
+ items: librarySearchResults.tags,
+ labelBuilder: (item) => Text(item),
+ onSave: (value) => libraryProvider.setTags(value),
+ onCancel: () => libraryProvider.setTags(librarySearchResults.tags),
+ onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)),
+ ),
+ FilterChip(
+ label: Text(context.localized.group),
+ selected: groupBy != GroupBy.none,
+ onSelected: (_) {
+ _openGroupDialogue(context, ref, libraryProvider, uniqueKey);
+ },
+ ),
+ CategoryChip(
+ label: Text(context.localized.filter(librarySearchResults.filters.length)),
+ items: librarySearchResults.filters,
+ labelBuilder: (item) => Text(item.label(context)),
+ onSave: (value) => libraryProvider.setFilters(value),
+ onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)),
+ ),
+ if (librarySearchResults.types[FladderItemType.series] == true)
+ FilterChip(
+ avatar: Icon(
+ hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ selected: hideEmpty,
+ showCheckmark: false,
+ label: Text(context.localized.hideEmpty),
+ onSelected: libraryProvider.setHideEmpty,
+ ),
+ if (librarySearchResults.officialRatings.isNotEmpty)
+ CategoryChip(
+ label: Text(context.localized.rating(librarySearchResults.officialRatings.length)),
+ activeIcon: Icons.star_rate_rounded,
+ items: librarySearchResults.officialRatings,
+ labelBuilder: (item) => Text(item),
+ onSave: (value) => libraryProvider.setRatings(value),
+ onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings),
+ onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)),
+ ),
+ if (librarySearchResults.years.isNotEmpty)
+ CategoryChip(
+ label: Text(context.localized.year(librarySearchResults.years.length)),
+ items: librarySearchResults.years,
+ labelBuilder: (item) => Text(item.toString()),
+ onSave: (value) => libraryProvider.setYears(value),
+ onCancel: () => libraryProvider.setYears(librarySearchResults.years),
+ onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
+ ),
+ ],
+ );
+ }
+
+ void _openGroupDialogue(
+ BuildContext context,
+ WidgetRef ref,
+ LibrarySearchNotifier provider,
+ Key uniqueKey,
+ ) {
+ showDialog(
context: context,
builder: (context) {
- return Consumer(
- builder: (context, ref, child) {
- return AlertDialog(
- content: SizedBox(
- width: MediaQuery.of(context).size.width * 0.65,
- child: ListView(
- shrinkWrap: true,
- children: [
- Text(context.localized.groupBy),
- ...GroupBy.values.map((groupBy) => RadioListTile.adaptive(
- value: groupBy,
- title: Text(groupBy.value(context)),
- groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)),
- onChanged: (value) {
- libraryProvider.setGroupBy(groupBy);
- Navigator.pop(context);
- },
- )),
- ],
+ final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
+ return AlertDialog(
+ content: SizedBox(
+ width: MediaQuery.of(context).size.width * 0.65,
+ child: ListView(
+ shrinkWrap: true,
+ children: [
+ Text(context.localized.groupBy),
+ ...GroupBy.values.map(
+ (group) => RadioListTile.adaptive(
+ value: group,
+ groupValue: groupBy,
+ title: Text(group.value(context)),
+ onChanged: (_) {
+ provider.setGroupBy(group);
+ Navigator.pop(context);
+ },
+ ),
),
- ),
- );
- },
+ ],
+ ),
+ ),
);
},
);
}
-
- return [
- if (librarySearchResults.folderOverwrite.isEmpty)
- CategoryChip(
- label: Text(context.localized.library(2)),
- items: librarySearchResults.views,
- labelBuilder: (item) => Text(item.name),
- onSave: (value) => libraryProvider.setViews(value),
- onCancel: () => libraryProvider.setViews(librarySearchResults.views),
- onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
- ),
- CategoryChip(
- label: Text(context.localized.type(librarySearchResults.types.length)),
- items: librarySearchResults.types,
- labelBuilder: (item) => Row(
- children: [
- Icon(item.icon),
- const SizedBox(width: 12),
- Text(item.label(context)),
- ],
- ),
- onSave: (value) => libraryProvider.setTypes(value),
- onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)),
- ),
- FilterChip(
- label: Text(context.localized.favorites),
- avatar: Icon(
- librarySearchResults.favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
- color: Theme.of(context).colorScheme.onSurface,
- ),
- selected: librarySearchResults.favourites,
- showCheckmark: false,
- onSelected: (value) {
- libraryProvider.toggleFavourite();
- context.refreshData();
- },
- ),
- FilterChip(
- label: Text(context.localized.recursive),
- selected: librarySearchResults.recursive,
- onSelected: (value) {
- libraryProvider.toggleRecursive();
- context.refreshData();
- },
- ),
- if (librarySearchResults.genres.isNotEmpty)
- CategoryChip(
- label: Text(context.localized.genre(librarySearchResults.genres.length)),
- activeIcon: IconsaxPlusBold.hierarchy_2,
- items: librarySearchResults.genres,
- labelBuilder: (item) => Text(item),
- onSave: (value) => libraryProvider.setGenres(value),
- onCancel: () => libraryProvider.setGenres(librarySearchResults.genres),
- onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)),
- ),
- if (librarySearchResults.studios.isNotEmpty)
- CategoryChip(
- label: Text(context.localized.studio(librarySearchResults.studios.length)),
- activeIcon: IconsaxPlusBold.airdrop,
- items: librarySearchResults.studios,
- labelBuilder: (item) => Text(item.name),
- onSave: (value) => libraryProvider.setStudios(value),
- onCancel: () => libraryProvider.setStudios(librarySearchResults.studios),
- onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)),
- ),
- if (librarySearchResults.tags.isNotEmpty)
- CategoryChip(
- label: Text(context.localized.label(librarySearchResults.tags.length)),
- activeIcon: Icons.label_rounded,
- items: librarySearchResults.tags,
- labelBuilder: (item) => Text(item),
- onSave: (value) => libraryProvider.setTags(value),
- onCancel: () => libraryProvider.setTags(librarySearchResults.tags),
- onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)),
- ),
- FilterChip(
- label: Text(context.localized.group),
- selected: librarySearchResults.groupBy != GroupBy.none,
- onSelected: (value) {
- openGroupDialogue();
- },
- ),
- CategoryChip(
- label: Text(context.localized.filter(librarySearchResults.filters.length)),
- items: librarySearchResults.filters,
- labelBuilder: (item) => Text(item.label(context)),
- onSave: (value) => libraryProvider.setFilters(value),
- onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)),
- ),
- if (librarySearchResults.types[FladderItemType.series] == true)
- FilterChip(
- avatar: Icon(
- librarySearchResults.hideEmptyShows ? Icons.visibility_off_rounded : Icons.visibility_rounded,
- color: Theme.of(context).colorScheme.onSurface,
- ),
- selected: librarySearchResults.hideEmptyShows,
- showCheckmark: false,
- label: Text(context.localized.hideEmpty),
- onSelected: libraryProvider.setHideEmpty,
- ),
- if (librarySearchResults.officialRatings.isNotEmpty)
- CategoryChip(
- label: Text(context.localized.rating(librarySearchResults.officialRatings.length)),
- activeIcon: Icons.star_rate_rounded,
- items: librarySearchResults.officialRatings,
- labelBuilder: (item) => Text(item),
- onSave: (value) => libraryProvider.setRatings(value),
- onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings),
- onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)),
- ),
- if (librarySearchResults.years.isNotEmpty)
- CategoryChip(
- label: Text(context.localized.year(librarySearchResults.years.length)),
- items: librarySearchResults.years,
- labelBuilder: (item) => Text(item.toString()),
- onSave: (value) => libraryProvider.setYears(value),
- onCancel: () => libraryProvider.setYears(librarySearchResults.years),
- onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
- ),
- ];
}
diff --git a/lib/screens/library_search/widgets/library_views.dart b/lib/screens/library_search/widgets/library_views.dart
index 5a2eee0..0c0713a 100644
--- a/lib/screens/library_search/widgets/library_views.dart
+++ b/lib/screens/library_search/widgets/library_views.dart
@@ -1,7 +1,16 @@
import 'dart:ui';
+import 'package:flutter/material.dart';
+
import 'package:collection/collection.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
+import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
+import 'package:intl/intl.dart';
+import 'package:page_transition/page_transition.dart';
+import 'package:sliver_tools/sliver_tools.dart';
+
import 'package:fladder/models/boxset_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
@@ -10,22 +19,15 @@ import 'package:fladder/models/playlist_model.dart';
import 'package:fladder/providers/library_search_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
-import 'package:fladder/screens/shared/media/poster_grid.dart';
import 'package:fladder/screens/shared/media/poster_list_item.dart';
import 'package:fladder/screens/shared/media/poster_widget.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/string_extensions.dart';
+import 'package:fladder/util/theme_extensions.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
-import 'package:intl/intl.dart';
-import 'package:page_transition/page_transition.dart';
-import 'package:sliver_tools/sliver_tools.dart';
-import 'package:sticky_headers/sticky_headers/widget.dart';
final libraryViewTypeProvider = StateProvider((ref) {
return LibraryViewTypes.grid;
@@ -107,179 +109,139 @@ class LibraryViews extends ConsumerWidget {
switch (ref.watch(libraryViewTypeProvider)) {
case LibraryViewTypes.grid:
- if (groupByType != GroupBy.none) {
- final groupedItems = groupItemsBy(context, items, groupByType);
- return SliverList.builder(
- itemCount: groupedItems.length,
+ Widget createGrid(List items) {
+ return SliverGrid.builder(
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: posterSize.toInt(),
+ mainAxisSpacing: (8 * decimal) + 8,
+ crossAxisSpacing: (8 * decimal) + 8,
+ childAspectRatio: items.getMostCommonType.aspectRatio,
+ ),
+ itemCount: items.length,
itemBuilder: (context, index) {
- final name = groupedItems.keys.elementAt(index);
- final group = groupedItems[name];
- if (group?.isEmpty ?? false || group == null) {
- return Text(context.localized.empty);
- }
- return PosterGrid(
- posters: group!,
- name: name,
- itemBuilder: (context, index) {
- final item = group[index];
- return PosterWidget(
- key: Key(item.id),
- poster: group[index],
- maxLines: 2,
- heroTag: true,
- subTitle: item.subTitle(sortingOptions),
- excludeActions: excludeActions,
- otherActions: otherActions(item),
- selected: selected.contains(item),
- onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
- onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
- onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
- onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
- );
- },
+ final item = items[index];
+ return PosterWidget(
+ key: Key(item.id),
+ poster: item,
+ maxLines: 2,
+ heroTag: true,
+ subTitle: item.subTitle(sortingOptions),
+ excludeActions: excludeActions,
+ otherActions: otherActions(item),
+ selected: selected.contains(item),
+ onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
+ onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
+ onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
);
+ }
+
+ if (groupByType != GroupBy.none) {
+ final groupedItems = groupItemsBy(context, items, groupByType);
+ return MultiSliver(
+ children: groupedItems.entries.map(
+ (element) {
+ final name = element.key;
+ final group = element.value;
+ return stickyHeaderBuilder(
+ context,
+ header: name,
+ sliver: createGrid(group),
+ );
+ },
+ ).toList());
} else {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
- sliver: SliverGrid.builder(
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: posterSize.toInt(),
- mainAxisSpacing: (8 * decimal) + 8,
- crossAxisSpacing: (8 * decimal) + 8,
- childAspectRatio: AdaptiveLayout.poster(context).ratio,
- ),
- itemCount: items.length,
- itemBuilder: (context, index) {
- final item = items[index];
- return PosterWidget(
- key: Key(item.id),
- poster: item,
- maxLines: 2,
- heroTag: true,
- subTitle: item.subTitle(sortingOptions),
- excludeActions: excludeActions,
- otherActions: otherActions(item),
- selected: selected.contains(item),
- onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
- onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
- onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
- onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
- );
- },
- ),
+ sliver: createGrid(items),
);
}
case LibraryViewTypes.list:
- if (groupByType != GroupBy.none) {
- final groupedItems = groupItemsBy(context, items, groupByType);
+ Widget listBuilder(List items) {
return SliverList.builder(
- itemCount: groupedItems.length,
+ itemCount: items.length,
itemBuilder: (context, index) {
- final name = groupedItems.keys.elementAt(index);
- final group = groupedItems[name];
- if (group?.isEmpty ?? false) {
- return Text(context.localized.empty);
- }
- return StickyHeader(
- header: Text(name, style: Theme.of(context).textTheme.headlineSmall),
- content: ListView.builder(
- shrinkWrap: true,
- padding: EdgeInsets.zero,
- physics: const NeverScrollableScrollPhysics(),
- itemCount: group?.length,
- itemBuilder: (context, index) {
- final poster = group![index];
- return PosterListItem(
- key: Key(poster.id),
- poster: poster,
- subTitle: poster.subTitle(sortingOptions),
- excludeActions: excludeActions,
- otherActions: otherActions(poster),
- selected: selected.contains(poster),
- onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
- onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
- onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
- onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
- );
- },
- ),
+ final poster = items[index];
+ return PosterListItem(
+ poster: poster,
+ selected: selected.contains(poster),
+ excludeActions: excludeActions,
+ otherActions: otherActions(poster),
+ subTitle: poster.subTitle(sortingOptions),
+ onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
+ onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
+ onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
+ onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
);
}
- return SliverList.builder(
- itemCount: items.length,
- itemBuilder: (context, index) {
- final poster = items[index];
- return PosterListItem(
- poster: poster,
- selected: selected.contains(poster),
- excludeActions: excludeActions,
- otherActions: otherActions(poster),
- subTitle: poster.subTitle(sortingOptions),
- onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
- onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
- onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
- onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
- );
- },
- );
+ if (groupByType != GroupBy.none) {
+ final groupedItems = groupItemsBy(context, items, groupByType);
+ return MultiSliver(
+ children: groupedItems.entries.map(
+ (element) {
+ final name = element.key;
+ final group = element.value;
+ return stickyHeaderBuilder(
+ context,
+ header: name,
+ sliver: listBuilder(group),
+ );
+ },
+ ).toList());
+ }
+ return listBuilder(items);
case LibraryViewTypes.masonry:
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
- return SliverList.builder(
- itemCount: groupedItems.length,
- itemBuilder: (context, index) {
- final name = groupedItems.keys.elementAt(index);
- final group = groupedItems[name];
- if (group?.isEmpty ?? false) {
- return Text(context.localized.empty);
- }
- return Padding(
- padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0),
- child: StickyHeader(
- header: Text(name, style: Theme.of(context).textTheme.headlineMedium),
- overlapHeaders: true,
- content: Padding(
- padding: const EdgeInsets.only(top: 16.0),
- child: MasonryGridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- mainAxisSpacing: (8 * decimal) + 8,
- crossAxisSpacing: (8 * decimal) + 8,
- gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
- maxCrossAxisExtent:
- (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
- .toDouble() *
- 20,
- ),
- itemCount: group!.length,
- itemBuilder: (context, index) {
- final item = group[index];
- return PosterWidget(
- key: Key(item.id),
- poster: item,
- aspectRatio: item.primaryRatio,
- selected: selected.contains(item),
- inlineTitle: true,
- heroTag: true,
- subTitle: item.subTitle(sortingOptions),
- excludeActions: excludeActions,
- otherActions: otherActions(group[index]),
- onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
- onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
- onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
- onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
- );
- },
- ),
- )),
+ return MultiSliver(
+ children: groupedItems.entries.map(
+ (element) {
+ final name = element.key;
+ final group = element.value;
+ return stickyHeaderBuilder(
+ context,
+ header: name,
+ //MasonryGridView because SliverMasonryGrid breaks scrolling
+ sliver: SliverToBoxAdapter(
+ child: MasonryGridView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ mainAxisSpacing: (8 * decimal) + 8,
+ crossAxisSpacing: (8 * decimal) + 8,
+ gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
+ maxCrossAxisExtent:
+ (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
+ .toDouble() *
+ 12,
+ ),
+ itemCount: group.length,
+ itemBuilder: (context, index) {
+ final item = group[index];
+ return PosterWidget(
+ key: Key(item.id),
+ poster: item,
+ aspectRatio: item.primaryRatio,
+ selected: selected.contains(item),
+ inlineTitle: true,
+ heroTag: true,
+ subTitle: item.subTitle(sortingOptions),
+ excludeActions: excludeActions,
+ otherActions: otherActions(group[index]),
+ onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
+ onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
+ onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
+ onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
+ );
+ },
+ ),
+ ),
);
},
- );
+ ).toList());
} else {
return SliverMasonryGrid.count(
mainAxisSpacing: (8 * decimal) + 8,
@@ -309,6 +271,36 @@ class LibraryViews extends ConsumerWidget {
}
}
+ SliverStickyHeader stickyHeaderBuilder(
+ BuildContext context, {
+ required String header,
+ Widget? sliver,
+ }) {
+ return SliverStickyHeader(
+ header: Container(
+ height: 50,
+ alignment: Alignment.centerLeft,
+ child: Transform.translate(
+ offset: const Offset(-20, 0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: context.colors.surface.withValues(alpha: 0.9),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Text(
+ header,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ),
+ ),
+ ),
+ sliver: sliver,
+ );
+ }
+
Map> groupItemsBy(BuildContext context, List list, GroupBy groupOption) {
switch (groupOption) {
case GroupBy.dateAdded:
diff --git a/lib/screens/library_search/widgets/suggestion_search_bar.dart b/lib/screens/library_search/widgets/suggestion_search_bar.dart
index b52b286..8acb64d 100644
--- a/lib/screens/library_search/widgets/suggestion_search_bar.dart
+++ b/lib/screens/library_search/widgets/suggestion_search_bar.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:page_transition/page_transition.dart';
import 'package:fladder/models/item_base_model.dart';
@@ -65,6 +65,9 @@ class _SearchBarState extends ConsumerState {
});
return Card(
elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: FladderTheme.largeShape.borderRadius,
+ ),
shadowColor: Colors.transparent,
child: TypeAheadField(
focusNode: focusNode,
@@ -80,7 +83,7 @@ class _SearchBarState extends ConsumerState {
decorationBuilder: (context, child) => DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
- borderRadius: FladderTheme.defaultShape.borderRadius,
+ borderRadius: FladderTheme.largeShape.borderRadius,
),
child: child,
),
@@ -133,39 +136,45 @@ class _SearchBarState extends ConsumerState {
}
},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
- title: SizedBox(
- height: 50,
- child: Row(
- children: [
- Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
- child: AspectRatio(
- aspectRatio: 0.8,
- child: FladderImage(
- image: suggestion.images?.primary,
- fit: BoxFit.cover,
+ title: ConstrainedBox(
+ constraints: const BoxConstraints(
+ minHeight: 50,
+ maxHeight: 65,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Card(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
+ child: AspectRatio(
+ aspectRatio: 0.8,
+ child: FladderImage(
+ image: suggestion.images?.primary,
+ fit: BoxFit.cover,
+ ),
),
),
- ),
- const SizedBox(width: 8),
- Flexible(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Flexible(
- child: Text(
- suggestion.name,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- )),
- if (suggestion.overview.yearAired.toString().isNotEmpty)
+ const SizedBox(width: 8),
+ Flexible(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
Flexible(
- child:
- Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
- ],
+ child: Text(
+ suggestion.name,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ )),
+ if (suggestion.overview.yearAired.toString().isNotEmpty)
+ Flexible(
+ child: Opacity(
+ opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
);
diff --git a/lib/screens/login/login_screen.dart b/lib/screens/login/login_screen.dart
index 3b03518..891d814 100644
--- a/lib/screens/login/login_screen.dart
+++ b/lib/screens/login/login_screen.dart
@@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/shared/outlined_text_field.dart';
import 'package:fladder/screens/shared/passcode_input.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/auth_service.dart';
import 'package:fladder/util/fladder_config.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/login/login_user_grid.dart b/lib/screens/login/login_user_grid.dart
index a2f6684..31c2b9b 100644
--- a/lib/screens/login/login_user_grid.dart
+++ b/lib/screens/login/login_user_grid.dart
@@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget {
return Card(
elevation: 1,
shadowColor: Colors.transparent,
- clipBehavior: Clip.antiAlias,
+ clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
diff --git a/lib/screens/login/widgets/login_icon.dart b/lib/screens/login/widgets/login_icon.dart
index d0f374e..30114f2 100644
--- a/lib/screens/login/widgets/login_icon.dart
+++ b/lib/screens/login/widgets/login_icon.dart
@@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget {
aspectRatio: 1.0,
child: Card(
elevation: 1,
- clipBehavior: Clip.antiAlias,
+ clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: Stack(
children: [
diff --git a/lib/screens/metadata/edit_item.dart b/lib/screens/metadata/edit_item.dart
index 9ef4a82..c7f8566 100644
--- a/lib/screens/metadata/edit_item.dart
+++ b/lib/screens/metadata/edit_item.dart
@@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart';
import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:flutter/material.dart';
diff --git a/lib/screens/metadata/edit_screens/edit_image_content.dart b/lib/screens/metadata/edit_screens/edit_image_content.dart
index b5fa60c..6d2abc0 100644
--- a/lib/screens/metadata/edit_screens/edit_image_content.dart
+++ b/lib/screens/metadata/edit_screens/edit_image_content.dart
@@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/shared/file_picker.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
class EditImageContent extends ConsumerStatefulWidget {
final ImageType type;
diff --git a/lib/screens/metadata/identifty_screen.dart b/lib/screens/metadata/identifty_screen.dart
index 9c662a2..1672150 100644
--- a/lib/screens/metadata/identifty_screen.dart
+++ b/lib/screens/metadata/identifty_screen.dart
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/items/identify_provider.dart';
@@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState with TickerProv
final state = ref.watch(provider);
final posters = state.results;
final processing = state.processing;
- return ActionContent(
- showDividers: false,
- title: Container(
- color: Theme.of(context).colorScheme.surface,
- child: Column(
+ return Card(
+ child: ActionContent(
+ showDividers: false,
+ title: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
@@ -89,137 +88,137 @@ class _IdentifyScreenState extends ConsumerState with TickerProv
)
],
),
- ),
- child: TabBarView(
- controller: tabController,
- children: [
- inputFields(state),
- if (posters.isEmpty)
- Center(
- child: processing
- ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round)
- : Text(context.localized.noResults),
- )
- else
- Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Text(context.localized.replaceAllImages),
- const SizedBox(width: 16),
- Switch.adaptive(
- value: state.replaceAllImages,
- onChanged: (value) {
- ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value));
- },
- ),
- ],
- ),
- Flexible(
- child: ListView(
- shrinkWrap: true,
- children: posters
- .map((result) => ListTile(
- title: Row(
- children: [
- SizedBox(
- width: 75,
- child: Card(
- child: CachedNetworkImage(
- imageUrl: result.imageUrl ?? "",
- errorWidget: (context, url, error) => SizedBox(
- height: 75,
- child: Card(
- child: Center(
- child: Text(result.name?.getInitials() ?? ""),
+ child: TabBarView(
+ controller: tabController,
+ children: [
+ inputFields(state),
+ if (posters.isEmpty)
+ Center(
+ child: processing
+ ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round)
+ : Text(context.localized.noResults),
+ )
+ else
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(context.localized.replaceAllImages),
+ const SizedBox(width: 16),
+ Switch.adaptive(
+ value: state.replaceAllImages,
+ onChanged: (value) {
+ ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value));
+ },
+ ),
+ ],
+ ),
+ Flexible(
+ child: ListView(
+ shrinkWrap: true,
+ children: posters
+ .map((result) => ListTile(
+ title: Row(
+ children: [
+ SizedBox(
+ width: 75,
+ child: Card(
+ child: CachedNetworkImage(
+ imageUrl: result.imageUrl ?? "",
+ errorWidget: (context, url, error) => SizedBox(
+ height: 75,
+ child: Card(
+ child: Center(
+ child: Text(result.name?.getInitials() ?? ""),
+ ),
),
),
),
),
),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"),
- Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? ""))
- ],
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"),
+ Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? ""))
+ ],
+ ),
),
- ),
- Tooltip(
- message: context.localized.openWebLink,
- child: IconButton(
- onPressed: () {
- final providerKeyEntry = result.providerIds?.entries.first;
- final providerKey = providerKeyEntry?.key;
- final providerValue = providerKeyEntry?.value;
+ Tooltip(
+ message: context.localized.openWebLink,
+ child: IconButton(
+ onPressed: () {
+ final providerKeyEntry = result.providerIds?.entries.first;
+ final providerKey = providerKeyEntry?.key;
+ final providerValue = providerKeyEntry?.value;
- final externalId = state.externalIds
- .firstWhereOrNull((element) => element.key == providerKey)
- // ignore: deprecated_member_use_from_same_package
- ?.urlFormatString;
+ final externalId = state.externalIds
+ .firstWhereOrNull((element) => element.key == providerKey)
+ // ignore: deprecated_member_use_from_same_package
+ ?.urlFormatString;
- final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? "");
+ final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? "");
- launchUrl(context, url ?? "");
- },
- icon: const Icon(Icons.launch_rounded)),
- ),
- Tooltip(
- message: "Select result",
- child: IconButton(
- onPressed: !processing
- ? () async {
- final response = await ref.read(provider.notifier).setIdentity(result);
- if (response?.isSuccessful == true) {
- fladderSnackbar(context,
- title: context.localized.setIdentityTo(result.name ?? ""));
- } else {
- fladderSnackbarResponse(context, response,
- altTitle: context.localized.somethingWentWrong);
+ launchUrl(context, url ?? "");
+ },
+ icon: const Icon(Icons.launch_rounded)),
+ ),
+ Tooltip(
+ message: "Select result",
+ child: IconButton(
+ onPressed: !processing
+ ? () async {
+ final response = await ref.read(provider.notifier).setIdentity(result);
+ if (response?.isSuccessful == true) {
+ fladderSnackbar(context,
+ title: context.localized.setIdentityTo(result.name ?? ""));
+ } else {
+ fladderSnackbarResponse(context, response,
+ altTitle: context.localized.somethingWentWrong);
+ }
+
+ Navigator.of(context).pop();
}
-
- Navigator.of(context).pop();
- }
- : null,
- icon: const Icon(Icons.save_alt_rounded),
- ),
- )
- ],
- ),
- ))
- .toList(),
+ : null,
+ icon: const Icon(Icons.save_alt_rounded),
+ ),
+ )
+ ],
+ ),
+ ))
+ .toList(),
+ ),
),
- ),
- ],
- )
+ ],
+ )
+ ],
+ ),
+ actions: [
+ ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
+ const SizedBox(width: 16),
+ FilledButton(
+ onPressed: !processing
+ ? () async {
+ await ref.read(provider.notifier).remoteSearch();
+ tabController.animateTo(1);
+ }
+ : null,
+ child: processing
+ ? SizedBox(
+ width: 21,
+ height: 21,
+ child: CircularProgressIndicator.adaptive(
+ backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round),
+ )
+ : Text(context.localized.search),
+ ),
],
),
- actions: [
- ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
- const SizedBox(width: 16),
- FilledButton(
- onPressed: !processing
- ? () async {
- await ref.read(provider.notifier).remoteSearch();
- tabController.animateTo(1);
- }
- : null,
- child: processing
- ? SizedBox(
- width: 21,
- height: 21,
- child: CircularProgressIndicator.adaptive(
- backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round),
- )
- : Text(context.localized.search),
- ),
- ],
);
}
@@ -248,7 +247,7 @@ class _IdentifyScreenState extends ConsumerState with TickerProv
final controller =
currentKey == "Name" ? currentController : TextEditingController(text: state.searchString);
return FocusedOutlinedTextField(
- label: context.localized.userName,
+ label: context.localized.name,
controller: controller,
onChanged: (value) {
currentController = controller;
diff --git a/lib/screens/metadata/info_screen.dart b/lib/screens/metadata/info_screen.dart
index 74b4fb1..c5eabed 100644
--- a/lib/screens/metadata/info_screen.dart
+++ b/lib/screens/metadata/info_screen.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/information_model.dart';
import 'package:fladder/models/item_base_model.dart';
diff --git a/lib/screens/metadata/refresh_metadata.dart b/lib/screens/metadata/refresh_metadata.dart
index 9494cbc..8fe9a08 100644
--- a/lib/screens/metadata/refresh_metadata.dart
+++ b/lib/screens/metadata/refresh_metadata.dart
@@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart
index 60c10c9..91e344d 100644
--- a/lib/screens/photo_viewer/photo_viewer_controls.dart
+++ b/lib/screens/photo_viewer/photo_viewer_controls.dart
@@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/input_fields.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
diff --git a/lib/screens/photo_viewer/photo_viewer_screen.dart b/lib/screens/photo_viewer/photo_viewer_screen.dart
index f8b8d6d..a12f9f2 100644
--- a/lib/screens/photo_viewer/photo_viewer_screen.dart
+++ b/lib/screens/photo_viewer/photo_viewer_screen.dart
@@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart';
import 'package:fladder/screens/photo_viewer/simple_video_player.dart';
import 'package:fladder/screens/shared/default_title_bar.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/custom_cache_manager.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart';
diff --git a/lib/screens/settings/about_settings_page.dart b/lib/screens/settings/about_settings_page.dart
index ac24bf4..47952a2 100644
--- a/lib/screens/settings/about_settings_page.dart
+++ b/lib/screens/settings/about_settings_page.dart
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
-import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/screens/crash_screen/crash_screen.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
@@ -42,75 +42,79 @@ class AboutSettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final applicationInfo = ref.watch(applicationInfoProvider);
- return Card(
- child: SettingsScaffold(
- label: "",
- items: [
- const FladderLogo(),
- Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
- Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
- const SizedBox(height: 16),
- Text(context.localized.aboutCreatedBy),
- ],
+ return SettingsScaffold(
+ label: "",
+ items: [
+ const FladderLogo(),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
+ Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
+ const SizedBox(height: 16),
+ Text(context.localized.aboutCreatedBy),
+ ],
+ ),
+ const FractionallySizedBox(
+ widthFactor: 0.25,
+ child: Divider(
+ indent: 16,
+ endIndent: 16,
),
- const Divider(),
- Column(
- children: [
- Text(
- context.localized.aboutSocials,
- style: Theme.of(context).textTheme.titleLarge,
- ),
- const SizedBox(height: 6),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: socials
- .map(
- (e) => IconButton.filledTonal(
- onPressed: () => launchUrl(context, e.url),
- icon: Column(
- children: [
- Icon(e.icon),
- Text(e.label),
- ],
- ),
+ ),
+ Column(
+ children: [
+ Text(
+ context.localized.aboutSocials,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ const SizedBox(height: 6),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: socials
+ .map(
+ (e) => IconButton.filledTonal(
+ onPressed: () => launchUrl(context, e.url),
+ icon: Column(
+ children: [
+ Icon(e.icon),
+ Text(e.label),
+ ],
),
- )
- .toList()
- .addInBetween(const SizedBox(width: 16)),
- )
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.tonal(
- onPressed: () => showLicensePage(
- context: context,
- applicationIcon: const FladderIcon(size: 55),
- applicationVersion: applicationInfo.versionPlatformBuild,
- applicationLegalese: "DonutWare",
- ),
- child: Text(context.localized.aboutLicenses),
- )
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.tonal(
- onPressed: () => showDialog(
- context: context,
- builder: (context) => const CrashScreen(),
- ),
- child: Text(context.localized.errorLogs),
- )
- ],
- ),
- ].addInBetween(const SizedBox(height: 16)),
- ),
+ ),
+ )
+ .toList()
+ .addInBetween(const SizedBox(width: 16)),
+ )
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FilledButton.tonal(
+ onPressed: () => showLicensePage(
+ context: context,
+ applicationIcon: const FladderIcon(size: 55),
+ applicationVersion: applicationInfo.versionPlatformBuild,
+ applicationLegalese: "DonutWare",
+ ),
+ child: Text(context.localized.aboutLicenses),
+ )
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FilledButton.tonal(
+ onPressed: () => showDialog(
+ context: context,
+ builder: (context) => const CrashScreen(),
+ ),
+ child: Text(context.localized.errorLogs),
+ )
+ ],
+ ),
+ ].addInBetween(const SizedBox(height: 16)),
);
}
}
diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart
index a0f1272..a736658 100644
--- a/lib/screens/settings/client_sections/client_settings_advanced.dart
+++ b/lib/screens/settings/client_sections/client_settings_advanced.dart
@@ -2,79 +2,83 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
- return [
+ return settingsListGroup(
+ context,
SettingsLabelDivider(label: context.localized.advanced),
- SettingsListTile(
- label: Text(context.localized.settingsLayoutSizesTitle),
- subLabel: Text(context.localized.settingsLayoutSizesDesc),
- onTap: () async {
- final newItems = await openMultiSelectOptions(
- context,
- label: context.localized.settingsLayoutSizesTitle,
- items: ViewSize.values,
- allowMultiSelection: true,
- selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
- itemBuilder: (type, selected, tap) => CheckboxListTile(
- contentPadding: EdgeInsets.zero,
- value: selected,
- onChanged: (value) => tap(),
- title: Text(type.label(context)),
+ [
+ SettingsListTile(
+ label: Text(context.localized.settingsLayoutSizesTitle),
+ subLabel: Text(context.localized.settingsLayoutSizesDesc),
+ onTap: () async {
+ final newItems = await openMultiSelectOptions(
+ context,
+ label: context.localized.settingsLayoutSizesTitle,
+ items: ViewSize.values,
+ allowMultiSelection: true,
+ selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
+ itemBuilder: (type, selected, tap) => CheckboxListTile(
+ contentPadding: EdgeInsets.zero,
+ value: selected,
+ onChanged: (value) => tap(),
+ title: Text(type.label(context)),
+ ),
+ );
+ ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
+ },
+ trailing: Card(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ shadowColor: Colors.transparent,
+ elevation: 0,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(ref
+ .watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
+ .map((e) => e.label(context))
+ .join(', ')),
),
- );
- ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
- },
- trailing: Card(
- color: Theme.of(context).colorScheme.primaryContainer,
- shadowColor: Colors.transparent,
- elevation: 0,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(ref
- .watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
- .map((e) => e.label(context))
- .join(', ')),
),
),
- ),
- SettingsListTile(
- label: Text(context.localized.settingsLayoutModesTitle),
- subLabel: Text(context.localized.settingsLayoutModesDesc),
- onTap: () async {
- final newItems = await openMultiSelectOptions(
- context,
- label: context.localized.settingsLayoutModesTitle,
- items: LayoutMode.values,
- allowMultiSelection: true,
- selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
- itemBuilder: (type, selected, tap) => CheckboxListTile(
- contentPadding: EdgeInsets.zero,
- value: selected,
- onChanged: (value) => tap(),
- title: Text(type.label(context)),
+ SettingsListTile(
+ label: Text(context.localized.settingsLayoutModesTitle),
+ subLabel: Text(context.localized.settingsLayoutModesDesc),
+ onTap: () async {
+ final newItems = await openMultiSelectOptions(
+ context,
+ label: context.localized.settingsLayoutModesTitle,
+ items: LayoutMode.values,
+ allowMultiSelection: true,
+ selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
+ itemBuilder: (type, selected, tap) => CheckboxListTile(
+ contentPadding: EdgeInsets.zero,
+ value: selected,
+ onChanged: (value) => tap(),
+ title: Text(type.label(context)),
+ ),
+ );
+ ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
+ },
+ trailing: Card(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ shadowColor: Colors.transparent,
+ elevation: 0,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(ref
+ .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
+ .map((e) => e.label(context))
+ .join(', ')),
),
- );
- ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
- },
- trailing: Card(
- color: Theme.of(context).colorScheme.primaryContainer,
- shadowColor: Colors.transparent,
- elevation: 0,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(ref
- .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
- .map((e) => e.label(context))
- .join(', ')),
),
),
- ),
- ];
+ ],
+ );
}
diff --git a/lib/screens/settings/client_sections/client_settings_dashboard.dart b/lib/screens/settings/client_sections/client_settings_dashboard.dart
index 8377695..fc31f9c 100644
--- a/lib/screens/settings/client_sections/client_settings_dashboard.dart
+++ b/lib/screens/settings/client_sections/client_settings_dashboard.dart
@@ -7,89 +7,92 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
List buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider);
- return [
+ return settingsListGroup(
+ context,
SettingsLabelDivider(label: context.localized.dashboard),
- SettingsListTile(
- label: Text(context.localized.settingsHomeBannerTitle),
- subLabel: Text(context.localized.settingsHomeBannerDescription),
- trailing: EnumBox(
- current: ref.watch(
- homeSettingsProvider.select(
- (value) => value.homeBanner.label(context),
- ),
- ),
- itemBuilder: (context) => HomeBanner.values
- .map(
- (entry) => PopupMenuItem(
- value: entry,
- child: Text(entry.label(context)),
- onTap: () =>
- ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
- ),
- )
- .toList(),
- ),
- ),
- if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
+ [
SettingsListTile(
- label: Text(context.localized.settingsHomeBannerInformationTitle),
- subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
+ label: Text(context.localized.settingsHomeBannerTitle),
+ subLabel: Text(context.localized.settingsHomeBannerDescription),
trailing: EnumBox(
current: ref.watch(
- homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
+ homeSettingsProvider.select(
+ (value) => value.homeBanner.label(context),
+ ),
),
- itemBuilder: (context) => HomeCarouselSettings.values
+ itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
- onTap: () => ref
- .read(homeSettingsProvider.notifier)
- .update((context) => context.copyWith(carouselSettings: entry)),
+ onTap: () =>
+ ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
.toList(),
),
),
- SettingsListTile(
- label: Text(context.localized.settingsHomeNextUpTitle),
- subLabel: Text(context.localized.settingsHomeNextUpDesc),
- trailing: EnumBox(
- current: ref.watch(
- homeSettingsProvider.select(
- (value) => value.nextUp.label(context),
+ if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
+ SettingsListTile(
+ label: Text(context.localized.settingsHomeBannerInformationTitle),
+ subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
+ trailing: EnumBox(
+ current: ref.watch(
+ homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
+ ),
+ itemBuilder: (context) => HomeCarouselSettings.values
+ .map(
+ (entry) => PopupMenuItem(
+ value: entry,
+ child: Text(entry.label(context)),
+ onTap: () => ref
+ .read(homeSettingsProvider.notifier)
+ .update((context) => context.copyWith(carouselSettings: entry)),
+ ),
+ )
+ .toList(),
),
),
- itemBuilder: (context) => HomeNextUp.values
- .map(
- (entry) => PopupMenuItem(
- value: entry,
- child: Text(entry.label(context)),
- onTap: () =>
- ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
- ),
- )
- .toList(),
+ SettingsListTile(
+ label: Text(context.localized.settingsHomeNextUpTitle),
+ subLabel: Text(context.localized.settingsHomeNextUpDesc),
+ trailing: EnumBox(
+ current: ref.watch(
+ homeSettingsProvider.select(
+ (value) => value.nextUp.label(context),
+ ),
+ ),
+ itemBuilder: (context) => HomeNextUp.values
+ .map(
+ (entry) => PopupMenuItem(
+ value: entry,
+ child: Text(entry.label(context)),
+ onTap: () =>
+ ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
+ ),
+ )
+ .toList(),
+ ),
),
- ),
- SettingsListTile(
- label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
- subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
- onTap: () => ref
- .read(clientSettingsProvider.notifier)
- .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
- trailing: Switch(
- value: clientSettings.showAllCollectionTypes,
- onChanged: (value) => ref
+ SettingsListTile(
+ label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
+ subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
+ onTap: () => ref
.read(clientSettingsProvider.notifier)
- .update((current) => current.copyWith(showAllCollectionTypes: value)),
+ .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
+ trailing: Switch(
+ value: clientSettings.showAllCollectionTypes,
+ onChanged: (value) => ref
+ .read(clientSettingsProvider.notifier)
+ .update((current) => current.copyWith(showAllCollectionTypes: value)),
+ ),
),
- ),
- const Divider(),
- ];
+ ],
+ );
}
diff --git a/lib/screens/settings/client_sections/client_settings_download.dart b/lib/screens/settings/client_sections/client_settings_download.dart
index 75303bd..73ddde8 100644
--- a/lib/screens/settings/client_sections/client_settings_download.dart
+++ b/lib/screens/settings/client_sections/client_settings_download.dart
@@ -11,9 +11,10 @@ import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/input_fields.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
@@ -24,121 +25,122 @@ List buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
return [
if (canSync && !kIsWeb) ...[
- SettingsLabelDivider(label: context.localized.downloadsTitle),
- if (AdaptiveLayout.of(context).isDesktop) ...[
- SettingsListTile(
- label: Text(context.localized.downloadsPath),
- subLabel: Text(currentFolder ?? "-"),
- onTap: currentFolder != null
- ? () async => await showDialog(
- context: context,
- builder: (context) => AlertDialog(
- title: Text(context.localized.pathEditTitle),
- content: Text(context.localized.pathEditDesc),
- actions: [
- ElevatedButton(
- onPressed: () async {
- String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
- dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
- if (selectedDirectory != null) {
- ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
- }
- Navigator.of(context).pop();
- },
- child: Text(context.localized.change),
- )
- ],
+ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [
+ if (AdaptiveLayout.of(context).isDesktop) ...[
+ SettingsListTile(
+ label: Text(context.localized.downloadsPath),
+ subLabel: Text(currentFolder ?? "-"),
+ onTap: currentFolder != null
+ ? () async => await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(context.localized.pathEditTitle),
+ content: Text(context.localized.pathEditDesc),
+ actions: [
+ ElevatedButton(
+ onPressed: () async {
+ String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
+ dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
+ if (selectedDirectory != null) {
+ ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
+ }
+ Navigator.of(context).pop();
+ },
+ child: Text(context.localized.change),
+ )
+ ],
+ ),
+ )
+ : () async {
+ String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
+ dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
+ if (selectedDirectory != null) {
+ ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
+ }
+ },
+ trailing: currentFolder?.isNotEmpty == true
+ ? IconButton(
+ color: Theme.of(context).colorScheme.error,
+ onPressed: () async => await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(context.localized.pathClearTitle),
+ content: Text(context.localized.pathEditDesc),
+ actions: [
+ ElevatedButton(
+ onPressed: () {
+ ref.read(clientSettingsProvider.notifier).setSyncPath(null);
+ Navigator.of(context).pop();
+ },
+ child: Text(context.localized.clear),
+ )
+ ],
+ ),
),
+ icon: const Icon(IconsaxPlusLinear.folder_minus),
)
- : () async {
- String? selectedDirectory = await FilePicker.platform
- .getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
- if (selectedDirectory != null) {
- ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
+ : null,
+ ),
+ ],
+ FutureBuilder(
+ future: ref.watch(syncProvider.notifier).directorySize,
+ builder: (context, snapshot) {
+ final data = snapshot.data ?? 0;
+ return SettingsListTile(
+ label: Text(context.localized.downloadsSyncedData),
+ subLabel: Text(data.byteFormat ?? ""),
+ trailing: FilledButton(
+ onPressed: () {
+ showDefaultAlertDialog(
+ context,
+ context.localized.downloadsClearTitle,
+ context.localized.downloadsClearDesc,
+ (context) async {
+ await ref.read(syncProvider.notifier).clear();
+ setState(() {});
+ Navigator.of(context).pop();
+ },
+ context.localized.clear,
+ (context) => Navigator.of(context).pop(),
+ context.localized.cancel,
+ );
+ },
+ child: Text(context.localized.clear),
+ ),
+ );
+ },
+ ),
+ SettingsListTile(
+ label: Text(context.localized.clientSettingsRequireWifiTitle),
+ subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
+ onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
+ trailing: Switch(
+ value: clientSettings.requireWifi,
+ onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
+ ),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.maxConcurrentDownloadsTitle),
+ subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
+ trailing: SizedBox(
+ width: 100,
+ child: IntInputField(
+ controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
+ onSubmitted: (value) {
+ if (value != null) {
+ ref.read(clientSettingsProvider.notifier).update(
+ (current) => current.copyWith(
+ maxConcurrentDownloads: value,
+ ),
+ );
+
+ ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
- trailing: currentFolder?.isNotEmpty == true
- ? IconButton(
- color: Theme.of(context).colorScheme.error,
- onPressed: () async => await showDialog(
- context: context,
- builder: (context) => AlertDialog(
- title: Text(context.localized.pathClearTitle),
- content: Text(context.localized.pathEditDesc),
- actions: [
- ElevatedButton(
- onPressed: () {
- ref.read(clientSettingsProvider.notifier).setSyncPath(null);
- Navigator.of(context).pop();
- },
- child: Text(context.localized.clear),
- )
- ],
- ),
- ),
- icon: const Icon(IconsaxPlusLinear.folder_minus),
- )
- : null,
+ )),
),
- ],
- FutureBuilder(
- future: ref.watch(syncProvider.notifier).directorySize,
- builder: (context, snapshot) {
- final data = snapshot.data ?? 0;
- return SettingsListTile(
- label: Text(context.localized.downloadsSyncedData),
- subLabel: Text(data.byteFormat ?? ""),
- trailing: FilledButton(
- onPressed: () {
- showDefaultAlertDialog(
- context,
- context.localized.downloadsClearTitle,
- context.localized.downloadsClearDesc,
- (context) async {
- await ref.read(syncProvider.notifier).clear();
- setState(() {});
- Navigator.of(context).pop();
- },
- context.localized.clear,
- (context) => Navigator.of(context).pop(),
- context.localized.cancel,
- );
- },
- child: Text(context.localized.clear),
- ),
- );
- },
- ),
- SettingsListTile(
- label: Text(context.localized.clientSettingsRequireWifiTitle),
- subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
- onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
- trailing: Switch(
- value: clientSettings.requireWifi,
- onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
- ),
- ),
- SettingsListTile(
- label: Text(context.localized.maxConcurrentDownloadsTitle),
- subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
- trailing: SizedBox(
- width: 100,
- child: IntInputField(
- controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
- onSubmitted: (value) {
- if (value != null) {
- ref.read(clientSettingsProvider.notifier).update(
- (current) => current.copyWith(
- maxConcurrentDownloads: value,
- ),
- );
-
- ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
- }
- },
- )),
- ),
- const Divider(),
+ ]),
+ const SizedBox(height: 12),
],
];
}
diff --git a/lib/screens/settings/client_sections/client_settings_theme.dart b/lib/screens/settings/client_sections/client_settings_theme.dart
index de3b055..cbef7c3 100644
--- a/lib/screens/settings/client_sections/client_settings_theme.dart
+++ b/lib/screens/settings/client_sections/client_settings_theme.dart
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/color_extensions.dart';
import 'package:fladder/util/custom_color_themes.dart';
import 'package:fladder/util/localization_helper.dart';
@@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart';
List buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider);
- return [
- SettingsLabelDivider(label: context.localized.theme),
+ return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [
SettingsListTile(
label: Text(context.localized.mode),
subLabel: Text(clientSettings.themeMode.label(context)),
@@ -107,6 +107,5 @@ List buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
),
),
- const Divider(),
- ];
+ ]);
}
diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart
index b5578df..015bfca 100644
--- a/lib/screens/settings/client_sections/client_settings_visual.dart
+++ b/lib/screens/settings/client_sections/client_settings_visual.dart
@@ -1,5 +1,3 @@
-import 'dart:developer';
-
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -8,8 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/input_fields.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
@@ -22,136 +21,136 @@ List buildClientSettingsVisual(
) {
final clientSettings = ref.watch(clientSettingsProvider);
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
- return [
+ return settingsListGroup(
+ context,
SettingsLabelDivider(label: context.localized.settingsVisual),
- SettingsListTile(
- label: Text(context.localized.displayLanguage),
- trailing: Localizations.override(
- context: context,
- locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
- child: Builder(builder: (context) {
- String language = "Unknown";
- try {
- language = context.localized.nativeName;
- } catch (e) {
- log(e.toString());
- }
- return EnumBox(
- current: language,
- itemBuilder: (context) {
- return [
- ...AppLocalizations.supportedLocales.map(
- (entry) => PopupMenuItem(
- value: entry,
- child: Localizations.override(
- context: context,
- locale: entry,
- child: Builder(builder: (context) {
- return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
- }),
+ [
+ SettingsListTile(
+ label: Text(context.localized.displayLanguage),
+ trailing: Localizations.override(
+ context: context,
+ locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
+ child: Builder(builder: (context) {
+ String language = "Unknown";
+ try {
+ language = context.localized.nativeName;
+ } catch (_) {}
+ return EnumBox(
+ current: language,
+ itemBuilder: (context) {
+ return [
+ ...AppLocalizations.supportedLocales.map(
+ (entry) => PopupMenuItem(
+ value: entry,
+ child: Localizations.override(
+ context: context,
+ locale: entry,
+ child: Builder(builder: (context) {
+ return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
+ }),
+ ),
+ onTap: () => ref
+ .read(clientSettingsProvider.notifier)
+ .update((state) => state.copyWith(selectedLocale: entry)),
),
- onTap: () => ref
- .read(clientSettingsProvider.notifier)
- .update((state) => state.copyWith(selectedLocale: entry)),
+ )
+ ];
+ },
+ );
+ }),
+ ),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.settingsBlurredPlaceholderTitle),
+ subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
+ onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
+ trailing: Switch(
+ value: clientSettings.blurPlaceHolders,
+ onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
+ ),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.settingsBlurEpisodesTitle),
+ subLabel: Text(context.localized.settingsBlurEpisodesDesc),
+ onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
+ trailing: Switch(
+ value: clientSettings.blurUpcomingEpisodes,
+ onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
+ ),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.settingsEnableOsMediaControls),
+ onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
+ trailing: Switch(
+ value: clientSettings.enableMediaKeys,
+ onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
+ ),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.settingsNextUpCutoffDays),
+ trailing: SizedBox(
+ width: 100,
+ child: IntInputField(
+ suffix: context.localized.days,
+ controller: nextUpDaysEditor,
+ onSubmitted: (value) {
+ if (value != null) {
+ ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
+ nextUpDateCutoff: Duration(days: value),
+ ));
+ }
+ },
+ )),
+ ),
+ SettingsListTile(
+ label: Text(context.localized.libraryPageSizeTitle),
+ subLabel: Text(context.localized.libraryPageSizeDesc),
+ trailing: SizedBox(
+ width: 100,
+ child: IntInputField(
+ controller: libraryPageSizeController,
+ placeHolder: "500",
+ onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
+ (current) => current.copyWith(libraryPageSize: value),
),
- )
- ];
- },
- );
- }),
+ )),
),
- ),
- SettingsListTile(
- label: Text(context.localized.settingsBlurredPlaceholderTitle),
- subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
- onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
- trailing: Switch(
- value: clientSettings.blurPlaceHolders,
- onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
- ),
- ),
- SettingsListTile(
- label: Text(context.localized.settingsBlurEpisodesTitle),
- subLabel: Text(context.localized.settingsBlurEpisodesDesc),
- onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
- trailing: Switch(
- value: clientSettings.blurUpcomingEpisodes,
- onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
- ),
- ),
- SettingsListTile(
- label: Text(context.localized.settingsEnableOsMediaControls),
- onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
- trailing: Switch(
- value: clientSettings.enableMediaKeys,
- onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
- ),
- ),
- SettingsListTile(
- label: Text(context.localized.settingsNextUpCutoffDays),
- trailing: SizedBox(
- width: 100,
- child: IntInputField(
- suffix: context.localized.days,
- controller: nextUpDaysEditor,
- onSubmitted: (value) {
- if (value != null) {
- ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
- nextUpDateCutoff: Duration(days: value),
- ));
- }
- },
- )),
- ),
- SettingsListTile(
- label: Text(context.localized.libraryPageSizeTitle),
- subLabel: Text(context.localized.libraryPageSizeDesc),
- trailing: SizedBox(
- width: 100,
- child: IntInputField(
- controller: libraryPageSizeController,
- placeHolder: "500",
- onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
- (current) => current.copyWith(libraryPageSize: value),
- ),
- )),
- ),
- SettingsListTile(
- label: Text(AdaptiveLayout.of(context).isDesktop
- ? context.localized.settingsShowScaleSlider
- : context.localized.settingsPosterPinch),
- onTap: () => ref.read(clientSettingsProvider.notifier).update(
- (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
- ),
- trailing: Switch(
- value: clientSettings.pinchPosterZoom,
- onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
- (current) => current.copyWith(pinchPosterZoom: value),
+ SettingsListTile(
+ label: Text(AdaptiveLayout.of(context).isDesktop
+ ? context.localized.settingsShowScaleSlider
+ : context.localized.settingsPosterPinch),
+ onTap: () => ref.read(clientSettingsProvider.notifier).update(
+ (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
),
+ trailing: Switch(
+ value: clientSettings.pinchPosterZoom,
+ onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
+ (current) => current.copyWith(pinchPosterZoom: value),
+ ),
+ ),
),
- ),
- Column(
- children: [
- SettingsListTile(
- label: Text(context.localized.settingsPosterSize),
- trailing: Text(
- clientSettings.posterSize.toString(),
- style: Theme.of(context).textTheme.bodyLarge,
+ Column(
+ children: [
+ SettingsListTile(
+ label: Text(context.localized.settingsPosterSize),
+ trailing: Text(
+ clientSettings.posterSize.toString(),
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: FladderSlider(
- min: 0.5,
- max: 1.5,
- value: clientSettings.posterSize,
- divisions: 20,
- onChanged: (value) =>
- ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: FladderSlider(
+ min: 0.5,
+ max: 1.5,
+ value: clientSettings.posterSize,
+ divisions: 20,
+ onChanged: (value) =>
+ ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
+ ),
),
- ),
- ],
- ),
- const Divider(),
- ];
+ ],
+ ),
+ ],
+ );
}
diff --git a/lib/screens/settings/client_settings_page.dart b/lib/screens/settings/client_settings_page.dart
index e594717..1282115 100644
--- a/lib/screens/settings/client_settings_page.dart
+++ b/lib/screens/settings/client_settings_page.dart
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
@@ -16,7 +15,8 @@ import 'package:fladder/screens/settings/client_sections/client_settings_visual.
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/simple_duration_picker.dart';
@@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState {
@override
Widget build(BuildContext context) {
final clientSettings = ref.watch(clientSettingsProvider);
- final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
- AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
- return Card(
- elevation: showBackground ? 2 : 0,
- child: SettingsScaffold(
- label: "Fladder",
- items: [
- ...buildClientSettingsDownload(context, ref, setState),
- SettingsLabelDivider(label: context.localized.lockscreen),
+ return SettingsScaffold(
+ label: "Fladder",
+ items: [
+ ...buildClientSettingsDownload(context, ref, setState),
+ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [
SettingsListTile(
label: Text(context.localized.timeOut),
subLabel: Text(timePickerString(context, clientSettings.timeOut)),
@@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState {
: null);
},
),
- const Divider(),
- ...buildClientSettingsDashboard(context, ref),
- ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
- ...buildClientSettingsTheme(context, ref),
- if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
- SettingsLabelDivider(label: context.localized.controls),
+ ]),
+ const SizedBox(height: 12),
+ ...buildClientSettingsDashboard(context, ref),
+ const SizedBox(height: 12),
+ ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
+ const SizedBox(height: 12),
+ ...buildClientSettingsTheme(context, ref),
+ const SizedBox(height: 12),
+ if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
+ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [
SettingsListTile(
label: Text(context.localized.mouseDragSupport),
subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled),
@@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState {
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
),
),
- const Divider(),
- ],
- ...buildClientSettingsAdvanced(context, ref),
- if (kDebugMode) ...[
- const SizedBox(height: 64),
- SettingsListTile(
- label: Text(
- context.localized.clearAllSettings,
- ),
- contentColor: Theme.of(context).colorScheme.error,
- onTap: () {
- showDialog(
- context: context,
- builder: (context) => Dialog(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- context.localized.clearAllSettingsQuestion,
- style: Theme.of(context).textTheme.titleLarge,
- ),
- const SizedBox(height: 16),
- Text(
- context.localized.unableToReverseAction,
- ),
- const SizedBox(height: 16),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- FilledButton(
- onPressed: () => Navigator.of(context).pop(),
- child: Text(context.localized.cancel),
- ),
- const SizedBox(width: 8),
- ElevatedButton(
- onPressed: () async {
- await ref.read(sharedPreferencesProvider).clear();
- context.router.push(const LoginRoute());
- },
- child: Text(context.localized.clear),
- )
- ],
- ),
- ],
- ),
+ ]),
+ const SizedBox(height: 12),
+ ],
+ ...buildClientSettingsAdvanced(context, ref),
+ if (kDebugMode) ...[
+ const SizedBox(height: 64),
+ SettingsListTile(
+ label: Text(
+ context.localized.clearAllSettings,
+ ),
+ contentColor: Theme.of(context).colorScheme.error,
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (context) => Dialog(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ context.localized.clearAllSettingsQuestion,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ context.localized.unableToReverseAction,
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ FilledButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(context.localized.cancel),
+ ),
+ const SizedBox(width: 8),
+ ElevatedButton(
+ onPressed: () async {
+ await ref.read(sharedPreferencesProvider).clear();
+ context.router.push(const LoginRoute());
+ },
+ child: Text(context.localized.clear),
+ )
+ ],
+ ),
+ ],
),
),
- );
- },
- ),
- ],
+ ),
+ );
+ },
+ ),
],
- ),
+ ],
);
}
}
diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart
index 93748b7..0a64edb 100644
--- a/lib/screens/settings/player_settings_page.dart
+++ b/lib/screens/settings/player_settings_page.dart
@@ -8,20 +8,20 @@ import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart';
-import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
-import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
+import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
+import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/settings/widgets/settings_message_box.dart';
import 'package:fladder/screens/settings/widgets/subtitle_editor.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
-import 'package:fladder/util/adaptive_layout.dart';
+import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/box_fit_extension.dart';
import 'package:fladder/util/localization_helper.dart';
@@ -41,100 +41,105 @@ class _PlayerSettingsPageState extends ConsumerState {
Widget build(BuildContext context) {
final videoSettings = ref.watch(videoPlayerSettingsProvider);
final provider = ref.read(videoPlayerSettingsProvider.notifier);
- final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
- AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
final connectionState = ref.watch(connectivityStatusProvider);
- return Card(
- elevation: showBackground ? 2 : 0,
- child: SettingsScaffold(
- label: context.localized.settingsPlayerTitle,
- items: [
+ return SettingsScaffold(
+ label: context.localized.settingsPlayerTitle,
+ items: [
+ ...settingsListGroup(
+ context,
SettingsLabelDivider(label: context.localized.video),
- if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
+ [
+ if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
+ Column(
+ children: [
+ SettingsListTile(
+ label: Text(context.localized.videoScalingFillScreenTitle),
+ subLabel: Text(context.localized.videoScalingFillScreenDesc),
+ onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
+ trailing: Switch(
+ value: videoSettings.fillScreen,
+ onChanged: (value) => provider.setFillScreen(value),
+ ),
+ ),
+ AnimatedFadeSize(
+ child: videoSettings.fillScreen
+ ? SettingsMessageBox(
+ context.localized.videoScalingFillScreenNotif,
+ messageType: MessageType.warning,
+ )
+ : Container(),
+ ),
+ ],
+ ),
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
- subLabel: Text(context.localized.videoScalingFillScreenDesc),
- onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
- trailing: Switch(
- value: videoSettings.fillScreen,
- onChanged: (value) => provider.setFillScreen(value),
+ subLabel: Text(videoSettings.videoFit.label(context)),
+ onTap: () => openMultiSelectOptions(
+ context,
+ label: context.localized.videoScalingFillScreenTitle,
+ items: BoxFit.values,
+ selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
+ onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
+ itemBuilder: (type, selected, tap) => RadioListTile(
+ groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
+ title: Text(type.label(context)),
+ value: type,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ contentPadding: EdgeInsets.zero,
+ onChanged: (value) => tap(),
+ ),
),
),
- AnimatedFadeSize(
- child: videoSettings.fillScreen
- ? SettingsMessageBox(
- context.localized.videoScalingFillScreenNotif,
- messageType: MessageType.warning,
- )
- : Container(),
- ),
- SettingsListTile(
- label: Text(context.localized.videoScalingFillScreenTitle),
- subLabel: Text(videoSettings.videoFit.label(context)),
- onTap: () => openMultiSelectOptions(
- context,
- label: context.localized.videoScalingFillScreenTitle,
- items: BoxFit.values,
- selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
- onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
- itemBuilder: (type, selected, tap) => RadioListTile(
- groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
- title: Text(type.label(context)),
- value: type,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
- contentPadding: EdgeInsets.zero,
- onChanged: (value) => tap(),
+ SettingsListTile(
+ label: _StatusIndicator(
+ homeInternet: connectionState.homeInternet,
+ label: Text(context.localized.homeStreamingQualityTitle),
+ ),
+ subLabel: Text(context.localized.homeStreamingQualityDesc),
+ trailing: EnumBox(
+ current: ref.watch(
+ videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
+ ),
+ itemBuilder: (context) => Bitrate.values
+ .map(
+ (entry) => PopupMenuItem(
+ value: entry,
+ child: Text(entry.label(context)),
+ onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
+ ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
+ ),
+ )
+ .toList(),
),
),
- ),
- SettingsListTile(
- label: _StatusIndicator(
- homeInternet: connectionState.homeInternet,
- label: Text(context.localized.homeStreamingQualityTitle),
- ),
- subLabel: Text(context.localized.homeStreamingQualityDesc),
- trailing: EnumBox(
- current: ref.watch(
- videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
+ SettingsListTile(
+ label: _StatusIndicator(
+ homeInternet: !connectionState.homeInternet,
+ label: Text(context.localized.internetStreamingQualityTitle),
),
- itemBuilder: (context) => Bitrate.values
- .map(
- (entry) => PopupMenuItem(
- value: entry,
- child: Text(entry.label(context)),
- onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
- ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
- ),
- )
- .toList(),
- ),
- ),
- SettingsListTile(
- label: _StatusIndicator(
- homeInternet: !connectionState.homeInternet,
- label: Text(context.localized.internetStreamingQualityTitle),
- ),
- subLabel: Text(context.localized.internetStreamingQualityDesc),
- trailing: EnumBox(
- current: ref.watch(
- videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
+ subLabel: Text(context.localized.internetStreamingQualityDesc),
+ trailing: EnumBox(
+ current: ref.watch(
+ videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
+ ),
+ itemBuilder: (context) => Bitrate.values
+ .map(
+ (entry) => PopupMenuItem(
+ value: entry,
+ child: Text(entry.label(context)),
+ onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
+ ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
+ ),
+ )
+ .toList(),
),
- itemBuilder: (context) => Bitrate.values
- .map(
- (entry) => PopupMenuItem(
- value: entry,
- child: Text(entry.label(context)),
- onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
- ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
- ),
- )
- .toList(),
),
- ),
- const Divider(),
- SettingsLabelDivider(label: context.localized.mediaSegmentActions),
+ ],
+ ),
+ const SizedBox(height: 12),
+ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [
...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState {
),
),
),
- SettingsLabelDivider(label: context.localized.playbackTrackSelection),
+ ]),
+ const SizedBox(height: 12),
+ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
SettingsListTile(
label: Text(context.localized.rememberAudioSelections),
subLabel: Text(context.localized.rememberAudioSelectionsDesc),
@@ -190,8 +197,9 @@ class _PlayerSettingsPageState extends ConsumerState