mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -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,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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue