Init repo

This commit is contained in:
PartyDonut 2024-09-15 14:12:28 +02:00
commit 764b6034e3
566 changed files with 212335 additions and 0 deletions

View 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 [];
}
}
}

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

View 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;
}

View 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;
}

View 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;
}

View 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;
}