feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
13
README.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 732 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 731 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
assets/marketing/screenshots/Mobile/Library_Search.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 895 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 996 KiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 930 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 3.3 MiB |
BIN
assets/marketing/screenshots/Tablet/Details_2.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/marketing/screenshots/Tablet/Favourites.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/marketing/screenshots/Tablet/Library.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
assets/marketing/screenshots/Tablet/Library_Search.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/marketing/screenshots/Tablet/Player.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/marketing/screenshots/Tablet/Resume_Tab.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 1.1 MiB |
|
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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._();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
219
lib/providers/library_screen_provider.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
301
lib/providers/library_screen_provider.freezed.dart
Normal 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;
|
||||||
|
}
|
||||||
26
lib/providers/library_screen_provider.g.dart
Normal 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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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() ?? ""))),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(', ')),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
]);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
],
|
]),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
],
|
]),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
43
lib/screens/settings/widgets/settings_list_group.dart
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||