feat: UI 2.0 and other Improvements (#357)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-06-01 10:37:19 +02:00 committed by GitHub
parent 9ca06eaa37
commit e7b5bb40ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 4584 additions and 3626 deletions

View file

@ -53,11 +53,14 @@
<details close> <details close>
<summary>Mobile</summary> <summary>Mobile</summary>
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Dashboard.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Dashboard.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Details.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Details_2.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Details_2.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Favourites.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Favourites.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Library.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Library.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Library_Search.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Resume_Tab.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Resume_Tab.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Sync.png?raw=true" alt="Fladder" width="200"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Sync.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Settings.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Player.png?raw=true" alt="Fladder" width="1280"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Player.png?raw=true" alt="Fladder" width="1280">
</details> </details>
@ -65,8 +68,14 @@
<summary>Tablet</summary> <summary>Tablet</summary>
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Dashboard.png?raw=true" alt="Fladder" width="1280"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Dashboard.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Details.png?raw=true" alt="Fladder" width="1280"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Details.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Settings.png?raw=true" alt="Fladder" width="1280"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Details_2.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Sync.png?raw=true" alt="Fladder" width="1280"> <img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Favourites.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Library.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Library_Search.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Resume_Tab.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Sync.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Settings.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Player.png?raw=true" alt="Fladder" width="1280">
</details> </details>
Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder) Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 930 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

@ -1211,5 +1211,14 @@
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.", "rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
"@rememberSubtitleSelectionsDesc": {}, "@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.", "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"
} }

View file

@ -19,7 +19,6 @@ import 'package:universal_html/html.dart' as html;
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/account_model.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/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_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/routes/auto_router.gr.dart';
import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/theme.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/application_info.dart';
import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/fladder_config.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -108,13 +107,7 @@ void main() async {
)) ))
], ],
child: AdaptiveLayoutBuilder( child: AdaptiveLayoutBuilder(
fallBack: ViewSize.tablet, child: (context) => const Main(),
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(),
), ),
), ),
); );
@ -304,6 +297,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
colorScheme: darkTheme.colorScheme.copyWith( colorScheme: darkTheme.colorScheme.copyWith(
surface: amoledOverwrite, surface: amoledOverwrite,
surfaceContainerHighest: amoledOverwrite, surfaceContainerHighest: amoledOverwrite,
surfaceContainerLow: amoledOverwrite,
), ),
), ),
themeMode: themeMode, themeMode: themeMode,

View file

@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/credentials_model.dart'; import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/models/library_filters_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'; import 'package:fladder/util/localization_helper.dart';
part 'account_model.freezed.dart'; part 'account_model.freezed.dart';

View file

@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType {
Set<FladderItemType> get itemKinds { Set<FladderItemType> get itemKinds {
switch (this) { switch (this) {
case CollectionType.music:
return {FladderItemType.musicAlbum};
case CollectionType.movies: case CollectionType.movies:
return {FladderItemType.movie}; return {FladderItemType.movie};
case CollectionType.tvshows: case CollectionType.tvshows:
@ -30,6 +32,8 @@ extension CollectionTypeExtension on CollectionType {
IconData getIconType(bool outlined) { IconData getIconType(bool outlined) {
switch (this) { switch (this) {
case CollectionType.music:
return outlined ? IconsaxPlusLinear.music_square : IconsaxPlusBold.music_square;
case CollectionType.movies: case CollectionType.movies:
return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal; return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal;
case CollectionType.tvshows: case CollectionType.tvshows:
@ -48,4 +52,16 @@ extension CollectionTypeExtension on CollectionType {
return IconsaxPlusLinear.information; 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,
};
} }

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:dart_mappable/dart_mappable.dart'; import 'package:dart_mappable/dart_mappable.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; 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}); 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<FladderItemType> get playable => { static Set<FladderItemType> get playable => {
FladderItemType.series, FladderItemType.series,
FladderItemType.episode, FladderItemType.episode,
@ -317,27 +326,25 @@ enum FladderItemType {
FladderItemType.video, FladderItemType.video,
}; };
String label(BuildContext context) { String label(BuildContext context) => switch (this) {
return switch (this) { FladderItemType.baseType => context.localized.mediaTypeBase,
FladderItemType.baseType => context.localized.mediaTypeBase, FladderItemType.audio => context.localized.audio,
FladderItemType.audio => context.localized.audio, FladderItemType.collectionFolder => context.localized.collectionFolder,
FladderItemType.collectionFolder => context.localized.collectionFolder, FladderItemType.musicAlbum => context.localized.musicAlbum,
FladderItemType.musicAlbum => context.localized.musicAlbum, FladderItemType.musicVideo => context.localized.video,
FladderItemType.musicVideo => context.localized.video, FladderItemType.video => context.localized.video,
FladderItemType.video => context.localized.video, FladderItemType.movie => context.localized.mediaTypeMovie,
FladderItemType.movie => context.localized.mediaTypeMovie, FladderItemType.series => context.localized.mediaTypeSeries,
FladderItemType.series => context.localized.mediaTypeSeries, FladderItemType.season => context.localized.mediaTypeSeason,
FladderItemType.season => context.localized.mediaTypeSeason, FladderItemType.episode => context.localized.mediaTypeEpisode,
FladderItemType.episode => context.localized.mediaTypeEpisode, FladderItemType.photo => context.localized.mediaTypePhoto,
FladderItemType.photo => context.localized.mediaTypePhoto, FladderItemType.person => context.localized.mediaTypePerson,
FladderItemType.person => context.localized.mediaTypePerson, FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum,
FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum, FladderItemType.folder => context.localized.mediaTypeFolder,
FladderItemType.folder => context.localized.mediaTypeFolder, FladderItemType.boxset => context.localized.mediaTypeBoxset,
FladderItemType.boxset => context.localized.mediaTypeBoxset, FladderItemType.playlist => context.localized.mediaTypePlaylist,
FladderItemType.playlist => context.localized.mediaTypePlaylist, FladderItemType.book => context.localized.mediaTypeBook,
FladderItemType.book => context.localized.mediaTypeBook, };
};
}
BaseItemKind get dtoKind => switch (this) { BaseItemKind get dtoKind => switch (this) {
FladderItemType.baseType => BaseItemKind.userrootfolder, FladderItemType.baseType => BaseItemKind.userrootfolder,

View file

@ -199,20 +199,20 @@ extension EpisodeListExtensions on List<EpisodeModel> {
} }
EpisodeModel? get nextUp { 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 final lastWatchedIndex = [
.lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available); episodes.lastIndexWhere((e) => e.userData.progress != 0),
final lastPlayed = episodes.lastIndexWhere((e) => e.userData.played),
episodes.lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available); ].reduce((a, b) => a > b ? a : b);
if (lastProgress == -1 && lastPlayed == -1) { if (lastWatchedIndex >= 0 && lastWatchedIndex + 1 < episodes.length) {
return episodes.firstWhereOrNull((element) => element.status == EpisodeStatus.available); final next = episodes.sublist(lastWatchedIndex + 1).firstWhereOrNull((e) => e.status == EpisodeStatus.available);
} else { if (next != null) return next;
return episodes
.getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, episodes.length)
.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
} }
return episodes.firstOrNull;
} }
bool get allPlayed { bool get allPlayed {

View file

@ -1,5 +1,6 @@
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; 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/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart'; import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'series_model.mapper.dart'; part 'series_model.mapper.dart';

View file

@ -1,9 +1,9 @@
import 'package:fladder/models/item_base_model.dart'; import 'package:flutter/material.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:flutter/material.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/util/localization_helper.dart';
enum SortingOptions { enum SortingOptions {
name([ItemSortBy.name]), name([ItemSortBy.name]),

View file

@ -198,17 +198,15 @@ class PlaybackModelHelper {
final streamModel = firstItemToPlay.streamModel; final streamModel = firstItemToPlay.streamModel;
final audioStreamIndex = selectAudioStream( final audioStreamIndex = selectAudioStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)), ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
oldModel?.mediaStreams?.currentAudioStream, oldModel?.mediaStreams?.currentAudioStream,
streamModel?.audioStreams, streamModel?.audioStreams,
streamModel?.defaultAudioStreamIndex streamModel?.defaultAudioStreamIndex);
);
final subStreamIndex = selectSubStream( final subStreamIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)), ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
oldModel?.mediaStreams?.currentSubStream, oldModel?.mediaStreams?.currentSubStream,
streamModel?.subStreams, streamModel?.subStreams,
streamModel?.defaultSubStreamIndex streamModel?.defaultSubStreamIndex);
);
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost( final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id, itemId: firstItemToPlay.id,
@ -345,14 +343,12 @@ class PlaybackModelHelper {
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)), ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
playbackModel.mediaStreams?.currentAudioStream, playbackModel.mediaStreams?.currentAudioStream,
playbackModel.audioStreams, playbackModel.audioStreams,
playbackModel.mediaStreams?.defaultAudioStreamIndex playbackModel.mediaStreams?.defaultAudioStreamIndex);
);
final subIndex = selectSubStream( final subIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)), ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
playbackModel.mediaStreams?.currentSubStream, playbackModel.mediaStreams?.currentSubStream,
playbackModel.subStreams, playbackModel.subStreams,
playbackModel.mediaStreams?.defaultSubStreamIndex playbackModel.mediaStreams?.defaultSubStreamIndex);
);
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost( Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id, itemId: item.id,

View file

@ -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/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 { class RecommendedModel {
final String name; final NameSwitch name;
final List<ItemBaseModel> posters; final List<ItemBaseModel> posters;
final String type; final RecommendationType? type;
RecommendedModel({ RecommendedModel({
required this.name, required this.name,
required this.posters, required this.posters,
required this.type, this.type,
}); });
RecommendedModel copyWith({ RecommendedModel copyWith({
String? name, NameSwitch? name,
List<ItemBaseModel>? posters, List<ItemBaseModel>? posters,
String? type, RecommendationType? type,
}) { }) {
return RecommendedModel( return RecommendedModel(
name: name ?? this.name, name: name ?? this.name,
@ -22,4 +68,12 @@ class RecommendedModel {
type: type ?? this.type, 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,
);
}
} }

View file

@ -33,7 +33,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
@Default(true) bool requireWifi, @Default(true) bool requireWifi,
@Default(false) bool showAllCollectionTypes, @Default(false) bool showAllCollectionTypes,
@Default(2) int maxConcurrentDownloads, @Default(2) int maxConcurrentDownloads,
@Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
int? libraryPageSize, int? libraryPageSize,
}) = _ClientSettingsModel; }) = _ClientSettingsModel;

View file

@ -375,7 +375,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
this.requireWifi = true, this.requireWifi = true,
this.showAllCollectionTypes = false, this.showAllCollectionTypes = false,
this.maxConcurrentDownloads = 2, this.maxConcurrentDownloads = 2,
this.schemeVariant = DynamicSchemeVariant.tonalSpot, this.schemeVariant = DynamicSchemeVariant.rainbow,
this.libraryPageSize}) this.libraryPageSize})
: super._(); : super._();

View file

@ -40,7 +40,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
(json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2, (json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2,
schemeVariant: $enumDecodeNullable( schemeVariant: $enumDecodeNullable(
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
DynamicSchemeVariant.tonalSpot, DynamicSchemeVariant.rainbow,
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
); );

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
part 'home_settings_model.freezed.dart'; part 'home_settings_model.freezed.dart';
@ -36,42 +37,6 @@ T selectAvailableOrSmaller<T>(T value, Set<T> availableOptions, List<T> allOptio
return availableOptions.first; 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 { enum HomeBanner {
hide, hide,
carousel, carousel,

View file

@ -1,9 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.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.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; 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/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 { class ViewModel {
final String name; final String name;
@ -16,7 +24,9 @@ class ViewModel {
final CollectionType collectionType; final CollectionType collectionType;
final dto.PlayAccess playAccess; final dto.PlayAccess playAccess;
final List<ItemBaseModel> recentlyAdded; final List<ItemBaseModel> recentlyAdded;
final ImagesData? imageData;
final int childCount; final int childCount;
final String? path;
ViewModel({ ViewModel({
required this.name, required this.name,
required this.id, required this.id,
@ -28,7 +38,9 @@ class ViewModel {
required this.collectionType, required this.collectionType,
required this.playAccess, required this.playAccess,
required this.recentlyAdded, required this.recentlyAdded,
required this.imageData,
required this.childCount, required this.childCount,
required this.path,
}); });
ViewModel copyWith({ ViewModel copyWith({
@ -42,7 +54,9 @@ class ViewModel {
CollectionType? collectionType, CollectionType? collectionType,
dto.PlayAccess? playAccess, dto.PlayAccess? playAccess,
List<ItemBaseModel>? recentlyAdded, List<ItemBaseModel>? recentlyAdded,
ImagesData? imageData,
int? childCount, int? childCount,
String? path,
}) { }) {
return ViewModel( return ViewModel(
name: name ?? this.name, name: name ?? this.name,
@ -55,7 +69,9 @@ class ViewModel {
collectionType: collectionType ?? this.collectionType, collectionType: collectionType ?? this.collectionType,
playAccess: playAccess ?? this.playAccess, playAccess: playAccess ?? this.playAccess,
recentlyAdded: recentlyAdded ?? this.recentlyAdded, recentlyAdded: recentlyAdded ?? this.recentlyAdded,
imageData: imageData ?? this.imageData,
childCount: childCount ?? this.childCount, childCount: childCount ?? this.childCount,
path: path ?? this.path,
); );
} }
@ -69,11 +85,13 @@ class ViewModel {
canDownload: item.canDownload ?? false, canDownload: item.canDownload ?? false,
parentId: item.parentId ?? "", parentId: item.parentId ?? "",
recentlyAdded: [], recentlyAdded: [],
imageData: ImagesData.fromBaseItem(item, ref),
collectionType: CollectionType.values collectionType: CollectionType.values
.firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ?? .firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ??
CollectionType.folders, CollectionType.folders,
playAccess: item.playAccess ?? PlayAccess.none, playAccess: item.playAccess ?? PlayAccess.none,
childCount: item.childCount ?? 0, childCount: item.childCount ?? 0,
path: "",
); );
} }
@ -88,6 +106,27 @@ class ViewModel {
return id.hashCode ^ serverId.hashCode; return id.hashCode ^ serverId.hashCode;
} }
NavigationButton toNavigationButton(
bool selected,
bool horizontal,
bool expanded,
FutureOr Function() action, {
FutureOr Function()? onLongPress,
List<ItemAction>? 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 @override
String toString() { 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)'; return 'ViewModel(name: $name, id: $id, serverId: $serverId, dateCreated: $dateCreated, canDelete: $canDelete, canDownload: $canDownload, parentId: $parentId, collectionType: $collectionType, playAccess: $playAccess, recentlyAdded: $recentlyAdded, childCount: $childCount)';

View file

@ -1,3 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/home_model.dart'; import 'package:fladder/models/home_model.dart';
import 'package:fladder/models/item_base_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/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/list_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dashboardProvider = StateNotifierProvider<DashboardNotifier, HomeModel>((ref) { final dashboardProvider = StateNotifierProvider<DashboardNotifier, HomeModel>((ref) {
return DashboardNotifier(ref); return DashboardNotifier(ref);
@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.candelete, ItemFields.candelete,
ItemFields.candownload, ItemFields.candownload,
ItemFields.primaryimageaspectratio,
], ],
mediaTypes: [MediaType.video], mediaTypes: [MediaType.video],
enableTotalRecordCount: false, enableTotalRecordCount: false,
@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.candelete, ItemFields.candelete,
ItemFields.candownload, ItemFields.candownload,
ItemFields.primaryimageaspectratio,
], ],
mediaTypes: [MediaType.audio], mediaTypes: [MediaType.audio],
enableTotalRecordCount: false, enableTotalRecordCount: false,
@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.candelete, ItemFields.candelete,
ItemFields.candownload, ItemFields.candownload,
ItemFields.primaryimageaspectratio,
], ],
mediaTypes: [MediaType.book], mediaTypes: [MediaType.book],
enableTotalRecordCount: false, enableTotalRecordCount: false,
@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
final nextResponse = await api.showsNextUpGet( final nextResponse = await api.showsNextUpGet(
limit: 16, limit: 16,
nextUpDateCutoff: DateTime.now() nextUpDateCutoff: DateTime.now().subtract(
.subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
fields: [ fields: [
ItemFields.parentid, ItemFields.parentid,
ItemFields.mediastreams, ItemFields.mediastreams,
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.candelete, ItemFields.candelete,
ItemFields.candownload, ItemFields.candownload,
ItemFields.primaryimageaspectratio,
], ],
); );

View file

@ -1,4 +1,6 @@
import 'package:chopper/chopper.dart'; import 'package:chopper/chopper.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/favourites_model.dart'; import 'package:fladder/models/favourites_model.dart';
import 'package:fladder/models/item_base_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/api_provider.dart';
import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final favouritesProvider = StateNotifierProvider<FavouritesNotifier, FavouritesModel>((ref) { final favouritesProvider = StateNotifierProvider<FavouritesNotifier, FavouritesModel>((ref) {
return FavouritesNotifier(ref); return FavouritesNotifier(ref);
@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
isFavorite: true, isFavorite: true,
limit: 10, limit: 10,
sortOrder: [SortOrder.ascending], sortOrder: [SortOrder.ascending],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
); );
final response2 = await api.itemsGet( final response2 = await api.itemsGet(
parentId: viewModel?.id, parentId: viewModel?.id,
@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
limit: 10, limit: 10,
includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder], includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder],
sortOrder: [SortOrder.ascending], sortOrder: [SortOrder.ascending],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
); );
return [...?response.body?.items, ...?response2.body?.items]; return [...?response.body?.items, ...?response2.body?.items];
} }

View file

@ -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<LibraryNotifier, LibraryModel?, String>((ref, id) {
return LibraryNotifier(ref);
});
class LibraryNotifier extends StateNotifier<LibraryModel?> {
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<void> setupLibrary(ViewModel viewModel) async {
state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie);
}
Future<Response?> 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<void> 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<Response?> 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<Response?> 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<Response?> 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;
}
}

View file

@ -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<ViewModel> views,
ViewModel? selectedViewModel,
@Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set<LibraryViewType> viewType,
@Default([]) List<RecommendedModel> recommendations,
@Default([]) List<RecommendedModel> genres,
@Default([]) List<ItemBaseModel> favourites,
}) = _LibraryScreenModel;
}
@Riverpod(keepAlive: true)
class LibraryScreen extends _$LibraryScreen {
late final JellyService api = ref.read(jellyApiProvider);
@override
LibraryScreenModel build() => LibraryScreenModel();
Future<void> 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<void> selectLibrary(ViewModel viewModel) async {
state = state.copyWith(selectedViewModel: viewModel);
}
Future<void> setViewType(Set<LibraryViewType> type) async {
state = state.copyWith(viewType: type);
}
Future<Response?> loadLibrary(ViewModel viewModel) async {
await loadRecommendations(viewModel);
await loadGenres(viewModel);
await loadFavourites(viewModel);
return null;
}
Future<void> loadRecommendations(ViewModel viewModel) async {
List<RecommendedModel> 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<Response?> 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<Response?> 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<RecommendedModel>().toList(),
);
return null;
}
}

View file

@ -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>(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<ViewModel> get views => throw _privateConstructorUsedError;
ViewModel? get selectedViewModel => throw _privateConstructorUsedError;
Set<LibraryViewType> get viewType => throw _privateConstructorUsedError;
List<RecommendedModel> get recommendations =>
throw _privateConstructorUsedError;
List<RecommendedModel> get genres => throw _privateConstructorUsedError;
List<ItemBaseModel> 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<LibraryScreenModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LibraryScreenModelCopyWith<$Res> {
factory $LibraryScreenModelCopyWith(
LibraryScreenModel value, $Res Function(LibraryScreenModel) then) =
_$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>;
@useResult
$Res call(
{List<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> 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<ViewModel>,
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<LibraryViewType>,
recommendations: null == recommendations
? _value.recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value.genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value.favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
) 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<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> 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<ViewModel>,
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<LibraryViewType>,
recommendations: null == recommendations
? _value._recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value._genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value._favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
));
}
}
/// @nodoc
class _$LibraryScreenModelImpl implements _LibraryScreenModel {
_$LibraryScreenModelImpl(
{final List<ViewModel> views = const [],
this.selectedViewModel,
final Set<LibraryViewType> viewType = const {
LibraryViewType.recommended,
LibraryViewType.favourites
},
final List<RecommendedModel> recommendations = const [],
final List<RecommendedModel> genres = const [],
final List<ItemBaseModel> favourites = const []})
: _views = views,
_viewType = viewType,
_recommendations = recommendations,
_genres = genres,
_favourites = favourites;
final List<ViewModel> _views;
@override
@JsonKey()
List<ViewModel> get views {
if (_views is EqualUnmodifiableListView) return _views;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_views);
}
@override
final ViewModel? selectedViewModel;
final Set<LibraryViewType> _viewType;
@override
@JsonKey()
Set<LibraryViewType> get viewType {
if (_viewType is EqualUnmodifiableSetView) return _viewType;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_viewType);
}
final List<RecommendedModel> _recommendations;
@override
@JsonKey()
List<RecommendedModel> get recommendations {
if (_recommendations is EqualUnmodifiableListView) return _recommendations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recommendations);
}
final List<RecommendedModel> _genres;
@override
@JsonKey()
List<RecommendedModel> get genres {
if (_genres is EqualUnmodifiableListView) return _genres;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_genres);
}
final List<ItemBaseModel> _favourites;
@override
@JsonKey()
List<ItemBaseModel> 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<ViewModel> views,
final ViewModel? selectedViewModel,
final Set<LibraryViewType> viewType,
final List<RecommendedModel> recommendations,
final List<RecommendedModel> genres,
final List<ItemBaseModel> favourites}) = _$LibraryScreenModelImpl;
@override
List<ViewModel> get views;
@override
ViewModel? get selectedViewModel;
@override
Set<LibraryViewType> get viewType;
@override
List<RecommendedModel> get recommendations;
@override
List<RecommendedModel> get genres;
@override
List<ItemBaseModel> 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;
}

View file

@ -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<LibraryScreen, LibraryScreenModel>.internal(
LibraryScreen.new,
name: r'libraryScreenProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$libraryScreenHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LibraryScreen = Notifier<LibraryScreenModel>;
// 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

View file

@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
.toSet() .toSet()
.toList(); .toList();
var tempState = state.copyWith(); var tempState = state.copyWith();
final genres = mappedList final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel))))
.expand((element) => element?.genres ?? <NameGuidPair>[]) .expand((element) => element)
.nonNulls .toSet()
.sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase())); .toList();
final tags = mappedList final tags = mappedList
.expand((element) => element?.tags ?? <String>[]) .expand((element) => element?.tags ?? <String>[])
.sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); .sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
tempState = tempState.copyWith( tempState = tempState.copyWith(
types: state.types.setAll(false).setKeys(enabledCollections, true), 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), studios: {for (var element in studios) element: false}.replaceMap(tempState.studios),
tags: {for (var element in tags) element: false}.replaceMap(tempState.tags), tags: {for (var element in tags) element: false}.replaceMap(tempState.tags),
); );
@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? []; return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
} }
Future<List<GenreItems>> _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<ServerQueryResult?> _loadLibrary( Future<ServerQueryResult?> _loadLibrary(
{ViewModel? viewModel, {ViewModel? viewModel,
bool? recursive, bool? recursive,

View file

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
final homeSettingsProvider = StateNotifierProvider<HomeSettingsNotifier, HomeSettingsModel>((ref) { final homeSettingsProvider = StateNotifierProvider<HomeSettingsNotifier, HomeSettingsModel>((ref) {
return HomeSettingsNotifier(ref); return HomeSettingsNotifier(ref);

View file

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

View file

@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>; typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900'; String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a';
/// See also [User]. /// See also [User].
@ProviderFor(User) @ProviderFor(User)

View file

@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
late final JellyService api = ref.read(jellyApiProvider); late final JellyService api = ref.read(jellyApiProvider);
Future<void> fetchViews() async { Future<ViewsModel?> fetchViews() async {
if (state.loading) return; if (state.loading) return null;
final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes)); final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes));
final response = await api.usersUserIdViewsGet( final response = await api.usersUserIdViewsGet(
includeExternalContent: showAllCollections, includeExternalContent: showAllCollections,
@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.candelete, ItemFields.candelete,
ItemFields.candownload, ItemFields.candownload,
ItemFields.primaryimageaspectratio,
], ],
); );
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList()); return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
.where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true)) .where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true))
.toList(), .toList(),
loading: false); loading: false);
return state;
} }
void clear() { void clear() {

View file

@ -51,6 +51,7 @@ final List<AutoRoute> homeRoutes = [
_dashboardRoute, _dashboardRoute,
_favouritesRoute, _favouritesRoute,
_syncedRoute, _syncedRoute,
_librariesRoute,
]; ];
final List<AutoRoute> _defaultRoutes = [ final List<AutoRoute> _defaultRoutes = [
@ -79,6 +80,13 @@ final AutoRoute _syncedRoute = CustomRoute(
path: 'synced', path: 'synced',
); );
final AutoRoute _librariesRoute = CustomRoute(
page: LibraryRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn,
maintainState: false,
path: 'libraries',
);
final List<AutoRoute> _settingsChildren = [ final List<AutoRoute> _settingsChildren = [
CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'), CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'),
CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'),

View file

@ -8,35 +8,36 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i16; import 'package:auto_route/auto_route.dart' as _i17;
import 'package:fladder/models/item_base_model.dart' as _i17; import 'package:fladder/models/item_base_model.dart' as _i18;
import 'package:fladder/models/items/photos_model.dart' as _i20; import 'package:fladder/models/items/photos_model.dart' as _i21;
import 'package:fladder/models/library_search/library_search_options.dart' 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/routes/nested_details_screen.dart' as _i4;
import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3; import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3;
import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5; import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5;
import 'package:fladder/screens/home_screen.dart' as _i6; 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' import 'package:fladder/screens/library_search/library_search_screen.dart'
as _i7; as _i8;
import 'package:fladder/screens/login/lock_screen.dart' as _i8; import 'package:fladder/screens/login/lock_screen.dart' as _i9;
import 'package:fladder/screens/login/login_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/about_settings_page.dart' as _i1;
import 'package:fladder/screens/settings/client_settings_page.dart' as _i2; 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/player_settings_page.dart' as _i11;
import 'package:fladder/screens/settings/security_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 _i12; import 'package:fladder/screens/settings/settings_screen.dart' as _i13;
import 'package:fladder/screens/settings/settings_selection_screen.dart' import 'package:fladder/screens/settings/settings_selection_screen.dart'
as _i13; as _i14;
import 'package:fladder/screens/splash_screen.dart' as _i14; import 'package:fladder/screens/splash_screen.dart' as _i15;
import 'package:fladder/screens/syncing/synced_screen.dart' as _i15; import 'package:fladder/screens/syncing/synced_screen.dart' as _i16;
import 'package:flutter/foundation.dart' as _i18; import 'package:flutter/foundation.dart' as _i19;
import 'package:flutter/material.dart' as _i21; import 'package:flutter/material.dart' as _i22;
/// generated route for /// generated route for
/// [_i1.AboutSettingsPage] /// [_i1.AboutSettingsPage]
class AboutSettingsRoute extends _i16.PageRouteInfo<void> { class AboutSettingsRoute extends _i17.PageRouteInfo<void> {
const AboutSettingsRoute({List<_i16.PageRouteInfo>? children}) const AboutSettingsRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
AboutSettingsRoute.name, AboutSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -44,7 +45,7 @@ class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
static const String name = 'AboutSettingsRoute'; static const String name = 'AboutSettingsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i1.AboutSettingsPage(); return const _i1.AboutSettingsPage();
@ -54,8 +55,8 @@ class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.ClientSettingsPage] /// [_i2.ClientSettingsPage]
class ClientSettingsRoute extends _i16.PageRouteInfo<void> { class ClientSettingsRoute extends _i17.PageRouteInfo<void> {
const ClientSettingsRoute({List<_i16.PageRouteInfo>? children}) const ClientSettingsRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
ClientSettingsRoute.name, ClientSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -63,7 +64,7 @@ class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
static const String name = 'ClientSettingsRoute'; static const String name = 'ClientSettingsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i2.ClientSettingsPage(); return const _i2.ClientSettingsPage();
@ -73,8 +74,8 @@ class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.DashboardScreen] /// [_i3.DashboardScreen]
class DashboardRoute extends _i16.PageRouteInfo<void> { class DashboardRoute extends _i17.PageRouteInfo<void> {
const DashboardRoute({List<_i16.PageRouteInfo>? children}) const DashboardRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
DashboardRoute.name, DashboardRoute.name,
initialChildren: children, initialChildren: children,
@ -82,7 +83,7 @@ class DashboardRoute extends _i16.PageRouteInfo<void> {
static const String name = 'DashboardRoute'; static const String name = 'DashboardRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i3.DashboardScreen(); return const _i3.DashboardScreen();
@ -92,12 +93,12 @@ class DashboardRoute extends _i16.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i4.DetailsScreen] /// [_i4.DetailsScreen]
class DetailsRoute extends _i16.PageRouteInfo<DetailsRouteArgs> { class DetailsRoute extends _i17.PageRouteInfo<DetailsRouteArgs> {
DetailsRoute({ DetailsRoute({
String id = '', String id = '',
_i17.ItemBaseModel? item, _i18.ItemBaseModel? item,
_i18.Key? key, _i19.Key? key,
List<_i16.PageRouteInfo>? children, List<_i17.PageRouteInfo>? children,
}) : super( }) : super(
DetailsRoute.name, DetailsRoute.name,
args: DetailsRouteArgs( args: DetailsRouteArgs(
@ -111,7 +112,7 @@ class DetailsRoute extends _i16.PageRouteInfo<DetailsRouteArgs> {
static const String name = 'DetailsRoute'; static const String name = 'DetailsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
final queryParams = data.queryParams; final queryParams = data.queryParams;
@ -139,9 +140,9 @@ class DetailsRouteArgs {
final String id; final String id;
final _i17.ItemBaseModel? item; final _i18.ItemBaseModel? item;
final _i18.Key? key; final _i19.Key? key;
@override @override
String toString() { String toString() {
@ -151,8 +152,8 @@ class DetailsRouteArgs {
/// generated route for /// generated route for
/// [_i5.FavouritesScreen] /// [_i5.FavouritesScreen]
class FavouritesRoute extends _i16.PageRouteInfo<void> { class FavouritesRoute extends _i17.PageRouteInfo<void> {
const FavouritesRoute({List<_i16.PageRouteInfo>? children}) const FavouritesRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
FavouritesRoute.name, FavouritesRoute.name,
initialChildren: children, initialChildren: children,
@ -160,7 +161,7 @@ class FavouritesRoute extends _i16.PageRouteInfo<void> {
static const String name = 'FavouritesRoute'; static const String name = 'FavouritesRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i5.FavouritesScreen(); return const _i5.FavouritesScreen();
@ -170,8 +171,8 @@ class FavouritesRoute extends _i16.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i6.HomeScreen] /// [_i6.HomeScreen]
class HomeRoute extends _i16.PageRouteInfo<void> { class HomeRoute extends _i17.PageRouteInfo<void> {
const HomeRoute({List<_i16.PageRouteInfo>? children}) const HomeRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
HomeRoute.name, HomeRoute.name,
initialChildren: children, initialChildren: children,
@ -179,7 +180,7 @@ class HomeRoute extends _i16.PageRouteInfo<void> {
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i6.HomeScreen(); return const _i6.HomeScreen();
@ -188,17 +189,36 @@ class HomeRoute extends _i16.PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [_i7.LibrarySearchScreen] /// [_i7.LibraryScreen]
class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> { class LibraryRoute extends _i17.PageRouteInfo<void> {
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<LibrarySearchRouteArgs> {
LibrarySearchRoute({ LibrarySearchRoute({
String? viewModelId, String? viewModelId,
List<String>? folderId, List<String>? folderId,
bool? favourites, bool? favourites,
_i19.SortingOrder? sortOrder, _i20.SortingOrder? sortOrder,
_i19.SortingOptions? sortingOptions, _i20.SortingOptions? sortingOptions,
_i20.PhotoModel? photoToView, _i21.PhotoModel? photoToView,
_i18.Key? key, _i19.Key? key,
List<_i16.PageRouteInfo>? children, List<_i17.PageRouteInfo>? children,
}) : super( }) : super(
LibrarySearchRoute.name, LibrarySearchRoute.name,
args: LibrarySearchRouteArgs( args: LibrarySearchRouteArgs(
@ -222,7 +242,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
static const String name = 'LibrarySearchRoute'; static const String name = 'LibrarySearchRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
final queryParams = data.queryParams; final queryParams = data.queryParams;
@ -234,7 +254,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
sortOrder: queryParams.get('sortOrder'), sortOrder: queryParams.get('sortOrder'),
sortingOptions: queryParams.get('sortOptions'), sortingOptions: queryParams.get('sortOptions'),
)); ));
return _i7.LibrarySearchScreen( return _i8.LibrarySearchScreen(
viewModelId: args.viewModelId, viewModelId: args.viewModelId,
folderId: args.folderId, folderId: args.folderId,
favourites: args.favourites, favourites: args.favourites,
@ -264,13 +284,13 @@ class LibrarySearchRouteArgs {
final bool? favourites; 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 @override
String toString() { String toString() {
@ -279,9 +299,9 @@ class LibrarySearchRouteArgs {
} }
/// generated route for /// generated route for
/// [_i8.LockScreen] /// [_i9.LockScreen]
class LockRoute extends _i16.PageRouteInfo<void> { class LockRoute extends _i17.PageRouteInfo<void> {
const LockRoute({List<_i16.PageRouteInfo>? children}) const LockRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
LockRoute.name, LockRoute.name,
initialChildren: children, initialChildren: children,
@ -289,18 +309,18 @@ class LockRoute extends _i16.PageRouteInfo<void> {
static const String name = 'LockRoute'; static const String name = 'LockRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i8.LockScreen(); return const _i9.LockScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i9.LoginScreen] /// [_i10.LoginScreen]
class LoginRoute extends _i16.PageRouteInfo<void> { class LoginRoute extends _i17.PageRouteInfo<void> {
const LoginRoute({List<_i16.PageRouteInfo>? children}) const LoginRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
LoginRoute.name, LoginRoute.name,
initialChildren: children, initialChildren: children,
@ -308,18 +328,18 @@ class LoginRoute extends _i16.PageRouteInfo<void> {
static const String name = 'LoginRoute'; static const String name = 'LoginRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i9.LoginScreen(); return const _i10.LoginScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i10.PlayerSettingsPage] /// [_i11.PlayerSettingsPage]
class PlayerSettingsRoute extends _i16.PageRouteInfo<void> { class PlayerSettingsRoute extends _i17.PageRouteInfo<void> {
const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children}) const PlayerSettingsRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
PlayerSettingsRoute.name, PlayerSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -327,18 +347,18 @@ class PlayerSettingsRoute extends _i16.PageRouteInfo<void> {
static const String name = 'PlayerSettingsRoute'; static const String name = 'PlayerSettingsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i10.PlayerSettingsPage(); return const _i11.PlayerSettingsPage();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i11.SecuritySettingsPage] /// [_i12.SecuritySettingsPage]
class SecuritySettingsRoute extends _i16.PageRouteInfo<void> { class SecuritySettingsRoute extends _i17.PageRouteInfo<void> {
const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children}) const SecuritySettingsRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
SecuritySettingsRoute.name, SecuritySettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -346,18 +366,18 @@ class SecuritySettingsRoute extends _i16.PageRouteInfo<void> {
static const String name = 'SecuritySettingsRoute'; static const String name = 'SecuritySettingsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i11.SecuritySettingsPage(); return const _i12.SecuritySettingsPage();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i12.SettingsScreen] /// [_i13.SettingsScreen]
class SettingsRoute extends _i16.PageRouteInfo<void> { class SettingsRoute extends _i17.PageRouteInfo<void> {
const SettingsRoute({List<_i16.PageRouteInfo>? children}) const SettingsRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
SettingsRoute.name, SettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -365,18 +385,18 @@ class SettingsRoute extends _i16.PageRouteInfo<void> {
static const String name = 'SettingsRoute'; static const String name = 'SettingsRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i12.SettingsScreen(); return const _i13.SettingsScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i13.SettingsSelectionScreen] /// [_i14.SettingsSelectionScreen]
class SettingsSelectionRoute extends _i16.PageRouteInfo<void> { class SettingsSelectionRoute extends _i17.PageRouteInfo<void> {
const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children}) const SettingsSelectionRoute({List<_i17.PageRouteInfo>? children})
: super( : super(
SettingsSelectionRoute.name, SettingsSelectionRoute.name,
initialChildren: children, initialChildren: children,
@ -384,21 +404,21 @@ class SettingsSelectionRoute extends _i16.PageRouteInfo<void> {
static const String name = 'SettingsSelectionRoute'; static const String name = 'SettingsSelectionRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i13.SettingsSelectionScreen(); return const _i14.SettingsSelectionScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i14.SplashScreen] /// [_i15.SplashScreen]
class SplashRoute extends _i16.PageRouteInfo<SplashRouteArgs> { class SplashRoute extends _i17.PageRouteInfo<SplashRouteArgs> {
SplashRoute({ SplashRoute({
dynamic Function(bool)? loggedIn, dynamic Function(bool)? loggedIn,
_i21.Key? key, _i22.Key? key,
List<_i16.PageRouteInfo>? children, List<_i17.PageRouteInfo>? children,
}) : super( }) : super(
SplashRoute.name, SplashRoute.name,
args: SplashRouteArgs( args: SplashRouteArgs(
@ -410,12 +430,12 @@ class SplashRoute extends _i16.PageRouteInfo<SplashRouteArgs> {
static const String name = 'SplashRoute'; static const String name = 'SplashRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = final args =
data.argsAs<SplashRouteArgs>(orElse: () => const SplashRouteArgs()); data.argsAs<SplashRouteArgs>(orElse: () => const SplashRouteArgs());
return _i14.SplashScreen( return _i15.SplashScreen(
loggedIn: args.loggedIn, loggedIn: args.loggedIn,
key: args.key, key: args.key,
); );
@ -431,7 +451,7 @@ class SplashRouteArgs {
final dynamic Function(bool)? loggedIn; final dynamic Function(bool)? loggedIn;
final _i21.Key? key; final _i22.Key? key;
@override @override
String toString() { String toString() {
@ -440,12 +460,12 @@ class SplashRouteArgs {
} }
/// generated route for /// generated route for
/// [_i15.SyncedScreen] /// [_i16.SyncedScreen]
class SyncedRoute extends _i16.PageRouteInfo<SyncedRouteArgs> { class SyncedRoute extends _i17.PageRouteInfo<SyncedRouteArgs> {
SyncedRoute({ SyncedRoute({
_i21.ScrollController? navigationScrollController, _i22.ScrollController? navigationScrollController,
_i21.Key? key, _i22.Key? key,
List<_i16.PageRouteInfo>? children, List<_i17.PageRouteInfo>? children,
}) : super( }) : super(
SyncedRoute.name, SyncedRoute.name,
args: SyncedRouteArgs( args: SyncedRouteArgs(
@ -457,12 +477,12 @@ class SyncedRoute extends _i16.PageRouteInfo<SyncedRouteArgs> {
static const String name = 'SyncedRoute'; static const String name = 'SyncedRoute';
static _i16.PageInfo page = _i16.PageInfo( static _i17.PageInfo page = _i17.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = final args =
data.argsAs<SyncedRouteArgs>(orElse: () => const SyncedRouteArgs()); data.argsAs<SyncedRouteArgs>(orElse: () => const SyncedRouteArgs());
return _i15.SyncedScreen( return _i16.SyncedScreen(
navigationScrollController: args.navigationScrollController, navigationScrollController: args.navigationScrollController,
key: args.key, key: args.key,
); );
@ -476,9 +496,9 @@ class SyncedRouteArgs {
this.key, this.key,
}); });
final _i21.ScrollController? navigationScrollController; final _i22.ScrollController? navigationScrollController;
final _i21.Key? key; final _i22.Key? key;
@override @override
String toString() { String toString() {

View file

@ -1,7 +1,7 @@
import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/book_model.dart';
import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/book_viewer_provider.dart';
import 'package:fladder/providers/items/book_details_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/util/fladder_image.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -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/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/shared/default_title_bar.dart'; import 'package:fladder/screens/shared/default_title_bar.dart';
import 'package:fladder/screens/shared/fladder_snackbar.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/input_handler.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/throttler.dart'; import 'package:fladder/util/throttler.dart';

View file

@ -1,5 +1,5 @@
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart'; 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/util/string_extensions.dart';
import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart';

View file

@ -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.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/dashboard_provider.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/media/poster_row.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
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/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sliver_list_padding.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/pinch_poster_zoom.dart';
import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/poster_size_slider.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart';
@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final padding = AdaptiveLayout.adaptivePadding(context);
final dashboardData = ref.watch(dashboardProvider); final dashboardData = ref.watch(dashboardProvider);
final views = ref.watch(viewsProvider); final views = ref.watch(viewsProvider);
final homeSettings = ref.watch(homeSettingsProvider); final homeSettings = ref.watch(homeSettingsProvider);
@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
return MediaQuery.removeViewInsets( return MediaQuery.removeViewInsets(
context: context, context: context,
child: NestedScaffold( child: NestedScaffold(
background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
body: PullToRefresh( body: PullToRefresh(
refreshKey: _refreshIndicatorKey, refreshKey: _refreshIndicatorKey,
displacement: 80 + MediaQuery.of(context).viewPadding.top, displacement: 80 + MediaQuery.of(context).viewPadding.top,
@ -104,7 +109,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: Transform.translate( child: Transform.translate(
offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0), 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<DashboardScreen> {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter( SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.dashboardContinueWatching, label: context.localized.dashboardContinueWatching,
posters: resumeVideo, posters: resumeVideo,
), ),
@ -130,6 +142,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter( SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.dashboardContinueListening, label: context.localized.dashboardContinueListening,
posters: resumeAudio, posters: resumeAudio,
), ),
@ -138,6 +151,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter( SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.dashboardContinueReading, label: context.localized.dashboardContinueReading,
posters: resumeBooks, posters: resumeBooks,
), ),
@ -146,6 +160,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
(homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate)) (homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate))
SliverToBoxAdapter( SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.nextUp, label: context.localized.nextUp,
posters: dashboardData.nextUp, posters: dashboardData.nextUp,
), ),
@ -153,6 +168,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined) if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined)
SliverToBoxAdapter( SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.dashboardContinue, label: context.localized.dashboardContinue,
posters: [...allResume, ...dashboardData.nextUp], posters: [...allResume, ...dashboardData.nextUp],
), ),
@ -161,7 +177,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
.where((element) => element.recentlyAdded.isNotEmpty) .where((element) => element.recentlyAdded.isNotEmpty)
.map((view) => SliverToBoxAdapter( .map((view) => SliverToBoxAdapter(
child: PosterRow( child: PosterRow(
contentPadding: padding,
label: context.localized.dashboardRecentlyAdded(view.name), label: context.localized.dashboardRecentlyAdded(view.name),
collectionAspectRatio: view.collectionType.aspectRatio,
onLabelClick: () => context.router.push(LibrarySearchRoute( onLabelClick: () => context.router.push(LibrarySearchRoute(
viewModelId: view.id, viewModelId: view.id,
sortingOptions: switch (view.collectionType) { sortingOptions: switch (view.collectionType) {

View file

@ -27,9 +27,12 @@ class HomeBannerWidget extends ConsumerWidget {
const SizedBox(height: 24) const SizedBox(height: 24)
], ],
), ),
HomeBanner.banner => MediaBanner( HomeBanner.banner => Padding(
items: posters, padding: const EdgeInsets.symmetric(horizontal: 6),
maxHeight: maxHeight, child: MediaBanner(
items: posters,
maxHeight: maxHeight,
),
), ),
_ => const SizedBox.shrink(), _ => const SizedBox.shrink(),
}; };

View file

@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_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/chip_button.dart';
import 'package:fladder/screens/shared/media/components/media_header.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/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/humanize_duration.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -37,36 +37,42 @@ class EmptyItem extends ConsumerWidget {
} }
}, },
), ),
content: (padding) => Column( content: (padding) => Center(
mainAxisAlignment: MainAxisAlignment.center, child: Padding(
children: [ padding: padding,
ConstrainedBox( child: Column(
constraints: const BoxConstraints(maxHeight: 350), mainAxisAlignment: MainAxisAlignment.center,
child: AspectRatio( crossAxisAlignment: CrossAxisAlignment.center,
aspectRatio: 0.67, children: [
child: Card( ConstrainedBox(
elevation: 6, constraints: const BoxConstraints(maxHeight: 350),
color: Theme.of(context).colorScheme.secondaryContainer, child: AspectRatio(
shape: RoundedRectangleBorder( aspectRatio: 0.67,
side: BorderSide( child: Card(
width: 1.0, elevation: 6,
color: Colors.white.withValues(alpha: 0.10), 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)),
), ),
); );
} }

View file

@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.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/items/episode_details_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.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/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.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/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/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.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/items/movies_details_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.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/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.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/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/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -1,4 +1,3 @@
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.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/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/poster_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/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/util/string_extensions.dart';
@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState<PersonDetailScreen> {
spacing: 32, spacing: 32,
children: [ children: [
Container( Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),

View file

@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/series_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/items/series_details_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/overview_header.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/people_row.dart';
import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart';
import 'package:fladder/screens/shared/media/season_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/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -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:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/favourites_provider.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/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'; import 'package:fladder/widgets/shared/pull_to_refresh.dart';
@RoutePage() @RoutePage()
@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final favourites = ref.watch(favouritesProvider); final favourites = ref.watch(favouritesProvider);
final padding = AdaptiveLayout.adaptivePadding(context);
return PullToRefresh( return PullToRefresh(
onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(), onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(),
child: NestedScaffold( child: NestedScaffold(
background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()),
body: PinchPosterZoom( body: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: CustomScrollView( child: CustomScrollView(
@ -52,25 +55,19 @@ class FavouritesScreen extends ConsumerWidget {
), ),
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map( ...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
(e) => SliverToBoxAdapter( (e) => SliverToBoxAdapter(
child: Padding( child: PosterRow(
padding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: padding,
child: PosterGrid( label: e.key.label(context),
stickyHeader: true, posters: e.value,
name: e.key.label(context),
posters: e.value,
),
), ),
), ),
), ),
if (favourites.people.isNotEmpty) if (favourites.people.isNotEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: PosterRow(
padding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: padding,
child: PosterGrid( label: context.localized.actor(favourites.people.length),
stickyHeader: true, posters: favourites.people,
name: "People",
posters: favourites.people,
),
), ),
), ),
const DefautlSliverBottomPadding(), const DefautlSliverBottomPadding(),

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart';
enum HomeTabs { enum HomeTabs {
dashboard, dashboard,
library,
favorites, 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() @RoutePage()
@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget {
case HomeTabs.dashboard: case HomeTabs.dashboard:
return DestinationModel( return DestinationModel(
label: context.localized.navigationDashboard, label: context.localized.navigationDashboard,
icon: const Icon(IconsaxPlusLinear.home), icon: Icon(e.icon),
selectedIcon: const Icon(IconsaxPlusBold.home), selectedIcon: Icon(e.selectedIcon),
route: const DashboardRoute(), route: const DashboardRoute(),
action: () => context.router.navigate(const DashboardRoute()), action: () => e.navigate(context),
floatingActionButton: AdaptiveFab( floatingActionButton: AdaptiveFab(
context: context, context: context,
title: context.localized.search, title: context.localized.search,
@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget {
case HomeTabs.favorites: case HomeTabs.favorites:
return DestinationModel( return DestinationModel(
label: context.localized.navigationFavorites, label: context.localized.navigationFavorites,
icon: const Icon(IconsaxPlusLinear.heart), icon: Icon(e.icon),
selectedIcon: const Icon(IconsaxPlusBold.heart), selectedIcon: Icon(e.selectedIcon),
route: const FavouritesRoute(), route: const FavouritesRoute(),
floatingActionButton: AdaptiveFab( floatingActionButton: AdaptiveFab(
context: context, context: context,
@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget {
onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)),
child: const Icon(IconsaxPlusLinear.heart_search), child: const Icon(IconsaxPlusLinear.heart_search),
), ),
action: () => context.router.navigate(const FavouritesRoute()), action: () => e.navigate(context),
); );
case HomeTabs.sync: case HomeTabs.sync:
if (canDownload) { if (canDownload) {
return DestinationModel( return DestinationModel(
label: context.localized.navigationSync, label: context.localized.navigationSync,
icon: const Icon(IconsaxPlusLinear.cloud), icon: Icon(e.icon),
selectedIcon: const Icon(IconsaxPlusBold.cloud), selectedIcon: Icon(e.selectedIcon),
route: SyncedRoute(), 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 .nonNulls

View file

@ -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<LibraryTabs> 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 [];
}
}
}

View file

@ -1,14 +1,33 @@
import 'package:fladder/models/view_model.dart'; import 'dart:async';
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 '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 { class LibraryScreen extends ConsumerStatefulWidget {
final ViewModel viewModel;
const LibraryScreen({ const LibraryScreen({
required this.viewModel,
super.key, super.key,
}); });
@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget {
} }
class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin { class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin {
late final List<LibraryTabs> tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType); final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey();
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(() {});
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final PreferredSizeWidget tabBar = TabBar( final libraryScreenState = ref.watch(libraryScreenProvider);
isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false, final views = libraryScreenState.views;
indicatorWeight: 3, final recommendations = libraryScreenState.recommendations;
controller: tabController, final favourites = libraryScreenState.favourites;
tabs: tabs final selectedView = libraryScreenState.selectedViewModel;
.map((e) => Tab( final viewTypes = libraryScreenState.viewType;
text: e.name, final genres = libraryScreenState.genres;
icon: e.icon, final padding = AdaptiveLayout.adaptivePadding(context);
)) return NestedScaffold(
.toList(), background: BackgroundImage(
); items: [
...recommendations.expand((e) => e.posters),
return Padding( ...favourites,
padding: AdaptiveLayout.of(context).isDesktop ],
? EdgeInsets.only(top: MediaQuery.of(context).padding.top) ),
: EdgeInsets.zero, body: PullToRefresh(
child: ClipRRect( refreshOnStart: true,
borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0), refreshKey: refreshKey,
child: Card( onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero, child: SizedBox.expand(
elevation: 2, child: CustomScrollView(
child: Scaffold( controller: AdaptiveLayout.scrollOf(context),
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, physics: const AlwaysScrollableScrollPhysics(),
floatingActionButton: tabs[tabController.index].floatingActionButton, slivers: [
floatingActionButtonLocation: FloatingActionButtonLocation.endContained, const DefaultSliverTopBadding(),
appBar: AppBar( if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
centerTitle: true, NestedSliverAppBar(
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, route: LibrarySearchRoute(),
title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, parent: context,
toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40, ),
bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, if (views.isNotEmpty)
), SliverToBoxAdapter(
extendBody: true, child: LibraryRow(
body: Padding( padding: padding,
padding: !AdaptiveLayout.of(context).isDesktop views: views,
? EdgeInsets.only( selectedView: libraryScreenState.selectedViewModel,
left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right) onSelected: (view) {
: EdgeInsets.zero, ref.read(libraryScreenProvider.notifier).selectLibrary(view);
child: TabBarView( refreshKey?.currentState?.show();
controller: tabController, },
children: tabs ),
.map((e) => Padding( ),
padding: const EdgeInsets.symmetric(horizontal: 8.0), if (selectedView != null)
child: e.page, SliverToBoxAdapter(
)) child: Padding(
.toList(), 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<ViewModel> 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<ItemActionButton> 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,
),
],
),
),
),
],
),
),
),
);
},
);
}
}

View file

@ -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<ConsumerStatefulWidget> createState() => _FavouritesTabState();
}
class _FavouritesTabState extends ConsumerState<FavouritesTab> 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;
}

View file

@ -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<ConsumerStatefulWidget> createState() => _LibraryTabState();
}
class _LibraryTabState extends ConsumerState<LibraryTab> 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;
}

View file

@ -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<ConsumerStatefulWidget> createState() => _RecommendationsTabState();
}
class _RecommendationsTabState extends ConsumerState<RecommendationsTab> 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;
}

View file

@ -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<ConsumerStatefulWidget> createState() => _TimelineTabState();
}
class _TimelineTabState extends ConsumerState<TimelineTab> 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<DateTime, List<PhotoModel>> items, List<PhotoModel> 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<DateTime, List<PhotoModel>> groupedItems(List<PhotoModel> items) {
Map<DateTime, List<PhotoModel>> 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;
}

View file

@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_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_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playlist_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/library_search_provider.dart';
import 'package:fladder/providers/settings/client_settings_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/collections/add_to_collection.dart';
import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart'; import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart';
import 'package:fladder/screens/library_search/widgets/library_play_options_.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/library_search/widgets/suggestion_search_bar.dart';
import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/shared/animated_fade_size.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/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/debouncer.dart';
import 'package:fladder/util/fab_extended_anim.dart'; import 'package:fladder/util/fab_extended_anim.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.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/map_bool_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/router_extension.dart'; import 'package:fladder/util/router_extension.dart';
import 'package:fladder/util/sliver_list_padding.dart'; import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
import 'package:fladder/widgets/shared/fladder_scrollbar.dart'; import 'package:fladder/widgets/shared/fladder_scrollbar.dart';
import 'package:fladder/widgets/shared/hide_on_scroll.dart'; import 'package:fladder/widgets/shared/hide_on_scroll.dart';
@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null; final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null;
final librarySearchResults = ref.watch(providerKey); final librarySearchResults = ref.watch(providerKey);
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows); final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows);
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
final libraryViewType = ref.watch(libraryViewTypeProvider); final libraryViewType = ref.watch(libraryViewTypeProvider);
ref.listen( ref.listen(
@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
libraryProvider.toggleSelectMode(); libraryProvider.toggleSelectMode();
} }
}, },
child: Scaffold( child: NestedScaffold(
extendBody: true, background: BackgroundImage(items: librarySearchResults.activePosters),
extendBodyBehindAppBar: true, body: Padding(
floatingActionButtonAnimator: padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, child: Scaffold(
floatingActionButtonLocation: extendBody: true,
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, extendBodyBehindAppBar: true,
floatingActionButton: switch (playerState) { floatingActionButton: HideOnScroll(
VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: FloatingPlayerBar(),
),
_ => HideOnScroll(
controller: scrollController, controller: scrollController,
visibleBuilder: (visible) => Column( visibleBuilder: (visible) => Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@ -206,29 +196,26 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
].addInBetween(const SizedBox(height: 10)), ].addInBetween(const SizedBox(height: 10)),
), ),
), ),
}, bottomNavigationBar: HideOnScroll(
bottomNavigationBar: HideOnScroll( controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, child: IgnorePointer(
child: IgnorePointer( ignoring: librarySearchResults.fetchingItems,
ignoring: librarySearchResults.fetchingItems, child: _LibrarySearchBottomBar(
child: _LibrarySearchBottomBar( uniqueKey: uniqueKey,
uniqueKey: uniqueKey, refreshKey: refreshKey,
refreshKey: refreshKey, scrollController: scrollController,
scrollController: scrollController, libraryProvider: libraryProvider,
libraryProvider: libraryProvider, postersList: postersList,
postersList: postersList, ),
),
), ),
), body: Stack(
), fit: StackFit.expand,
body: Stack( children: [
children: [ Positioned.fill(
Positioned.fill( child: PinchPosterZoom(
child: Card( scaleDifference: (difference) =>
elevation: 1, ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: MediaQuery.removeViewInsets(
context: context,
child: ClipRRect( child: ClipRRect(
borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
? BorderRadius.circular(15) ? BorderRadius.circular(15)
@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
onTapUp: (details) async { onTapUp: (details) async {
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) { if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
double left = details.globalPosition.dx; double left = details.globalPosition.dx;
double top = details.globalPosition.dy + 20; double top = details.globalPosition.dy;
await showMenu( await showMenu(
context: context, context: context,
position: RelativeRect.fromLTRB(left, top, 40, 100), position: RelativeRect.fromLTRB(left, top, 40, 100),
@ -463,16 +450,10 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: LibraryFilterChips( child: LibraryFilterChips(
controller: scrollController, key: uniqueKey,
libraryProvider: libraryProvider,
librarySearchResults: librarySearchResults,
uniqueKey: uniqueKey,
postersList: postersList,
libraryViewType: libraryViewType,
), ),
), ),
), ),
const Row(),
], ],
), ),
), ),
@ -500,13 +481,12 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
), ),
) )
else else
SliverToBoxAdapter( SliverFillRemaining(
child: Center( child: Center(
child: Text(context.localized.noItemsToShow), child: Text(context.localized.noItemsToShow),
), ),
), ),
const DefautlSliverBottomPadding(), SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20))
const SliverPadding(padding: EdgeInsets.only(bottom: 80))
], ],
), ),
), ),
@ -514,36 +494,36 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
), ),
), ),
), ),
), if (librarySearchResults.fetchingItems) ...[
), Container(
if (librarySearchResults.fetchingItems) ...[ color: Colors.black.withValues(alpha: 0.1),
Container(
color: Colors.black.withValues(alpha: 0.1),
),
Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
), ),
child: Padding( Center(
padding: const EdgeInsets.all(16), child: Container(
child: Row( decoration: BoxDecoration(
mainAxisSize: MainAxisSize.min, color: Theme.of(context).colorScheme.primaryContainer,
children: [ borderRadius: BorderRadius.circular(16),
const CircularProgressIndicator.adaptive(), ),
Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), child: Padding(
IconButton( padding: const EdgeInsets.all(16),
onPressed: () => libraryProvider.cancelFetch(), child: Row(
icon: const Icon(IconsaxPlusLinear.close_square), mainAxisSize: MainAxisSize.min,
) children: [
].addInBetween(const SizedBox(width: 16)), 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), icon: const Icon(IconsaxPlusLinear.save_add),
), ),
]; ];
return NestedBottomAppBar(
child: Column( return Padding(
mainAxisAlignment: MainAxisAlignment.end, padding: EdgeInsets.only(left: MediaQuery.paddingOf(context).left),
mainAxisSize: MainAxisSize.min, child: NestedBottomAppBar(
children: [ child: Padding(
Row( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [ children: [
ScrollStatePosition( Row(
controller: scrollController, spacing: 6,
positionBuilder: (state) => AnimatedFadeSize( children: [
child: state != ScrollState.top ScrollStatePosition(
? Tooltip( controller: scrollController,
message: context.localized.scrollToTop, positionBuilder: (state) => AnimatedFadeSize(
child: FlatButton( child: state != ScrollState.top
clipBehavior: Clip.antiAlias, ? Tooltip(
elevation: 0, message: context.localized.scrollToTop,
borderRadiusGeometry: BorderRadius.circular(6), child: IconButton.filled(
onTap: () => scrollController.animateTo(0, onPressed: () => scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic), duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
child: Container( icon: const Icon(
decoration: BoxDecoration( IconsaxPlusLinear.arrow_up,
color: Theme.of(context).colorScheme.primaryContainer, ),
borderRadius: BorderRadius.circular(8),
), ),
padding: const EdgeInsets.all(6), )
child: Icon( : const SizedBox(),
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,
),
), ),
), ),
), if (!librarySearchResults.selecteMode) ...{
if (librarySearchResults.activePosters.isNotEmpty) IconButton(
IconButton( tooltip: context.localized.sortBy,
tooltip: context.localized.shuffleVideos, onPressed: () async {
onPressed: () async { final newOptions = await openSortByDialogue(
if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) { context,
libraryProvider.viewGallery(context, shuffle: true); libraryProvider: libraryProvider,
return; uniqueKey: uniqueKey,
} else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) { options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
libraryProvider.playLibraryItems(context, ref, shuffle: true); );
return; if (newOptions != null) {
} if (newOptions.$1 != null) {
libraryProvider.setSortBy(newOptions.$1!);
await showLibraryPlayOptions( }
context, if (newOptions.$2 != null) {
context.localized.libraryShuffleAndPlayItems, libraryProvider.setSortOrder(newOptions.$2!);
playVideos: librarySearchResults.showPlayButtons }
? () => libraryProvider.playLibraryItems(context, ref, shuffle: true) }
: null, },
viewGallery: librarySearchResults.showGalleryButtons icon: const Icon(IconsaxPlusLinear.sort),
? () => libraryProvider.viewGallery(context, shuffle: true) ),
: null, 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), ),
],
), ),
); );
} }

View file

@ -1,221 +1,193 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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.enums.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.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/models/library_search/library_search_options.dart';
import 'package:fladder/providers/library_search_provider.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/screens/shared/chips/category_chip.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/shared/scroll_position.dart';
class LibraryFilterChips extends ConsumerWidget { class LibraryFilterChips extends ConsumerStatefulWidget {
final Key uniqueKey; const LibraryFilterChips({super.key});
final ScrollController controller;
final LibrarySearchModel librarySearchResults;
final LibrarySearchNotifier libraryProvider;
final List<ItemBaseModel> 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,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<ConsumerStatefulWidget> createState() => _LibraryFilterChipsState();
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)),
);
},
);
}
} }
List<Widget> libraryFilterChips( class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
BuildContext context, @override
WidgetRef ref, Widget build(BuildContext context) {
Key uniqueKey, { final uniqueKey = widget.key ?? UniqueKey();
required LibrarySearchModel librarySearchResults, final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier);
required LibrarySearchNotifier libraryProvider, final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
required List<ItemBaseModel> postersList, final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites));
required LibraryViewTypes libraryViewType, final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive));
}) { final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows));
Future<dynamic> openGroupDialogue() { final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey));
return showDialog(
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<FladderItemType>(
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<String>(
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<Studio>(
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<String>(
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<ItemFilter>(
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<String>(
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<int>(
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, context: context,
builder: (context) { builder: (context) {
return Consumer( final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
builder: (context, ref, child) { return AlertDialog(
return AlertDialog( content: SizedBox(
content: SizedBox( width: MediaQuery.of(context).size.width * 0.65,
width: MediaQuery.of(context).size.width * 0.65, child: ListView(
child: ListView( shrinkWrap: true,
shrinkWrap: true, children: [
children: [ Text(context.localized.groupBy),
Text(context.localized.groupBy), ...GroupBy.values.map(
...GroupBy.values.map((groupBy) => RadioListTile.adaptive( (group) => RadioListTile.adaptive(
value: groupBy, value: group,
title: Text(groupBy.value(context)), groupValue: groupBy,
groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)), title: Text(group.value(context)),
onChanged: (value) { onChanged: (_) {
libraryProvider.setGroupBy(groupBy); provider.setGroupBy(group);
Navigator.pop(context); 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<FladderItemType>(
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<String>(
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<Studio>(
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<String>(
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<ItemFilter>(
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<String>(
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<int>(
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)),
),
];
} }

View file

@ -1,7 +1,16 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:collection/collection.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: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/boxset_model.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/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/library_search_provider.dart';
import 'package:fladder/providers/settings/client_settings_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/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_list_item.dart';
import 'package:fladder/screens/shared/media/poster_widget.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/string_extensions.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: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<LibraryViewTypes>((ref) { final libraryViewTypeProvider = StateProvider<LibraryViewTypes>((ref) {
return LibraryViewTypes.grid; return LibraryViewTypes.grid;
@ -107,179 +109,139 @@ class LibraryViews extends ConsumerWidget {
switch (ref.watch(libraryViewTypeProvider)) { switch (ref.watch(libraryViewTypeProvider)) {
case LibraryViewTypes.grid: case LibraryViewTypes.grid:
if (groupByType != GroupBy.none) { Widget createGrid(List<ItemBaseModel> items) {
final groupedItems = groupItemsBy(context, items, groupByType); return SliverGrid.builder(
return SliverList.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
itemCount: groupedItems.length, crossAxisCount: posterSize.toInt(),
mainAxisSpacing: (8 * decimal) + 8,
crossAxisSpacing: (8 * decimal) + 8,
childAspectRatio: items.getMostCommonType.aspectRatio,
),
itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final name = groupedItems.keys.elementAt(index); final item = items[index];
final group = groupedItems[name]; return PosterWidget(
if (group?.isEmpty ?? false || group == null) { key: Key(item.id),
return Text(context.localized.empty); poster: item,
} maxLines: 2,
return PosterGrid( heroTag: true,
posters: group!, subTitle: item.subTitle(sortingOptions),
name: name, excludeActions: excludeActions,
itemBuilder: (context, index) { otherActions: otherActions(item),
final item = group[index]; selected: selected.contains(item),
return PosterWidget( onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
key: Key(item.id), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
poster: group[index], onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
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),
);
},
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), 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 { } else {
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverGrid.builder( sliver: createGrid(items),
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),
);
},
),
); );
} }
case LibraryViewTypes.list: case LibraryViewTypes.list:
if (groupByType != GroupBy.none) { Widget listBuilder(List<ItemBaseModel> items) {
final groupedItems = groupItemsBy(context, items, groupByType);
return SliverList.builder( return SliverList.builder(
itemCount: groupedItems.length, itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final name = groupedItems.keys.elementAt(index); final poster = items[index];
final group = groupedItems[name]; return PosterListItem(
if (group?.isEmpty ?? false) { poster: poster,
return Text(context.localized.empty); selected: selected.contains(poster),
} excludeActions: excludeActions,
return StickyHeader( otherActions: otherActions(poster),
header: Text(name, style: Theme.of(context).textTheme.headlineSmall), subTitle: poster.subTitle(sortingOptions),
content: ListView.builder( onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
shrinkWrap: true, onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
padding: EdgeInsets.zero, onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
physics: const NeverScrollableScrollPhysics(), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
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),
);
},
),
); );
}, },
); );
} }
return SliverList.builder( if (groupByType != GroupBy.none) {
itemCount: items.length, final groupedItems = groupItemsBy(context, items, groupByType);
itemBuilder: (context, index) { return MultiSliver(
final poster = items[index]; children: groupedItems.entries.map(
return PosterListItem( (element) {
poster: poster, final name = element.key;
selected: selected.contains(poster), final group = element.value;
excludeActions: excludeActions, return stickyHeaderBuilder(
otherActions: otherActions(poster), context,
subTitle: poster.subTitle(sortingOptions), header: name,
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), sliver: listBuilder(group),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), );
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), },
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ).toList());
); }
}, return listBuilder(items);
);
case LibraryViewTypes.masonry: case LibraryViewTypes.masonry:
if (groupByType != GroupBy.none) { if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType); final groupedItems = groupItemsBy(context, items, groupByType);
return SliverList.builder( return MultiSliver(
itemCount: groupedItems.length, children: groupedItems.entries.map(
itemBuilder: (context, index) { (element) {
final name = groupedItems.keys.elementAt(index); final name = element.key;
final group = groupedItems[name]; final group = element.value;
if (group?.isEmpty ?? false) { return stickyHeaderBuilder(
return Text(context.localized.empty); context,
} header: name,
return Padding( //MasonryGridView because SliverMasonryGrid breaks scrolling
padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0), sliver: SliverToBoxAdapter(
child: StickyHeader( child: MasonryGridView.builder(
header: Text(name, style: Theme.of(context).textTheme.headlineMedium), shrinkWrap: true,
overlapHeaders: true, physics: const NeverScrollableScrollPhysics(),
content: Padding( mainAxisSpacing: (8 * decimal) + 8,
padding: const EdgeInsets.only(top: 16.0), crossAxisSpacing: (8 * decimal) + 8,
child: MasonryGridView.builder( gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
shrinkWrap: true, maxCrossAxisExtent:
physics: const NeverScrollableScrollPhysics(), (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
mainAxisSpacing: (8 * decimal) + 8, .toDouble() *
crossAxisSpacing: (8 * decimal) + 8, 12,
gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( ),
maxCrossAxisExtent: itemCount: group.length,
(MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) itemBuilder: (context, index) {
.toDouble() * final item = group[index];
20, return PosterWidget(
), key: Key(item.id),
itemCount: group!.length, poster: item,
itemBuilder: (context, index) { aspectRatio: item.primaryRatio,
final item = group[index]; selected: selected.contains(item),
return PosterWidget( inlineTitle: true,
key: Key(item.id), heroTag: true,
poster: item, subTitle: item.subTitle(sortingOptions),
aspectRatio: item.primaryRatio, excludeActions: excludeActions,
selected: selected.contains(item), otherActions: otherActions(group[index]),
inlineTitle: true, onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
heroTag: true, onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
subTitle: item.subTitle(sortingOptions), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
excludeActions: excludeActions, onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
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 { } else {
return SliverMasonryGrid.count( return SliverMasonryGrid.count(
mainAxisSpacing: (8 * decimal) + 8, 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<String, List<ItemBaseModel>> groupItemsBy(BuildContext context, List<ItemBaseModel> list, GroupBy groupOption) { Map<String, List<ItemBaseModel>> groupItemsBy(BuildContext context, List<ItemBaseModel> list, GroupBy groupOption) {
switch (groupOption) { switch (groupOption) {
case GroupBy.dateAdded: case GroupBy.dateAdded:

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:page_transition/page_transition.dart'; import 'package:page_transition/page_transition.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
@ -65,6 +65,9 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
}); });
return Card( return Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: FladderTheme.largeShape.borderRadius,
),
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
child: TypeAheadField<ItemBaseModel>( child: TypeAheadField<ItemBaseModel>(
focusNode: focusNode, focusNode: focusNode,
@ -80,7 +83,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
decorationBuilder: (context, child) => DecoratedBox( decorationBuilder: (context, child) => DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: FladderTheme.defaultShape.borderRadius, borderRadius: FladderTheme.largeShape.borderRadius,
), ),
child: child, child: child,
), ),
@ -133,39 +136,45 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
} }
}, },
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: SizedBox( title: ConstrainedBox(
height: 50, constraints: const BoxConstraints(
child: Row( minHeight: 50,
children: [ maxHeight: 65,
Card( ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), child: Padding(
child: AspectRatio( padding: const EdgeInsets.all(8.0),
aspectRatio: 0.8, child: Row(
child: FladderImage( children: [
image: suggestion.images?.primary, Card(
fit: BoxFit.cover, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: AspectRatio(
aspectRatio: 0.8,
child: FladderImage(
image: suggestion.images?.primary,
fit: BoxFit.cover,
),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Flexible(
Flexible( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [
Flexible(
child: Text(
suggestion.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
if (suggestion.overview.yearAired.toString().isNotEmpty)
Flexible( Flexible(
child: child: Text(
Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))), 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() ?? ""))),
],
),
), ),
), ],
], ),
), ),
), ),
); );

View file

@ -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/fladder_snackbar.dart';
import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart';
import 'package:fladder/screens/shared/passcode_input.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/auth_service.dart';
import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/fladder_config.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget {
return Card( return Card(
elevation: 1, elevation: 1,
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150), constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),

View file

@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget {
aspectRatio: 1.0, aspectRatio: 1.0,
child: Card( child: Card(
elevation: 1, elevation: 1,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Stack( child: Stack(
children: [ children: [

View file

@ -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_fields.dart';
import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart'; import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart';
import 'package:fladder/screens/shared/fladder_snackbar.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/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/shared/file_picker.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 { class EditImageContent extends ConsumerStatefulWidget {
final ImageType type; final ImageType type;

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/items/identify_provider.dart'; import 'package:fladder/providers/items/identify_provider.dart';
@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
final state = ref.watch(provider); final state = ref.watch(provider);
final posters = state.results; final posters = state.results;
final processing = state.processing; final processing = state.processing;
return ActionContent( return Card(
showDividers: false, child: ActionContent(
title: Container( showDividers: false,
color: Theme.of(context).colorScheme.surface, title: Column(
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Row(
@ -89,137 +88,137 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
) )
], ],
), ),
), child: TabBarView(
child: TabBarView( controller: tabController,
controller: tabController, children: [
children: [ inputFields(state),
inputFields(state), if (posters.isEmpty)
if (posters.isEmpty) Center(
Center( child: processing
child: processing ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round)
? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round) : Text(context.localized.noResults),
: Text(context.localized.noResults), )
) else
else Column(
Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Row(
Row( mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, children: [
children: [ Text(context.localized.replaceAllImages),
Text(context.localized.replaceAllImages), const SizedBox(width: 16),
const SizedBox(width: 16), Switch.adaptive(
Switch.adaptive( value: state.replaceAllImages,
value: state.replaceAllImages, onChanged: (value) {
onChanged: (value) { ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value));
ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value)); },
}, ),
), ],
], ),
), Flexible(
Flexible( child: ListView(
child: ListView( shrinkWrap: true,
shrinkWrap: true, children: posters
children: posters .map((result) => ListTile(
.map((result) => ListTile( title: Row(
title: Row( children: [
children: [ SizedBox(
SizedBox( width: 75,
width: 75, child: Card(
child: Card( child: CachedNetworkImage(
child: CachedNetworkImage( imageUrl: result.imageUrl ?? "",
imageUrl: result.imageUrl ?? "", errorWidget: (context, url, error) => SizedBox(
errorWidget: (context, url, error) => SizedBox( height: 75,
height: 75, child: Card(
child: Card( child: Center(
child: Center( child: Text(result.name?.getInitials() ?? ""),
child: Text(result.name?.getInitials() ?? ""), ),
), ),
), ),
), ),
), ),
), ),
), const SizedBox(width: 16),
const SizedBox(width: 16), Expanded(
Expanded( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"),
"${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"), Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? ""))
Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? "")) ],
], ),
), ),
), Tooltip(
Tooltip( message: context.localized.openWebLink,
message: context.localized.openWebLink, child: IconButton(
child: IconButton( onPressed: () {
onPressed: () { final providerKeyEntry = result.providerIds?.entries.first;
final providerKeyEntry = result.providerIds?.entries.first; final providerKey = providerKeyEntry?.key;
final providerKey = providerKeyEntry?.key; final providerValue = providerKeyEntry?.value;
final providerValue = providerKeyEntry?.value;
final externalId = state.externalIds final externalId = state.externalIds
.firstWhereOrNull((element) => element.key == providerKey) .firstWhereOrNull((element) => element.key == providerKey)
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
?.urlFormatString; ?.urlFormatString;
final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? ""); final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? "");
launchUrl(context, url ?? ""); launchUrl(context, url ?? "");
}, },
icon: const Icon(Icons.launch_rounded)), icon: const Icon(Icons.launch_rounded)),
), ),
Tooltip( Tooltip(
message: "Select result", message: "Select result",
child: IconButton( child: IconButton(
onPressed: !processing onPressed: !processing
? () async { ? () async {
final response = await ref.read(provider.notifier).setIdentity(result); final response = await ref.read(provider.notifier).setIdentity(result);
if (response?.isSuccessful == true) { if (response?.isSuccessful == true) {
fladderSnackbar(context, fladderSnackbar(context,
title: context.localized.setIdentityTo(result.name ?? "")); title: context.localized.setIdentityTo(result.name ?? ""));
} else { } else {
fladderSnackbarResponse(context, response, fladderSnackbarResponse(context, response,
altTitle: context.localized.somethingWentWrong); altTitle: context.localized.somethingWentWrong);
}
Navigator.of(context).pop();
} }
: null,
Navigator.of(context).pop(); icon: const Icon(Icons.save_alt_rounded),
} ),
: null, )
icon: const Icon(Icons.save_alt_rounded), ],
), ),
) ))
], .toList(),
), ),
))
.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<IdentifyScreen> with TickerProv
final controller = final controller =
currentKey == "Name" ? currentController : TextEditingController(text: state.searchString); currentKey == "Name" ? currentController : TextEditingController(text: state.searchString);
return FocusedOutlinedTextField( return FocusedOutlinedTextField(
label: context.localized.userName, label: context.localized.name,
controller: controller, controller: controller,
onChanged: (value) { onChanged: (value) {
currentController = controller; currentController = controller;

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/information_model.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';

View file

@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/shared/fladder_snackbar.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/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/enum_selection.dart';

View file

@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/input_fields.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/input_handler.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';

View file

@ -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/photo_viewer_controls.dart';
import 'package:fladder/screens/photo_viewer/simple_video_player.dart'; import 'package:fladder/screens/photo_viewer/simple_video_player.dart';
import 'package:fladder/screens/shared/default_title_bar.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/custom_cache_manager.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/crash_screen/crash_screen.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart';
@ -42,75 +42,79 @@ class AboutSettingsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final applicationInfo = ref.watch(applicationInfoProvider); final applicationInfo = ref.watch(applicationInfoProvider);
return Card( return SettingsScaffold(
child: SettingsScaffold( label: "",
label: "", items: [
items: [ const FladderLogo(),
const FladderLogo(), Column(
Column( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)), Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
Text(context.localized.aboutBuild(applicationInfo.buildNumber)), const SizedBox(height: 16),
const SizedBox(height: 16), Text(context.localized.aboutCreatedBy),
Text(context.localized.aboutCreatedBy), ],
], ),
const FractionallySizedBox(
widthFactor: 0.25,
child: Divider(
indent: 16,
endIndent: 16,
), ),
const Divider(), ),
Column( Column(
children: [ children: [
Text( Text(
context.localized.aboutSocials, context.localized.aboutSocials,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: socials children: socials
.map( .map(
(e) => IconButton.filledTonal( (e) => IconButton.filledTonal(
onPressed: () => launchUrl(context, e.url), onPressed: () => launchUrl(context, e.url),
icon: Column( icon: Column(
children: [ children: [
Icon(e.icon), Icon(e.icon),
Text(e.label), Text(e.label),
], ],
),
), ),
) ),
.toList() )
.addInBetween(const SizedBox(width: 16)), .toList()
) .addInBetween(const SizedBox(width: 16)),
], )
), ],
Row( ),
mainAxisAlignment: MainAxisAlignment.center, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
FilledButton.tonal( children: [
onPressed: () => showLicensePage( FilledButton.tonal(
context: context, onPressed: () => showLicensePage(
applicationIcon: const FladderIcon(size: 55), context: context,
applicationVersion: applicationInfo.versionPlatformBuild, applicationIcon: const FladderIcon(size: 55),
applicationLegalese: "DonutWare", applicationVersion: applicationInfo.versionPlatformBuild,
), applicationLegalese: "DonutWare",
child: Text(context.localized.aboutLicenses), ),
) child: Text(context.localized.aboutLicenses),
], )
), ],
Row( ),
mainAxisAlignment: MainAxisAlignment.center, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
FilledButton.tonal( children: [
onPressed: () => showDialog( FilledButton.tonal(
context: context, onPressed: () => showDialog(
builder: (context) => const CrashScreen(), context: context,
), builder: (context) => const CrashScreen(),
child: Text(context.localized.errorLogs), ),
) child: Text(context.localized.errorLogs),
], )
), ],
].addInBetween(const SizedBox(height: 16)), ),
), ].addInBetween(const SizedBox(height: 16)),
); );
} }
} }

View file

@ -2,79 +2,83 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.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_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/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart'; import 'package:fladder/util/option_dialogue.dart';
List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
return [ return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.advanced), SettingsLabelDivider(label: context.localized.advanced),
SettingsListTile( [
label: Text(context.localized.settingsLayoutSizesTitle), SettingsListTile(
subLabel: Text(context.localized.settingsLayoutSizesDesc), label: Text(context.localized.settingsLayoutSizesTitle),
onTap: () async { subLabel: Text(context.localized.settingsLayoutSizesDesc),
final newItems = await openMultiSelectOptions<ViewSize>( onTap: () async {
context, final newItems = await openMultiSelectOptions<ViewSize>(
label: context.localized.settingsLayoutSizesTitle, context,
items: ViewSize.values, label: context.localized.settingsLayoutSizesTitle,
allowMultiSelection: true, items: ViewSize.values,
selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), allowMultiSelection: true,
itemBuilder: (type, selected, tap) => CheckboxListTile( selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
contentPadding: EdgeInsets.zero, itemBuilder: (type, selected, tap) => CheckboxListTile(
value: selected, contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(), value: selected,
title: Text(type.label(context)), 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(
SettingsListTile( label: Text(context.localized.settingsLayoutModesTitle),
label: Text(context.localized.settingsLayoutModesTitle), subLabel: Text(context.localized.settingsLayoutModesDesc),
subLabel: Text(context.localized.settingsLayoutModesDesc), onTap: () async {
onTap: () async { final newItems = await openMultiSelectOptions<LayoutMode>(
final newItems = await openMultiSelectOptions<LayoutMode>( context,
context, label: context.localized.settingsLayoutModesTitle,
label: context.localized.settingsLayoutModesTitle, items: LayoutMode.values,
items: LayoutMode.values, allowMultiSelection: true,
allowMultiSelection: true, selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), itemBuilder: (type, selected, tap) => CheckboxListTile(
itemBuilder: (type, selected, tap) => CheckboxListTile( contentPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero, value: selected,
value: selected, onChanged: (value) => tap(),
onChanged: (value) => tap(), title: Text(type.label(context)),
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(', ')),
), ),
), ),
), ],
]; );
} }

View file

@ -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/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.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_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/enum_selection.dart';
List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) { List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider); final clientSettings = ref.watch(clientSettingsProvider);
return [ return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.dashboard), 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( SettingsListTile(
label: Text(context.localized.settingsHomeBannerInformationTitle), label: Text(context.localized.settingsHomeBannerTitle),
subLabel: Text(context.localized.settingsHomeBannerInformationDesc), subLabel: Text(context.localized.settingsHomeBannerDescription),
trailing: EnumBox( trailing: EnumBox(
current: ref.watch( 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( .map(
(entry) => PopupMenuItem( (entry) => PopupMenuItem(
value: entry, value: entry,
child: Text(entry.label(context)), child: Text(entry.label(context)),
onTap: () => ref onTap: () =>
.read(homeSettingsProvider.notifier) ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
.update((context) => context.copyWith(carouselSettings: entry)),
), ),
) )
.toList(), .toList(),
), ),
), ),
SettingsListTile( if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
label: Text(context.localized.settingsHomeNextUpTitle), SettingsListTile(
subLabel: Text(context.localized.settingsHomeNextUpDesc), label: Text(context.localized.settingsHomeBannerInformationTitle),
trailing: EnumBox( subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
current: ref.watch( trailing: EnumBox(
homeSettingsProvider.select( current: ref.watch(
(value) => value.nextUp.label(context), 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 SettingsListTile(
.map( label: Text(context.localized.settingsHomeNextUpTitle),
(entry) => PopupMenuItem( subLabel: Text(context.localized.settingsHomeNextUpDesc),
value: entry, trailing: EnumBox(
child: Text(entry.label(context)), current: ref.watch(
onTap: () => homeSettingsProvider.select(
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), (value) => value.nextUp.label(context),
), ),
) ),
.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(
SettingsListTile( label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
label: Text(context.localized.clientSettingsShowAllCollectionsTitle), subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), onTap: () => ref
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
trailing: Switch(
value: clientSettings.showAllCollectionTypes,
onChanged: (value) => ref
.read(clientSettingsProvider.notifier) .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(), );
];
} }

View file

@ -11,9 +11,10 @@ import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.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_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/default_alert_dialog.dart';
import 'package:fladder/screens/shared/input_fields.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/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/util/size_formatting.dart';
@ -24,121 +25,122 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
return [ return [
if (canSync && !kIsWeb) ...[ if (canSync && !kIsWeb) ...[
SettingsLabelDivider(label: context.localized.downloadsTitle), ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [
if (AdaptiveLayout.of(context).isDesktop) ...[ if (AdaptiveLayout.of(context).isDesktop) ...[
SettingsListTile( SettingsListTile(
label: Text(context.localized.downloadsPath), label: Text(context.localized.downloadsPath),
subLabel: Text(currentFolder ?? "-"), subLabel: Text(currentFolder ?? "-"),
onTap: currentFolder != null onTap: currentFolder != null
? () async => await showDialog( ? () async => await showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(context.localized.pathEditTitle), title: Text(context.localized.pathEditTitle),
content: Text(context.localized.pathEditDesc), content: Text(context.localized.pathEditDesc),
actions: [ actions: [
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath( String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) { if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(context.localized.change), 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 { : null,
String? selectedDirectory = await FilePicker.platform ),
.getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); ],
if (selectedDirectory != null) { FutureBuilder(
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); 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( const SizedBox(height: 12),
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(),
], ],
]; ];
} }

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.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_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/color_extensions.dart'; import 'package:fladder/util/color_extensions.dart';
import 'package:fladder/util/custom_color_themes.dart'; import 'package:fladder/util/custom_color_themes.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart';
List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) { List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider); final clientSettings = ref.watch(clientSettingsProvider);
return [ return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [
SettingsLabelDivider(label: context.localized.theme),
SettingsListTile( SettingsListTile(
label: Text(context.localized.mode), label: Text(context.localized.mode),
subLabel: Text(clientSettings.themeMode.label(context)), subLabel: Text(clientSettings.themeMode.label(context)),
@ -107,6 +107,5 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value), onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
), ),
), ),
const Divider(), ]);
];
} }

View file

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.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_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/input_fields.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/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart';
@ -22,136 +21,136 @@ List<Widget> buildClientSettingsVisual(
) { ) {
final clientSettings = ref.watch(clientSettingsProvider); final clientSettings = ref.watch(clientSettingsProvider);
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale; Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
return [ return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.settingsVisual), SettingsLabelDivider(label: context.localized.settingsVisual),
SettingsListTile( [
label: Text(context.localized.displayLanguage), SettingsListTile(
trailing: Localizations.override( label: Text(context.localized.displayLanguage),
context: context, trailing: Localizations.override(
locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))), context: context,
child: Builder(builder: (context) { locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
String language = "Unknown"; child: Builder(builder: (context) {
try { String language = "Unknown";
language = context.localized.nativeName; try {
} catch (e) { language = context.localized.nativeName;
log(e.toString()); } catch (_) {}
} return EnumBox(
return EnumBox( current: language,
current: language, itemBuilder: (context) {
itemBuilder: (context) { return [
return [ ...AppLocalizations.supportedLocales.map(
...AppLocalizations.supportedLocales.map( (entry) => PopupMenuItem(
(entry) => PopupMenuItem( value: entry,
value: entry, child: Localizations.override(
child: Localizations.override( context: context,
context: context, locale: entry,
locale: entry, child: Builder(builder: (context) {
child: Builder(builder: (context) { return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
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(
SettingsListTile( label: Text(AdaptiveLayout.of(context).isDesktop
label: Text(context.localized.settingsBlurredPlaceholderTitle), ? context.localized.settingsShowScaleSlider
subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), : context.localized.settingsPosterPinch),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), onTap: () => ref.read(clientSettingsProvider.notifier).update(
trailing: Switch( (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
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),
), ),
trailing: Switch(
value: clientSettings.pinchPosterZoom,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: value),
),
),
), ),
), Column(
Column( children: [
children: [ SettingsListTile(
SettingsListTile( label: Text(context.localized.settingsPosterSize),
label: Text(context.localized.settingsPosterSize), trailing: Text(
trailing: Text( clientSettings.posterSize.toString(),
clientSettings.posterSize.toString(), style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodyLarge, ),
), ),
), Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: FladderSlider(
child: FladderSlider( min: 0.5,
min: 0.5, max: 1.5,
max: 1.5, value: clientSettings.posterSize,
value: clientSettings.posterSize, divisions: 20,
divisions: 20, onChanged: (value) =>
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), ),
), ),
), ],
], ),
), ],
const Divider(), );
];
} }

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/routes/auto_router.gr.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_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.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_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/localization_helper.dart';
import 'package:fladder/util/simple_duration_picker.dart'; import 'package:fladder/util/simple_duration_picker.dart';
@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final clientSettings = ref.watch(clientSettingsProvider); final clientSettings = ref.watch(clientSettingsProvider);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
return Card( return SettingsScaffold(
elevation: showBackground ? 2 : 0, label: "Fladder",
child: SettingsScaffold( items: [
label: "Fladder", ...buildClientSettingsDownload(context, ref, setState),
items: [ ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [
...buildClientSettingsDownload(context, ref, setState),
SettingsLabelDivider(label: context.localized.lockscreen),
SettingsListTile( SettingsListTile(
label: Text(context.localized.timeOut), label: Text(context.localized.timeOut),
subLabel: Text(timePickerString(context, clientSettings.timeOut)), subLabel: Text(timePickerString(context, clientSettings.timeOut)),
@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
: null); : null);
}, },
), ),
const Divider(), ]),
...buildClientSettingsDashboard(context, ref), const SizedBox(height: 12),
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), ...buildClientSettingsDashboard(context, ref),
...buildClientSettingsTheme(context, ref), const SizedBox(height: 12),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
SettingsLabelDivider(label: context.localized.controls), 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( SettingsListTile(
label: Text(context.localized.mouseDragSupport), label: Text(context.localized.mouseDragSupport),
subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled), subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled),
@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
), ),
), ),
const Divider(), ]),
], const SizedBox(height: 12),
...buildClientSettingsAdvanced(context, ref), ],
if (kDebugMode) ...[ ...buildClientSettingsAdvanced(context, ref),
const SizedBox(height: 64), if (kDebugMode) ...[
SettingsListTile( const SizedBox(height: 64),
label: Text( SettingsListTile(
context.localized.clearAllSettings, label: Text(
), context.localized.clearAllSettings,
contentColor: Theme.of(context).colorScheme.error, ),
onTap: () { contentColor: Theme.of(context).colorScheme.error,
showDialog( onTap: () {
context: context, showDialog(
builder: (context) => Dialog( context: context,
child: Padding( builder: (context) => Dialog(
padding: const EdgeInsets.all(16), child: Padding(
child: Column( padding: const EdgeInsets.all(16),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Text( children: [
context.localized.clearAllSettingsQuestion, Text(
style: Theme.of(context).textTheme.titleLarge, context.localized.clearAllSettingsQuestion,
), style: Theme.of(context).textTheme.titleLarge,
const SizedBox(height: 16), ),
Text( const SizedBox(height: 16),
context.localized.unableToReverseAction, Text(
), context.localized.unableToReverseAction,
const SizedBox(height: 16), ),
Row( const SizedBox(height: 16),
mainAxisSize: MainAxisSize.min, Row(
children: [ mainAxisSize: MainAxisSize.min,
FilledButton( children: [
onPressed: () => Navigator.of(context).pop(), FilledButton(
child: Text(context.localized.cancel), onPressed: () => Navigator.of(context).pop(),
), child: Text(context.localized.cancel),
const SizedBox(width: 8), ),
ElevatedButton( const SizedBox(width: 8),
onPressed: () async { ElevatedButton(
await ref.read(sharedPreferencesProvider).clear(); onPressed: () async {
context.router.push(const LoginRoute()); await ref.read(sharedPreferencesProvider).clear();
}, context.router.push(const LoginRoute());
child: Text(context.localized.clear), },
) child: Text(context.localized.clear),
], )
), ],
], ),
), ],
), ),
), ),
); ),
}, );
), },
], ),
], ],
), ],
); );
} }
} }

View file

@ -8,20 +8,20 @@ import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.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/models/settings/video_player_settings.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/connectivity_provider.dart'; import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_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_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.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_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/settings_message_box.dart';
import 'package:fladder/screens/settings/widgets/subtitle_editor.dart'; import 'package:fladder/screens/settings/widgets/subtitle_editor.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/screens/video_player/components/video_player_options_sheet.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/bitrate_helper.dart';
import 'package:fladder/util/box_fit_extension.dart'; import 'package:fladder/util/box_fit_extension.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -41,100 +41,105 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoSettings = ref.watch(videoPlayerSettingsProvider); final videoSettings = ref.watch(videoPlayerSettingsProvider);
final provider = ref.read(videoPlayerSettingsProvider.notifier); final provider = ref.read(videoPlayerSettingsProvider.notifier);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
final connectionState = ref.watch(connectivityStatusProvider); final connectionState = ref.watch(connectivityStatusProvider);
return Card( return SettingsScaffold(
elevation: showBackground ? 2 : 0, label: context.localized.settingsPlayerTitle,
child: SettingsScaffold( items: [
label: context.localized.settingsPlayerTitle, ...settingsListGroup(
items: [ context,
SettingsLabelDivider(label: context.localized.video), 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( SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle), label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(context.localized.videoScalingFillScreenDesc), subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => provider.setFillScreen(!videoSettings.fillScreen), onTap: () => openMultiSelectOptions(
trailing: Switch( context,
value: videoSettings.fillScreen, label: context.localized.videoScalingFillScreenTitle,
onChanged: (value) => provider.setFillScreen(value), 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( SettingsListTile(
child: videoSettings.fillScreen label: _StatusIndicator(
? SettingsMessageBox( homeInternet: connectionState.homeInternet,
context.localized.videoScalingFillScreenNotif, label: Text(context.localized.homeStreamingQualityTitle),
messageType: MessageType.warning, ),
) subLabel: Text(context.localized.homeStreamingQualityDesc),
: Container(), trailing: EnumBox(
), current: ref.watch(
SettingsListTile( videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
label: Text(context.localized.videoScalingFillScreenTitle), ),
subLabel: Text(videoSettings.videoFit.label(context)), itemBuilder: (context) => Bitrate.values
onTap: () => openMultiSelectOptions( .map(
context, (entry) => PopupMenuItem(
label: context.localized.videoScalingFillScreenTitle, value: entry,
items: BoxFit.values, child: Text(entry.label(context)),
selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))], onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first), ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
itemBuilder: (type, selected, tap) => RadioListTile( ),
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), )
title: Text(type.label(context)), .toList(),
value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(),
), ),
), ),
), SettingsListTile(
SettingsListTile( label: _StatusIndicator(
label: _StatusIndicator( homeInternet: !connectionState.homeInternet,
homeInternet: connectionState.homeInternet, label: Text(context.localized.internetStreamingQualityTitle),
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 subLabel: Text(context.localized.internetStreamingQualityDesc),
.map( trailing: EnumBox(
(entry) => PopupMenuItem( current: ref.watch(
value: entry, videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
child: Text(entry.label(context)), ),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = itemBuilder: (context) => Bitrate.values
ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry), .map(
), (entry) => PopupMenuItem(
) value: entry,
.toList(), child: Text(entry.label(context)),
), onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
), ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
SettingsListTile( ),
label: _StatusIndicator( )
homeInternet: !connectionState.homeInternet, .toList(),
label: Text(context.localized.internetStreamingQualityTitle),
),
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(),
), ),
), ],
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( ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
(entry) => Padding( (entry) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
), ),
), ),
), ),
SettingsLabelDivider(label: context.localized.playbackTrackSelection), ]),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
SettingsListTile( SettingsListTile(
label: Text(context.localized.rememberAudioSelections), label: Text(context.localized.rememberAudioSelections),
subLabel: Text(context.localized.rememberAudioSelectionsDesc), subLabel: Text(context.localized.rememberAudioSelectionsDesc),
@ -190,8 +197,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
), ),
), ),
const Divider(), ]),
SettingsLabelDivider(label: context.localized.advanced), const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [
if (PlayerOptions.available.length != 1) if (PlayerOptions.available.length != 1)
SettingsListTile( SettingsListTile(
label: Text(context.localized.playerSettingsBackendTitle), label: Text(context.localized.playerSettingsBackendTitle),
@ -236,7 +244,7 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (value) => provider.setHardwareAccel(value), onChanged: (value) => provider.setHardwareAccel(value),
), ),
), ),
if (!kIsWeb) ...[ if (!kIsWeb)
SettingsListTile( SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
@ -246,15 +254,14 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (value) => provider.setUseLibass(value), onChanged: (value) => provider.setUseLibass(value),
), ),
), ),
AnimatedFadeSize( AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox( ? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning, context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning, messageType: MessageType.warning,
) )
: Container(), : Container(),
), ),
],
SettingsListTile( SettingsListTile(
label: Text(context.localized.settingsPlayerBufferSizeTitle), label: Text(context.localized.settingsPlayerBufferSizeTitle),
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
@ -291,33 +298,37 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}") "${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
}, },
), ),
SettingsListTile( Column(
label: Text(context.localized.settingsAutoNextTitle), children: [
subLabel: Text(context.localized.settingsAutoNextDesc), SettingsListTile(
trailing: EnumBox( label: Text(context.localized.settingsAutoNextTitle),
current: ref.watch( subLabel: Text(context.localized.settingsAutoNextDesc),
videoPlayerSettingsProvider.select( trailing: EnumBox(
(value) => value.nextVideoType.label(context), current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry),
),
)
.toList(),
), ),
), ),
itemBuilder: (context) => AutoNextType.values AnimatedFadeSize(
.map( child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
(entry) => PopupMenuItem( AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
value: entry, AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
child: Text(entry.label(context)), _ => const SizedBox.shrink(),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = },
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry), ),
), ],
)
.toList(),
),
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
), ),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SettingsListTile( SettingsListTile(
@ -325,8 +336,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
subLabel: Text(context.localized.playerSettingsOrientationDesc), subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref), onTap: () => showOrientationOptions(context, ref),
), ),
], ]),
), ],
); );
} }
} }

View file

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.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_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/authenticate_button_options.dart'; import 'package:fladder/screens/shared/authenticate_button_options.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@RoutePage() @RoutePage()
class SecuritySettingsPage extends ConsumerStatefulWidget { class SecuritySettingsPage extends ConsumerStatefulWidget {
@ -22,14 +23,10 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userProvider); final user = ref.watch(userProvider);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && return SettingsScaffold(
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; label: context.localized.settingsProfileTitle,
return Card( items: [
elevation: showBackground ? 2 : 0, ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), [
child: SettingsScaffold(
label: context.localized.settingsProfileTitle,
items: [
SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle),
SettingsListTile( SettingsListTile(
label: Text(context.localized.settingSecurityApplockTitle), label: Text(context.localized.settingSecurityApplockTitle),
subLabel: Text(user?.authMethod.name(context) ?? ""), subLabel: Text(user?.authMethod.name(context) ?? ""),
@ -37,8 +34,8 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
ref.read(userProvider.notifier).updateUser(newUser); ref.read(userProvider.notifier).updateUser(newUser);
}), }),
), ),
], ]),
), ],
); );
} }
} }

View file

@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget {
), ),
if (subLabel != null) if (subLabel != null)
Opacity( Opacity(
opacity: 0.75, opacity: 0.65,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
textStyle: Theme.of(context).textTheme.labelLarge, textStyle: Theme.of(context).textTheme.labelLarge,

View file

@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/user_icon.dart'; import 'package:fladder/screens/shared/user_icon.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/router_extension.dart'; import 'package:fladder/util/router_extension.dart';
class SettingsScaffold extends ConsumerWidget { class SettingsScaffold extends ConsumerWidget {
@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget {
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single; final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single;
return Scaffold( return Scaffold(
backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: floatingActionButton, floatingActionButton: floatingActionButton,
body: Column( body: Column(
@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget {
), ),
), ),
SliverPadding( SliverPadding(
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8), padding: MediaQuery.paddingOf(context).copyWith(top: 0),
sliver: SliverList( sliver: SliverList.builder(
delegate: SliverChildListDelegate(items), itemBuilder: (context, index) => items[index],
itemCount: items.length,
), ),
), ),
if (bottomActions.isEmpty) if (bottomActions.isEmpty)

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
@ -12,7 +11,7 @@ import 'package:fladder/screens/settings/quick_connect_window.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/shared/fladder_icon.dart'; import 'package:fladder/screens/shared/fladder_icon.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/util/theme_extensions.dart';
@ -97,119 +96,122 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final quickConnectAvailable = final quickConnectAvailable =
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false)); ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
return Container( return Padding(
color: context.colors.surface, padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
child: SettingsScaffold( child: Container(
label: context.localized.settings, color: context.colors.surface,
scrollController: scrollController, child: SettingsScaffold(
showBackButtonNested: true, label: context.localized.settings,
showUserIcon: true, scrollController: scrollController,
items: [ showBackButtonNested: true,
SettingsListTile( showUserIcon: true,
label: Text(context.localized.settingsClientTitle), items: [
subLabel: Text(context.localized.settingsClientDesc),
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
),
if (quickConnectAvailable)
SettingsListTile( SettingsListTile(
label: Text(context.localized.settingsQuickConnectTitle), label: Text(context.localized.settingsClientTitle),
icon: IconsaxPlusLinear.password_check, subLabel: Text(context.localized.settingsClientDesc),
onTap: () => openQuickConnectDialog(context), selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
), ),
SettingsListTile( if (quickConnectAvailable)
label: Text(context.localized.settingsProfileTitle), SettingsListTile(
subLabel: Text(context.localized.settingsProfileDesc), label: Text(context.localized.settingsQuickConnectTitle),
selected: containsRoute(const SecuritySettingsRoute()), icon: IconsaxPlusLinear.password_check,
icon: IconsaxPlusLinear.security_user, onTap: () => openQuickConnectDialog(context),
onTap: () => navigateTo(const SecuritySettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerTitle),
subLabel: Text(context.localized.settingsPlayerDesc),
selected: containsRoute(const PlayerSettingsRoute()),
icon: IconsaxPlusLinear.video_play,
onTap: () => navigateTo(const PlayerSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.about),
subLabel: const Text("Fladder"),
selected: containsRoute(const AboutSettingsRoute()),
suffix: Opacity(
opacity: 1,
child: FladderIconOutlined(
size: 24,
color: context.colors.onSurfaceVariant,
), ),
SettingsListTile(
label: Text(context.localized.settingsProfileTitle),
subLabel: Text(context.localized.settingsProfileDesc),
selected: containsRoute(const SecuritySettingsRoute()),
icon: IconsaxPlusLinear.security_user,
onTap: () => navigateTo(const SecuritySettingsRoute()),
), ),
onTap: () => navigateTo(const AboutSettingsRoute()), SettingsListTile(
), label: Text(context.localized.settingsPlayerTitle),
], subLabel: Text(context.localized.settingsPlayerDesc),
floatingActionButton: Padding( selected: containsRoute(const PlayerSettingsRoute()),
padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), icon: IconsaxPlusLinear.video_play,
child: Padding( onTap: () => navigateTo(const PlayerSettingsRoute()),
padding: const EdgeInsets.symmetric(horizontal: 16), ),
child: Row( SettingsListTile(
mainAxisAlignment: MainAxisAlignment.end, label: Text(context.localized.about),
children: [ subLabel: const Text("Fladder"),
const Spacer(), selected: containsRoute(const AboutSettingsRoute()),
FloatingActionButton( suffix: Opacity(
key: Key(context.localized.switchUser), opacity: 1,
tooltip: context.localized.switchUser, child: FladderIconOutlined(
onPressed: () async { size: 24,
await ref.read(userProvider.notifier).logoutUser(); color: context.colors.onSurfaceVariant,
context.router.replaceAll([const LoginRoute()]);
},
child: const Icon(
IconsaxPlusLinear.arrow_swap_horizontal,
),
), ),
const SizedBox(width: 16), ),
FloatingActionButton( onTap: () => navigateTo(const AboutSettingsRoute()),
heroTag: context.localized.logout, ),
key: Key(context.localized.logout), ],
tooltip: context.localized.logout, floatingActionButton: Padding(
backgroundColor: Theme.of(context).colorScheme.errorContainer, padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal),
onPressed: () { child: Padding(
final user = ref.read(userProvider); padding: const EdgeInsets.symmetric(horizontal: 16),
showDialog( child: Row(
context: context, mainAxisAlignment: MainAxisAlignment.end,
builder: (context) => AlertDialog( children: [
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")), const Spacer(),
scrollable: true, FloatingActionButton(
content: Text( key: Key(context.localized.switchUser),
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""), tooltip: context.localized.switchUser,
), onPressed: () async {
actions: [ await ref.read(userProvider.notifier).logoutUser();
ElevatedButton( context.router.replaceAll([const LoginRoute()]);
onPressed: () => Navigator.pop(context), },
child: Text(context.localized.cancel), child: const Icon(
IconsaxPlusLinear.arrow_swap_horizontal,
),
),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: context.localized.logout,
key: Key(context.localized.logout),
tooltip: context.localized.logout,
backgroundColor: Theme.of(context).colorScheme.errorContainer,
onPressed: () {
final user = ref.read(userProvider);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
scrollable: true,
content: Text(
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
), ),
ElevatedButton( actions: [
style: ElevatedButton.styleFrom().copyWith( ElevatedButton(
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), onPressed: () => Navigator.pop(context),
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), child: Text(context.localized.cancel),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
), ),
onPressed: () async { ElevatedButton(
await ref.read(authProvider.notifier).logOutUser(); style: ElevatedButton.styleFrom().copyWith(
if (context.mounted) { iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
context.router.replaceAll([const LoginRoute()]); foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
} backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
}, ),
child: Text(context.localized.logout), onPressed: () async {
), await ref.read(authProvider.notifier).logOutUser();
], if (context.mounted) {
), context.router.replaceAll([const LoginRoute()]);
); }
}, },
child: Icon( child: Text(context.localized.logout),
IconsaxPlusLinear.logout, ),
color: Theme.of(context).colorScheme.onErrorContainer, ],
),
);
},
child: Icon(
IconsaxPlusLinear.logout,
color: Theme.of(context).colorScheme.onErrorContainer,
),
), ),
), ],
], ),
), ),
), ),
), ),

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
List<Widget> settingsListGroup(BuildContext context, Widget label, List<Widget> children) {
final radius = BorderRadius.circular(24);
final radiusSmall = const Radius.circular(6);
final color = Theme.of(context).colorScheme.surfaceContainerLow.harmonizeWith(Colors.red);
return [
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
color: color,
shape: RoundedRectangleBorder(
borderRadius: radius.copyWith(
bottomLeft: radiusSmall,
bottomRight: radiusSmall,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: label,
),
),
...children.map(
(e) {
return Card(
elevation: 0,
color: color,
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
shape: RoundedRectangleBorder(
borderRadius: radius.copyWith(
topLeft: radiusSmall,
topRight: radiusSmall,
bottomLeft: e != children.last ? radiusSmall : null,
bottomRight: e != children.last ? radiusSmall : null,
)),
child: e,
);
},
)
];
}

View file

@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
Future<void> showDialogAdaptive( Future<void> showDialogAdaptive(
{required BuildContext context, required Widget Function(BuildContext context) builder}) { {required BuildContext context, required Widget Function(BuildContext context) builder}) {

View file

@ -1,12 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class AnimatedFadeSize extends ConsumerWidget { class AnimatedFadeSize extends ConsumerWidget {
final Duration duration; final Duration duration;
final Widget child; final Widget child;
final Alignment alignment;
const AnimatedFadeSize({ const AnimatedFadeSize({
this.duration = const Duration(milliseconds: 125), this.duration = const Duration(milliseconds: 125),
required this.child, required this.child,
this.alignment = Alignment.center,
super.key, super.key,
}); });
@ -14,6 +17,7 @@ class AnimatedFadeSize extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AnimatedSize( return AnimatedSize(
duration: duration, duration: duration,
alignment: alignment,
curve: Curves.easeInOutCubic, curve: Curves.easeInOutCubic,
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: duration, duration: duration,

View file

@ -3,8 +3,7 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/map_bool_helper.dart';
@ -100,33 +99,37 @@ class CategoryChip<T> extends StatelessWidget {
label: Text(context.localized.clear), label: Text(context.localized.clear),
) )
].addInBetween(const SizedBox(width: 6)); ].addInBetween(const SizedBox(width: 6));
Widget header() => Row( Widget header(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Material( Material(
color: Colors.transparent, color: Colors.transparent,
textStyle: Theme.of(context).textTheme.titleLarge, textStyle: Theme.of(context).textTheme.titleLarge,
child: dialogueTitle ?? label, child: dialogueTitle ?? label,
), ),
const Spacer(), Row(
FilledButton.tonal( children: [
onPressed: () { FilledButton.tonal(
Navigator.of(context).pop(); onPressed: () {
newEntry = null; Navigator.of(context).pop();
onCancel?.call(); newEntry = null;
}, onCancel?.call();
child: Text(context.localized.cancel), },
child: Text(context.localized.cancel),
),
if (onClear != null)
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6)),
), ),
if (onClear != null) ],
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6)),
); );
if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) { if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) {
@ -156,7 +159,7 @@ class CategoryChip<T> extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: header(), child: header(context),
), ),
const Divider(), const Divider(),
CategoryChipEditor( CategoryChipEditor(

Some files were not shown because too many files have changed in this diff Show more