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

@ -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,7 +109,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
SliverToBoxAdapter(
child: Transform.translate(
offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0),
child: HomeBannerWidget(posters: homeCarouselItems),
child: Padding(
padding: AdaptiveLayout.adaptivePadding(
context,
horizontalPadding: 0,
),
child: HomeBannerWidget(posters: homeCarouselItems),
),
),
),
},
@ -122,6 +133,7 @@ class _DashboardScreenState extends ConsumerState<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,9 +27,12 @@ class HomeBannerWidget extends ConsumerWidget {
const SizedBox(height: 24)
],
),
HomeBanner.banner => MediaBanner(
items: posters,
maxHeight: maxHeight,
HomeBanner.banner => Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: MediaBanner(
items: posters,
maxHeight: maxHeight,
),
),
_ => const SizedBox.shrink(),
};

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,36 +37,42 @@ class EmptyItem extends ConsumerWidget {
}
},
),
content: (padding) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: AspectRatio(
aspectRatio: 0.67,
child: Card(
elevation: 6,
color: Theme.of(context).colorScheme.secondaryContainer,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1.0,
color: Colors.white.withValues(alpha: 0.10),
content: (padding) => Center(
child: Padding(
padding: padding,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: AspectRatio(
aspectRatio: 0.67,
child: Card(
elevation: 6,
color: Theme.of(context).colorScheme.secondaryContainer,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1.0,
color: Colors.white.withValues(alpha: 0.10),
),
borderRadius: FladderTheme.defaultShape.borderRadius,
),
child: FladderImage(
image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull,
placeHolder: PosterPlaceholder(item: item),
),
),
borderRadius: FladderTheme.defaultShape.borderRadius,
),
child: FladderImage(
image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull,
placeHolder: PosterPlaceholder(item: item),
),
),
),
Text(
item.title,
style: Theme.of(context).textTheme.titleLarge,
),
Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."),
].addInBetween(const SizedBox(height: 32)),
),
Text(
item.title,
style: Theme.of(context).textTheme.titleLarge,
),
Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."),
].addInBetween(const SizedBox(height: 32)),
),
),
);
}

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,25 +55,19 @@ class FavouritesScreen extends ConsumerWidget {
),
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
(e) => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: PosterGrid(
stickyHeader: true,
name: e.key.label(context),
posters: e.value,
),
child: PosterRow(
contentPadding: padding,
label: e.key.label(context),
posters: e.value,
),
),
),
if (favourites.people.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: PosterGrid(
stickyHeader: true,
name: "People",
posters: favourites.people,
),
child: PosterRow(
contentPadding: padding,
label: context.localized.actor(favourites.people.length),
posters: favourites.people,
),
),
const DefautlSliverBottomPadding(),

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,
))
.toList(),
);
return Padding(
padding: AdaptiveLayout.of(context).isDesktop
? EdgeInsets.only(top: MediaQuery.of(context).padding.top)
: EdgeInsets.zero,
child: ClipRRect(
borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0),
child: Card(
margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero,
elevation: 2,
child: Scaffold(
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
floatingActionButton: tabs[tabController.index].floatingActionButton,
floatingActionButtonLocation: FloatingActionButtonLocation.endContained,
appBar: AppBar(
centerTitle: true,
backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null,
title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40,
bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null,
),
extendBody: true,
body: Padding(
padding: !AdaptiveLayout.of(context).isDesktop
? EdgeInsets.only(
left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right)
: EdgeInsets.zero,
child: TabBarView(
controller: tabController,
children: tabs
.map((e) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: e.page,
))
.toList(),
),
),
final libraryScreenState = ref.watch(libraryScreenProvider);
final views = libraryScreenState.views;
final recommendations = libraryScreenState.recommendations;
final favourites = libraryScreenState.favourites;
final selectedView = libraryScreenState.selectedViewModel;
final viewTypes = libraryScreenState.viewType;
final genres = libraryScreenState.genres;
final padding = AdaptiveLayout.adaptivePadding(context);
return NestedScaffold(
background: BackgroundImage(
items: [
...recommendations.expand((e) => e.posters),
...favourites,
],
),
body: PullToRefresh(
refreshOnStart: true,
refreshKey: refreshKey,
onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
child: SizedBox.expand(
child: CustomScrollView(
controller: AdaptiveLayout.scrollOf(context),
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
const DefaultSliverTopBadding(),
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar(
route: LibrarySearchRoute(),
parent: context,
),
if (views.isNotEmpty)
SliverToBoxAdapter(
child: LibraryRow(
padding: padding,
views: views,
selectedView: libraryScreenState.selectedViewModel,
onSelected: (view) {
ref.read(libraryScreenProvider.notifier).selectLibrary(view);
refreshKey?.currentState?.show();
},
),
),
if (selectedView != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 16),
child: SizedBox(
height: 40,
child: ListView(
padding: padding,
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
FilledButton.tonalIcon(
onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)),
label: Text("${context.localized.search} ${selectedView.name}..."),
icon: const Icon(IconsaxPlusLinear.search_normal),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ExpressiveButtonGroup(
multiSelection: true,
options: LibraryViewType.values
.map((element) => ButtonGroupOption(
value: element,
icon: Icon(element.icon),
selected: Icon(element.iconSelected),
child: Text(
element.label(context),
)))
.toList(),
selectedValues: viewTypes,
onSelected: (value) {
ref.read(libraryScreenProvider.notifier).setViewType(value);
},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ElevatedButton.icon(
onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name),
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
),
],
),
),
),
),
if (viewTypes.isEmpty)
SliverFillRemaining(
child: Center(child: Text(context.localized.noResults)),
),
if (viewTypes.contains(LibraryViewType.recommended)) ...[
if (recommendations.isNotEmpty)
...recommendations.where((element) => element.posters.isNotEmpty).map(
(element) => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
posters: element.posters,
label: element.type != null
? "${element.type?.label(context)} - ${element.name.label(context)}"
: element.name.label(context),
),
),
),
),
],
if (viewTypes.contains(LibraryViewType.favourites))
if (favourites.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
posters: favourites,
label: context.localized.favorites,
),
),
),
if (viewTypes.contains(LibraryViewType.genres)) ...[
if (genres.isNotEmpty)
...genres.where((element) => element.posters.isNotEmpty).map(
(element) => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
posters: element.posters,
label: element.type != null
? "${element.type?.label(context)} - ${element.name.label(context)}"
: element.name.label(context),
),
),
),
)
],
const DefautlSliverBottomPadding(),
],
),
),
),
);
}
}
class LibraryRow extends ConsumerWidget {
const LibraryRow({
super.key,
required this.views,
this.selectedView,
required this.padding,
this.onSelected,
});
final List<ViewModel> views;
final ViewModel? selectedView;
final EdgeInsets padding;
final FutureOr Function(ViewModel selected)? onSelected;
@override
Widget build(BuildContext context, WidgetRef ref) {
return HorizontalList(
label: context.localized.library(views.length),
items: views,
startIndex: selectedView != null ? views.indexOf(selectedView!) : null,
height: 165,
contentPadding: padding,
itemBuilder: (context, index) {
final view = views[index];
final isSelected = selectedView == view;
final List<ItemActionButton> viewActions = [
ItemActionButton(
label: Text(context.localized.search),
icon: const Icon(IconsaxPlusLinear.search_normal),
action: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
),
ItemActionButton(
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
action: () => showRefreshPopup(context, view.id, view.name),
)
];
return FlatButton(
onTap: isSelected ? null : () => onSelected?.call(view),
onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position =
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
await showMenu(
context: context,
position: position,
items: viewActions.popupMenuItems(useIcons: true),
);
},
child: Card(
color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null,
shadowColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
SizedBox(
width: 200,
child: Card(
child: AspectRatio(
aspectRatio: 1.60,
child: FladderImage(
image: view.imageData?.primary,
fit: BoxFit.cover,
placeHolder: Center(
child: Text(
view.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
),
),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Row(
spacing: 8,
children: [
if (isSelected)
Container(
height: 12,
width: 12,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
),
Text(
view.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
),
],
),
),
),
],
),
),
),
);
},
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/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: Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
floatingActionButtonAnimator:
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
floatingActionButtonLocation:
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
floatingActionButton: switch (playerState) {
VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: FloatingPlayerBar(),
),
_ => HideOnScroll(
child: NestedScaffold(
background: BackgroundImage(items: librarySearchResults.activePosters),
body: Padding(
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
child: Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
floatingActionButton: HideOnScroll(
controller: scrollController,
visibleBuilder: (visible) => Column(
crossAxisAlignment: CrossAxisAlignment.end,
@ -206,29 +196,26 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
].addInBetween(const SizedBox(height: 10)),
),
),
},
bottomNavigationBar: HideOnScroll(
controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
child: IgnorePointer(
ignoring: librarySearchResults.fetchingItems,
child: _LibrarySearchBottomBar(
uniqueKey: uniqueKey,
refreshKey: refreshKey,
scrollController: scrollController,
libraryProvider: libraryProvider,
postersList: postersList,
bottomNavigationBar: HideOnScroll(
controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
child: IgnorePointer(
ignoring: librarySearchResults.fetchingItems,
child: _LibrarySearchBottomBar(
uniqueKey: uniqueKey,
refreshKey: refreshKey,
scrollController: scrollController,
libraryProvider: libraryProvider,
postersList: postersList,
),
),
),
),
),
body: Stack(
children: [
Positioned.fill(
child: Card(
elevation: 1,
child: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: MediaQuery.removeViewInsets(
context: context,
body: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: PinchPosterZoom(
scaleDifference: (difference) =>
ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
child: ClipRRect(
borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
? BorderRadius.circular(15)
@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState<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,36 +494,36 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
),
),
),
),
),
if (librarySearchResults.fetchingItems) ...[
Container(
color: Colors.black.withValues(alpha: 0.1),
),
Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
if (librarySearchResults.fetchingItems) ...[
Container(
color: Colors.black.withValues(alpha: 0.1),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator.adaptive(),
Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium),
IconButton(
onPressed: () => libraryProvider.cancelFetch(),
icon: const Icon(IconsaxPlusLinear.close_square),
)
].addInBetween(const SizedBox(width: 16)),
Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator.adaptive(),
Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium),
IconButton(
onPressed: () => libraryProvider.cancelFetch(),
icon: const Icon(IconsaxPlusLinear.close_square),
)
].addInBetween(const SizedBox(width: 16)),
),
),
),
),
),
)
],
],
)
],
],
),
),
),
),
);
@ -663,173 +643,156 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
icon: const Icon(IconsaxPlusLinear.save_add),
),
];
return NestedBottomAppBar(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Row(
return Padding(
padding: EdgeInsets.only(left: MediaQuery.paddingOf(context).left),
child: NestedBottomAppBar(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
ScrollStatePosition(
controller: scrollController,
positionBuilder: (state) => AnimatedFadeSize(
child: state != ScrollState.top
? Tooltip(
message: context.localized.scrollToTop,
child: FlatButton(
clipBehavior: Clip.antiAlias,
elevation: 0,
borderRadiusGeometry: BorderRadius.circular(6),
onTap: () => scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
Row(
spacing: 6,
children: [
ScrollStatePosition(
controller: scrollController,
positionBuilder: (state) => AnimatedFadeSize(
child: state != ScrollState.top
? Tooltip(
message: context.localized.scrollToTop,
child: IconButton.filled(
onPressed: () => scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic),
icon: const Icon(
IconsaxPlusLinear.arrow_up,
),
),
padding: const EdgeInsets.all(6),
child: Icon(
IconsaxPlusLinear.arrow_up,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
)
: const SizedBox(),
),
),
const SizedBox(width: 6),
if (!librarySearchResults.selecteMode) ...{
const SizedBox(width: 6),
IconButton(
tooltip: context.localized.sortBy,
onPressed: () async {
final newOptions = await openSortByDialogue(
context,
libraryProvider: libraryProvider,
uniqueKey: uniqueKey,
options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
);
if (newOptions != null) {
if (newOptions.$1 != null) {
libraryProvider.setSortBy(newOptions.$1!);
}
if (newOptions.$2 != null) {
libraryProvider.setSortOrder(newOptions.$2!);
}
}
},
icon: const Icon(IconsaxPlusLinear.sort),
),
if (librarySearchResults.hasActiveFilters) ...{
const SizedBox(width: 6),
IconButton(
tooltip: context.localized.disableFilters,
onPressed: disableFilters(librarySearchResults, libraryProvider),
icon: const Icon(IconsaxPlusLinear.filter_remove),
),
},
},
const SizedBox(width: 6),
IconButton(
onPressed: () => libraryProvider.toggleSelectMode(),
color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null,
icon: const Icon(IconsaxPlusLinear.category_2),
),
const SizedBox(width: 6),
AnimatedFadeSize(
child: librarySearchResults.selecteMode
? Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16)),
child: Row(
children: [
Tooltip(
message: context.localized.selectAll,
child: IconButton(
onPressed: () => libraryProvider.selectAll(true),
icon: const Icon(IconsaxPlusLinear.box_add),
),
),
const SizedBox(width: 6),
Tooltip(
message: context.localized.clearSelection,
child: IconButton(
onPressed: () => libraryProvider.selectAll(false),
icon: const Icon(IconsaxPlusLinear.box_remove),
),
),
const SizedBox(width: 6),
if (librarySearchResults.selectedPosters.isNotEmpty) ...{
if (AdaptiveLayout.of(context).isDesktop)
PopupMenuButton(
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
)
else
IconButton(
onPressed: () {
showBottomSheetPill(
context: context,
content: (context, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: actions.listTileItems(context, useIcons: true),
),
);
},
icon: const Icon(IconsaxPlusLinear.more))
},
],
),
)
: const SizedBox(),
),
const Spacer(),
if (librarySearchResults.activePosters.isNotEmpty)
IconButton(
tooltip: context.localized.random,
onPressed: () => libraryProvider.openRandom(context),
icon: Card(
color: Theme.of(context).colorScheme.secondary,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Icon(
IconsaxPlusBold.arrow_up_1,
color: Theme.of(context).colorScheme.onSecondary,
),
)
: const SizedBox(),
),
),
),
if (librarySearchResults.activePosters.isNotEmpty)
IconButton(
tooltip: context.localized.shuffleVideos,
onPressed: () async {
if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) {
libraryProvider.viewGallery(context, shuffle: true);
return;
} else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) {
libraryProvider.playLibraryItems(context, ref, shuffle: true);
return;
}
await showLibraryPlayOptions(
context,
context.localized.libraryShuffleAndPlayItems,
playVideos: librarySearchResults.showPlayButtons
? () => libraryProvider.playLibraryItems(context, ref, shuffle: true)
: null,
viewGallery: librarySearchResults.showGalleryButtons
? () => libraryProvider.viewGallery(context, shuffle: true)
: null,
);
if (!librarySearchResults.selecteMode) ...{
IconButton(
tooltip: context.localized.sortBy,
onPressed: () async {
final newOptions = await openSortByDialogue(
context,
libraryProvider: libraryProvider,
uniqueKey: uniqueKey,
options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
);
if (newOptions != null) {
if (newOptions.$1 != null) {
libraryProvider.setSortBy(newOptions.$1!);
}
if (newOptions.$2 != null) {
libraryProvider.setSortOrder(newOptions.$2!);
}
}
},
icon: const Icon(IconsaxPlusLinear.sort),
),
if (librarySearchResults.hasActiveFilters) ...{
IconButton(
tooltip: context.localized.disableFilters,
onPressed: disableFilters(librarySearchResults, libraryProvider),
icon: const Icon(IconsaxPlusLinear.filter_remove),
),
},
},
icon: const Icon(IconsaxPlusLinear.shuffle),
),
IconButton(
onPressed: () => libraryProvider.toggleSelectMode(),
color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null,
icon: const Icon(IconsaxPlusLinear.category_2),
),
AnimatedFadeSize(
child: librarySearchResults.selecteMode
? Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16)),
child: Row(
spacing: 6,
children: [
Tooltip(
message: context.localized.selectAll,
child: IconButton(
onPressed: () => libraryProvider.selectAll(true),
icon: const Icon(IconsaxPlusLinear.box_add),
),
),
Tooltip(
message: context.localized.clearSelection,
child: IconButton(
onPressed: () => libraryProvider.selectAll(false),
icon: const Icon(IconsaxPlusLinear.box_remove),
),
),
if (librarySearchResults.selectedPosters.isNotEmpty) ...{
if (AdaptiveLayout.of(context).isDesktop)
PopupMenuButton(
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
)
else
IconButton(
onPressed: () {
showBottomSheetPill(
context: context,
content: (context, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: actions.listTileItems(context, useIcons: true),
),
);
},
icon: const Icon(IconsaxPlusLinear.more))
},
],
),
)
: const SizedBox(),
),
const Spacer(),
if (librarySearchResults.activePosters.isNotEmpty)
IconButton.filledTonal(
tooltip: context.localized.random,
onPressed: () => libraryProvider.openRandom(context),
icon: const Icon(
IconsaxPlusBold.arrow_up_1,
),
),
if (librarySearchResults.activePosters.isNotEmpty)
IconButton(
tooltip: context.localized.shuffleVideos,
onPressed: () async {
if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) {
libraryProvider.viewGallery(context, shuffle: true);
return;
} else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) {
libraryProvider.playLibraryItems(context, ref, shuffle: true);
return;
}
await showLibraryPlayOptions(
context,
context.localized.libraryShuffleAndPlayItems,
playVideos: librarySearchResults.showPlayButtons
? () => libraryProvider.playLibraryItems(context, ref, shuffle: true)
: null,
viewGallery: librarySearchResults.showGalleryButtons
? () => libraryProvider.viewGallery(context, shuffle: true)
: null,
);
},
icon: const Icon(IconsaxPlusLinear.shuffle),
),
],
),
],
),
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 8),
],
),
),
);
}

View file

@ -1,221 +1,193 @@
import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/library_search/library_search_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/providers/library_search_provider.dart';
import 'package:fladder/screens/library_search/widgets/library_views.dart';
import 'package:fladder/screens/shared/chips/category_chip.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/shared/scroll_position.dart';
class LibraryFilterChips extends ConsumerWidget {
final Key uniqueKey;
final ScrollController controller;
final LibrarySearchModel librarySearchResults;
final LibrarySearchNotifier libraryProvider;
final List<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) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: libraryFilterChips(
context,
ref,
uniqueKey,
librarySearchResults: librarySearchResults,
libraryProvider: libraryProvider,
postersList: postersList,
libraryViewType: libraryViewType,
).addPadding(const EdgeInsets.symmetric(horizontal: 8)),
);
},
);
}
ConsumerState<ConsumerStatefulWidget> createState() => _LibraryFilterChipsState();
}
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(
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(
spacing: 8,
children: [
if (librarySearchResults.folderOverwrite.isEmpty)
CategoryChip(
label: Text(context.localized.library(2)),
items: librarySearchResults.views,
labelBuilder: (item) => Text(item.name),
onSave: (value) => libraryProvider.setViews(value),
onCancel: () => libraryProvider.setViews(librarySearchResults.views),
onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
),
CategoryChip<FladderItemType>(
label: Text(context.localized.type(librarySearchResults.types.length)),
items: librarySearchResults.types,
labelBuilder: (item) => Row(
children: [
Icon(item.icon),
const SizedBox(width: 12),
Text(item.label(context)),
],
),
onSave: (value) => libraryProvider.setTypes(value),
onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)),
),
FilterChip(
label: Text(context.localized.favorites),
avatar: Icon(
favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
color: Theme.of(context).colorScheme.onSurface,
),
selected: favourites,
showCheckmark: false,
onSelected: (_) {
libraryProvider.toggleFavourite();
context.refreshData();
},
),
FilterChip(
label: Text(context.localized.recursive),
selected: recursive,
onSelected: (_) {
libraryProvider.toggleRecursive();
context.refreshData();
},
),
if (librarySearchResults.genres.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.genre(librarySearchResults.genres.length)),
activeIcon: IconsaxPlusBold.hierarchy_2,
items: librarySearchResults.genres,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setGenres(value),
onCancel: () => libraryProvider.setGenres(librarySearchResults.genres),
onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)),
),
if (librarySearchResults.studios.isNotEmpty)
CategoryChip<Studio>(
label: Text(context.localized.studio(librarySearchResults.studios.length)),
activeIcon: IconsaxPlusBold.airdrop,
items: librarySearchResults.studios,
labelBuilder: (item) => Text(item.name),
onSave: (value) => libraryProvider.setStudios(value),
onCancel: () => libraryProvider.setStudios(librarySearchResults.studios),
onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)),
),
if (librarySearchResults.tags.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.label(librarySearchResults.tags.length)),
activeIcon: Icons.label_rounded,
items: librarySearchResults.tags,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setTags(value),
onCancel: () => libraryProvider.setTags(librarySearchResults.tags),
onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)),
),
FilterChip(
label: Text(context.localized.group),
selected: groupBy != GroupBy.none,
onSelected: (_) {
_openGroupDialogue(context, ref, libraryProvider, uniqueKey);
},
),
CategoryChip<ItemFilter>(
label: Text(context.localized.filter(librarySearchResults.filters.length)),
items: librarySearchResults.filters,
labelBuilder: (item) => Text(item.label(context)),
onSave: (value) => libraryProvider.setFilters(value),
onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)),
),
if (librarySearchResults.types[FladderItemType.series] == true)
FilterChip(
avatar: Icon(
hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded,
color: Theme.of(context).colorScheme.onSurface,
),
selected: hideEmpty,
showCheckmark: false,
label: Text(context.localized.hideEmpty),
onSelected: libraryProvider.setHideEmpty,
),
if (librarySearchResults.officialRatings.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.rating(librarySearchResults.officialRatings.length)),
activeIcon: Icons.star_rate_rounded,
items: librarySearchResults.officialRatings,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setRatings(value),
onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings),
onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)),
),
if (librarySearchResults.years.isNotEmpty)
CategoryChip<int>(
label: Text(context.localized.year(librarySearchResults.years.length)),
items: librarySearchResults.years,
labelBuilder: (item) => Text(item.toString()),
onSave: (value) => libraryProvider.setYears(value),
onCancel: () => libraryProvider.setYears(librarySearchResults.years),
onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
),
],
);
}
void _openGroupDialogue(
BuildContext context,
WidgetRef ref,
LibrarySearchNotifier provider,
Key uniqueKey,
) {
showDialog(
context: context,
builder: (context) {
return Consumer(
builder: (context, ref, child) {
return AlertDialog(
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.65,
child: ListView(
shrinkWrap: true,
children: [
Text(context.localized.groupBy),
...GroupBy.values.map((groupBy) => RadioListTile.adaptive(
value: groupBy,
title: Text(groupBy.value(context)),
groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)),
onChanged: (value) {
libraryProvider.setGroupBy(groupBy);
Navigator.pop(context);
},
)),
],
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
return AlertDialog(
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.65,
child: ListView(
shrinkWrap: true,
children: [
Text(context.localized.groupBy),
...GroupBy.values.map(
(group) => RadioListTile.adaptive(
value: group,
groupValue: groupBy,
title: Text(group.value(context)),
onChanged: (_) {
provider.setGroupBy(group);
Navigator.pop(context);
},
),
),
),
);
},
],
),
),
);
},
);
}
return [
if (librarySearchResults.folderOverwrite.isEmpty)
CategoryChip(
label: Text(context.localized.library(2)),
items: librarySearchResults.views,
labelBuilder: (item) => Text(item.name),
onSave: (value) => libraryProvider.setViews(value),
onCancel: () => libraryProvider.setViews(librarySearchResults.views),
onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
),
CategoryChip<FladderItemType>(
label: Text(context.localized.type(librarySearchResults.types.length)),
items: librarySearchResults.types,
labelBuilder: (item) => Row(
children: [
Icon(item.icon),
const SizedBox(width: 12),
Text(item.label(context)),
],
),
onSave: (value) => libraryProvider.setTypes(value),
onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)),
),
FilterChip(
label: Text(context.localized.favorites),
avatar: Icon(
librarySearchResults.favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
color: Theme.of(context).colorScheme.onSurface,
),
selected: librarySearchResults.favourites,
showCheckmark: false,
onSelected: (value) {
libraryProvider.toggleFavourite();
context.refreshData();
},
),
FilterChip(
label: Text(context.localized.recursive),
selected: librarySearchResults.recursive,
onSelected: (value) {
libraryProvider.toggleRecursive();
context.refreshData();
},
),
if (librarySearchResults.genres.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.genre(librarySearchResults.genres.length)),
activeIcon: IconsaxPlusBold.hierarchy_2,
items: librarySearchResults.genres,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setGenres(value),
onCancel: () => libraryProvider.setGenres(librarySearchResults.genres),
onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)),
),
if (librarySearchResults.studios.isNotEmpty)
CategoryChip<Studio>(
label: Text(context.localized.studio(librarySearchResults.studios.length)),
activeIcon: IconsaxPlusBold.airdrop,
items: librarySearchResults.studios,
labelBuilder: (item) => Text(item.name),
onSave: (value) => libraryProvider.setStudios(value),
onCancel: () => libraryProvider.setStudios(librarySearchResults.studios),
onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)),
),
if (librarySearchResults.tags.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.label(librarySearchResults.tags.length)),
activeIcon: Icons.label_rounded,
items: librarySearchResults.tags,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setTags(value),
onCancel: () => libraryProvider.setTags(librarySearchResults.tags),
onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)),
),
FilterChip(
label: Text(context.localized.group),
selected: librarySearchResults.groupBy != GroupBy.none,
onSelected: (value) {
openGroupDialogue();
},
),
CategoryChip<ItemFilter>(
label: Text(context.localized.filter(librarySearchResults.filters.length)),
items: librarySearchResults.filters,
labelBuilder: (item) => Text(item.label(context)),
onSave: (value) => libraryProvider.setFilters(value),
onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)),
),
if (librarySearchResults.types[FladderItemType.series] == true)
FilterChip(
avatar: Icon(
librarySearchResults.hideEmptyShows ? Icons.visibility_off_rounded : Icons.visibility_rounded,
color: Theme.of(context).colorScheme.onSurface,
),
selected: librarySearchResults.hideEmptyShows,
showCheckmark: false,
label: Text(context.localized.hideEmpty),
onSelected: libraryProvider.setHideEmpty,
),
if (librarySearchResults.officialRatings.isNotEmpty)
CategoryChip<String>(
label: Text(context.localized.rating(librarySearchResults.officialRatings.length)),
activeIcon: Icons.star_rate_rounded,
items: librarySearchResults.officialRatings,
labelBuilder: (item) => Text(item),
onSave: (value) => libraryProvider.setRatings(value),
onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings),
onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)),
),
if (librarySearchResults.years.isNotEmpty)
CategoryChip<int>(
label: Text(context.localized.year(librarySearchResults.years.length)),
items: librarySearchResults.years,
labelBuilder: (item) => Text(item.toString()),
onSave: (value) => libraryProvider.setYears(value),
onCancel: () => libraryProvider.setYears(librarySearchResults.years),
onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
),
];
}

View file

@ -1,7 +1,16 @@
import 'dart:ui';
import '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,179 +109,139 @@ class LibraryViews extends ConsumerWidget {
switch (ref.watch(libraryViewTypeProvider)) {
case LibraryViewTypes.grid:
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
return SliverList.builder(
itemCount: groupedItems.length,
Widget createGrid(List<ItemBaseModel> items) {
return SliverGrid.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: posterSize.toInt(),
mainAxisSpacing: (8 * decimal) + 8,
crossAxisSpacing: (8 * decimal) + 8,
childAspectRatio: items.getMostCommonType.aspectRatio,
),
itemCount: items.length,
itemBuilder: (context, index) {
final name = groupedItems.keys.elementAt(index);
final group = groupedItems[name];
if (group?.isEmpty ?? false || group == null) {
return Text(context.localized.empty);
}
return PosterGrid(
posters: group!,
name: name,
itemBuilder: (context, index) {
final item = group[index];
return PosterWidget(
key: Key(item.id),
poster: group[index],
maxLines: 2,
heroTag: true,
subTitle: item.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(item),
selected: selected.contains(item),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
final item = items[index];
return PosterWidget(
key: Key(item.id),
poster: item,
maxLines: 2,
heroTag: true,
subTitle: item.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(item),
selected: selected.contains(item),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
);
}
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
return MultiSliver(
children: groupedItems.entries.map(
(element) {
final name = element.key;
final group = element.value;
return stickyHeaderBuilder(
context,
header: name,
sliver: createGrid(group),
);
},
).toList());
} else {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverGrid.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: posterSize.toInt(),
mainAxisSpacing: (8 * decimal) + 8,
crossAxisSpacing: (8 * decimal) + 8,
childAspectRatio: AdaptiveLayout.poster(context).ratio,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return PosterWidget(
key: Key(item.id),
poster: item,
maxLines: 2,
heroTag: true,
subTitle: item.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(item),
selected: selected.contains(item),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
),
sliver: createGrid(items),
);
}
case LibraryViewTypes.list:
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
Widget listBuilder(List<ItemBaseModel> items) {
return SliverList.builder(
itemCount: groupedItems.length,
itemCount: items.length,
itemBuilder: (context, index) {
final name = groupedItems.keys.elementAt(index);
final group = groupedItems[name];
if (group?.isEmpty ?? false) {
return Text(context.localized.empty);
}
return StickyHeader(
header: Text(name, style: Theme.of(context).textTheme.headlineSmall),
content: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: group?.length,
itemBuilder: (context, index) {
final poster = group![index];
return PosterListItem(
key: Key(poster.id),
poster: poster,
subTitle: poster.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(poster),
selected: selected.contains(poster),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
),
final poster = items[index];
return PosterListItem(
poster: poster,
selected: selected.contains(poster),
excludeActions: excludeActions,
otherActions: otherActions(poster),
subTitle: poster.subTitle(sortingOptions),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
);
}
return SliverList.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final poster = items[index];
return PosterListItem(
poster: poster,
selected: selected.contains(poster),
excludeActions: excludeActions,
otherActions: otherActions(poster),
subTitle: poster.subTitle(sortingOptions),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
);
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
return MultiSliver(
children: groupedItems.entries.map(
(element) {
final name = element.key;
final group = element.value;
return stickyHeaderBuilder(
context,
header: name,
sliver: listBuilder(group),
);
},
).toList());
}
return listBuilder(items);
case LibraryViewTypes.masonry:
if (groupByType != GroupBy.none) {
final groupedItems = groupItemsBy(context, items, groupByType);
return SliverList.builder(
itemCount: groupedItems.length,
itemBuilder: (context, index) {
final name = groupedItems.keys.elementAt(index);
final group = groupedItems[name];
if (group?.isEmpty ?? false) {
return Text(context.localized.empty);
}
return Padding(
padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0),
child: StickyHeader(
header: Text(name, style: Theme.of(context).textTheme.headlineMedium),
overlapHeaders: true,
content: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: MasonryGridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: (8 * decimal) + 8,
crossAxisSpacing: (8 * decimal) + 8,
gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
(MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
.toDouble() *
20,
),
itemCount: group!.length,
itemBuilder: (context, index) {
final item = group[index];
return PosterWidget(
key: Key(item.id),
poster: item,
aspectRatio: item.primaryRatio,
selected: selected.contains(item),
inlineTitle: true,
heroTag: true,
subTitle: item.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(group[index]),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
),
)),
return MultiSliver(
children: groupedItems.entries.map(
(element) {
final name = element.key;
final group = element.value;
return stickyHeaderBuilder(
context,
header: name,
//MasonryGridView because SliverMasonryGrid breaks scrolling
sliver: SliverToBoxAdapter(
child: MasonryGridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: (8 * decimal) + 8,
crossAxisSpacing: (8 * decimal) + 8,
gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
(MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0))
.toDouble() *
12,
),
itemCount: group.length,
itemBuilder: (context, index) {
final item = group[index];
return PosterWidget(
key: Key(item.id),
poster: item,
aspectRatio: item.primaryRatio,
selected: selected.contains(item),
inlineTitle: true,
heroTag: true,
subTitle: item.subTitle(sortingOptions),
excludeActions: excludeActions,
otherActions: otherActions(group[index]),
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
);
},
),
),
);
},
);
).toList());
} else {
return SliverMasonryGrid.count(
mainAxisSpacing: (8 * decimal) + 8,
@ -309,6 +271,36 @@ class LibraryViews extends ConsumerWidget {
}
}
SliverStickyHeader stickyHeaderBuilder(
BuildContext context, {
required String header,
Widget? sliver,
}) {
return SliverStickyHeader(
header: Container(
height: 50,
alignment: Alignment.centerLeft,
child: Transform.translate(
offset: const Offset(-20, 0),
child: Container(
decoration: BoxDecoration(
color: context.colors.surface.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
header,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
),
),
),
sliver: sliver,
);
}
Map<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,39 +136,45 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
}
},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: SizedBox(
height: 50,
child: Row(
children: [
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: AspectRatio(
aspectRatio: 0.8,
child: FladderImage(
image: suggestion.images?.primary,
fit: BoxFit.cover,
title: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 50,
maxHeight: 65,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: AspectRatio(
aspectRatio: 0.8,
child: FladderImage(
image: suggestion.images?.primary,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: Text(
suggestion.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
if (suggestion.overview.yearAired.toString().isNotEmpty)
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child:
Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
],
child: Text(
suggestion.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
if (suggestion.overview.yearAired.toString().isNotEmpty)
Flexible(
child: Opacity(
opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
],
),
),
),
],
],
),
),
),
);

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(
showDividers: false,
title: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
return Card(
child: ActionContent(
showDividers: false,
title: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
@ -89,137 +88,137 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
)
],
),
),
child: TabBarView(
controller: tabController,
children: [
inputFields(state),
if (posters.isEmpty)
Center(
child: processing
? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round)
: Text(context.localized.noResults),
)
else
Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(context.localized.replaceAllImages),
const SizedBox(width: 16),
Switch.adaptive(
value: state.replaceAllImages,
onChanged: (value) {
ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value));
},
),
],
),
Flexible(
child: ListView(
shrinkWrap: true,
children: posters
.map((result) => ListTile(
title: Row(
children: [
SizedBox(
width: 75,
child: Card(
child: CachedNetworkImage(
imageUrl: result.imageUrl ?? "",
errorWidget: (context, url, error) => SizedBox(
height: 75,
child: Card(
child: Center(
child: Text(result.name?.getInitials() ?? ""),
child: TabBarView(
controller: tabController,
children: [
inputFields(state),
if (posters.isEmpty)
Center(
child: processing
? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round)
: Text(context.localized.noResults),
)
else
Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(context.localized.replaceAllImages),
const SizedBox(width: 16),
Switch.adaptive(
value: state.replaceAllImages,
onChanged: (value) {
ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value));
},
),
],
),
Flexible(
child: ListView(
shrinkWrap: true,
children: posters
.map((result) => ListTile(
title: Row(
children: [
SizedBox(
width: 75,
child: Card(
child: CachedNetworkImage(
imageUrl: result.imageUrl ?? "",
errorWidget: (context, url, error) => SizedBox(
height: 75,
child: Card(
child: Center(
child: Text(result.name?.getInitials() ?? ""),
),
),
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"),
Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? ""))
],
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"),
Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? ""))
],
),
),
),
Tooltip(
message: context.localized.openWebLink,
child: IconButton(
onPressed: () {
final providerKeyEntry = result.providerIds?.entries.first;
final providerKey = providerKeyEntry?.key;
final providerValue = providerKeyEntry?.value;
Tooltip(
message: context.localized.openWebLink,
child: IconButton(
onPressed: () {
final providerKeyEntry = result.providerIds?.entries.first;
final providerKey = providerKeyEntry?.key;
final providerValue = providerKeyEntry?.value;
final externalId = state.externalIds
.firstWhereOrNull((element) => element.key == providerKey)
// ignore: deprecated_member_use_from_same_package
?.urlFormatString;
final externalId = state.externalIds
.firstWhereOrNull((element) => element.key == providerKey)
// ignore: deprecated_member_use_from_same_package
?.urlFormatString;
final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? "");
final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? "");
launchUrl(context, url ?? "");
},
icon: const Icon(Icons.launch_rounded)),
),
Tooltip(
message: "Select result",
child: IconButton(
onPressed: !processing
? () async {
final response = await ref.read(provider.notifier).setIdentity(result);
if (response?.isSuccessful == true) {
fladderSnackbar(context,
title: context.localized.setIdentityTo(result.name ?? ""));
} else {
fladderSnackbarResponse(context, response,
altTitle: context.localized.somethingWentWrong);
launchUrl(context, url ?? "");
},
icon: const Icon(Icons.launch_rounded)),
),
Tooltip(
message: "Select result",
child: IconButton(
onPressed: !processing
? () async {
final response = await ref.read(provider.notifier).setIdentity(result);
if (response?.isSuccessful == true) {
fladderSnackbar(context,
title: context.localized.setIdentityTo(result.name ?? ""));
} else {
fladderSnackbarResponse(context, response,
altTitle: context.localized.somethingWentWrong);
}
Navigator.of(context).pop();
}
Navigator.of(context).pop();
}
: null,
icon: const Icon(Icons.save_alt_rounded),
),
)
],
),
))
.toList(),
: null,
icon: const Icon(Icons.save_alt_rounded),
),
)
],
),
))
.toList(),
),
),
),
],
)
],
)
],
),
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
const SizedBox(width: 16),
FilledButton(
onPressed: !processing
? () async {
await ref.read(provider.notifier).remoteSearch();
tabController.animateTo(1);
}
: null,
child: processing
? SizedBox(
width: 21,
height: 21,
child: CircularProgressIndicator.adaptive(
backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round),
)
: Text(context.localized.search),
),
],
),
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
const SizedBox(width: 16),
FilledButton(
onPressed: !processing
? () async {
await ref.read(provider.notifier).remoteSearch();
tabController.animateTo(1);
}
: null,
child: processing
? SizedBox(
width: 21,
height: 21,
child: CircularProgressIndicator.adaptive(
backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round),
)
: Text(context.localized.search),
),
],
);
}
@ -248,7 +247,7 @@ class _IdentifyScreenState extends ConsumerState<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,75 +42,79 @@ class AboutSettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final applicationInfo = ref.watch(applicationInfoProvider);
return Card(
child: SettingsScaffold(
label: "",
items: [
const FladderLogo(),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
const SizedBox(height: 16),
Text(context.localized.aboutCreatedBy),
],
return SettingsScaffold(
label: "",
items: [
const FladderLogo(),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
const SizedBox(height: 16),
Text(context.localized.aboutCreatedBy),
],
),
const FractionallySizedBox(
widthFactor: 0.25,
child: Divider(
indent: 16,
endIndent: 16,
),
const Divider(),
Column(
children: [
Text(
context.localized.aboutSocials,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: socials
.map(
(e) => IconButton.filledTonal(
onPressed: () => launchUrl(context, e.url),
icon: Column(
children: [
Icon(e.icon),
Text(e.label),
],
),
),
Column(
children: [
Text(
context.localized.aboutSocials,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: socials
.map(
(e) => IconButton.filledTonal(
onPressed: () => launchUrl(context, e.url),
icon: Column(
children: [
Icon(e.icon),
Text(e.label),
],
),
)
.toList()
.addInBetween(const SizedBox(width: 16)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showLicensePage(
context: context,
applicationIcon: const FladderIcon(size: 55),
applicationVersion: applicationInfo.versionPlatformBuild,
applicationLegalese: "DonutWare",
),
child: Text(context.localized.aboutLicenses),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showDialog(
context: context,
builder: (context) => const CrashScreen(),
),
child: Text(context.localized.errorLogs),
)
],
),
].addInBetween(const SizedBox(height: 16)),
),
),
)
.toList()
.addInBetween(const SizedBox(width: 16)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showLicensePage(
context: context,
applicationIcon: const FladderIcon(size: 55),
applicationVersion: applicationInfo.versionPlatformBuild,
applicationLegalese: "DonutWare",
),
child: Text(context.localized.aboutLicenses),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showDialog(
context: context,
builder: (context) => const CrashScreen(),
),
child: Text(context.localized.errorLogs),
)
],
),
].addInBetween(const SizedBox(height: 16)),
);
}
}

View file

@ -2,79 +2,83 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
List<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),
onTap: () async {
final newItems = await openMultiSelectOptions<ViewSize>(
context,
label: context.localized.settingsLayoutSizesTitle,
items: ViewSize.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
[
SettingsListTile(
label: Text(context.localized.settingsLayoutSizesTitle),
subLabel: Text(context.localized.settingsLayoutSizesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<ViewSize>(
context,
label: context.localized.settingsLayoutSizesTitle,
items: ViewSize.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
),
);
ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
.map((e) => e.label(context))
.join(', ')),
),
);
ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
.map((e) => e.label(context))
.join(', ')),
),
),
),
SettingsListTile(
label: Text(context.localized.settingsLayoutModesTitle),
subLabel: Text(context.localized.settingsLayoutModesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<LayoutMode>(
context,
label: context.localized.settingsLayoutModesTitle,
items: LayoutMode.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
SettingsListTile(
label: Text(context.localized.settingsLayoutModesTitle),
subLabel: Text(context.localized.settingsLayoutModesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<LayoutMode>(
context,
label: context.localized.settingsLayoutModesTitle,
items: LayoutMode.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
),
);
ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
.map((e) => e.label(context))
.join(', ')),
),
);
ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
.map((e) => e.label(context))
.join(', ')),
),
),
),
];
],
);
}

View file

@ -7,89 +7,92 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/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),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.homeBanner.label(context),
),
),
itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
.toList(),
),
),
if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
[
SettingsListTile(
label: Text(context.localized.settingsHomeBannerInformationTitle),
subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
label: Text(context.localized.settingsHomeBannerTitle),
subLabel: Text(context.localized.settingsHomeBannerDescription),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
homeSettingsProvider.select(
(value) => value.homeBanner.label(context),
),
),
itemBuilder: (context) => HomeCarouselSettings.values
itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref
.read(homeSettingsProvider.notifier)
.update((context) => context.copyWith(carouselSettings: entry)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
.toList(),
),
),
SettingsListTile(
label: Text(context.localized.settingsHomeNextUpTitle),
subLabel: Text(context.localized.settingsHomeNextUpDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.nextUp.label(context),
if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
SettingsListTile(
label: Text(context.localized.settingsHomeBannerInformationTitle),
subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
),
itemBuilder: (context) => HomeCarouselSettings.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref
.read(homeSettingsProvider.notifier)
.update((context) => context.copyWith(carouselSettings: entry)),
),
)
.toList(),
),
),
itemBuilder: (context) => HomeNextUp.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
),
)
.toList(),
SettingsListTile(
label: Text(context.localized.settingsHomeNextUpTitle),
subLabel: Text(context.localized.settingsHomeNextUpDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.nextUp.label(context),
),
),
itemBuilder: (context) => HomeNextUp.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
),
)
.toList(),
),
),
),
SettingsListTile(
label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
trailing: Switch(
value: clientSettings.showAllCollectionTypes,
onChanged: (value) => ref
SettingsListTile(
label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: value)),
.update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
trailing: Switch(
value: clientSettings.showAllCollectionTypes,
onChanged: (value) => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: value)),
),
),
),
const Divider(),
];
],
);
}

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,121 +25,122 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
return [
if (canSync && !kIsWeb) ...[
SettingsLabelDivider(label: context.localized.downloadsTitle),
if (AdaptiveLayout.of(context).isDesktop) ...[
SettingsListTile(
label: Text(context.localized.downloadsPath),
subLabel: Text(currentFolder ?? "-"),
onTap: currentFolder != null
? () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathEditTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
Navigator.of(context).pop();
},
child: Text(context.localized.change),
)
],
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [
if (AdaptiveLayout.of(context).isDesktop) ...[
SettingsListTile(
label: Text(context.localized.downloadsPath),
subLabel: Text(currentFolder ?? "-"),
onTap: currentFolder != null
? () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathEditTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
Navigator.of(context).pop();
},
child: Text(context.localized.change),
)
],
),
)
: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
},
trailing: currentFolder?.isNotEmpty == true
? IconButton(
color: Theme.of(context).colorScheme.error,
onPressed: () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathClearTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () {
ref.read(clientSettingsProvider.notifier).setSyncPath(null);
Navigator.of(context).pop();
},
child: Text(context.localized.clear),
)
],
),
),
icon: const Icon(IconsaxPlusLinear.folder_minus),
)
: () async {
String? selectedDirectory = await FilePicker.platform
.getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
: null,
),
],
FutureBuilder(
future: ref.watch(syncProvider.notifier).directorySize,
builder: (context, snapshot) {
final data = snapshot.data ?? 0;
return SettingsListTile(
label: Text(context.localized.downloadsSyncedData),
subLabel: Text(data.byteFormat ?? ""),
trailing: FilledButton(
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.downloadsClearTitle,
context.localized.downloadsClearDesc,
(context) async {
await ref.read(syncProvider.notifier).clear();
setState(() {});
Navigator.of(context).pop();
},
context.localized.clear,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
child: Text(context.localized.clear),
),
);
},
),
SettingsListTile(
label: Text(context.localized.clientSettingsRequireWifiTitle),
subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
trailing: Switch(
value: clientSettings.requireWifi,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
),
),
SettingsListTile(
label: Text(context.localized.maxConcurrentDownloadsTitle),
subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(
maxConcurrentDownloads: value,
),
);
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
trailing: currentFolder?.isNotEmpty == true
? IconButton(
color: Theme.of(context).colorScheme.error,
onPressed: () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathClearTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () {
ref.read(clientSettingsProvider.notifier).setSyncPath(null);
Navigator.of(context).pop();
},
child: Text(context.localized.clear),
)
],
),
),
icon: const Icon(IconsaxPlusLinear.folder_minus),
)
: null,
)),
),
],
FutureBuilder(
future: ref.watch(syncProvider.notifier).directorySize,
builder: (context, snapshot) {
final data = snapshot.data ?? 0;
return SettingsListTile(
label: Text(context.localized.downloadsSyncedData),
subLabel: Text(data.byteFormat ?? ""),
trailing: FilledButton(
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.downloadsClearTitle,
context.localized.downloadsClearDesc,
(context) async {
await ref.read(syncProvider.notifier).clear();
setState(() {});
Navigator.of(context).pop();
},
context.localized.clear,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
child: Text(context.localized.clear),
),
);
},
),
SettingsListTile(
label: Text(context.localized.clientSettingsRequireWifiTitle),
subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
trailing: Switch(
value: clientSettings.requireWifi,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
),
),
SettingsListTile(
label: Text(context.localized.maxConcurrentDownloadsTitle),
subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(
maxConcurrentDownloads: value,
),
);
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
)),
),
const Divider(),
]),
const SizedBox(height: 12),
],
];
}

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,136 +21,136 @@ 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(
context: context,
locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
child: Builder(builder: (context) {
String language = "Unknown";
try {
language = context.localized.nativeName;
} catch (e) {
log(e.toString());
}
return EnumBox(
current: language,
itemBuilder: (context) {
return [
...AppLocalizations.supportedLocales.map(
(entry) => PopupMenuItem(
value: entry,
child: Localizations.override(
context: context,
locale: entry,
child: Builder(builder: (context) {
return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
}),
[
SettingsListTile(
label: Text(context.localized.displayLanguage),
trailing: Localizations.override(
context: context,
locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
child: Builder(builder: (context) {
String language = "Unknown";
try {
language = context.localized.nativeName;
} catch (_) {}
return EnumBox(
current: language,
itemBuilder: (context) {
return [
...AppLocalizations.supportedLocales.map(
(entry) => PopupMenuItem(
value: entry,
child: Localizations.override(
context: context,
locale: entry,
child: Builder(builder: (context) {
return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
}),
),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((state) => state.copyWith(selectedLocale: entry)),
),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((state) => state.copyWith(selectedLocale: entry)),
)
];
},
);
}),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurredPlaceholderTitle),
subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
trailing: Switch(
value: clientSettings.blurPlaceHolders,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurEpisodesTitle),
subLabel: Text(context.localized.settingsBlurEpisodesDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
trailing: Switch(
value: clientSettings.blurUpcomingEpisodes,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsEnableOsMediaControls),
onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
trailing: Switch(
value: clientSettings.enableMediaKeys,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsNextUpCutoffDays),
trailing: SizedBox(
width: 100,
child: IntInputField(
suffix: context.localized.days,
controller: nextUpDaysEditor,
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
nextUpDateCutoff: Duration(days: value),
));
}
},
)),
),
SettingsListTile(
label: Text(context.localized.libraryPageSizeTitle),
subLabel: Text(context.localized.libraryPageSizeDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: libraryPageSizeController,
placeHolder: "500",
onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(libraryPageSize: value),
),
)
];
},
);
}),
)),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurredPlaceholderTitle),
subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
trailing: Switch(
value: clientSettings.blurPlaceHolders,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurEpisodesTitle),
subLabel: Text(context.localized.settingsBlurEpisodesDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
trailing: Switch(
value: clientSettings.blurUpcomingEpisodes,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsEnableOsMediaControls),
onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
trailing: Switch(
value: clientSettings.enableMediaKeys,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsNextUpCutoffDays),
trailing: SizedBox(
width: 100,
child: IntInputField(
suffix: context.localized.days,
controller: nextUpDaysEditor,
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
nextUpDateCutoff: Duration(days: value),
));
}
},
)),
),
SettingsListTile(
label: Text(context.localized.libraryPageSizeTitle),
subLabel: Text(context.localized.libraryPageSizeDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: libraryPageSizeController,
placeHolder: "500",
onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(libraryPageSize: value),
),
)),
),
SettingsListTile(
label: Text(AdaptiveLayout.of(context).isDesktop
? context.localized.settingsShowScaleSlider
: context.localized.settingsPosterPinch),
onTap: () => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
),
trailing: Switch(
value: clientSettings.pinchPosterZoom,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: value),
SettingsListTile(
label: Text(AdaptiveLayout.of(context).isDesktop
? context.localized.settingsShowScaleSlider
: context.localized.settingsPosterPinch),
onTap: () => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
),
trailing: Switch(
value: clientSettings.pinchPosterZoom,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: value),
),
),
),
),
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPosterSize),
trailing: Text(
clientSettings.posterSize.toString(),
style: Theme.of(context).textTheme.bodyLarge,
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPosterSize),
trailing: Text(
clientSettings.posterSize.toString(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FladderSlider(
min: 0.5,
max: 1.5,
value: clientSettings.posterSize,
divisions: 20,
onChanged: (value) =>
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FladderSlider(
min: 0.5,
max: 1.5,
value: clientSettings.posterSize,
divisions: 20,
onChanged: (value) =>
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
),
),
),
],
),
const Divider(),
];
],
),
],
);
}

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(
label: "Fladder",
items: [
...buildClientSettingsDownload(context, ref, setState),
SettingsLabelDivider(label: context.localized.lockscreen),
return SettingsScaffold(
label: "Fladder",
items: [
...buildClientSettingsDownload(context, ref, setState),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [
SettingsListTile(
label: Text(context.localized.timeOut),
subLabel: Text(timePickerString(context, clientSettings.timeOut)),
@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
: null);
},
),
const Divider(),
...buildClientSettingsDashboard(context, ref),
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
...buildClientSettingsTheme(context, ref),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
SettingsLabelDivider(label: context.localized.controls),
]),
const SizedBox(height: 12),
...buildClientSettingsDashboard(context, ref),
const SizedBox(height: 12),
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
const SizedBox(height: 12),
...buildClientSettingsTheme(context, ref),
const SizedBox(height: 12),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [
SettingsListTile(
label: Text(context.localized.mouseDragSupport),
subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled),
@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
),
),
const Divider(),
],
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[
const SizedBox(height: 64),
SettingsListTile(
label: Text(
context.localized.clearAllSettings,
),
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.clearAllSettingsQuestion,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
context.localized.unableToReverseAction,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.localized.cancel),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await ref.read(sharedPreferencesProvider).clear();
context.router.push(const LoginRoute());
},
child: Text(context.localized.clear),
)
],
),
],
),
]),
const SizedBox(height: 12),
],
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[
const SizedBox(height: 64),
SettingsListTile(
label: Text(
context.localized.clearAllSettings,
),
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.clearAllSettingsQuestion,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
context.localized.unableToReverseAction,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.localized.cancel),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await ref.read(sharedPreferencesProvider).clear();
context.router.push(const LoginRoute());
},
child: Text(context.localized.clear),
)
],
),
],
),
),
);
},
),
],
),
);
},
),
],
),
],
);
}
}

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,100 +41,105 @@ 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(
label: context.localized.settingsPlayerTitle,
items: [
return SettingsScaffold(
label: context.localized.settingsPlayerTitle,
items: [
...settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.video),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
[
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
Column(
children: [
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(context.localized.videoScalingFillScreenDesc),
onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
trailing: Switch(
value: videoSettings.fillScreen,
onChanged: (value) => provider.setFillScreen(value),
),
),
AnimatedFadeSize(
child: videoSettings.fillScreen
? SettingsMessageBox(
context.localized.videoScalingFillScreenNotif,
messageType: MessageType.warning,
)
: Container(),
),
],
),
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(context.localized.videoScalingFillScreenDesc),
onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
trailing: Switch(
value: videoSettings.fillScreen,
onChanged: (value) => provider.setFillScreen(value),
subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => openMultiSelectOptions(
context,
label: context.localized.videoScalingFillScreenTitle,
items: BoxFit.values,
selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
itemBuilder: (type, selected, tap) => RadioListTile(
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
title: Text(type.label(context)),
value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(),
),
),
),
AnimatedFadeSize(
child: videoSettings.fillScreen
? SettingsMessageBox(
context.localized.videoScalingFillScreenNotif,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => openMultiSelectOptions(
context,
label: context.localized.videoScalingFillScreenTitle,
items: BoxFit.values,
selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
itemBuilder: (type, selected, tap) => RadioListTile(
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
title: Text(type.label(context)),
value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(),
SettingsListTile(
label: _StatusIndicator(
homeInternet: connectionState.homeInternet,
label: Text(context.localized.homeStreamingQualityTitle),
),
subLabel: Text(context.localized.homeStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
),
)
.toList(),
),
),
),
SettingsListTile(
label: _StatusIndicator(
homeInternet: connectionState.homeInternet,
label: Text(context.localized.homeStreamingQualityTitle),
),
subLabel: Text(context.localized.homeStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
SettingsListTile(
label: _StatusIndicator(
homeInternet: !connectionState.homeInternet,
label: Text(context.localized.internetStreamingQualityTitle),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
),
)
.toList(),
),
),
SettingsListTile(
label: _StatusIndicator(
homeInternet: !connectionState.homeInternet,
label: Text(context.localized.internetStreamingQualityTitle),
),
subLabel: Text(context.localized.internetStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
subLabel: Text(context.localized.internetStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
),
)
.toList(),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
),
)
.toList(),
),
),
const Divider(),
SettingsLabelDivider(label: context.localized.mediaSegmentActions),
],
),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [
...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState<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),
@ -246,15 +254,14 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (value) => provider.setUseLibass(value),
),
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
],
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerBufferSizeTitle),
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
@ -291,33 +298,37 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
},
),
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry),
),
)
.toList(),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry),
),
)
.toList(),
),
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
),
],
),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SettingsListTile(
@ -325,8 +336,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
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(
label: context.localized.settingsProfileTitle,
items: [
SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle),
return SettingsScaffold(
label: context.localized.settingsProfileTitle,
items: [
...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,119 +96,122 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final quickConnectAvailable =
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
return Container(
color: context.colors.surface,
child: SettingsScaffold(
label: context.localized.settings,
scrollController: scrollController,
showBackButtonNested: true,
showUserIcon: true,
items: [
SettingsListTile(
label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc),
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
),
if (quickConnectAvailable)
return Padding(
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
child: Container(
color: context.colors.surface,
child: SettingsScaffold(
label: context.localized.settings,
scrollController: scrollController,
showBackButtonNested: true,
showUserIcon: true,
items: [
SettingsListTile(
label: Text(context.localized.settingsQuickConnectTitle),
icon: IconsaxPlusLinear.password_check,
onTap: () => openQuickConnectDialog(context),
label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc),
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.settingsProfileTitle),
subLabel: Text(context.localized.settingsProfileDesc),
selected: containsRoute(const SecuritySettingsRoute()),
icon: IconsaxPlusLinear.security_user,
onTap: () => navigateTo(const SecuritySettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerTitle),
subLabel: Text(context.localized.settingsPlayerDesc),
selected: containsRoute(const PlayerSettingsRoute()),
icon: IconsaxPlusLinear.video_play,
onTap: () => navigateTo(const PlayerSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.about),
subLabel: const Text("Fladder"),
selected: containsRoute(const AboutSettingsRoute()),
suffix: Opacity(
opacity: 1,
child: FladderIconOutlined(
size: 24,
color: context.colors.onSurfaceVariant,
if (quickConnectAvailable)
SettingsListTile(
label: Text(context.localized.settingsQuickConnectTitle),
icon: IconsaxPlusLinear.password_check,
onTap: () => openQuickConnectDialog(context),
),
SettingsListTile(
label: Text(context.localized.settingsProfileTitle),
subLabel: Text(context.localized.settingsProfileDesc),
selected: containsRoute(const SecuritySettingsRoute()),
icon: IconsaxPlusLinear.security_user,
onTap: () => navigateTo(const SecuritySettingsRoute()),
),
onTap: () => navigateTo(const AboutSettingsRoute()),
),
],
floatingActionButton: Padding(
padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
FloatingActionButton(
key: Key(context.localized.switchUser),
tooltip: context.localized.switchUser,
onPressed: () async {
await ref.read(userProvider.notifier).logoutUser();
context.router.replaceAll([const LoginRoute()]);
},
child: const Icon(
IconsaxPlusLinear.arrow_swap_horizontal,
),
SettingsListTile(
label: Text(context.localized.settingsPlayerTitle),
subLabel: Text(context.localized.settingsPlayerDesc),
selected: containsRoute(const PlayerSettingsRoute()),
icon: IconsaxPlusLinear.video_play,
onTap: () => navigateTo(const PlayerSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.about),
subLabel: const Text("Fladder"),
selected: containsRoute(const AboutSettingsRoute()),
suffix: Opacity(
opacity: 1,
child: FladderIconOutlined(
size: 24,
color: context.colors.onSurfaceVariant,
),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: context.localized.logout,
key: Key(context.localized.logout),
tooltip: context.localized.logout,
backgroundColor: Theme.of(context).colorScheme.errorContainer,
onPressed: () {
final user = ref.read(userProvider);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
scrollable: true,
content: Text(
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(context.localized.cancel),
),
onTap: () => navigateTo(const AboutSettingsRoute()),
),
],
floatingActionButton: Padding(
padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
FloatingActionButton(
key: Key(context.localized.switchUser),
tooltip: context.localized.switchUser,
onPressed: () async {
await ref.read(userProvider.notifier).logoutUser();
context.router.replaceAll([const LoginRoute()]);
},
child: const Icon(
IconsaxPlusLinear.arrow_swap_horizontal,
),
),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: context.localized.logout,
key: Key(context.localized.logout),
tooltip: context.localized.logout,
backgroundColor: Theme.of(context).colorScheme.errorContainer,
onPressed: () {
final user = ref.read(userProvider);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
scrollable: true,
content: Text(
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
),
ElevatedButton(
style: ElevatedButton.styleFrom().copyWith(
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(context.localized.cancel),
),
onPressed: () async {
await ref.read(authProvider.notifier).logOutUser();
if (context.mounted) {
context.router.replaceAll([const LoginRoute()]);
}
},
child: Text(context.localized.logout),
),
],
),
);
},
child: Icon(
IconsaxPlusLinear.logout,
color: Theme.of(context).colorScheme.onErrorContainer,
ElevatedButton(
style: ElevatedButton.styleFrom().copyWith(
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
),
onPressed: () async {
await ref.read(authProvider.notifier).logOutUser();
if (context.mounted) {
context.router.replaceAll([const LoginRoute()]);
}
},
child: Text(context.localized.logout),
),
],
),
);
},
child: Icon(
IconsaxPlusLinear.logout,
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
],
),
),
),
),

View file

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

View file

@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/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,33 +99,37 @@ 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(),
FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onCancel?.call();
},
child: Text(context.localized.cancel),
Row(
children: [
FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onCancel?.call();
},
child: Text(context.localized.cancel),
),
if (onClear != null)
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6)),
),
if (onClear != null)
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6)),
],
);
if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) {
@ -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(

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
class DefaultTitleBar extends ConsumerStatefulWidget {
final String? label;

View file

@ -1,22 +1,18 @@
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/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/routes/auto_router.gr.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/fladder_image.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/router_extension.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
@ -64,9 +60,9 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
Widget build(BuildContext context) {
final padding = EdgeInsets.symmetric(horizontal: MediaQuery.sizeOf(context).width / 25);
final backGroundColor = Theme.of(context).colorScheme.surface.withValues(alpha: 0.8);
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
final minHeight = 450.0.clamp(0, MediaQuery.sizeOf(context).height).toDouble();
final maxHeight = MediaQuery.sizeOf(context).height - 10;
final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth;
return PullToRefresh(
onRefresh: () async {
await widget.onRefresh?.call();
@ -78,16 +74,6 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
},
refreshOnStart: true,
child: Scaffold(
floatingActionButtonAnimator:
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: switch (playerState) {
VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.all(8.0),
child: FloatingPlayerBar(),
),
_ => null,
},
backgroundColor: Theme.of(context).colorScheme.surface,
extendBodyBehindAppBar: true,
body: Stack(
@ -164,7 +150,6 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
Padding(
padding: EdgeInsets.only(
bottom: 0,
left: MediaQuery.of(context).padding.left,
top: MediaQuery.of(context).padding.top,
),
child: ConstrainedBox(
@ -172,7 +157,9 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
minHeight: MediaQuery.sizeOf(context).height,
maxWidth: MediaQuery.sizeOf(context).width,
),
child: widget.content(padding),
child: widget.content(padding.copyWith(
left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left,
)),
),
),
],
@ -182,9 +169,11 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
child: Padding(
padding: MediaQuery.paddingOf(context).add(
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
padding: MediaQuery.paddingOf(context)
.copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left)
.add(
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
child: Row(
children: [
IconButton.filledTonal(
@ -255,13 +244,13 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
child: SettingsUserIcon(),
),
),
Tooltip(
message: context.localized.home,
child: IconButton(
onPressed: () => context.router.navigate(const DashboardRoute()),
icon: const Icon(IconsaxPlusLinear.home),
),
),
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single)
Tooltip(
message: context.localized.home,
child: IconButton(
onPressed: () => context.navigateTo(const DashboardRoute()),
icon: const Icon(IconsaxPlusLinear.home),
)),
],
),
),

View file

@ -1,7 +1,7 @@
import 'package:desktop_drop/desktop_drop.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/screens/shared/outlined_text_field.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;

View file

@ -47,7 +47,7 @@ class _FloatingSearchBarState extends ConsumerState<FloatingSearchBar> {
closedColor: Colors.transparent,
closedElevation: 0,
closedBuilder: (context, openAction) => Card(
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
shadowColor: Colors.transparent,
elevation: 5,
margin: EdgeInsets.zero,

View file

@ -7,7 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/banner_play_button.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart';
@ -51,16 +51,15 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4)
.copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10),
padding: EdgeInsets.only(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10),
child: Stack(
children: [
CarouselView(
elevation: 3,
shrinkExtent: 0,
controller: carouselController,
shape: RoundedRectangleBorder(borderRadius: border),
padding: const EdgeInsets.symmetric(horizontal: 6),
shape: RoundedRectangleBorder(borderRadius: border),
enableSplash: false,
itemExtent: itemExtent,
children: [
@ -146,8 +145,8 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
? null
: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320,
localPosition.dy, localPosition.dx, localPosition.dy);
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
final poster = widget.items[index];
await showMenu(

View file

@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/disable_keypad_focus.dart';
import 'package:fladder/util/humanize_duration.dart';
import 'package:fladder/util/localization_helper.dart';
@ -67,8 +67,8 @@ class ChapterRow extends ConsumerWidget {
FlatButton(
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx - 80, localPosition.dy, localPosition.dx, localPosition.dy);
RelativeRect position =
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
await showMenu(
context: context,
position: position,

View file

@ -7,7 +7,7 @@ import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
import 'package:fladder/screens/shared/media/episode_posters.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/sticky_header_text.dart';
import 'package:fladder/util/string_extensions.dart';

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/book_model.dart';
import 'package:fladder/models/item_base_model.dart';
@ -11,7 +11,7 @@ import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/components/poster_placeholder.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/disable_keypad_focus.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/humanize_duration.dart';
@ -137,7 +137,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
borderRadius: FladderTheme.defaultShape.borderRadius,
),
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: Stack(
alignment: Alignment.topCenter,
children: [
@ -225,7 +225,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
await showMenu(
context: context,
position: position,

View file

@ -2,7 +2,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/media/episode_posters.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/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';

View file

@ -9,7 +9,7 @@ import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/disable_keypad_focus.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
@ -241,12 +241,13 @@ class EpisodePoster extends ConsumerWidget {
LayoutBuilder(
builder: (context, constraints) {
return FlatButton(
onSecondaryTapDown: (details) {
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy);
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
showMenu(context: context, position: position, items: actions.popupMenuItems(useIcons: true));
await showMenu(
context: context, position: position, items: actions.popupMenuItems(useIcons: true));
},
onTap: onTap,
onLongPress: onLongPress,

View file

@ -1,9 +1,11 @@
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sticky_header_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sticky_header_text.dart';
class ExpandingOverview extends ConsumerStatefulWidget {
final String text;
@ -68,11 +70,11 @@ class _ExpandingOverviewState extends ConsumerState<ExpandingOverview> {
child: expanded
? IconButton.filledTonal(
onPressed: toggleState,
icon: const Icon(IconsaxPlusLinear.arrow_up_2),
icon: const Icon(IconsaxPlusLinear.arrow_up_1),
)
: IconButton.filledTonal(
onPressed: toggleState,
icon: const Icon(IconsaxPlusLinear.arrow_down_1),
icon: const Icon(IconsaxPlusLinear.arrow_down),
),
),
),

View file

@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart' as urilauncher;
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fladder/models/items/item_shared_models.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/sticky_header_text.dart';

View file

@ -25,7 +25,7 @@ class _ItemDetailListWidgetState extends ConsumerState<ItemDetailListWidget> {
return Card(
elevation: widget.elevation,
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
FlatButton(

View file

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:async/async.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/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/banner_play_button.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart';
@ -87,7 +87,7 @@ class _MediaBannerState extends ConsumerState<MediaBanner> {
final double dragOpacity = (1 - dragOffset.abs()).clamp(0, 1);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

View file

@ -7,7 +7,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/screens/details_screens/person_detail_screen.dart';
import 'package:fladder/screens/shared/flat_button.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/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
@ -22,12 +22,14 @@ class PeopleRow extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget placeHolder(String name) {
return Card(
child: FractionallySizedBox(
widthFactor: 0.4,
return Center(
child: SizedBox(
height: 75,
width: 75,
child: Card(
elevation: 5,
shape: const CircleBorder(),
shadowColor: Colors.transparent,
color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.50),
child: Center(
child: Text(
name.getInitials(),

View file

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sticky_headers/sticky_headers.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.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/sticky_header_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sticky_headers/sticky_headers.dart';
class PosterGrid extends ConsumerWidget {
final String? name;

View file

@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/shared/flat_button.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart';
@ -12,8 +15,6 @@ import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/clickable_text.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PosterListItem extends ConsumerWidget {
final ItemBaseModel poster;
@ -64,12 +65,12 @@ class PosterListItem extends ConsumerWidget {
color: Theme.of(context).colorScheme.primary.withValues(alpha: selected == true ? 0.25 : 0),
borderRadius: BorderRadius.circular(6),
),
child: FlatButton(
child: InkWell(
onTap: () => pressedWidget(context),
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position =
RelativeRect.fromLTRB(localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
await showMenu(
context: context,
position: position,

View file

@ -1,18 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/screens/shared/media/poster_widget.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/widgets/shared/horizontal_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PosterRow extends ConsumerStatefulWidget {
final List<ItemBaseModel> posters;
final String label;
final double? collectionAspectRatio;
final Function()? onLabelClick;
final EdgeInsets contentPadding;
const PosterRow({
required this.posters,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 16),
required this.label,
this.collectionAspectRatio,
this.onLabelClick,
super.key,
});
@ -32,6 +37,7 @@ class _PosterRowState extends ConsumerState<PosterRow> {
@override
Widget build(BuildContext context) {
final dominantRatio = widget.collectionAspectRatio ?? widget.posters.getMostCommonType.aspectRatio;
return HorizontalList(
contentPadding: widget.contentPadding,
label: widget.label,
@ -41,6 +47,7 @@ class _PosterRowState extends ConsumerState<PosterRow> {
final poster = widget.posters[index];
return PosterWidget(
poster: poster,
aspectRatio: dominantRatio,
key: Key(poster.id),
);
},

View file

@ -4,9 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.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/poster_image.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/widgets/shared/clickable_text.dart';

View file

@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/disable_keypad_focus.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
@ -124,11 +124,11 @@ class SeasonPoster extends ConsumerWidget {
LayoutBuilder(
builder: (context, constraints) {
return FlatButton(
onSecondaryTapDown: (details) {
onSecondaryTapDown: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy);
showMenu(
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
await showMenu(
context: context,
position: position,
items: season.generateActions(context, ref).popupMenuItems(useIcons: true));

View file

@ -1,7 +1,5 @@
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/widgets/shared/shapes.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NestedBottomAppBar extends ConsumerWidget {
@ -10,27 +8,17 @@ class NestedBottomAppBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final double bottomPadding =
(AdaptiveLayout.of(context).isDesktop || kIsWeb) ? 12 : MediaQuery.of(context).padding.bottom;
return Card(
color: Theme.of(context).colorScheme.surface,
shape: BottomBarShape(),
elevation: 0,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: SizedBox(
height: kBottomNavigationBarHeight + 12 + bottomPadding,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12)
.copyWith(
bottom: bottomPadding,
)
.add(EdgeInsets.only(
left: MediaQuery.of(context).padding.left,
right: MediaQuery.of(context).padding.right,
)),
child: child,
),
return Padding(
padding: const EdgeInsets.all(8.0).copyWith(bottom: MediaQuery.paddingOf(context).bottom),
child: Card(
color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.circular(24),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: child,
),
),
);

View file

@ -2,38 +2,38 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
class NestedScaffold extends ConsumerWidget {
final Widget body;
const NestedScaffold({required this.body, super.key});
final Widget? background;
const NestedScaffold({
required this.body,
this.background,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
return Card(
child: Scaffold(
backgroundColor: Colors.transparent,
floatingActionButtonAnimator:
playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) {
ViewSize.phone => null,
_ => switch (playerState) {
VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: FloatingPlayerBar(),
),
_ => null,
},
},
body: body,
),
return Stack(
alignment: Alignment.bottomCenter,
children: [
if (background != null) background!,
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).colorScheme.surface.withValues(alpha: 0.98),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
],
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: body,
),
),
],
);
}
}

View file

@ -1,11 +1,12 @@
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/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
import 'package:fladder/widgets/shared/shapes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NestedSliverAppBar extends ConsumerWidget {
final BuildContext parent;
@ -29,15 +30,14 @@ class NestedSliverAppBar extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 10,
children: [
IconButton.filledTonal(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.surface),
),
onPressed: () => Scaffold.of(parent).openDrawer(),
icon: const Icon(
IconsaxPlusBold.menu,
size: 28,
SizedBox(
width: 30,
child: IconButton(
onPressed: () => Scaffold.of(parent).openDrawer(),
icon: const Icon(IconsaxPlusLinear.menu),
padding: EdgeInsets.zero,
),
),
Expanded(
@ -62,8 +62,9 @@ class NestedSliverAppBar extends ConsumerWidget {
const Icon(IconsaxPlusLinear.search_normal),
const SizedBox(width: 16),
Transform.translate(
offset: const Offset(0, 2.5),
child: Text(searchTitle ?? "${context.localized.search}...")),
offset: const Offset(0, 1.5),
child: Text(searchTitle ?? "${context.localized.search}..."),
),
],
),
),
@ -73,7 +74,7 @@ class NestedSliverAppBar extends ConsumerWidget {
),
),
const SettingsUserIcon()
].addInBetween(const SizedBox(width: 16)),
],
),
),
),

View file

@ -49,7 +49,7 @@ class UserIcon extends ConsumerWidget {
elevation: 0,
surfaceTintColor: Colors.transparent,
color: Colors.transparent,
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: SizedBox.fromSize(
size: size,
child: Stack(

View file

@ -17,7 +17,7 @@ import 'package:fladder/screens/syncing/sync_child_item.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.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/size_formatting.dart';

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/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
@ -35,7 +35,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
syncedItem: syncedItem,
child: Card(
elevation: 1,
color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.2),
color: Theme.of(context).colorScheme.surfaceDim,
shadowColor: Colors.transparent,
child: Dismissible(
background: Container(

View file

@ -1,19 +1,19 @@
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/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_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/screens/syncing/sync_list_item.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/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/pull_to_refresh.dart';
@ -31,48 +31,49 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
@override
Widget build(BuildContext context) {
final items = ref.watch(syncProvider.select((value) => value.items));
final padding = AdaptiveLayout.adaptivePadding(context);
return PullToRefresh(
refreshOnStart: true,
onRefresh: () => ref.read(syncProvider.notifier).refresh(),
child: NestedScaffold(
background: BackgroundImage(images: items.map((value) => value.images).nonNulls.toList()),
body: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: items.isNotEmpty
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: widget.navigationScrollController,
slivers: [
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar(
searchTitle: "${context.localized.search} ...",
parent: context,
route: LibrarySearchRoute(),
)
else
const DefaultSliverTopBadding(),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
context.localized.syncedItems,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.builder(
itemBuilder: (context, index) {
final item = items[index];
return SyncListItem(syncedItem: item);
},
itemCount: items.length,
),
),
const DefautlSliverBottomPadding(),
],
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: widget.navigationScrollController,
slivers: [
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar(
searchTitle: "${context.localized.search} ...",
parent: context,
route: LibrarySearchRoute(),
)
: Center(
else
const DefaultSliverTopBadding(),
if (items.isNotEmpty) ...[
SliverToBoxAdapter(
child: Padding(
padding: padding,
child: Text(
context.localized.syncedItems,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverPadding(
padding: padding,
sliver: SliverList.builder(
itemBuilder: (context, index) {
final item = items[index];
return SyncListItem(syncedItem: item);
},
itemCount: items.length,
),
),
] else ...[
SliverFillRemaining(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
@ -87,7 +88,11 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
)
],
),
),
)
],
const DefautlSliverBottomPadding(),
],
),
),
),
);

View file

@ -2,8 +2,8 @@ import 'dart:ui';
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/playback/playback_model.dart';
import 'package:fladder/providers/session_info_provider.dart';
@ -33,112 +33,117 @@ class _VideoPlaybackInformation extends ConsumerWidget {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Player info", style: Theme.of(context).textTheme.titleMedium),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.80,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('url: '),
const SizedBox(width: 8),
Flexible(
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: 3.0,
sigmaY: 3.0,
),
child: Text(
playbackModel?.media?.url ?? "No url",
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: SingleChildScrollView(
child: IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Player info", style: Theme.of(context).textTheme.titleMedium),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.80,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('backend: '),
Text(backend?.label(context) ?? context.localized.unknown)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('url: '),
const SizedBox(width: 8),
Flexible(
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: 3.0,
sigmaY: 3.0,
),
child: Text(
playbackModel?.media?.url ?? "No url",
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
IconButton.filled(
onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"),
icon: const Icon(IconsaxPlusLinear.copy),
)
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
IconButton.filled(
onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"),
icon: const Icon(IconsaxPlusLinear.copy),
)
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
),
),
),
),
const Divider(),
if (playbackState != null) _PlayerInformation(state: playbackState),
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('type: '), Text(playbackModel.label ?? "")],
),
if (sessionInfo.transCodeInfo != null) ...[
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('reason: '),
Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")
],
),
if (sessionInfo.transCodeInfo?.completionPercentage != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('transcode progress: '),
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
],
),
if (sessionInfo.transCodeInfo?.container != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(sessionInfo.transCodeInfo!.container.toString())
],
),
],
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('resolution: '),
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
const Divider(),
if (playbackState != null) _PlayerInformation(state: playbackState),
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('type: '), Text(playbackModel.label ?? "")],
),
if (sessionInfo.transCodeInfo != null) ...[
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('reason: '),
Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")
],
),
if (sessionInfo.transCodeInfo?.completionPercentage != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('transcode progress: '),
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
],
),
if (sessionInfo.transCodeInfo?.container != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(sessionInfo.transCodeInfo!.container.toString())
],
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('resolution: '),
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
),
),
),
),
],
],
),
),
),
),

View file

@ -50,7 +50,7 @@ class VideoPlayerChapters extends ConsumerWidget {
final isCurrent = chapter == currentChapter;
return Card(
color: Colors.black,
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
Center(

View file

@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/shared/animated_fade_size.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/fladder_image.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';

View file

@ -22,7 +22,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart';
import 'package:fladder/screens/video_player/components/video_player_queue.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/device_orientation_extension.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';

View file

@ -199,7 +199,7 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
(e) => FlatButton(
onTap: () => provider.setSubColor(e),
borderRadiusGeometry: BorderRadius.circular(5),
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: Container(
height: 25,
width: 25,
@ -223,7 +223,7 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
onTap: () => provider
.setOutlineColor(e == Colors.transparent ? e : e.withValues(alpha: 0.85)),
borderRadiusGeometry: BorderRadius.circular(5),
clipBehavior: Clip.antiAlias,
clipBehavior: Clip.hardEdge,
child: Container(
height: 25,
width: 25,

View file

@ -11,7 +11,7 @@ import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/video_player/components/video_player_next_wrapper.dart';
import 'package:fladder/screens/video_player/video_player_controls.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/themes_data.dart';
class VideoPlayer extends ConsumerStatefulWidget {

View file

@ -13,7 +13,6 @@ import 'package:screen_brightness/screen_brightness.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
@ -26,7 +25,7 @@ import 'package:fladder/screens/video_player/components/video_player_quality_con
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';