mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
9ca06eaa37
commit
e7b5bb40ff
169 changed files with 4584 additions and 3626 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:fladder/models/book_model.dart';
|
||||
import 'package:fladder/providers/book_viewer_provider.dart';
|
||||
import 'package:fladder/providers/items/book_details_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart';
|
|||
import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
|
||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/throttler.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/dashboard_provider.dart';
|
||||
|
|
@ -19,10 +20,11 @@ import 'package:fladder/screens/dashboard/home_banner_widget.dart';
|
|||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
|
@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
|
||||
final dashboardData = ref.watch(dashboardProvider);
|
||||
final views = ref.watch(viewsProvider);
|
||||
final homeSettings = ref.watch(homeSettingsProvider);
|
||||
|
|
@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
|
||||
body: PullToRefresh(
|
||||
refreshKey: _refreshIndicatorKey,
|
||||
displacement: 80 + MediaQuery.of(context).viewPadding.top,
|
||||
|
|
@ -104,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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/screens/shared/media/components/chip_button.dart';
|
||||
import 'package:fladder/screens/shared/media/components/media_header.dart';
|
||||
import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -37,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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/episode_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
|
||||
|
|
@ -18,7 +17,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart';
|
|||
import 'package:fladder/screens/shared/media/expanding_overview.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/movies_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
|
||||
|
|
@ -17,7 +16,7 @@ import 'package:fladder/screens/shared/media/expanding_overview.dart';
|
|||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -11,7 +10,7 @@ import 'package:fladder/providers/user_provider.dart';
|
|||
import 'package:fladder/screens/shared/detail_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
|
|
@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState<PersonDetailScreen> {
|
|||
spacing: 32,
|
||||
children: [
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/items/series_details_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/details_screens/components/overview_header.dart';
|
||||
|
|
@ -19,7 +18,7 @@ import 'package:fladder/screens/shared/media/external_urls.dart';
|
|||
import 'package:fladder/screens/shared/media/people_row.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/media/season_row.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/favourites_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final favourites = ref.watch(favouritesProvider);
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
|
||||
return PullToRefresh(
|
||||
onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(),
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()),
|
||||
body: PinchPosterZoom(
|
||||
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
|
||||
child: CustomScrollView(
|
||||
|
|
@ -52,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(),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
|
|
@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart';
|
|||
|
||||
enum HomeTabs {
|
||||
dashboard,
|
||||
library,
|
||||
favorites,
|
||||
sync;
|
||||
sync,
|
||||
;
|
||||
|
||||
const HomeTabs();
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
HomeTabs.dashboard => IconsaxPlusLinear.home_1,
|
||||
HomeTabs.library => IconsaxPlusLinear.book,
|
||||
HomeTabs.favorites => IconsaxPlusLinear.heart,
|
||||
HomeTabs.sync => IconsaxPlusLinear.cloud,
|
||||
};
|
||||
|
||||
IconData get selectedIcon => switch (this) {
|
||||
HomeTabs.dashboard => IconsaxPlusBold.home_1,
|
||||
HomeTabs.library => IconsaxPlusBold.book,
|
||||
HomeTabs.favorites => IconsaxPlusBold.heart,
|
||||
HomeTabs.sync => IconsaxPlusBold.cloud,
|
||||
};
|
||||
|
||||
Future navigate(BuildContext context) => switch (this) {
|
||||
HomeTabs.dashboard => context.router.navigate(const DashboardRoute()),
|
||||
HomeTabs.library => context.router.navigate(const LibraryRoute()),
|
||||
HomeTabs.favorites => context.router.navigate(const FavouritesRoute()),
|
||||
HomeTabs.sync => context.router.navigate(SyncedRoute()),
|
||||
};
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
HomeTabs.dashboard => context.localized.dashboard,
|
||||
HomeTabs.library => context.localized.library(0),
|
||||
HomeTabs.favorites => context.localized.favorites,
|
||||
HomeTabs.sync => context.localized.sync,
|
||||
};
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget {
|
|||
case HomeTabs.dashboard:
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationDashboard,
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.home),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const DashboardRoute(),
|
||||
action: () => context.router.navigate(const DashboardRoute()),
|
||||
action: () => e.navigate(context),
|
||||
floatingActionButton: AdaptiveFab(
|
||||
context: context,
|
||||
title: context.localized.search,
|
||||
|
|
@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget {
|
|||
case HomeTabs.favorites:
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationFavorites,
|
||||
icon: const Icon(IconsaxPlusLinear.heart),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.heart),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const FavouritesRoute(),
|
||||
floatingActionButton: AdaptiveFab(
|
||||
context: context,
|
||||
|
|
@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget {
|
|||
onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)),
|
||||
child: const Icon(IconsaxPlusLinear.heart_search),
|
||||
),
|
||||
action: () => context.router.navigate(const FavouritesRoute()),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
case HomeTabs.sync:
|
||||
if (canDownload) {
|
||||
return DestinationModel(
|
||||
label: context.localized.navigationSync,
|
||||
icon: const Icon(IconsaxPlusLinear.cloud),
|
||||
selectedIcon: const Icon(IconsaxPlusBold.cloud),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: SyncedRoute(),
|
||||
action: () => context.router.navigate(SyncedRoute()),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case HomeTabs.library:
|
||||
return DestinationModel(
|
||||
label: context.localized.library(0),
|
||||
icon: Icon(e.icon),
|
||||
selectedIcon: Icon(e.selectedIcon),
|
||||
route: const LibraryRoute(),
|
||||
action: () => e.navigate(context),
|
||||
);
|
||||
}
|
||||
})
|
||||
.nonNulls
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/screens/library/tabs/favourites_tab.dart';
|
||||
import 'package:fladder/screens/library/tabs/library_tab.dart';
|
||||
import 'package:fladder/screens/library/tabs/timeline_tab.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/screens/library/tabs/recommendations_tab.dart';
|
||||
|
||||
class LibraryTabs {
|
||||
final String name;
|
||||
final Icon icon;
|
||||
final Widget page;
|
||||
final FloatingActionButton? floatingActionButton;
|
||||
LibraryTabs({
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.page,
|
||||
this.floatingActionButton,
|
||||
});
|
||||
|
||||
static List<LibraryTabs> getLibraryForType(ViewModel viewModel, CollectionType type) {
|
||||
LibraryTabs recommendTab() {
|
||||
return LibraryTabs(
|
||||
name: "Recommended",
|
||||
icon: const Icon(Icons.recommend_rounded),
|
||||
page: RecommendationsTab(viewModel: viewModel),
|
||||
);
|
||||
}
|
||||
|
||||
LibraryTabs timelineTab() {
|
||||
return LibraryTabs(
|
||||
name: "Timeline",
|
||||
icon: const Icon(Icons.timeline),
|
||||
page: TimelineTab(viewModel: viewModel),
|
||||
);
|
||||
}
|
||||
|
||||
LibraryTabs favouritesTab() {
|
||||
return LibraryTabs(
|
||||
name: "Favourites",
|
||||
icon: const Icon(Icons.favorite_rounded),
|
||||
page: FavouritesTab(viewModel: viewModel),
|
||||
);
|
||||
}
|
||||
|
||||
LibraryTabs libraryTab() {
|
||||
return LibraryTabs(
|
||||
name: "Library",
|
||||
icon: const Icon(Icons.book_rounded),
|
||||
page: LibraryTab(viewModel: viewModel),
|
||||
);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CollectionType.tvshows:
|
||||
case CollectionType.movies:
|
||||
return [
|
||||
libraryTab(),
|
||||
recommendTab(),
|
||||
favouritesTab(),
|
||||
];
|
||||
case CollectionType.books:
|
||||
case CollectionType.homevideos:
|
||||
return [
|
||||
libraryTab(),
|
||||
timelineTab(),
|
||||
recommendTab(),
|
||||
favouritesTab(),
|
||||
];
|
||||
case CollectionType.boxsets:
|
||||
case CollectionType.playlists:
|
||||
case CollectionType.folders:
|
||||
return [
|
||||
libraryTab(),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,33 @@
|
|||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/library/components/library_tabs.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/recommended_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_screen_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/button_group.dart';
|
||||
import 'package:fladder/widgets/shared/horizontal_list.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LibraryScreen extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
const LibraryScreen({
|
||||
required this.viewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin {
|
||||
late final List<LibraryTabs> tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType);
|
||||
late final TabController tabController = TabController(length: tabs.length, vsync: this);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() {
|
||||
ref.read(libraryProvider(widget.viewModel.id).notifier).setupLibrary(widget.viewModel);
|
||||
});
|
||||
|
||||
tabController.addListener(() {
|
||||
if (tabController.previousIndex != tabController.index) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PreferredSizeWidget tabBar = TabBar(
|
||||
isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false,
|
||||
indicatorWeight: 3,
|
||||
controller: tabController,
|
||||
tabs: tabs
|
||||
.map((e) => Tab(
|
||||
text: e.name,
|
||||
icon: e.icon,
|
||||
))
|
||||
.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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class FavouritesTab extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
const FavouritesTab({required this.viewModel, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _FavouritesTabState();
|
||||
}
|
||||
|
||||
class _FavouritesTabState extends ConsumerState<FavouritesTab> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final favourites = ref.watch(libraryProvider(widget.viewModel.id))?.favourites ?? [];
|
||||
super.build(context);
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadFavourites(widget.viewModel);
|
||||
},
|
||||
child: favourites.isNotEmpty
|
||||
? ListView(
|
||||
children: [
|
||||
PosterGrid(posters: favourites),
|
||||
],
|
||||
)
|
||||
: const Center(child: Text("No favourites, add some using the heart icon.")),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/util/grouping.dart';
|
||||
import 'package:fladder/util/keyed_list_view.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LibraryTab extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
const LibraryTab({required this.viewModel, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _LibraryTabState();
|
||||
}
|
||||
|
||||
class _LibraryTabState extends ConsumerState<LibraryTab> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final library = ref.watch(libraryProvider(widget.viewModel.id).select((value) => value?.posters)) ?? [];
|
||||
final items = groupByName(library);
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadLibrary(widget.viewModel);
|
||||
},
|
||||
child: KeyedListView(
|
||||
map: items,
|
||||
itemBuilder: (context, index) {
|
||||
final currentIndex = items.entries.elementAt(index);
|
||||
return PosterGrid(name: currentIndex.key, posters: currentIndex.value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_grid.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class RecommendationsTab extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
|
||||
const RecommendationsTab({required this.viewModel, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _RecommendationsTabState();
|
||||
}
|
||||
|
||||
class _RecommendationsTabState extends ConsumerState<RecommendationsTab> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final recommendations = ref.watch(libraryProvider(widget.viewModel.id)
|
||||
.select((value) => value?.recommendations.where((element) => element.posters.isNotEmpty))) ??
|
||||
[];
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadRecommendations(widget.viewModel);
|
||||
},
|
||||
child: recommendations.isNotEmpty
|
||||
? ListView(
|
||||
children: recommendations
|
||||
.map(
|
||||
(e) => PosterGrid(name: e.name, posters: e.posters),
|
||||
)
|
||||
.toList()
|
||||
.addPadding(
|
||||
const EdgeInsets.only(
|
||||
bottom: 32,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: Text("No recommendations, add more movies and or shows to receive more recomendations")),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_provider.dart';
|
||||
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/sticky_header_text.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:sticky_headers/sticky_headers.dart';
|
||||
|
||||
class TimelineTab extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
|
||||
const TimelineTab({required this.viewModel, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _TimelineTabState();
|
||||
}
|
||||
|
||||
class _TimelineTabState extends ConsumerState<TimelineTab> with AutomaticKeepAliveClientMixin {
|
||||
final itemScrollController = ItemScrollController();
|
||||
double get posterCount {
|
||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) {
|
||||
return 200;
|
||||
}
|
||||
return 125;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final timeLine = ref.watch(libraryProvider(widget.viewModel.id))?.timelinePhotos ?? [];
|
||||
final items = groupedItems(timeLine);
|
||||
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await ref.read(libraryProvider(widget.viewModel.id).notifier).loadTimeline(widget.viewModel);
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
itemScrollController: itemScrollController,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items.entries.elementAt(index);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 64.0),
|
||||
child: StickyHeader(
|
||||
header: StickyHeaderText(
|
||||
label: item.key.year != DateTime.now().year
|
||||
? DateFormat('E dd MMM. y').format(item.key)
|
||||
: DateFormat('E dd MMM.').format(item.key)),
|
||||
content: StaggeredGrid.count(
|
||||
crossAxisCount: MediaQuery.of(context).size.width ~/ posterCount,
|
||||
mainAxisSpacing: 0,
|
||||
crossAxisSpacing: 0,
|
||||
axisDirection: AxisDirection.down,
|
||||
children: item.value
|
||||
.map(
|
||||
(e) => Hero(
|
||||
tag: e.id,
|
||||
child: AspectRatio(
|
||||
aspectRatio: e.primaryRatio ?? 0.0,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(4),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
FladderImage(image: e.thumbnail?.primary),
|
||||
FlatButton(
|
||||
onLongPress: () {},
|
||||
onTap: () async {
|
||||
final position = await Navigator.of(context, rootNavigator: true).push(
|
||||
PageTransition(
|
||||
child: PhotoViewerScreen(
|
||||
items: timeLine,
|
||||
indexOfSelected: timeLine.indexOf(e),
|
||||
),
|
||||
type: PageTransitionType.fade),
|
||||
);
|
||||
getParentPosition(items, timeLine, position);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void getParentPosition(Map<DateTime, List<PhotoModel>> items, List<PhotoModel> timeLine, int position) {
|
||||
items.forEach(
|
||||
(key, value) {
|
||||
if (value.contains(timeLine[position])) {
|
||||
itemScrollController.scrollTo(
|
||||
index: items.keys.toList().indexOf(key), duration: const Duration(milliseconds: 250));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<DateTime, List<PhotoModel>> groupedItems(List<PhotoModel> items) {
|
||||
Map<DateTime, List<PhotoModel>> groupedItems = {};
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
DateTime curretDate = items[i].dateTaken ?? DateTime.now();
|
||||
DateTime key = DateTime(curretDate.year, curretDate.month, curretDate.day);
|
||||
if (!groupedItems.containsKey(key)) {
|
||||
groupedItems[key] = [items[i]];
|
||||
} else {
|
||||
groupedItems[key]?.add(items[i]);
|
||||
}
|
||||
}
|
||||
return groupedItems;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart';
|
|||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/playlist_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/screens/collections/add_to_collection.dart';
|
||||
import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart';
|
||||
import 'package:fladder/screens/library_search/widgets/library_play_options_.dart';
|
||||
|
|
@ -26,9 +23,9 @@ import 'package:fladder/screens/library_search/widgets/library_views.dart';
|
|||
import 'package:fladder/screens/library_search/widgets/suggestion_search_bar.dart';
|
||||
import 'package:fladder/screens/playlists/add_to_playlists.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/debouncer.dart';
|
||||
import 'package:fladder/util/fab_extended_anim.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
|
|
@ -37,8 +34,7 @@ import 'package:fladder/util/localization_helper.dart';
|
|||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_scrollbar.dart';
|
||||
import 'package:fladder/widgets/shared/hide_on_scroll.dart';
|
||||
|
|
@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null;
|
||||
final librarySearchResults = ref.watch(providerKey);
|
||||
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows);
|
||||
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||
final libraryViewType = ref.watch(libraryViewTypeProvider);
|
||||
|
||||
ref.listen(
|
||||
|
|
@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
libraryProvider.toggleSelectMode();
|
||||
}
|
||||
},
|
||||
child: 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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() ?? ""))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart';
|
|||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/screens/shared/passcode_input.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/auth_service.dart';
|
||||
import 'package:fladder/util/fladder_config.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget {
|
|||
return Card(
|
||||
elevation: 1,
|
||||
shadowColor: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget {
|
|||
aspectRatio: 1.0,
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
|
|||
import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart';
|
||||
import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/shared/file_picker.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
class EditImageContent extends ConsumerStatefulWidget {
|
||||
final ImageType type;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/items/identify_provider.dart';
|
||||
|
|
@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState<IdentifyScreen> with TickerProv
|
|||
final state = ref.watch(provider);
|
||||
final posters = state.results;
|
||||
final processing = state.processing;
|
||||
return ActionContent(
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/information_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
|
|||
import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart';
|
||||
import 'package:fladder/screens/photo_viewer/simple_video_player.dart';
|
||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/custom_cache_manager.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/screens/crash_screen/crash_screen.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
|
|
@ -42,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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(', ')),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/color_extensions.dart';
|
||||
import 'package:fladder/util/custom_color_themes.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart';
|
|||
|
||||
List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
return [
|
||||
SettingsLabelDivider(label: context.localized.theme),
|
||||
return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.mode),
|
||||
subLabel: Text(clientSettings.themeMode.label(context)),
|
||||
|
|
@ -107,6 +107,5 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
|||
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
|
@ -8,8 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/shared/input_fields.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
|
|
@ -22,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(),
|
||||
];
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
),
|
||||
if (subLabel != null)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
opacity: 0.65,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
|
||||
class SettingsScaffold extends ConsumerWidget {
|
||||
|
|
@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
final padding = MediaQuery.of(context).padding;
|
||||
final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single;
|
||||
return Scaffold(
|
||||
backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: floatingActionButton,
|
||||
body: Column(
|
||||
|
|
@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate(items),
|
||||
padding: MediaQuery.paddingOf(context).copyWith(top: 0),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) => items[index],
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
if (bottomActions.isEmpty)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
|
|
@ -12,7 +11,7 @@ import 'package:fladder/screens/settings/quick_connect_window.dart';
|
|||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/fladder_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
|
||||
|
|
@ -97,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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
43
lib/screens/settings/widgets/settings_list_group.dart
Normal file
43
lib/screens/settings/widgets/settings_list_group.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
||||
List<Widget> settingsListGroup(BuildContext context, Widget label, List<Widget> children) {
|
||||
final radius = BorderRadius.circular(24);
|
||||
final radiusSmall = const Radius.circular(6);
|
||||
final color = Theme.of(context).colorScheme.surfaceContainerLow.harmonizeWith(Colors.red);
|
||||
return [
|
||||
Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
color: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: radius.copyWith(
|
||||
bottomLeft: radiusSmall,
|
||||
bottomRight: radiusSmall,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: label,
|
||||
),
|
||||
),
|
||||
...children.map(
|
||||
(e) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: color,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: radius.copyWith(
|
||||
topLeft: radiusSmall,
|
||||
topRight: radiusSmall,
|
||||
bottomLeft: e != children.last ? radiusSmall : null,
|
||||
bottomRight: e != children.last ? radiusSmall : null,
|
||||
)),
|
||||
child: e,
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
|||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
Future<void> showDialogAdaptive(
|
||||
{required BuildContext context, required Widget Function(BuildContext context) builder}) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class AnimatedFadeSize extends ConsumerWidget {
|
||||
final Duration duration;
|
||||
final Widget child;
|
||||
final Alignment alignment;
|
||||
const AnimatedFadeSize({
|
||||
this.duration = const Duration(milliseconds: 125),
|
||||
required this.child,
|
||||
this.alignment = Alignment.center,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -14,6 +17,7 @@ class AnimatedFadeSize extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AnimatedSize(
|
||||
duration: duration,
|
||||
alignment: alignment,
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: AnimatedSwitcher(
|
||||
duration: duration,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
|
|
@ -100,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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue