mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
83
lib/screens/library/components/library_tabs.dart
Normal file
83
lib/screens/library/components/library_tabs.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/screens/library/library_screen.dart
Normal file
92
lib/screens/library/library_screen.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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';
|
||||
|
||||
class LibraryScreen extends ConsumerStatefulWidget {
|
||||
final ViewModel viewModel;
|
||||
const LibraryScreen({
|
||||
required this.viewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _LibraryScreenState();
|
||||
}
|
||||
|
||||
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(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/screens/library/tabs/favourites_tab.dart
Normal file
37
lib/screens/library/tabs/favourites_tab.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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;
|
||||
}
|
||||
40
lib/screens/library/tabs/library_tab.dart
Normal file
40
lib/screens/library/tabs/library_tab.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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;
|
||||
}
|
||||
49
lib/screens/library/tabs/recommendations_tab.dart
Normal file
49
lib/screens/library/tabs/recommendations_tab.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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;
|
||||
}
|
||||
132
lib/screens/library/tabs/timeline_tab.dart
Normal file
132
lib/screens/library/tabs/timeline_tab.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:fladder/models/items/photos_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.of(context).layout == LayoutState.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