feat: UI 2.0 and other Improvements (#357)

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

View file

@ -53,11 +53,14 @@
<details close>
<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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 930 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

@ -1211,5 +1211,14 @@
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.",
"@rememberAudioSelectionsDesc": {}
"@rememberAudioSelectionsDesc": {},
"similarToRecentlyPlayed": "Similar to recently played",
"similarToLikedItem": "Similar to liked item",
"hasDirectorFromRecentlyPlayed": "Has director from recently played",
"hasActorFromRecentlyPlayed": "Has actor from recently played",
"hasLikedDirector": "Has liked director",
"hasLikedActor": "Has liked actor",
"latest": "Latest",
"recommended": "Recommended"
}

View file

@ -19,7 +19,6 @@ import 'package:universal_html/html.dart' as html;
import 'package:window_manager/window_manager.dart';
import 'package: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,

View file

@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/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';

View file

@ -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,
};
}

View file

@ -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,

View file

@ -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 {

View file

@ -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';

View file

@ -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]),

View file

@ -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,

View file

@ -1,20 +1,66 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/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,
);
}
}

View file

@ -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;

View file

@ -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._();

View file

@ -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(),
);

View file

@ -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,

View file

@ -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)';

View file

@ -1,3 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/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,
],
);

View file

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

View file

@ -1,174 +0,0 @@
import 'package:chopper/chopper.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/library_model.dart';
import 'package:fladder/models/recommended_model.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
bool _useFolders(ViewModel model) {
switch (model.collectionType) {
case CollectionType.boxsets:
case CollectionType.homevideos:
case CollectionType.folders:
return true;
default:
return false;
}
}
final libraryProvider = StateNotifierProvider.autoDispose.family<LibraryNotifier, LibraryModel?, String>((ref, id) {
return LibraryNotifier(ref);
});
class LibraryNotifier extends StateNotifier<LibraryModel?> {
LibraryNotifier(this.ref) : super(null);
final Ref ref;
late final JellyService api = ref.read(jellyApiProvider);
set loading(bool value) {
state = state?.copyWith(loading: value);
}
bool get loading => state?.loading ?? true;
Future<void> setupLibrary(ViewModel viewModel) async {
state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie);
}
Future<Response?> loadLibrary(ViewModel viewModel) async {
final response = await api.itemsGet(
parentId: viewModel.id,
sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear],
isMissing: false,
excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [],
fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid],
);
state = state?.copyWith(posters: response.body?.items);
loading = false;
return response;
}
Future<void> loadRecommendations(ViewModel viewModel) async {
loading = true;
//Clear recommendations because of all the copying
state = state?.copyWith(recommendations: []);
final latest = await api.usersUserIdItemsLatestGet(
parentId: viewModel.id,
limit: 14,
isPlayed: false,
imageTypeLimit: 1,
includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null,
);
state = state?.copyWith(
recommendations: [
...?state?.recommendations,
RecommendedModel(
name: "Latest",
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: "Latest",
),
],
);
if (viewModel.collectionType == CollectionType.movies) {
final response = await api.moviesRecommendationsGet(
parentId: viewModel.id,
categoryLimit: 6,
itemLimit: 8,
fields: [ItemFields.mediasourcecount],
);
state = state?.copyWith(recommendations: [
...?state?.recommendations,
...response.body?.map(
(e) => RecommendedModel(
name: e.baselineItemName ?? "",
posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: e.recommendationType.toString(),
),
) ??
[],
]);
loading = false;
} else {
final nextUp = await api.showsNextUpGet(
parentId: viewModel.id,
limit: 14,
imageTypeLimit: 1,
fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
);
state = state?.copyWith(recommendations: [
...?state?.recommendations,
...[
RecommendedModel(
name: "Next up",
posters: nextUp.body?.items
?.map(
(e) => ItemBaseModel.fromBaseDto(
e,
ref,
),
)
.toList() ??
[],
type: "Latest series")
],
]);
loading = false;
}
}
Future<Response?> loadFavourites(ViewModel viewModel) async {
loading = true;
final response = await api.itemsGet(
parentId: viewModel.id,
isFavorite: true,
recursive: true,
);
state = state?.copyWith(favourites: response.body?.items);
loading = false;
return response;
}
Future<Response?> loadTimeline(ViewModel viewModel) async {
loading = true;
final response = await api.itemsGet(
parentId: viewModel.id,
recursive: true,
fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated],
sortBy: [ItemSortBy.datecreated],
sortOrder: [SortOrder.descending],
includeItemTypes: [
BaseItemKind.photo,
BaseItemKind.video,
],
);
state = state?.copyWith(
timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(),
);
loading = false;
return response;
}
Future<Response?> loadGenres(ViewModel viewModel) async {
final genres = await api.genresGet(
sortBy: [ItemSortBy.sortname],
sortOrder: [SortOrder.ascending],
includeItemTypes: viewModel.collectionType == CollectionType.movies
? [BaseItemKind.movie]
: [
BaseItemKind.series,
],
parentId: viewModel.id,
);
state = state?.copyWith(
genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList());
return null;
}
}

View file

@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/recommended_model.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/localization_helper.dart';
part 'library_screen_provider.freezed.dart';
part 'library_screen_provider.g.dart';
enum LibraryViewType {
recommended,
favourites,
genres;
const LibraryViewType();
String label(BuildContext context) => switch (this) {
LibraryViewType.recommended => context.localized.recommended,
LibraryViewType.favourites => context.localized.favorites,
LibraryViewType.genres => context.localized.genre(2),
};
IconData get icon => switch (this) {
LibraryViewType.recommended => IconsaxPlusLinear.star,
LibraryViewType.favourites => IconsaxPlusLinear.heart,
LibraryViewType.genres => IconsaxPlusLinear.hierarchy_3,
};
IconData get iconSelected => switch (this) {
LibraryViewType.recommended => IconsaxPlusBold.star,
LibraryViewType.favourites => IconsaxPlusBold.heart,
LibraryViewType.genres => IconsaxPlusBold.hierarchy_3,
};
}
@Freezed(fromJson: false, toJson: false)
class LibraryScreenModel with _$LibraryScreenModel {
factory LibraryScreenModel({
@Default([]) List<ViewModel> views,
ViewModel? selectedViewModel,
@Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set<LibraryViewType> viewType,
@Default([]) List<RecommendedModel> recommendations,
@Default([]) List<RecommendedModel> genres,
@Default([]) List<ItemBaseModel> favourites,
}) = _LibraryScreenModel;
}
@Riverpod(keepAlive: true)
class LibraryScreen extends _$LibraryScreen {
late final JellyService api = ref.read(jellyApiProvider);
@override
LibraryScreenModel build() => LibraryScreenModel();
Future<void> fetchAllLibraries() async {
final views = await ref.read(viewsProvider.notifier).fetchViews();
state = state.copyWith(views: views?.views ?? []);
if (state.views.isEmpty) return;
final viewModel = state.selectedViewModel ?? state.views.firstOrNull;
if (viewModel == null) return;
selectLibrary(viewModel);
await loadLibrary(viewModel);
}
Future<void> selectLibrary(ViewModel viewModel) async {
state = state.copyWith(selectedViewModel: viewModel);
}
Future<void> setViewType(Set<LibraryViewType> type) async {
state = state.copyWith(viewType: type);
}
Future<Response?> loadLibrary(ViewModel viewModel) async {
await loadRecommendations(viewModel);
await loadGenres(viewModel);
await loadFavourites(viewModel);
return null;
}
Future<void> loadRecommendations(ViewModel viewModel) async {
List<RecommendedModel> newRecommendations = [];
final latest = await api.usersUserIdItemsLatestGet(
parentId: viewModel.id,
limit: 14,
isPlayed: false,
imageTypeLimit: 1,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
);
newRecommendations = [
...newRecommendations,
RecommendedModel(
name: const Latest(),
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: null,
),
];
if (viewModel.collectionType == CollectionType.movies) {
final response = await api.moviesRecommendationsGet(
parentId: viewModel.id,
categoryLimit: 6,
itemLimit: 14,
fields: [ItemFields.mediasourcecount],
);
newRecommendations = [
...newRecommendations,
...(response.body?.map(
(e) => RecommendedModel.fromBaseDto(e, ref),
) ??
[])
];
} else {
final nextUp = await api.showsNextUpGet(
parentId: viewModel.id,
limit: 14,
imageTypeLimit: 1,
fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
);
newRecommendations = [
...newRecommendations,
RecommendedModel(
name: const NextUp(),
posters: nextUp.body?.items
?.map(
(e) => ItemBaseModel.fromBaseDto(
e,
ref,
),
)
.toList() ??
[],
type: null,
)
];
}
state = state.copyWith(
recommendations: newRecommendations,
);
}
Future<Response?> loadFavourites(ViewModel viewModel) async {
final response = await api.itemsGet(
parentId: viewModel.id,
isFavorite: true,
recursive: true,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
enableImageTypes: [ImageType.primary],
fields: [
ItemFields.primaryimageaspectratio,
ItemFields.mediasourcecount,
],
enableTotalRecordCount: false,
);
state = state.copyWith(favourites: response.body?.items ?? []);
return response;
}
Future<Response?> loadGenres(ViewModel viewModel) async {
final genres = await api.genresGet(
sortBy: [ItemSortBy.sortname],
sortOrder: [SortOrder.ascending],
includeItemTypes:
viewModel.collectionType == CollectionType.movies ? [BaseItemKind.movie] : [BaseItemKind.series],
parentId: viewModel.id,
);
final filteredGenres = (genres.body?.items?.map(
(item) => GenreItems(id: item.id ?? "", name: item.name ?? ""),
) ??
[])
.toList();
if (filteredGenres.isEmpty) return null;
final results = await Future.wait(filteredGenres.map((genre) async {
final response = await api.itemsGet(
parentId: viewModel.id,
genreIds: [genre.id],
limit: 9,
recursive: true,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
enableImageTypes: [ImageType.primary],
fields: [
ItemFields.primaryimageaspectratio,
ItemFields.mediasourcecount,
],
sortBy: [ItemSortBy.random],
enableTotalRecordCount: false,
imageTypeLimit: 1,
sortOrder: [SortOrder.ascending],
);
final items = response.body?.items;
if (items != null && items.isNotEmpty) {
return RecommendedModel(name: Other(genre.name), posters: items);
}
return null;
}));
state = state.copyWith(
genres: results.whereType<RecommendedModel>().toList(),
);
return null;
}
}

View file

@ -0,0 +1,301 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'library_screen_provider.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$LibraryScreenModel {
List<ViewModel> get views => throw _privateConstructorUsedError;
ViewModel? get selectedViewModel => throw _privateConstructorUsedError;
Set<LibraryViewType> get viewType => throw _privateConstructorUsedError;
List<RecommendedModel> get recommendations =>
throw _privateConstructorUsedError;
List<RecommendedModel> get genres => throw _privateConstructorUsedError;
List<ItemBaseModel> get favourites => throw _privateConstructorUsedError;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LibraryScreenModelCopyWith<LibraryScreenModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LibraryScreenModelCopyWith<$Res> {
factory $LibraryScreenModelCopyWith(
LibraryScreenModel value, $Res Function(LibraryScreenModel) then) =
_$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>;
@useResult
$Res call(
{List<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> favourites});
}
/// @nodoc
class _$LibraryScreenModelCopyWithImpl<$Res, $Val extends LibraryScreenModel>
implements $LibraryScreenModelCopyWith<$Res> {
_$LibraryScreenModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? views = null,
Object? selectedViewModel = freezed,
Object? viewType = null,
Object? recommendations = null,
Object? genres = null,
Object? favourites = null,
}) {
return _then(_value.copyWith(
views: null == views
? _value.views
: views // ignore: cast_nullable_to_non_nullable
as List<ViewModel>,
selectedViewModel: freezed == selectedViewModel
? _value.selectedViewModel
: selectedViewModel // ignore: cast_nullable_to_non_nullable
as ViewModel?,
viewType: null == viewType
? _value.viewType
: viewType // ignore: cast_nullable_to_non_nullable
as Set<LibraryViewType>,
recommendations: null == recommendations
? _value.recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value.genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value.favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
) as $Val);
}
}
/// @nodoc
abstract class _$$LibraryScreenModelImplCopyWith<$Res>
implements $LibraryScreenModelCopyWith<$Res> {
factory _$$LibraryScreenModelImplCopyWith(_$LibraryScreenModelImpl value,
$Res Function(_$LibraryScreenModelImpl) then) =
__$$LibraryScreenModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> favourites});
}
/// @nodoc
class __$$LibraryScreenModelImplCopyWithImpl<$Res>
extends _$LibraryScreenModelCopyWithImpl<$Res, _$LibraryScreenModelImpl>
implements _$$LibraryScreenModelImplCopyWith<$Res> {
__$$LibraryScreenModelImplCopyWithImpl(_$LibraryScreenModelImpl _value,
$Res Function(_$LibraryScreenModelImpl) _then)
: super(_value, _then);
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? views = null,
Object? selectedViewModel = freezed,
Object? viewType = null,
Object? recommendations = null,
Object? genres = null,
Object? favourites = null,
}) {
return _then(_$LibraryScreenModelImpl(
views: null == views
? _value._views
: views // ignore: cast_nullable_to_non_nullable
as List<ViewModel>,
selectedViewModel: freezed == selectedViewModel
? _value.selectedViewModel
: selectedViewModel // ignore: cast_nullable_to_non_nullable
as ViewModel?,
viewType: null == viewType
? _value._viewType
: viewType // ignore: cast_nullable_to_non_nullable
as Set<LibraryViewType>,
recommendations: null == recommendations
? _value._recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value._genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value._favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
));
}
}
/// @nodoc
class _$LibraryScreenModelImpl implements _LibraryScreenModel {
_$LibraryScreenModelImpl(
{final List<ViewModel> views = const [],
this.selectedViewModel,
final Set<LibraryViewType> viewType = const {
LibraryViewType.recommended,
LibraryViewType.favourites
},
final List<RecommendedModel> recommendations = const [],
final List<RecommendedModel> genres = const [],
final List<ItemBaseModel> favourites = const []})
: _views = views,
_viewType = viewType,
_recommendations = recommendations,
_genres = genres,
_favourites = favourites;
final List<ViewModel> _views;
@override
@JsonKey()
List<ViewModel> get views {
if (_views is EqualUnmodifiableListView) return _views;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_views);
}
@override
final ViewModel? selectedViewModel;
final Set<LibraryViewType> _viewType;
@override
@JsonKey()
Set<LibraryViewType> get viewType {
if (_viewType is EqualUnmodifiableSetView) return _viewType;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_viewType);
}
final List<RecommendedModel> _recommendations;
@override
@JsonKey()
List<RecommendedModel> get recommendations {
if (_recommendations is EqualUnmodifiableListView) return _recommendations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recommendations);
}
final List<RecommendedModel> _genres;
@override
@JsonKey()
List<RecommendedModel> get genres {
if (_genres is EqualUnmodifiableListView) return _genres;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_genres);
}
final List<ItemBaseModel> _favourites;
@override
@JsonKey()
List<ItemBaseModel> get favourites {
if (_favourites is EqualUnmodifiableListView) return _favourites;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_favourites);
}
@override
String toString() {
return 'LibraryScreenModel(views: $views, selectedViewModel: $selectedViewModel, viewType: $viewType, recommendations: $recommendations, genres: $genres, favourites: $favourites)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LibraryScreenModelImpl &&
const DeepCollectionEquality().equals(other._views, _views) &&
(identical(other.selectedViewModel, selectedViewModel) ||
other.selectedViewModel == selectedViewModel) &&
const DeepCollectionEquality().equals(other._viewType, _viewType) &&
const DeepCollectionEquality()
.equals(other._recommendations, _recommendations) &&
const DeepCollectionEquality().equals(other._genres, _genres) &&
const DeepCollectionEquality()
.equals(other._favourites, _favourites));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_views),
selectedViewModel,
const DeepCollectionEquality().hash(_viewType),
const DeepCollectionEquality().hash(_recommendations),
const DeepCollectionEquality().hash(_genres),
const DeepCollectionEquality().hash(_favourites));
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
__$$LibraryScreenModelImplCopyWithImpl<_$LibraryScreenModelImpl>(
this, _$identity);
}
abstract class _LibraryScreenModel implements LibraryScreenModel {
factory _LibraryScreenModel(
{final List<ViewModel> views,
final ViewModel? selectedViewModel,
final Set<LibraryViewType> viewType,
final List<RecommendedModel> recommendations,
final List<RecommendedModel> genres,
final List<ItemBaseModel> favourites}) = _$LibraryScreenModelImpl;
@override
List<ViewModel> get views;
@override
ViewModel? get selectedViewModel;
@override
Set<LibraryViewType> get viewType;
@override
List<RecommendedModel> get recommendations;
@override
List<RecommendedModel> get genres;
@override
List<ItemBaseModel> get favourites;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'library_screen_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c';
/// See also [LibraryScreen].
@ProviderFor(LibraryScreen)
final libraryScreenProvider =
NotifierProvider<LibraryScreen, LibraryScreenModel>.internal(
LibraryScreen.new,
name: r'libraryScreenProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$libraryScreenHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LibraryScreen = Notifier<LibraryScreenModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
.toSet()
.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,

View file

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

View file

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

View file

@ -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)

View file

@ -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() {

View file

@ -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'),

View file

@ -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() {

View file

@ -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';

View file

@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart';
import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/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';

View file

@ -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';

View file

@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.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) {

View file

@ -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(),
};
}

View file

@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/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';

View file

@ -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)),
),
),
),
);
}
}

View file

@ -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';

View file

@ -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';

View file

@ -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),
),

View file

@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/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';

View file

@ -1,19 +1,20 @@
import 'package:auto_route/auto_route.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
import 'package:fladder/widgets/shared/poster_size_slider.dart';
import 'package:flutter/material.dart';
import 'package: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(),
],
),

View file

@ -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

View file

@ -1,83 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/screens/library/tabs/favourites_tab.dart';
import 'package:fladder/screens/library/tabs/library_tab.dart';
import 'package:fladder/screens/library/tabs/timeline_tab.dart';
import 'package:flutter/material.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/screens/library/tabs/recommendations_tab.dart';
class LibraryTabs {
final String name;
final Icon icon;
final Widget page;
final FloatingActionButton? floatingActionButton;
LibraryTabs({
required this.name,
required this.icon,
required this.page,
this.floatingActionButton,
});
static List<LibraryTabs> getLibraryForType(ViewModel viewModel, CollectionType type) {
LibraryTabs recommendTab() {
return LibraryTabs(
name: "Recommended",
icon: const Icon(Icons.recommend_rounded),
page: RecommendationsTab(viewModel: viewModel),
);
}
LibraryTabs timelineTab() {
return LibraryTabs(
name: "Timeline",
icon: const Icon(Icons.timeline),
page: TimelineTab(viewModel: viewModel),
);
}
LibraryTabs favouritesTab() {
return LibraryTabs(
name: "Favourites",
icon: const Icon(Icons.favorite_rounded),
page: FavouritesTab(viewModel: viewModel),
);
}
LibraryTabs libraryTab() {
return LibraryTabs(
name: "Library",
icon: const Icon(Icons.book_rounded),
page: LibraryTab(viewModel: viewModel),
);
}
switch (type) {
case CollectionType.tvshows:
case CollectionType.movies:
return [
libraryTab(),
recommendTab(),
favouritesTab(),
];
case CollectionType.books:
case CollectionType.homevideos:
return [
libraryTab(),
timelineTab(),
recommendTab(),
favouritesTab(),
];
case CollectionType.boxsets:
case CollectionType.playlists:
case CollectionType.folders:
return [
libraryTab(),
];
default:
return [];
}
}
}

View file

@ -1,14 +1,33 @@
import 'package:fladder/models/view_model.dart';
import '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,
),
],
),
),
),
],
),
),
),
);
},
);
}
}

View file

@ -1,37 +0,0 @@
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/library_provider.dart';
import 'package:fladder/screens/shared/media/poster_grid.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FavouritesTab extends ConsumerStatefulWidget {
final ViewModel viewModel;
const FavouritesTab({required this.viewModel, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _FavouritesTabState();
}
class _FavouritesTabState extends ConsumerState<FavouritesTab> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
final favourites = ref.watch(libraryProvider(widget.viewModel.id))?.favourites ?? [];
super.build(context);
return PullToRefresh(
onRefresh: () async {
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadFavourites(widget.viewModel);
},
child: favourites.isNotEmpty
? ListView(
children: [
PosterGrid(posters: favourites),
],
)
: const Center(child: Text("No favourites, add some using the heart icon.")),
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,40 +0,0 @@
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/library_provider.dart';
import 'package:fladder/screens/shared/media/poster_grid.dart';
import 'package:fladder/util/grouping.dart';
import 'package:fladder/util/keyed_list_view.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LibraryTab extends ConsumerStatefulWidget {
final ViewModel viewModel;
const LibraryTab({required this.viewModel, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _LibraryTabState();
}
class _LibraryTabState extends ConsumerState<LibraryTab> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
final library = ref.watch(libraryProvider(widget.viewModel.id).select((value) => value?.posters)) ?? [];
final items = groupByName(library);
return PullToRefresh(
onRefresh: () async {
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadLibrary(widget.viewModel);
},
child: KeyedListView(
map: items,
itemBuilder: (context, index) {
final currentIndex = items.entries.elementAt(index);
return PosterGrid(name: currentIndex.key, posters: currentIndex.value);
},
),
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,49 +0,0 @@
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/library_provider.dart';
import 'package:fladder/screens/shared/media/poster_grid.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class RecommendationsTab extends ConsumerStatefulWidget {
final ViewModel viewModel;
const RecommendationsTab({required this.viewModel, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _RecommendationsTabState();
}
class _RecommendationsTabState extends ConsumerState<RecommendationsTab> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
final recommendations = ref.watch(libraryProvider(widget.viewModel.id)
.select((value) => value?.recommendations.where((element) => element.posters.isNotEmpty))) ??
[];
return PullToRefresh(
onRefresh: () async {
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadRecommendations(widget.viewModel);
},
child: recommendations.isNotEmpty
? ListView(
children: recommendations
.map(
(e) => PosterGrid(name: e.name, posters: e.posters),
)
.toList()
.addPadding(
const EdgeInsets.only(
bottom: 32,
),
),
)
: const Center(
child: Text("No recommendations, add more movies and or shows to receive more recomendations")),
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,133 +0,0 @@
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/library_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/sticky_header_text.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:intl/intl.dart';
import 'package:page_transition/page_transition.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sticky_headers/sticky_headers.dart';
class TimelineTab extends ConsumerStatefulWidget {
final ViewModel viewModel;
const TimelineTab({required this.viewModel, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _TimelineTabState();
}
class _TimelineTabState extends ConsumerState<TimelineTab> with AutomaticKeepAliveClientMixin {
final itemScrollController = ItemScrollController();
double get posterCount {
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) {
return 200;
}
return 125;
}
@override
Widget build(BuildContext context) {
super.build(context);
final timeLine = ref.watch(libraryProvider(widget.viewModel.id))?.timelinePhotos ?? [];
final items = groupedItems(timeLine);
return PullToRefresh(
onRefresh: () async {
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadTimeline(widget.viewModel);
},
child: ScrollablePositionedList.builder(
itemScrollController: itemScrollController,
itemCount: items.length,
itemBuilder: (context, index) {
final item = items.entries.elementAt(index);
return Padding(
padding: const EdgeInsets.only(bottom: 64.0),
child: StickyHeader(
header: StickyHeaderText(
label: item.key.year != DateTime.now().year
? DateFormat('E dd MMM. y').format(item.key)
: DateFormat('E dd MMM.').format(item.key)),
content: StaggeredGrid.count(
crossAxisCount: MediaQuery.of(context).size.width ~/ posterCount,
mainAxisSpacing: 0,
crossAxisSpacing: 0,
axisDirection: AxisDirection.down,
children: item.value
.map(
(e) => Hero(
tag: e.id,
child: AspectRatio(
aspectRatio: e.primaryRatio ?? 0.0,
child: Card(
margin: const EdgeInsets.all(4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
FladderImage(image: e.thumbnail?.primary),
FlatButton(
onLongPress: () {},
onTap: () async {
final position = await Navigator.of(context, rootNavigator: true).push(
PageTransition(
child: PhotoViewerScreen(
items: timeLine,
indexOfSelected: timeLine.indexOf(e),
),
type: PageTransitionType.fade),
);
getParentPosition(items, timeLine, position);
},
)
],
),
),
),
),
)
.toList(),
),
),
);
},
),
);
}
void getParentPosition(Map<DateTime, List<PhotoModel>> items, List<PhotoModel> timeLine, int position) {
items.forEach(
(key, value) {
if (value.contains(timeLine[position])) {
itemScrollController.scrollTo(
index: items.keys.toList().indexOf(key), duration: const Duration(milliseconds: 250));
}
},
);
}
Map<DateTime, List<PhotoModel>> groupedItems(List<PhotoModel> items) {
Map<DateTime, List<PhotoModel>> groupedItems = {};
for (int i = 0; i < items.length; i++) {
DateTime curretDate = items[i].dateTaken ?? DateTime.now();
DateTime key = DateTime(curretDate.year, curretDate.month, curretDate.day);
if (!groupedItems.containsKey(key)) {
groupedItems[key] = [items[i]];
} else {
groupedItems[key]?.add(items[i]);
}
}
return groupedItems;
}
@override
bool get wantKeepAlive => true;
}

View file

@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/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),
],
),
),
),
);
}

View file

@ -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);
},
),
),
],
),
),
);
},
);
}
}

View file

@ -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:

View file

@ -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 {

View file

@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/shared/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';

View file

@ -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),

View file

@ -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: [

View file

@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart';
import 'package:fladder/screens/metadata/edit_screens/edit_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';

View file

@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/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;

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/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';

View file

@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart';
import 'package:fladder/screens/photo_viewer/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';

View file

@ -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)),
),
);
}
}

View file

@ -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) {
),
),
),
];
],
);
}

View file

@ -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(),
];
],
);
}

View file

@ -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),
],
];
}

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/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(),
];
]);
}

View file

@ -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(),
];
],
);
}

View file

@ -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> {
),
],
],
),
);
}
}

View file

@ -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),
),
]),
],
),
);
}
}

View file

@ -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);
}),
),
]),
],
),
);
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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> {
),
),
),
),
);
}
}

View file

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

View file

@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/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';

View file

@ -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}) {

View file

@ -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,

View file

@ -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(

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