feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
11
README.md
|
|
@ -53,11 +53,14 @@
|
|||
<details close>
|
||||
<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/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/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_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/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">
|
||||
</details>
|
||||
|
||||
|
|
@ -65,8 +68,14 @@
|
|||
<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/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/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>
|
||||
|
||||
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": {},
|
||||
"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:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/models/syncing/i_synced_item.dart';
|
||||
import 'package:fladder/providers/crash_log_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
|
|
@ -31,7 +30,7 @@ import 'package:fladder/routes/auto_router.dart';
|
|||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/application_info.dart';
|
||||
import 'package:fladder/util/fladder_config.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -108,13 +107,7 @@ void main() async {
|
|||
))
|
||||
],
|
||||
child: AdaptiveLayoutBuilder(
|
||||
fallBack: ViewSize.tablet,
|
||||
layoutPoints: [
|
||||
LayoutPoints(start: 0, end: 599, type: ViewSize.phone),
|
||||
LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet),
|
||||
LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop),
|
||||
],
|
||||
child: const Main(),
|
||||
child: (context) => const Main(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -304,6 +297,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
|
|||
colorScheme: darkTheme.colorScheme.copyWith(
|
||||
surface: amoledOverwrite,
|
||||
surfaceContainerHighest: amoledOverwrite,
|
||||
surfaceContainerLow: amoledOverwrite,
|
||||
),
|
||||
),
|
||||
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/models/credentials_model.dart';
|
||||
import 'package:fladder/models/library_filters_model.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
part 'account_model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType {
|
|||
|
||||
Set<FladderItemType> get itemKinds {
|
||||
switch (this) {
|
||||
case CollectionType.music:
|
||||
return {FladderItemType.musicAlbum};
|
||||
case CollectionType.movies:
|
||||
return {FladderItemType.movie};
|
||||
case CollectionType.tvshows:
|
||||
|
|
@ -30,6 +32,8 @@ extension CollectionTypeExtension on CollectionType {
|
|||
|
||||
IconData getIconType(bool outlined) {
|
||||
switch (this) {
|
||||
case CollectionType.music:
|
||||
return outlined ? IconsaxPlusLinear.music_square : IconsaxPlusBold.music_square;
|
||||
case CollectionType.movies:
|
||||
return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal;
|
||||
case CollectionType.tvshows:
|
||||
|
|
@ -48,4 +52,16 @@ extension CollectionTypeExtension on CollectionType {
|
|||
return IconsaxPlusLinear.information;
|
||||
}
|
||||
}
|
||||
|
||||
double? get aspectRatio => switch (this) {
|
||||
CollectionType.music ||
|
||||
CollectionType.homevideos ||
|
||||
CollectionType.boxsets ||
|
||||
CollectionType.photos ||
|
||||
CollectionType.livetv ||
|
||||
CollectionType.playlists =>
|
||||
0.8,
|
||||
CollectionType.folders => 1.3,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
|
||||
|
|
@ -304,6 +304,15 @@ enum FladderItemType {
|
|||
|
||||
const FladderItemType({required this.icon, required this.selectedicon});
|
||||
|
||||
double get aspectRatio => switch (this) {
|
||||
FladderItemType.video => 0.8,
|
||||
FladderItemType.photo => 0.8,
|
||||
FladderItemType.photoAlbum => 0.8,
|
||||
FladderItemType.musicAlbum => 0.8,
|
||||
FladderItemType.baseType => 0.8,
|
||||
_ => 0.55,
|
||||
};
|
||||
|
||||
static Set<FladderItemType> get playable => {
|
||||
FladderItemType.series,
|
||||
FladderItemType.episode,
|
||||
|
|
@ -317,8 +326,7 @@ enum FladderItemType {
|
|||
FladderItemType.video,
|
||||
};
|
||||
|
||||
String label(BuildContext context) {
|
||||
return switch (this) {
|
||||
String label(BuildContext context) => switch (this) {
|
||||
FladderItemType.baseType => context.localized.mediaTypeBase,
|
||||
FladderItemType.audio => context.localized.audio,
|
||||
FladderItemType.collectionFolder => context.localized.collectionFolder,
|
||||
|
|
@ -337,7 +345,6 @@ enum FladderItemType {
|
|||
FladderItemType.playlist => context.localized.mediaTypePlaylist,
|
||||
FladderItemType.book => context.localized.mediaTypeBook,
|
||||
};
|
||||
}
|
||||
|
||||
BaseItemKind get dtoKind => switch (this) {
|
||||
FladderItemType.baseType => BaseItemKind.userrootfolder,
|
||||
|
|
|
|||
|
|
@ -199,20 +199,20 @@ extension EpisodeListExtensions on List<EpisodeModel> {
|
|||
}
|
||||
|
||||
EpisodeModel? get nextUp {
|
||||
final episodes = whereNot((element) => element.season <= 0).toList();
|
||||
final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList();
|
||||
if (episodes.isEmpty) return null;
|
||||
|
||||
final lastProgress = episodes
|
||||
.lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available);
|
||||
final lastPlayed =
|
||||
episodes.lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available);
|
||||
final lastWatchedIndex = [
|
||||
episodes.lastIndexWhere((e) => e.userData.progress != 0),
|
||||
episodes.lastIndexWhere((e) => e.userData.played),
|
||||
].reduce((a, b) => a > b ? a : b);
|
||||
|
||||
if (lastProgress == -1 && lastPlayed == -1) {
|
||||
return episodes.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
|
||||
} else {
|
||||
return episodes
|
||||
.getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, episodes.length)
|
||||
.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
|
||||
if (lastWatchedIndex >= 0 && lastWatchedIndex + 1 < episodes.length) {
|
||||
final next = episodes.sublist(lastWatchedIndex + 1).firstWhereOrNull((e) => e.status == EpisodeStatus.available);
|
||||
if (next != null) return next;
|
||||
}
|
||||
|
||||
return episodes.firstOrNull;
|
||||
}
|
||||
|
||||
bool get allPlayed {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
|
||||
|
|
@ -9,8 +10,7 @@ import 'package:fladder/models/items/images_models.dart';
|
|||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/overview_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
|
||||
|
||||
part 'series_model.mapper.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
enum SortingOptions {
|
||||
name([ItemSortBy.name]),
|
||||
|
|
|
|||
|
|
@ -201,14 +201,12 @@ class PlaybackModelHelper {
|
|||
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
||||
oldModel?.mediaStreams?.currentAudioStream,
|
||||
streamModel?.audioStreams,
|
||||
streamModel?.defaultAudioStreamIndex
|
||||
);
|
||||
streamModel?.defaultAudioStreamIndex);
|
||||
final subStreamIndex = selectSubStream(
|
||||
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
|
||||
oldModel?.mediaStreams?.currentSubStream,
|
||||
streamModel?.subStreams,
|
||||
streamModel?.defaultSubStreamIndex
|
||||
);
|
||||
streamModel?.defaultSubStreamIndex);
|
||||
|
||||
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
||||
itemId: firstItemToPlay.id,
|
||||
|
|
@ -345,14 +343,12 @@ class PlaybackModelHelper {
|
|||
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
||||
playbackModel.mediaStreams?.currentAudioStream,
|
||||
playbackModel.audioStreams,
|
||||
playbackModel.mediaStreams?.defaultAudioStreamIndex
|
||||
);
|
||||
playbackModel.mediaStreams?.defaultAudioStreamIndex);
|
||||
final subIndex = selectSubStream(
|
||||
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
|
||||
playbackModel.mediaStreams?.currentSubStream,
|
||||
playbackModel.subStreams,
|
||||
playbackModel.mediaStreams?.defaultSubStreamIndex
|
||||
);
|
||||
playbackModel.mediaStreams?.defaultSubStreamIndex);
|
||||
|
||||
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
||||
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/util/localization_helper.dart';
|
||||
|
||||
sealed class NameSwitch {
|
||||
const NameSwitch();
|
||||
|
||||
String label(BuildContext context);
|
||||
}
|
||||
|
||||
class NextUp extends NameSwitch {
|
||||
const NextUp();
|
||||
|
||||
@override
|
||||
String label(BuildContext context) => context.localized.nextUp;
|
||||
}
|
||||
|
||||
class Latest extends NameSwitch {
|
||||
const Latest();
|
||||
|
||||
@override
|
||||
String label(BuildContext context) => context.localized.latest;
|
||||
}
|
||||
|
||||
class Other extends NameSwitch {
|
||||
final String customLabel;
|
||||
|
||||
const Other(this.customLabel);
|
||||
|
||||
@override
|
||||
String label(BuildContext context) => customLabel;
|
||||
}
|
||||
|
||||
extension RecommendationTypeExtenstion on RecommendationType {
|
||||
String label(BuildContext context) => switch (this) {
|
||||
RecommendationType.similartorecentlyplayed => context.localized.similarToRecentlyPlayed,
|
||||
RecommendationType.similartolikeditem => context.localized.similarToLikedItem,
|
||||
RecommendationType.hasdirectorfromrecentlyplayed => context.localized.hasDirectorFromRecentlyPlayed,
|
||||
RecommendationType.hasactorfromrecentlyplayed => context.localized.hasActorFromRecentlyPlayed,
|
||||
RecommendationType.haslikeddirector => context.localized.hasLikedDirector,
|
||||
RecommendationType.haslikedactor => context.localized.hasLikedActor,
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
class RecommendedModel {
|
||||
final String name;
|
||||
final NameSwitch name;
|
||||
final List<ItemBaseModel> posters;
|
||||
final String type;
|
||||
final RecommendationType? type;
|
||||
RecommendedModel({
|
||||
required this.name,
|
||||
required this.posters,
|
||||
required this.type,
|
||||
this.type,
|
||||
});
|
||||
|
||||
RecommendedModel copyWith({
|
||||
String? name,
|
||||
NameSwitch? name,
|
||||
List<ItemBaseModel>? posters,
|
||||
String? type,
|
||||
RecommendationType? type,
|
||||
}) {
|
||||
return RecommendedModel(
|
||||
name: name ?? this.name,
|
||||
|
|
@ -22,4 +68,12 @@ class RecommendedModel {
|
|||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
|
||||
factory RecommendedModel.fromBaseDto(RecommendationDto e, Ref ref) {
|
||||
return RecommendedModel(
|
||||
name: Other(e.baselineItemName ?? ""),
|
||||
posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
|
||||
type: e.recommendationType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
|
|||
@Default(true) bool requireWifi,
|
||||
@Default(false) bool showAllCollectionTypes,
|
||||
@Default(2) int maxConcurrentDownloads,
|
||||
@Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant,
|
||||
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
||||
int? libraryPageSize,
|
||||
}) = _ClientSettingsModel;
|
||||
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
this.requireWifi = true,
|
||||
this.showAllCollectionTypes = false,
|
||||
this.maxConcurrentDownloads = 2,
|
||||
this.schemeVariant = DynamicSchemeVariant.tonalSpot,
|
||||
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
||||
this.libraryPageSize})
|
||||
: super._();
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
|||
(json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2,
|
||||
schemeVariant: $enumDecodeNullable(
|
||||
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
|
||||
DynamicSchemeVariant.tonalSpot,
|
||||
DynamicSchemeVariant.rainbow,
|
||||
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
part 'home_settings_model.freezed.dart';
|
||||
|
|
@ -36,42 +37,6 @@ T selectAvailableOrSmaller<T>(T value, Set<T> availableOptions, List<T> allOptio
|
|||
return availableOptions.first;
|
||||
}
|
||||
|
||||
enum ViewSize {
|
||||
phone,
|
||||
tablet,
|
||||
desktop;
|
||||
|
||||
const ViewSize();
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
ViewSize.phone => context.localized.phone,
|
||||
ViewSize.tablet => context.localized.tablet,
|
||||
ViewSize.desktop => context.localized.desktop,
|
||||
};
|
||||
|
||||
bool operator >(ViewSize other) => index > other.index;
|
||||
bool operator >=(ViewSize other) => index >= other.index;
|
||||
bool operator <(ViewSize other) => index < other.index;
|
||||
bool operator <=(ViewSize other) => index <= other.index;
|
||||
}
|
||||
|
||||
enum LayoutMode {
|
||||
single,
|
||||
dual;
|
||||
|
||||
const LayoutMode();
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
LayoutMode.single => context.localized.layoutModeSingle,
|
||||
LayoutMode.dual => context.localized.layoutModeDual,
|
||||
};
|
||||
|
||||
bool operator >(ViewSize other) => index > other.index;
|
||||
bool operator >=(ViewSize other) => index >= other.index;
|
||||
bool operator <(ViewSize other) => index < other.index;
|
||||
bool operator <=(ViewSize other) => index <= other.index;
|
||||
}
|
||||
|
||||
enum HomeBanner {
|
||||
hide,
|
||||
carousel,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
class ViewModel {
|
||||
final String name;
|
||||
|
|
@ -16,7 +24,9 @@ class ViewModel {
|
|||
final CollectionType collectionType;
|
||||
final dto.PlayAccess playAccess;
|
||||
final List<ItemBaseModel> recentlyAdded;
|
||||
final ImagesData? imageData;
|
||||
final int childCount;
|
||||
final String? path;
|
||||
ViewModel({
|
||||
required this.name,
|
||||
required this.id,
|
||||
|
|
@ -28,7 +38,9 @@ class ViewModel {
|
|||
required this.collectionType,
|
||||
required this.playAccess,
|
||||
required this.recentlyAdded,
|
||||
required this.imageData,
|
||||
required this.childCount,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
ViewModel copyWith({
|
||||
|
|
@ -42,7 +54,9 @@ class ViewModel {
|
|||
CollectionType? collectionType,
|
||||
dto.PlayAccess? playAccess,
|
||||
List<ItemBaseModel>? recentlyAdded,
|
||||
ImagesData? imageData,
|
||||
int? childCount,
|
||||
String? path,
|
||||
}) {
|
||||
return ViewModel(
|
||||
name: name ?? this.name,
|
||||
|
|
@ -55,7 +69,9 @@ class ViewModel {
|
|||
collectionType: collectionType ?? this.collectionType,
|
||||
playAccess: playAccess ?? this.playAccess,
|
||||
recentlyAdded: recentlyAdded ?? this.recentlyAdded,
|
||||
imageData: imageData ?? this.imageData,
|
||||
childCount: childCount ?? this.childCount,
|
||||
path: path ?? this.path,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -69,11 +85,13 @@ class ViewModel {
|
|||
canDownload: item.canDownload ?? false,
|
||||
parentId: item.parentId ?? "",
|
||||
recentlyAdded: [],
|
||||
imageData: ImagesData.fromBaseItem(item, ref),
|
||||
collectionType: CollectionType.values
|
||||
.firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ??
|
||||
CollectionType.folders,
|
||||
playAccess: item.playAccess ?? PlayAccess.none,
|
||||
childCount: item.childCount ?? 0,
|
||||
path: "",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +106,27 @@ class ViewModel {
|
|||
return id.hashCode ^ serverId.hashCode;
|
||||
}
|
||||
|
||||
NavigationButton toNavigationButton(
|
||||
bool selected,
|
||||
bool horizontal,
|
||||
bool expanded,
|
||||
FutureOr Function() action, {
|
||||
FutureOr Function()? onLongPress,
|
||||
List<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
|
||||
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)';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/models/home_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -6,7 +8,6 @@ import 'package:fladder/providers/service_provider.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final dashboardProvider = StateNotifierProvider<DashboardNotifier, HomeModel>((ref) {
|
||||
return DashboardNotifier(ref);
|
||||
|
|
@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
mediaTypes: [MediaType.video],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
mediaTypes: [MediaType.audio],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
mediaTypes: [MediaType.book],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
|
||||
final nextResponse = await api.showsNextUpGet(
|
||||
limit: 16,
|
||||
nextUpDateCutoff: DateTime.now()
|
||||
.subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
|
||||
nextUpDateCutoff: DateTime.now().subtract(
|
||||
ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/favourites_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -6,7 +8,6 @@ import 'package:fladder/models/view_model.dart';
|
|||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final favouritesProvider = StateNotifierProvider<FavouritesNotifier, FavouritesModel>((ref) {
|
||||
return FavouritesNotifier(ref);
|
||||
|
|
@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
|
|||
isFavorite: true,
|
||||
limit: 10,
|
||||
sortOrder: [SortOrder.ascending],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
|
||||
);
|
||||
final response2 = await api.itemsGet(
|
||||
parentId: viewModel?.id,
|
||||
|
|
@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
|
|||
limit: 10,
|
||||
includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder],
|
||||
sortOrder: [SortOrder.ascending],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
|
||||
);
|
||||
return [...?response.body?.items, ...?response2.body?.items];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
.toList();
|
||||
var tempState = state.copyWith();
|
||||
final genres = mappedList
|
||||
.expand((element) => element?.genres ?? <NameGuidPair>[])
|
||||
.nonNulls
|
||||
.sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
|
||||
final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel))))
|
||||
.expand((element) => element)
|
||||
.toSet()
|
||||
.toList();
|
||||
final tags = mappedList
|
||||
.expand((element) => element?.tags ?? <String>[])
|
||||
.sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||
tempState = tempState.copyWith(
|
||||
types: state.types.setAll(false).setKeys(enabledCollections, true),
|
||||
genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres),
|
||||
genres: {for (var element in genres) element.name: false}.replaceMap(tempState.genres),
|
||||
studios: {for (var element in studios) element: false}.replaceMap(tempState.studios),
|
||||
tags: {for (var element in tags) element: false}.replaceMap(tempState.tags),
|
||||
);
|
||||
|
|
@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
|
|||
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(
|
||||
{ViewModel? viewModel,
|
||||
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/providers/shared_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
final homeSettingsProvider = StateNotifierProvider<HomeSettingsNotifier, HomeSettingsModel>((ref) {
|
||||
return HomeSettingsNotifier(ref);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$backgroundDownloaderHash() =>
|
||||
r'df72b6338a8e80178935985ba17c43bf720f4522';
|
||||
r'dc27f708fc2f1695d37afcb99f8814bc024037af';
|
||||
|
||||
/// See also [BackgroundDownloader].
|
||||
@ProviderFor(BackgroundDownloader)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
|
|||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
|
||||
String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900';
|
||||
String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a';
|
||||
|
||||
/// See also [User].
|
||||
@ProviderFor(User)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
|
|||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<void> fetchViews() async {
|
||||
if (state.loading) return;
|
||||
Future<ViewsModel?> fetchViews() async {
|
||||
if (state.loading) return null;
|
||||
final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes));
|
||||
final response = await api.usersUserIdViewsGet(
|
||||
includeExternalContent: showAllCollections,
|
||||
|
|
@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
|
|||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
);
|
||||
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
|
||||
|
|
@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
|
|||
.where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true))
|
||||
.toList(),
|
||||
loading: false);
|
||||
return state;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ final List<AutoRoute> homeRoutes = [
|
|||
_dashboardRoute,
|
||||
_favouritesRoute,
|
||||
_syncedRoute,
|
||||
_librariesRoute,
|
||||
];
|
||||
|
||||
final List<AutoRoute> _defaultRoutes = [
|
||||
|
|
@ -79,6 +80,13 @@ final AutoRoute _syncedRoute = CustomRoute(
|
|||
path: 'synced',
|
||||
);
|
||||
|
||||
final AutoRoute _librariesRoute = CustomRoute(
|
||||
page: LibraryRoute.page,
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
maintainState: false,
|
||||
path: 'libraries',
|
||||
);
|
||||
|
||||
final List<AutoRoute> _settingsChildren = [
|
||||
CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'),
|
||||
CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'),
|
||||
|
|
|
|||
|
|
@ -8,35 +8,36 @@
|
|||
// coverage:ignore-file
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:auto_route/auto_route.dart' as _i16;
|
||||
import 'package:fladder/models/item_base_model.dart' as _i17;
|
||||
import 'package:fladder/models/items/photos_model.dart' as _i20;
|
||||
import 'package:auto_route/auto_route.dart' as _i17;
|
||||
import 'package:fladder/models/item_base_model.dart' as _i18;
|
||||
import 'package:fladder/models/items/photos_model.dart' as _i21;
|
||||
import 'package:fladder/models/library_search/library_search_options.dart'
|
||||
as _i19;
|
||||
as _i20;
|
||||
import 'package:fladder/routes/nested_details_screen.dart' as _i4;
|
||||
import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3;
|
||||
import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5;
|
||||
import 'package:fladder/screens/home_screen.dart' as _i6;
|
||||
import 'package:fladder/screens/library/library_screen.dart' as _i7;
|
||||
import 'package:fladder/screens/library_search/library_search_screen.dart'
|
||||
as _i7;
|
||||
import 'package:fladder/screens/login/lock_screen.dart' as _i8;
|
||||
import 'package:fladder/screens/login/login_screen.dart' as _i9;
|
||||
as _i8;
|
||||
import 'package:fladder/screens/login/lock_screen.dart' as _i9;
|
||||
import 'package:fladder/screens/login/login_screen.dart' as _i10;
|
||||
import 'package:fladder/screens/settings/about_settings_page.dart' as _i1;
|
||||
import 'package:fladder/screens/settings/client_settings_page.dart' as _i2;
|
||||
import 'package:fladder/screens/settings/player_settings_page.dart' as _i10;
|
||||
import 'package:fladder/screens/settings/security_settings_page.dart' as _i11;
|
||||
import 'package:fladder/screens/settings/settings_screen.dart' as _i12;
|
||||
import 'package:fladder/screens/settings/player_settings_page.dart' as _i11;
|
||||
import 'package:fladder/screens/settings/security_settings_page.dart' as _i12;
|
||||
import 'package:fladder/screens/settings/settings_screen.dart' as _i13;
|
||||
import 'package:fladder/screens/settings/settings_selection_screen.dart'
|
||||
as _i13;
|
||||
import 'package:fladder/screens/splash_screen.dart' as _i14;
|
||||
import 'package:fladder/screens/syncing/synced_screen.dart' as _i15;
|
||||
import 'package:flutter/foundation.dart' as _i18;
|
||||
import 'package:flutter/material.dart' as _i21;
|
||||
as _i14;
|
||||
import 'package:fladder/screens/splash_screen.dart' as _i15;
|
||||
import 'package:fladder/screens/syncing/synced_screen.dart' as _i16;
|
||||
import 'package:flutter/foundation.dart' as _i19;
|
||||
import 'package:flutter/material.dart' as _i22;
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AboutSettingsPage]
|
||||
class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
|
||||
const AboutSettingsRoute({List<_i16.PageRouteInfo>? children})
|
||||
class AboutSettingsRoute extends _i17.PageRouteInfo<void> {
|
||||
const AboutSettingsRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
AboutSettingsRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -44,7 +45,7 @@ class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'AboutSettingsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i1.AboutSettingsPage();
|
||||
|
|
@ -54,8 +55,8 @@ class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i2.ClientSettingsPage]
|
||||
class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
|
||||
const ClientSettingsRoute({List<_i16.PageRouteInfo>? children})
|
||||
class ClientSettingsRoute extends _i17.PageRouteInfo<void> {
|
||||
const ClientSettingsRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
ClientSettingsRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -63,7 +64,7 @@ class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'ClientSettingsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i2.ClientSettingsPage();
|
||||
|
|
@ -73,8 +74,8 @@ class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i3.DashboardScreen]
|
||||
class DashboardRoute extends _i16.PageRouteInfo<void> {
|
||||
const DashboardRoute({List<_i16.PageRouteInfo>? children})
|
||||
class DashboardRoute extends _i17.PageRouteInfo<void> {
|
||||
const DashboardRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
DashboardRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -82,7 +83,7 @@ class DashboardRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'DashboardRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i3.DashboardScreen();
|
||||
|
|
@ -92,12 +93,12 @@ class DashboardRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i4.DetailsScreen]
|
||||
class DetailsRoute extends _i16.PageRouteInfo<DetailsRouteArgs> {
|
||||
class DetailsRoute extends _i17.PageRouteInfo<DetailsRouteArgs> {
|
||||
DetailsRoute({
|
||||
String id = '',
|
||||
_i17.ItemBaseModel? item,
|
||||
_i18.Key? key,
|
||||
List<_i16.PageRouteInfo>? children,
|
||||
_i18.ItemBaseModel? item,
|
||||
_i19.Key? key,
|
||||
List<_i17.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
DetailsRoute.name,
|
||||
args: DetailsRouteArgs(
|
||||
|
|
@ -111,7 +112,7 @@ class DetailsRoute extends _i16.PageRouteInfo<DetailsRouteArgs> {
|
|||
|
||||
static const String name = 'DetailsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final queryParams = data.queryParams;
|
||||
|
|
@ -139,9 +140,9 @@ class DetailsRouteArgs {
|
|||
|
||||
final String id;
|
||||
|
||||
final _i17.ItemBaseModel? item;
|
||||
final _i18.ItemBaseModel? item;
|
||||
|
||||
final _i18.Key? key;
|
||||
final _i19.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
@ -151,8 +152,8 @@ class DetailsRouteArgs {
|
|||
|
||||
/// generated route for
|
||||
/// [_i5.FavouritesScreen]
|
||||
class FavouritesRoute extends _i16.PageRouteInfo<void> {
|
||||
const FavouritesRoute({List<_i16.PageRouteInfo>? children})
|
||||
class FavouritesRoute extends _i17.PageRouteInfo<void> {
|
||||
const FavouritesRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
FavouritesRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -160,7 +161,7 @@ class FavouritesRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'FavouritesRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i5.FavouritesScreen();
|
||||
|
|
@ -170,8 +171,8 @@ class FavouritesRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i6.HomeScreen]
|
||||
class HomeRoute extends _i16.PageRouteInfo<void> {
|
||||
const HomeRoute({List<_i16.PageRouteInfo>? children})
|
||||
class HomeRoute extends _i17.PageRouteInfo<void> {
|
||||
const HomeRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
HomeRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -179,7 +180,7 @@ class HomeRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'HomeRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.HomeScreen();
|
||||
|
|
@ -188,17 +189,36 @@ class HomeRoute extends _i16.PageRouteInfo<void> {
|
|||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i7.LibrarySearchScreen]
|
||||
class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
|
||||
/// [_i7.LibraryScreen]
|
||||
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({
|
||||
String? viewModelId,
|
||||
List<String>? folderId,
|
||||
bool? favourites,
|
||||
_i19.SortingOrder? sortOrder,
|
||||
_i19.SortingOptions? sortingOptions,
|
||||
_i20.PhotoModel? photoToView,
|
||||
_i18.Key? key,
|
||||
List<_i16.PageRouteInfo>? children,
|
||||
_i20.SortingOrder? sortOrder,
|
||||
_i20.SortingOptions? sortingOptions,
|
||||
_i21.PhotoModel? photoToView,
|
||||
_i19.Key? key,
|
||||
List<_i17.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
LibrarySearchRoute.name,
|
||||
args: LibrarySearchRouteArgs(
|
||||
|
|
@ -222,7 +242,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
|
|||
|
||||
static const String name = 'LibrarySearchRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final queryParams = data.queryParams;
|
||||
|
|
@ -234,7 +254,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
|
|||
sortOrder: queryParams.get('sortOrder'),
|
||||
sortingOptions: queryParams.get('sortOptions'),
|
||||
));
|
||||
return _i7.LibrarySearchScreen(
|
||||
return _i8.LibrarySearchScreen(
|
||||
viewModelId: args.viewModelId,
|
||||
folderId: args.folderId,
|
||||
favourites: args.favourites,
|
||||
|
|
@ -264,13 +284,13 @@ class LibrarySearchRouteArgs {
|
|||
|
||||
final bool? favourites;
|
||||
|
||||
final _i19.SortingOrder? sortOrder;
|
||||
final _i20.SortingOrder? sortOrder;
|
||||
|
||||
final _i19.SortingOptions? sortingOptions;
|
||||
final _i20.SortingOptions? sortingOptions;
|
||||
|
||||
final _i20.PhotoModel? photoToView;
|
||||
final _i21.PhotoModel? photoToView;
|
||||
|
||||
final _i18.Key? key;
|
||||
final _i19.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
@ -279,9 +299,9 @@ class LibrarySearchRouteArgs {
|
|||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i8.LockScreen]
|
||||
class LockRoute extends _i16.PageRouteInfo<void> {
|
||||
const LockRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i9.LockScreen]
|
||||
class LockRoute extends _i17.PageRouteInfo<void> {
|
||||
const LockRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
LockRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -289,18 +309,18 @@ class LockRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'LockRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i8.LockScreen();
|
||||
return const _i9.LockScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i9.LoginScreen]
|
||||
class LoginRoute extends _i16.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i10.LoginScreen]
|
||||
class LoginRoute extends _i17.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
LoginRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -308,18 +328,18 @@ class LoginRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'LoginRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i9.LoginScreen();
|
||||
return const _i10.LoginScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.PlayerSettingsPage]
|
||||
class PlayerSettingsRoute extends _i16.PageRouteInfo<void> {
|
||||
const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i11.PlayerSettingsPage]
|
||||
class PlayerSettingsRoute extends _i17.PageRouteInfo<void> {
|
||||
const PlayerSettingsRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
PlayerSettingsRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -327,18 +347,18 @@ class PlayerSettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'PlayerSettingsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i10.PlayerSettingsPage();
|
||||
return const _i11.PlayerSettingsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i11.SecuritySettingsPage]
|
||||
class SecuritySettingsRoute extends _i16.PageRouteInfo<void> {
|
||||
const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i12.SecuritySettingsPage]
|
||||
class SecuritySettingsRoute extends _i17.PageRouteInfo<void> {
|
||||
const SecuritySettingsRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
SecuritySettingsRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -346,18 +366,18 @@ class SecuritySettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'SecuritySettingsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i11.SecuritySettingsPage();
|
||||
return const _i12.SecuritySettingsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i12.SettingsScreen]
|
||||
class SettingsRoute extends _i16.PageRouteInfo<void> {
|
||||
const SettingsRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i13.SettingsScreen]
|
||||
class SettingsRoute extends _i17.PageRouteInfo<void> {
|
||||
const SettingsRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -365,18 +385,18 @@ class SettingsRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'SettingsRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i12.SettingsScreen();
|
||||
return const _i13.SettingsScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i13.SettingsSelectionScreen]
|
||||
class SettingsSelectionRoute extends _i16.PageRouteInfo<void> {
|
||||
const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children})
|
||||
/// [_i14.SettingsSelectionScreen]
|
||||
class SettingsSelectionRoute extends _i17.PageRouteInfo<void> {
|
||||
const SettingsSelectionRoute({List<_i17.PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsSelectionRoute.name,
|
||||
initialChildren: children,
|
||||
|
|
@ -384,21 +404,21 @@ class SettingsSelectionRoute extends _i16.PageRouteInfo<void> {
|
|||
|
||||
static const String name = 'SettingsSelectionRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i13.SettingsSelectionScreen();
|
||||
return const _i14.SettingsSelectionScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i14.SplashScreen]
|
||||
class SplashRoute extends _i16.PageRouteInfo<SplashRouteArgs> {
|
||||
/// [_i15.SplashScreen]
|
||||
class SplashRoute extends _i17.PageRouteInfo<SplashRouteArgs> {
|
||||
SplashRoute({
|
||||
dynamic Function(bool)? loggedIn,
|
||||
_i21.Key? key,
|
||||
List<_i16.PageRouteInfo>? children,
|
||||
_i22.Key? key,
|
||||
List<_i17.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SplashRoute.name,
|
||||
args: SplashRouteArgs(
|
||||
|
|
@ -410,12 +430,12 @@ class SplashRoute extends _i16.PageRouteInfo<SplashRouteArgs> {
|
|||
|
||||
static const String name = 'SplashRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args =
|
||||
data.argsAs<SplashRouteArgs>(orElse: () => const SplashRouteArgs());
|
||||
return _i14.SplashScreen(
|
||||
return _i15.SplashScreen(
|
||||
loggedIn: args.loggedIn,
|
||||
key: args.key,
|
||||
);
|
||||
|
|
@ -431,7 +451,7 @@ class SplashRouteArgs {
|
|||
|
||||
final dynamic Function(bool)? loggedIn;
|
||||
|
||||
final _i21.Key? key;
|
||||
final _i22.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
@ -440,12 +460,12 @@ class SplashRouteArgs {
|
|||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i15.SyncedScreen]
|
||||
class SyncedRoute extends _i16.PageRouteInfo<SyncedRouteArgs> {
|
||||
/// [_i16.SyncedScreen]
|
||||
class SyncedRoute extends _i17.PageRouteInfo<SyncedRouteArgs> {
|
||||
SyncedRoute({
|
||||
_i21.ScrollController? navigationScrollController,
|
||||
_i21.Key? key,
|
||||
List<_i16.PageRouteInfo>? children,
|
||||
_i22.ScrollController? navigationScrollController,
|
||||
_i22.Key? key,
|
||||
List<_i17.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SyncedRoute.name,
|
||||
args: SyncedRouteArgs(
|
||||
|
|
@ -457,12 +477,12 @@ class SyncedRoute extends _i16.PageRouteInfo<SyncedRouteArgs> {
|
|||
|
||||
static const String name = 'SyncedRoute';
|
||||
|
||||
static _i16.PageInfo page = _i16.PageInfo(
|
||||
static _i17.PageInfo page = _i17.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args =
|
||||
data.argsAs<SyncedRouteArgs>(orElse: () => const SyncedRouteArgs());
|
||||
return _i15.SyncedScreen(
|
||||
return _i16.SyncedScreen(
|
||||
navigationScrollController: args.navigationScrollController,
|
||||
key: args.key,
|
||||
);
|
||||
|
|
@ -476,9 +496,9 @@ class SyncedRouteArgs {
|
|||
this.key,
|
||||
});
|
||||
|
||||
final _i21.ScrollController? navigationScrollController;
|
||||
final _i22.ScrollController? navigationScrollController;
|
||||
|
||||
final _i21.Key? key;
|
||||
final _i22.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:fladder/models/book_model.dart';
|
||||
import 'package:fladder/providers/book_viewer_provider.dart';
|
||||
import 'package:fladder/providers/items/book_details_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart';
|
|||
import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
|
||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/throttler.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/dashboard_provider.dart';
|
||||
|
|
@ -19,10 +20,11 @@ import 'package:fladder/screens/dashboard/home_banner_widget.dart';
|
|||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
|
@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
|
||||
final dashboardData = ref.watch(dashboardProvider);
|
||||
final views = ref.watch(viewsProvider);
|
||||
final homeSettings = ref.watch(homeSettingsProvider);
|
||||
|
|
@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
|
||||
body: PullToRefresh(
|
||||
refreshKey: _refreshIndicatorKey,
|
||||
displacement: 80 + MediaQuery.of(context).viewPadding.top,
|
||||
|
|
@ -104,9 +109,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
SliverToBoxAdapter(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0),
|
||||
child: Padding(
|
||||
padding: AdaptiveLayout.adaptivePadding(
|
||||
context,
|
||||
horizontalPadding: 0,
|
||||
),
|
||||
child: HomeBannerWidget(posters: homeCarouselItems),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
const SliverToBoxAdapter(
|
||||
|
|
@ -122,6 +133,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueWatching,
|
||||
posters: resumeVideo,
|
||||
),
|
||||
|
|
@ -130,6 +142,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueListening,
|
||||
posters: resumeAudio,
|
||||
),
|
||||
|
|
@ -138,6 +151,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueReading,
|
||||
posters: resumeBooks,
|
||||
),
|
||||
|
|
@ -146,6 +160,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
(homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.nextUp,
|
||||
posters: dashboardData.nextUp,
|
||||
),
|
||||
|
|
@ -153,6 +168,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined)
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinue,
|
||||
posters: [...allResume, ...dashboardData.nextUp],
|
||||
),
|
||||
|
|
@ -161,7 +177,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
.where((element) => element.recentlyAdded.isNotEmpty)
|
||||
.map((view) => SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardRecentlyAdded(view.name),
|
||||
collectionAspectRatio: view.collectionType.aspectRatio,
|
||||
onLabelClick: () => context.router.push(LibrarySearchRoute(
|
||||
viewModelId: view.id,
|
||||
sortingOptions: switch (view.collectionType) {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,13 @@ class HomeBannerWidget extends ConsumerWidget {
|
|||
const SizedBox(height: 24)
|
||||
],
|
||||
),
|
||||
HomeBanner.banner => MediaBanner(
|
||||
HomeBanner.banner => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: MediaBanner(
|
||||
items: posters,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
),
|
||||
_ => 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/item_shared_models.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/screens/shared/media/components/chip_button.dart';
|
||||
import 'package:fladder/screens/shared/media/components/media_header.dart';
|
||||
import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -37,8 +37,12 @@ class EmptyItem extends ConsumerWidget {
|
|||
}
|
||||
},
|
||||
),
|
||||
content: (padding) => Column(
|
||||
content: (padding) => Center(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 350),
|
||||
|
|
@ -68,6 +72,8 @@ class EmptyItem extends ConsumerWidget {
|
|||
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:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/episode_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
|
||||
|
|
@ -18,7 +17,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart';
|
|||
import 'package:fladder/screens/shared/media/expanding_overview.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/movies_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
|
||||
|
|
@ -17,7 +16,7 @@ import 'package:fladder/screens/shared/media/expanding_overview.dart';
|
|||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -11,7 +10,7 @@ import 'package:fladder/providers/user_provider.dart';
|
|||
import 'package:fladder/screens/shared/detail_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
|
|
@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState<PersonDetailScreen> {
|
|||
spacing: 32,
|
||||
children: [
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
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/items/series_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/series_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/overview_header.dart';
|
||||
|
|
@ -19,7 +18,7 @@ import 'package:fladder/screens/shared/media/external_urls.dart';
|
|||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/media/season_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/favourites_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final favourites = ref.watch(favouritesProvider);
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
|
||||
return PullToRefresh(
|
||||
onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(),
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()),
|
||||
body: PinchPosterZoom(
|
||||
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
|
||||
child: CustomScrollView(
|
||||
|
|
@ -52,27 +55,21 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
),
|
||||
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
|
||||
(e) => SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PosterGrid(
|
||||
stickyHeader: true,
|
||||
name: e.key.label(context),
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: e.key.label(context),
|
||||
posters: e.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (favourites.people.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PosterGrid(
|
||||
stickyHeader: true,
|
||||
name: "People",
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.actor(favourites.people.length),
|
||||
posters: favourites.people,
|
||||
),
|
||||
),
|
||||
),
|
||||
const DefautlSliverBottomPadding(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
|
|
@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart';
|
|||
|
||||
enum HomeTabs {
|
||||
dashboard,
|
||||
library,
|
||||
favorites,
|
||||
sync;
|
||||
sync,
|
||||
;
|
||||
|
||||
const HomeTabs();
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
HomeTabs.dashboard => IconsaxPlusLinear.home_1,
|
||||
HomeTabs.library => IconsaxPlusLinear.book,
|
||||
HomeTabs.favorites => IconsaxPlusLinear.heart,
|
||||
HomeTabs.sync => IconsaxPlusLinear.cloud,
|
||||
};
|
||||
|
||||
IconData get selectedIcon => switch (this) {
|
||||
HomeTabs.dashboard => IconsaxPlusBold.home_1,
|
||||
HomeTabs.library => IconsaxPlusBold.book,
|
||||
HomeTabs.favorites => IconsaxPlusBold.heart,
|
||||
HomeTabs.sync => IconsaxPlusBold.cloud,
|
||||
};
|
||||
|
||||
Future navigate(BuildContext context) => switch (this) {
|
||||
HomeTabs.dashboard => context.router.navigate(const DashboardRoute()),
|
||||
HomeTabs.library => context.router.navigate(const LibraryRoute()),
|
||||
HomeTabs.favorites => context.router.navigate(const FavouritesRoute()),
|
||||
HomeTabs.sync => context.router.navigate(SyncedRoute()),
|
||||
};
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
HomeTabs.dashboard => context.localized.dashboard,
|
||||
HomeTabs.library => context.localized.library(0),
|
||||
HomeTabs.favorites => context.localized.favorites,
|
||||
HomeTabs.sync => context.localized.sync,
|
||||
};
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget {
|
|||
case HomeTabs.dashboard:
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationDashboard,
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.home),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const DashboardRoute(),
|
||||
action: () => context.router.navigate(const DashboardRoute()),
|
||||
action: () => e.navigate(context),
|
||||
floatingActionButton: AdaptiveFab(
|
||||
context: context,
|
||||
title: context.localized.search,
|
||||
|
|
@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget {
|
|||
case HomeTabs.favorites:
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationFavorites,
|
||||
icon: const Icon(IconsaxPlusLinear.heart),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.heart),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const FavouritesRoute(),
|
||||
floatingActionButton: AdaptiveFab(
|
||||
context: context,
|
||||
|
|
@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget {
|
|||
onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)),
|
||||
child: const Icon(IconsaxPlusLinear.heart_search),
|
||||
),
|
||||
action: () => context.router.navigate(const FavouritesRoute()),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
case HomeTabs.sync:
|
||||
if (canDownload) {
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationSync,
|
||||
icon: const Icon(IconsaxPlusLinear.cloud),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.cloud),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: SyncedRoute(),
|
||||
action: () => context.router.navigate(SyncedRoute()),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case HomeTabs.library:
|
||||
return DestinationModel(
|
||||
label: context.localized.library(0),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const LibraryRoute(),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
}
|
||||
})
|
||||
.nonNulls
|
||||
|
|
|
|||
|
|
@ -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 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/library/components/library_tabs.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/recommended_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_screen_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/button_group.dart';
|
||||
import 'package:fladder/widgets/shared/horizontal_list.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LibraryScreen extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
const LibraryScreen({
|
||||
required this.viewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin {
|
||||
late final List<LibraryTabs> tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType);
|
||||
late final TabController tabController = TabController(length: tabs.length, vsync: this);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() {
|
||||
ref.read(libraryProvider(widget.viewModel.id).notifier).setupLibrary(widget.viewModel);
|
||||
});
|
||||
|
||||
tabController.addListener(() {
|
||||
if (tabController.previousIndex != tabController.index) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PreferredSizeWidget tabBar = TabBar(
|
||||
isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false,
|
||||
indicatorWeight: 3,
|
||||
controller: tabController,
|
||||
tabs: tabs
|
||||
.map((e) => Tab(
|
||||
text: e.name,
|
||||
icon: e.icon,
|
||||
))
|
||||
final libraryScreenState = ref.watch(libraryScreenProvider);
|
||||
final views = libraryScreenState.views;
|
||||
final recommendations = libraryScreenState.recommendations;
|
||||
final favourites = libraryScreenState.favourites;
|
||||
final selectedView = libraryScreenState.selectedViewModel;
|
||||
final viewTypes = libraryScreenState.viewType;
|
||||
final genres = libraryScreenState.genres;
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
return NestedScaffold(
|
||||
background: BackgroundImage(
|
||||
items: [
|
||||
...recommendations.expand((e) => e.posters),
|
||||
...favourites,
|
||||
],
|
||||
),
|
||||
body: PullToRefresh(
|
||||
refreshOnStart: true,
|
||||
refreshKey: refreshKey,
|
||||
onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
|
||||
child: SizedBox.expand(
|
||||
child: CustomScrollView(
|
||||
controller: AdaptiveLayout.scrollOf(context),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
const DefaultSliverTopBadding(),
|
||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
NestedSliverAppBar(
|
||||
route: LibrarySearchRoute(),
|
||||
parent: context,
|
||||
),
|
||||
if (views.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: LibraryRow(
|
||||
padding: padding,
|
||||
views: views,
|
||||
selectedView: libraryScreenState.selectedViewModel,
|
||||
onSelected: (view) {
|
||||
ref.read(libraryScreenProvider.notifier).selectLibrary(view);
|
||||
refreshKey?.currentState?.show();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (selectedView != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 16),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
padding: padding,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)),
|
||||
label: Text("${context.localized.search} ${selectedView.name}..."),
|
||||
icon: const Icon(IconsaxPlusLinear.search_normal),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: VerticalDivider(),
|
||||
),
|
||||
ExpressiveButtonGroup(
|
||||
multiSelection: true,
|
||||
options: LibraryViewType.values
|
||||
.map((element) => ButtonGroupOption(
|
||||
value: element,
|
||||
icon: Icon(element.icon),
|
||||
selected: Icon(element.iconSelected),
|
||||
child: Text(
|
||||
element.label(context),
|
||||
)))
|
||||
.toList(),
|
||||
selectedValues: viewTypes,
|
||||
onSelected: (value) {
|
||||
ref.read(libraryScreenProvider.notifier).setViewType(value);
|
||||
},
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: VerticalDivider(),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name),
|
||||
label: Text(context.localized.scanLibrary),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (viewTypes.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text(context.localized.noResults)),
|
||||
),
|
||||
if (viewTypes.contains(LibraryViewType.recommended)) ...[
|
||||
if (recommendations.isNotEmpty)
|
||||
...recommendations.where((element) => element.posters.isNotEmpty).map(
|
||||
(element) => SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
posters: element.posters,
|
||||
label: element.type != null
|
||||
? "${element.type?.label(context)} - ${element.name.label(context)}"
|
||||
: element.name.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (viewTypes.contains(LibraryViewType.favourites))
|
||||
if (favourites.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
posters: favourites,
|
||||
label: context.localized.favorites,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (viewTypes.contains(LibraryViewType.genres)) ...[
|
||||
if (genres.isNotEmpty)
|
||||
...genres.where((element) => element.posters.isNotEmpty).map(
|
||||
(element) => SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
posters: element.posters,
|
||||
label: element.type != null
|
||||
? "${element.type?.label(context)} - ${element.name.label(context)}"
|
||||
: element.name.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
const DefautlSliverBottomPadding(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: AdaptiveLayout.of(context).isDesktop
|
||||
? EdgeInsets.only(top: MediaQuery.of(context).padding.top)
|
||||
: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0),
|
||||
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(
|
||||
margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
child: Scaffold(
|
||||
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
|
||||
floatingActionButton: tabs[tabController.index].floatingActionButton,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endContained,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
|
||||
title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
|
||||
toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40,
|
||||
bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
|
||||
),
|
||||
extendBody: true,
|
||||
body: Padding(
|
||||
padding: !AdaptiveLayout.of(context).isDesktop
|
||||
? EdgeInsets.only(
|
||||
left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right)
|
||||
: EdgeInsets.zero,
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: tabs
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: e.page,
|
||||
))
|
||||
.toList(),
|
||||
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/library_search/library_search_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/playlist_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/screens/collections/add_to_collection.dart';
|
||||
import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart';
|
||||
import 'package:fladder/screens/library_search/widgets/library_play_options_.dart';
|
||||
|
|
@ -26,9 +23,9 @@ import 'package:fladder/screens/library_search/widgets/library_views.dart';
|
|||
import 'package:fladder/screens/library_search/widgets/suggestion_search_bar.dart';
|
||||
import 'package:fladder/screens/playlists/add_to_playlists.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/debouncer.dart';
|
||||
import 'package:fladder/util/fab_extended_anim.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
|
|
@ -37,8 +34,7 @@ import 'package:fladder/util/localization_helper.dart';
|
|||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_scrollbar.dart';
|
||||
import 'package:fladder/widgets/shared/hide_on_scroll.dart';
|
||||
|
|
@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null;
|
||||
final librarySearchResults = ref.watch(providerKey);
|
||||
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows);
|
||||
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||
final libraryViewType = ref.watch(libraryViewTypeProvider);
|
||||
|
||||
ref.listen(
|
||||
|
|
@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
libraryProvider.toggleSelectMode();
|
||||
}
|
||||
},
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: librarySearchResults.activePosters),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
floatingActionButtonAnimator:
|
||||
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
|
||||
floatingActionButtonLocation:
|
||||
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
|
||||
floatingActionButton: switch (playerState) {
|
||||
VideoPlayerState.minimized => const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: FloatingPlayerBar(),
|
||||
),
|
||||
_ => HideOnScroll(
|
||||
floatingActionButton: HideOnScroll(
|
||||
controller: scrollController,
|
||||
visibleBuilder: (visible) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
@ -206,7 +196,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
].addInBetween(const SizedBox(height: 10)),
|
||||
),
|
||||
),
|
||||
},
|
||||
bottomNavigationBar: HideOnScroll(
|
||||
controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
|
||||
child: IgnorePointer(
|
||||
|
|
@ -221,14 +210,12 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
),
|
||||
),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
child: PinchPosterZoom(
|
||||
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
|
||||
child: MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
scaleDifference: (difference) =>
|
||||
ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
|
||||
child: ClipRRect(
|
||||
borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
|
||||
? BorderRadius.circular(15)
|
||||
|
|
@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
onTapUp: (details) async {
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
double left = details.globalPosition.dx;
|
||||
double top = details.globalPosition.dy + 20;
|
||||
double top = details.globalPosition.dy;
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, 40, 100),
|
||||
|
|
@ -463,16 +450,10 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
padding: const EdgeInsets.all(8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: LibraryFilterChips(
|
||||
controller: scrollController,
|
||||
libraryProvider: libraryProvider,
|
||||
librarySearchResults: librarySearchResults,
|
||||
uniqueKey: uniqueKey,
|
||||
postersList: postersList,
|
||||
libraryViewType: libraryViewType,
|
||||
key: uniqueKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Row(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -500,13 +481,12 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
),
|
||||
)
|
||||
else
|
||||
SliverToBoxAdapter(
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text(context.localized.noItemsToShow),
|
||||
),
|
||||
),
|
||||
const DefautlSliverBottomPadding(),
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 80))
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -514,8 +494,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (librarySearchResults.fetchingItems) ...[
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
|
|
@ -546,6 +524,8 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -663,12 +643,18 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
icon: const Icon(IconsaxPlusLinear.save_add),
|
||||
),
|
||||
];
|
||||
return NestedBottomAppBar(
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: MediaQuery.paddingOf(context).left),
|
||||
child: NestedBottomAppBar(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
ScrollStatePosition(
|
||||
controller: scrollController,
|
||||
|
|
@ -676,31 +662,18 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
child: state != ScrollState.top
|
||||
? Tooltip(
|
||||
message: context.localized.scrollToTop,
|
||||
child: FlatButton(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: 0,
|
||||
borderRadiusGeometry: BorderRadius.circular(6),
|
||||
onTap: () => scrollController.animateTo(0,
|
||||
child: IconButton.filled(
|
||||
onPressed: () => scrollController.animateTo(0,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_up,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (!librarySearchResults.selecteMode) ...{
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: context.localized.sortBy,
|
||||
onPressed: () async {
|
||||
|
|
@ -722,7 +695,6 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
icon: const Icon(IconsaxPlusLinear.sort),
|
||||
),
|
||||
if (librarySearchResults.hasActiveFilters) ...{
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: context.localized.disableFilters,
|
||||
onPressed: disableFilters(librarySearchResults, libraryProvider),
|
||||
|
|
@ -730,13 +702,11 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
),
|
||||
},
|
||||
},
|
||||
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(
|
||||
|
|
@ -744,6 +714,7 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: context.localized.selectAll,
|
||||
|
|
@ -752,7 +723,6 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
icon: const Icon(IconsaxPlusLinear.box_add),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Tooltip(
|
||||
message: context.localized.clearSelection,
|
||||
child: IconButton(
|
||||
|
|
@ -760,7 +730,6 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
icon: const Icon(IconsaxPlusLinear.box_remove),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (librarySearchResults.selectedPosters.isNotEmpty) ...{
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
PopupMenuButton(
|
||||
|
|
@ -787,18 +756,11 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
),
|
||||
const Spacer(),
|
||||
if (librarySearchResults.activePosters.isNotEmpty)
|
||||
IconButton(
|
||||
IconButton.filledTonal(
|
||||
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(
|
||||
icon: const Icon(
|
||||
IconsaxPlusBold.arrow_up_1,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (librarySearchResults.activePosters.isNotEmpty)
|
||||
|
|
@ -828,9 +790,10 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,105 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/library_search/library_search_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/screens/library_search/widgets/library_views.dart';
|
||||
import 'package:fladder/screens/shared/chips/category_chip.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/widgets/shared/scroll_position.dart';
|
||||
|
||||
class LibraryFilterChips extends ConsumerWidget {
|
||||
final Key uniqueKey;
|
||||
final ScrollController controller;
|
||||
final LibrarySearchModel librarySearchResults;
|
||||
final LibrarySearchNotifier libraryProvider;
|
||||
final List<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,
|
||||
});
|
||||
class LibraryFilterChips extends ConsumerStatefulWidget {
|
||||
const LibraryFilterChips({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ScrollStatePosition(
|
||||
controller: controller,
|
||||
positionBuilder: (state) {
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _LibraryFilterChipsState();
|
||||
}
|
||||
|
||||
class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final uniqueKey = widget.key ?? UniqueKey();
|
||||
final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier);
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
|
||||
final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites));
|
||||
final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive));
|
||||
final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows));
|
||||
final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey));
|
||||
|
||||
return Row(
|
||||
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(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Key uniqueKey, {
|
||||
required LibrarySearchModel librarySearchResults,
|
||||
required LibrarySearchNotifier libraryProvider,
|
||||
required List<ItemBaseModel> postersList,
|
||||
required LibraryViewTypes libraryViewType,
|
||||
}) {
|
||||
Future<dynamic> openGroupDialogue() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(context.localized.groupBy),
|
||||
...GroupBy.values.map((groupBy) => RadioListTile.adaptive(
|
||||
value: groupBy,
|
||||
title: Text(groupBy.value(context)),
|
||||
groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)),
|
||||
onChanged: (value) {
|
||||
libraryProvider.setGroupBy(groupBy);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
if (librarySearchResults.folderOverwrite.isEmpty)
|
||||
CategoryChip(
|
||||
label: Text(context.localized.library(2)),
|
||||
|
|
@ -125,20 +59,20 @@ List<Widget> libraryFilterChips(
|
|||
FilterChip(
|
||||
label: Text(context.localized.favorites),
|
||||
avatar: Icon(
|
||||
librarySearchResults.favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
|
||||
favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
selected: librarySearchResults.favourites,
|
||||
selected: favourites,
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {
|
||||
onSelected: (_) {
|
||||
libraryProvider.toggleFavourite();
|
||||
context.refreshData();
|
||||
},
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.localized.recursive),
|
||||
selected: librarySearchResults.recursive,
|
||||
onSelected: (value) {
|
||||
selected: recursive,
|
||||
onSelected: (_) {
|
||||
libraryProvider.toggleRecursive();
|
||||
context.refreshData();
|
||||
},
|
||||
|
|
@ -175,9 +109,9 @@ List<Widget> libraryFilterChips(
|
|||
),
|
||||
FilterChip(
|
||||
label: Text(context.localized.group),
|
||||
selected: librarySearchResults.groupBy != GroupBy.none,
|
||||
onSelected: (value) {
|
||||
openGroupDialogue();
|
||||
selected: groupBy != GroupBy.none,
|
||||
onSelected: (_) {
|
||||
_openGroupDialogue(context, ref, libraryProvider, uniqueKey);
|
||||
},
|
||||
),
|
||||
CategoryChip<ItemFilter>(
|
||||
|
|
@ -190,10 +124,10 @@ List<Widget> libraryFilterChips(
|
|||
if (librarySearchResults.types[FladderItemType.series] == true)
|
||||
FilterChip(
|
||||
avatar: Icon(
|
||||
librarySearchResults.hideEmptyShows ? Icons.visibility_off_rounded : Icons.visibility_rounded,
|
||||
hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
selected: librarySearchResults.hideEmptyShows,
|
||||
selected: hideEmpty,
|
||||
showCheckmark: false,
|
||||
label: Text(context.localized.hideEmpty),
|
||||
onSelected: libraryProvider.setHideEmpty,
|
||||
|
|
@ -217,5 +151,43 @@ List<Widget> libraryFilterChips(
|
|||
onCancel: () => libraryProvider.setYears(librarySearchResults.years),
|
||||
onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
|
||||
),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _openGroupDialogue(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
LibrarySearchNotifier provider,
|
||||
Key uniqueKey,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Text(context.localized.groupBy),
|
||||
...GroupBy.values.map(
|
||||
(group) => RadioListTile.adaptive(
|
||||
value: group,
|
||||
groupValue: groupBy,
|
||||
title: Text(group.value(context)),
|
||||
onChanged: (_) {
|
||||
provider.setGroupBy(group);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
import 'package:fladder/models/boxset_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
|
|
@ -10,22 +19,15 @@ import 'package:fladder/models/playlist_model.dart';
|
|||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_list_item.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_widget.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
import 'package:sticky_headers/sticky_headers/widget.dart';
|
||||
|
||||
final libraryViewTypeProvider = StateProvider<LibraryViewTypes>((ref) {
|
||||
return LibraryViewTypes.grid;
|
||||
|
|
@ -107,49 +109,13 @@ class LibraryViews extends ConsumerWidget {
|
|||
|
||||
switch (ref.watch(libraryViewTypeProvider)) {
|
||||
case LibraryViewTypes.grid:
|
||||
if (groupByType != GroupBy.none) {
|
||||
final groupedItems = groupItemsBy(context, items, groupByType);
|
||||
return SliverList.builder(
|
||||
itemCount: groupedItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final name = groupedItems.keys.elementAt(index);
|
||||
final group = groupedItems[name];
|
||||
if (group?.isEmpty ?? false || group == null) {
|
||||
return Text(context.localized.empty);
|
||||
}
|
||||
return PosterGrid(
|
||||
posters: group!,
|
||||
name: name,
|
||||
itemBuilder: (context, index) {
|
||||
final item = group[index];
|
||||
return PosterWidget(
|
||||
key: Key(item.id),
|
||||
poster: group[index],
|
||||
maxLines: 2,
|
||||
heroTag: true,
|
||||
subTitle: item.subTitle(sortingOptions),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(item),
|
||||
selected: selected.contains(item),
|
||||
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
|
||||
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
|
||||
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
);
|
||||
},
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: SliverGrid.builder(
|
||||
Widget createGrid(List<ItemBaseModel> items) {
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: posterSize.toInt(),
|
||||
mainAxisSpacing: (8 * decimal) + 8,
|
||||
crossAxisSpacing: (8 * decimal) + 8,
|
||||
childAspectRatio: AdaptiveLayout.poster(context).ratio,
|
||||
childAspectRatio: items.getMostCommonType.aspectRatio,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
|
@ -169,47 +135,31 @@ class LibraryViews extends ConsumerWidget {
|
|||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (groupByType != GroupBy.none) {
|
||||
final groupedItems = groupItemsBy(context, items, groupByType);
|
||||
return MultiSliver(
|
||||
children: groupedItems.entries.map(
|
||||
(element) {
|
||||
final name = element.key;
|
||||
final group = element.value;
|
||||
return stickyHeaderBuilder(
|
||||
context,
|
||||
header: name,
|
||||
sliver: createGrid(group),
|
||||
);
|
||||
},
|
||||
).toList());
|
||||
} else {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: createGrid(items),
|
||||
);
|
||||
}
|
||||
case LibraryViewTypes.list:
|
||||
if (groupByType != GroupBy.none) {
|
||||
final groupedItems = groupItemsBy(context, items, groupByType);
|
||||
return SliverList.builder(
|
||||
itemCount: groupedItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final name = groupedItems.keys.elementAt(index);
|
||||
final group = groupedItems[name];
|
||||
if (group?.isEmpty ?? false) {
|
||||
return Text(context.localized.empty);
|
||||
}
|
||||
return StickyHeader(
|
||||
header: Text(name, style: Theme.of(context).textTheme.headlineSmall),
|
||||
content: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: group?.length,
|
||||
itemBuilder: (context, index) {
|
||||
final poster = group![index];
|
||||
return PosterListItem(
|
||||
key: Key(poster.id),
|
||||
poster: poster,
|
||||
subTitle: poster.subTitle(sortingOptions),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(poster),
|
||||
selected: selected.contains(poster),
|
||||
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
|
||||
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
|
||||
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Widget listBuilder(List<ItemBaseModel> items) {
|
||||
return SliverList.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
|
@ -227,24 +177,36 @@ class LibraryViews extends ConsumerWidget {
|
|||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (groupByType != GroupBy.none) {
|
||||
final groupedItems = groupItemsBy(context, items, groupByType);
|
||||
return MultiSliver(
|
||||
children: groupedItems.entries.map(
|
||||
(element) {
|
||||
final name = element.key;
|
||||
final group = element.value;
|
||||
return stickyHeaderBuilder(
|
||||
context,
|
||||
header: name,
|
||||
sliver: listBuilder(group),
|
||||
);
|
||||
},
|
||||
).toList());
|
||||
}
|
||||
return listBuilder(items);
|
||||
case LibraryViewTypes.masonry:
|
||||
if (groupByType != GroupBy.none) {
|
||||
final groupedItems = groupItemsBy(context, items, groupByType);
|
||||
return SliverList.builder(
|
||||
itemCount: groupedItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final name = groupedItems.keys.elementAt(index);
|
||||
final group = groupedItems[name];
|
||||
if (group?.isEmpty ?? false) {
|
||||
return Text(context.localized.empty);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0),
|
||||
child: StickyHeader(
|
||||
header: Text(name, style: Theme.of(context).textTheme.headlineMedium),
|
||||
overlapHeaders: true,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
return MultiSliver(
|
||||
children: groupedItems.entries.map(
|
||||
(element) {
|
||||
final name = element.key;
|
||||
final group = element.value;
|
||||
return stickyHeaderBuilder(
|
||||
context,
|
||||
header: name,
|
||||
//MasonryGridView because SliverMasonryGrid breaks scrolling
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: MasonryGridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
|
|
@ -254,9 +216,9 @@ class LibraryViews extends ConsumerWidget {
|
|||
maxCrossAxisExtent:
|
||||
(MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
|
||||
.toDouble() *
|
||||
20,
|
||||
12,
|
||||
),
|
||||
itemCount: group!.length,
|
||||
itemCount: group.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = group[index];
|
||||
return PosterWidget(
|
||||
|
|
@ -276,10 +238,10 @@ class LibraryViews extends ConsumerWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
).toList());
|
||||
} else {
|
||||
return SliverMasonryGrid.count(
|
||||
mainAxisSpacing: (8 * decimal) + 8,
|
||||
|
|
@ -309,6 +271,36 @@ class LibraryViews extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
SliverStickyHeader stickyHeaderBuilder(
|
||||
BuildContext context, {
|
||||
required String header,
|
||||
Widget? sliver,
|
||||
}) {
|
||||
return SliverStickyHeader(
|
||||
header: Container(
|
||||
height: 50,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(-20, 0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surface.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
header,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
sliver: sliver,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, List<ItemBaseModel>> groupItemsBy(BuildContext context, List<ItemBaseModel> list, GroupBy groupOption) {
|
||||
switch (groupOption) {
|
||||
case GroupBy.dateAdded:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -65,6 +65,9 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
});
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: FladderTheme.largeShape.borderRadius,
|
||||
),
|
||||
shadowColor: Colors.transparent,
|
||||
child: TypeAheadField<ItemBaseModel>(
|
||||
focusNode: focusNode,
|
||||
|
|
@ -80,7 +83,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
decorationBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
borderRadius: FladderTheme.largeShape.borderRadius,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
|
|
@ -133,8 +136,13 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
}
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
title: SizedBox(
|
||||
height: 50,
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 50,
|
||||
maxHeight: 65,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Card(
|
||||
|
|
@ -160,14 +168,15 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
)),
|
||||
if (suggestion.overview.yearAired.toString().isNotEmpty)
|
||||
Flexible(
|
||||
child:
|
||||
Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
|
||||
child: Opacity(
|
||||
opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) async {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart';
|
|||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/screens/shared/passcode_input.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/auth_service.dart';
|
||||
import 'package:fladder/util/fladder_config.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget {
|
|||
return Card(
|
||||
elevation: 1,
|
||||
shadowColor: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget {
|
|||
aspectRatio: 1.0,
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
|
|||
import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart';
|
||||
import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/shared/file_picker.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
class EditImageContent extends ConsumerStatefulWidget {
|
||||
final ImageType type;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/items/identify_provider.dart';
|
||||
|
|
@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
|
|||
final state = ref.watch(provider);
|
||||
final posters = state.results;
|
||||
final processing = state.processing;
|
||||
return ActionContent(
|
||||
return Card(
|
||||
child: ActionContent(
|
||||
showDividers: false,
|
||||
title: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
|
|
@ -89,7 +88,6 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
|
|||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
|
|
@ -220,6 +218,7 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
|
|||
: Text(context.localized.search),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +247,7 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
|
|||
final controller =
|
||||
currentKey == "Name" ? currentController : TextEditingController(text: state.searchString);
|
||||
return FocusedOutlinedTextField(
|
||||
label: context.localized.userName,
|
||||
label: context.localized.name,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
currentController = controller;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/information_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
|
|||
import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart';
|
||||
import 'package:fladder/screens/photo_viewer/simple_video_player.dart';
|
||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/custom_cache_manager.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/screens/crash_screen/crash_screen.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
|
|
@ -42,8 +42,7 @@ class AboutSettingsPage extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final applicationInfo = ref.watch(applicationInfoProvider);
|
||||
return Card(
|
||||
child: SettingsScaffold(
|
||||
return SettingsScaffold(
|
||||
label: "",
|
||||
items: [
|
||||
const FladderLogo(),
|
||||
|
|
@ -56,7 +55,13 @@ class AboutSettingsPage extends ConsumerWidget {
|
|||
Text(context.localized.aboutCreatedBy),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
const FractionallySizedBox(
|
||||
widthFactor: 0.25,
|
||||
child: Divider(
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
|
|
@ -110,7 +115,6 @@ class AboutSettingsPage extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,19 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/option_dialogue.dart';
|
||||
|
||||
List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
|
||||
return [
|
||||
return settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.advanced),
|
||||
[
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsLayoutSizesTitle),
|
||||
subLabel: Text(context.localized.settingsLayoutSizesDesc),
|
||||
|
|
@ -76,5 +79,6 @@ List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
|
|||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
|
|||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
|
||||
List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
return [
|
||||
return settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.dashboard),
|
||||
[
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsHomeBannerTitle),
|
||||
subLabel: Text(context.localized.settingsHomeBannerDescription),
|
||||
|
|
@ -90,6 +93,6 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
|||
.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/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/shared/default_alert_dialog.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/size_formatting.dart';
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
|
|||
|
||||
return [
|
||||
if (canSync && !kIsWeb) ...[
|
||||
SettingsLabelDivider(label: context.localized.downloadsTitle),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [
|
||||
if (AdaptiveLayout.of(context).isDesktop) ...[
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.downloadsPath),
|
||||
|
|
@ -51,8 +52,8 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
|
|||
),
|
||||
)
|
||||
: () async {
|
||||
String? selectedDirectory = await FilePicker.platform
|
||||
.getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
|
||||
if (selectedDirectory != null) {
|
||||
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
|
||||
}
|
||||
|
|
@ -138,7 +139,8 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
|
|||
},
|
||||
)),
|
||||
),
|
||||
const Divider(),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/color_extensions.dart';
|
||||
import 'package:fladder/util/custom_color_themes.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart';
|
|||
|
||||
List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
return [
|
||||
SettingsLabelDivider(label: context.localized.theme),
|
||||
return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.mode),
|
||||
subLabel: Text(clientSettings.themeMode.label(context)),
|
||||
|
|
@ -107,6 +107,5 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
|||
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_gen/gen_l10n/app_localizations.dart';
|
||||
|
|
@ -8,8 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
|
|
@ -22,8 +21,10 @@ List<Widget> buildClientSettingsVisual(
|
|||
) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
|
||||
return [
|
||||
return settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.settingsVisual),
|
||||
[
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.displayLanguage),
|
||||
trailing: Localizations.override(
|
||||
|
|
@ -33,9 +34,7 @@ List<Widget> buildClientSettingsVisual(
|
|||
String language = "Unknown";
|
||||
try {
|
||||
language = context.localized.nativeName;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
} catch (_) {}
|
||||
return EnumBox(
|
||||
current: language,
|
||||
itemBuilder: (context) {
|
||||
|
|
@ -152,6 +151,6 @@ List<Widget> buildClientSettingsVisual(
|
|||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
|
|
@ -16,7 +15,8 @@ import 'package:fladder/screens/settings/client_sections/client_settings_visual.
|
|||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/simple_duration_picker.dart';
|
||||
|
||||
|
|
@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
|
||||
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||
|
||||
return Card(
|
||||
elevation: showBackground ? 2 : 0,
|
||||
child: SettingsScaffold(
|
||||
return SettingsScaffold(
|
||||
label: "Fladder",
|
||||
items: [
|
||||
...buildClientSettingsDownload(context, ref, setState),
|
||||
SettingsLabelDivider(label: context.localized.lockscreen),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.timeOut),
|
||||
subLabel: Text(timePickerString(context, clientSettings.timeOut)),
|
||||
|
|
@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
: null);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
...buildClientSettingsDashboard(context, ref),
|
||||
const SizedBox(height: 12),
|
||||
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
|
||||
const SizedBox(height: 12),
|
||||
...buildClientSettingsTheme(context, ref),
|
||||
const SizedBox(height: 12),
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
|
||||
SettingsLabelDivider(label: context.localized.controls),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.mouseDragSupport),
|
||||
subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled),
|
||||
|
|
@ -83,7 +83,8 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
...buildClientSettingsAdvanced(context, ref),
|
||||
if (kDebugMode) ...[
|
||||
|
|
@ -137,7 +138,6 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,20 +8,20 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/providers/connectivity_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_message_box.dart';
|
||||
import 'package:fladder/screens/settings/widgets/subtitle_editor.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/bitrate_helper.dart';
|
||||
import 'package:fladder/util/box_fit_extension.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -41,18 +41,19 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final videoSettings = ref.watch(videoPlayerSettingsProvider);
|
||||
final provider = ref.read(videoPlayerSettingsProvider.notifier);
|
||||
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
|
||||
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||
|
||||
final connectionState = ref.watch(connectivityStatusProvider);
|
||||
|
||||
return Card(
|
||||
elevation: showBackground ? 2 : 0,
|
||||
child: SettingsScaffold(
|
||||
return SettingsScaffold(
|
||||
label: context.localized.settingsPlayerTitle,
|
||||
items: [
|
||||
...settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.video),
|
||||
[
|
||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
||||
Column(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.videoScalingFillScreenTitle),
|
||||
subLabel: Text(context.localized.videoScalingFillScreenDesc),
|
||||
|
|
@ -70,6 +71,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
)
|
||||
: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.videoScalingFillScreenTitle),
|
||||
subLabel: Text(videoSettings.videoFit.label(context)),
|
||||
|
|
@ -133,8 +136,10 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
.toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
SettingsLabelDivider(label: context.localized.mediaSegmentActions),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [
|
||||
...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
|
||||
(entry) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
|
|
@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
SettingsLabelDivider(label: context.localized.playbackTrackSelection),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.rememberAudioSelections),
|
||||
subLabel: Text(context.localized.rememberAudioSelectionsDesc),
|
||||
|
|
@ -190,8 +197,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
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)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsBackendTitle),
|
||||
|
|
@ -236,7 +244,7 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
onChanged: (value) => provider.setHardwareAccel(value),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb) ...[
|
||||
if (!kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||
|
|
@ -254,7 +262,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
)
|
||||
: Container(),
|
||||
),
|
||||
],
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
||||
|
|
@ -291,6 +298,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsAutoNextTitle),
|
||||
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||
|
|
@ -319,14 +328,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||
subLabel: Text(context.localized.playerSettingsOrientationDesc),
|
||||
onTap: () => showOrientationOptions(context, ref),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import 'package:flutter/material.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/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/shared/authenticate_button_options.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SecuritySettingsPage extends ConsumerStatefulWidget {
|
||||
|
|
@ -22,14 +23,10 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProvider);
|
||||
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
|
||||
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||
return Card(
|
||||
elevation: showBackground ? 2 : 0,
|
||||
child: SettingsScaffold(
|
||||
return SettingsScaffold(
|
||||
label: context.localized.settingsProfileTitle,
|
||||
items: [
|
||||
SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingSecurityApplockTitle),
|
||||
subLabel: Text(user?.authMethod.name(context) ?? ""),
|
||||
|
|
@ -37,8 +34,8 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
|
|||
ref.read(userProvider.notifier).updateUser(newUser);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
),
|
||||
if (subLabel != null)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
opacity: 0.65,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/user_provider.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';
|
||||
|
||||
class SettingsScaffold extends ConsumerWidget {
|
||||
|
|
@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
final padding = MediaQuery.of(context).padding;
|
||||
final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single;
|
||||
return Scaffold(
|
||||
backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: floatingActionButton,
|
||||
body: Column(
|
||||
|
|
@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate(items),
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: 0),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) => items[index],
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
if (bottomActions.isEmpty)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package: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/user_provider.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_scaffold.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/theme_extensions.dart';
|
||||
|
||||
|
|
@ -97,7 +96,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
final quickConnectAvailable =
|
||||
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
|
||||
|
||||
return Container(
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
||||
child: Container(
|
||||
color: context.colors.surface,
|
||||
child: SettingsScaffold(
|
||||
label: context.localized.settings,
|
||||
|
|
@ -214,6 +215,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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/video_player_settings_provider.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/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
Future<void> showDialogAdaptive(
|
||||
{required BuildContext context, required Widget Function(BuildContext context) builder}) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class AnimatedFadeSize extends ConsumerWidget {
|
||||
final Duration duration;
|
||||
final Widget child;
|
||||
final Alignment alignment;
|
||||
const AnimatedFadeSize({
|
||||
this.duration = const Duration(milliseconds: 125),
|
||||
required this.child,
|
||||
this.alignment = Alignment.center,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -14,6 +17,7 @@ class AnimatedFadeSize extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AnimatedSize(
|
||||
duration: duration,
|
||||
alignment: alignment,
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: AnimatedSwitcher(
|
||||
duration: duration,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
|
|
@ -100,14 +99,16 @@ class CategoryChip<T> extends StatelessWidget {
|
|||
label: Text(context.localized.clear),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 6));
|
||||
Widget header() => Row(
|
||||
Widget header(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
child: dialogueTitle ?? label,
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
|
|
@ -127,6 +128,8 @@ class CategoryChip<T> extends StatelessWidget {
|
|||
label: Text(context.localized.clear),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 6)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) {
|
||||
|
|
@ -156,7 +159,7 @@ class CategoryChip<T> extends StatelessWidget {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: header(),
|
||||
child: header(context),
|
||||
),
|
||||
const Divider(),
|
||||
CategoryChipEditor(
|
||||
|
|
|
|||