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,228 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/providers/items/book_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/components/media_play_button.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/poster_list_item.dart';
import 'package:fladder/util/fladder_image.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';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class BookDetailScreen extends ConsumerStatefulWidget {
final BookModel item;
const BookDetailScreen({required this.item, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _BookDetailScreenState();
}
class _BookDetailScreenState extends ConsumerState<BookDetailScreen> {
late final provider = bookDetailsProvider(widget.item.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(provider);
return DetailScaffold(
label: widget.item.name,
item: details.book,
actions: (context) => details.book?.generateActions(
context,
ref,
exclude: {
ItemActions.play,
ItemActions.playFromStart,
ItemActions.details,
},
onDeleteSuccesFully: (item) {
if (context.mounted) {
context.pop();
}
},
),
backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
onRefresh: () async => await ref.read(provider.notifier).fetchDetails(widget.item),
backDrops: details.cover,
content: (padding) => details.book != null
? Padding(
padding: const EdgeInsets.only(bottom: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.2),
if (MediaQuery.sizeOf(context).width < 500)
Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.75),
child: AspectRatio(
aspectRatio: 0.76,
child: Card(
child: FladderImage(image: details.cover?.primary),
),
),
).padding(padding),
),
Row(
children: [
if (MediaQuery.sizeOf(context).width > 500) ...{
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width * 0.3,
maxHeight: MediaQuery.sizeOf(context).height * 0.75),
child: AspectRatio(
aspectRatio: 0.76,
child: Card(
child: FladderImage(image: details.cover?.primary),
),
),
),
const SizedBox(width: 32),
},
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (details.nextUp != null)
OverviewHeader(
subTitle: details.book!.parentName ?? details.parentModel?.name,
name: details.nextUp!.name,
productionYear: details.nextUp!.overview.productionYear,
runTime: details.nextUp!.overview.runTime,
genres: details.nextUp!.overview.genreItems,
studios: details.nextUp!.overview.studios,
officialRating: details.nextUp!.overview.parentalRating,
communityRating: details.nextUp!.overview.communityRating,
externalUrls: details.nextUp!.overview.externalUrls,
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
//Wrapped so the correct context is used for refreshing the pages
Builder(
builder: (context) {
return MediaPlayButton(
item: details.nextUp!,
onPressed: () async => details.nextUp.play(context, ref, provider: provider));
},
),
if (details.parentModel != null)
SelectableIconButton(
onPressed: () async => await details.parentModel?.navigateTo(context),
selected: false,
selectedIcon: IconsaxBold.book,
icon: IconsaxOutline.book,
),
if (details.parentModel != null)
SelectableIconButton(
onPressed: () async => await ref.read(userProvider.notifier).setAsFavorite(
!details.parentModel!.userData.isFavourite, details.parentModel!.id),
selected: details.parentModel!.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
)
else
SelectableIconButton(
onPressed: () async => await ref
.read(userProvider.notifier)
.setAsFavorite(!details.book!.userData.isFavourite, details.book!.id),
selected: details.book!.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
),
//This one toggles all books in a collection
Builder(builder: (context) {
return Tooltip(
message: "Mark all chapters as read",
child: SelectableIconButton(
onPressed: () async => await Future.forEach(
details.allBooks,
(element) async => await ref
.read(userProvider.notifier)
.markAsPlayed(!details.collectionPlayed, element.id)),
selected: details.collectionPlayed,
selectedIcon: Icons.check_circle_rounded,
icon: Icons.check_circle_outline_rounded,
),
);
}),
],
)
],
),
),
],
).padding(padding),
if (details.nextUp!.overview.summary.isNotEmpty == true)
ExpandingOverview(
text: details.nextUp!.overview.summary,
).padding(padding),
if (details.chapters.length > 1)
Builder(builder: (context) {
final parentContext = context;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.localized.chapter(details.chapters.length),
style: Theme.of(context).textTheme.titleLarge),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Divider(),
),
...details.chapters.map(
(e) {
final current = e == details.nextUp;
return Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Opacity(
opacity: e.userData.played ? 0.65 : 1,
child: Card(
color: current ? Theme.of(context).colorScheme.surfaceContainerHighest : null,
child: PosterListItem(
poster: e,
onPressed: (action, item) => showBottomSheetPill(
context: context,
item: item,
content: (context, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: item
.generateActions(
parentContext,
ref,
)
.listTileItems(context, useIcons: true),
),
),
),
),
),
);
},
)
],
).padding(padding);
})
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
),
)
: Container(),
);
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LabelTitleItem extends ConsumerWidget {
final Text? title;
final String? label;
final Widget? content;
const LabelTitleItem({
this.title,
this.label,
this.content,
super.key,
}) : assert(label != null || content != null);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Material(
color: Colors.transparent,
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Opacity(
opacity: 0.6,
child: Material(
color: Colors.transparent, textStyle: Theme.of(context).textTheme.titleMedium, child: title)),
const SizedBox(width: 12),
label != null
? SelectableText(
label!,
)
: content!,
].whereNotNull().toList(),
),
);
}
}

View file

@ -0,0 +1,146 @@
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/screens/details_screens/components/label_title_item.dart';
class MediaStreamInformation extends ConsumerWidget {
final MediaStreamsModel mediaStream;
final Function(int index)? onAudioIndexChanged;
final Function(int index)? onSubIndexChanged;
const MediaStreamInformation(
{required this.mediaStream, this.onAudioIndexChanged, this.onSubIndexChanged, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mediaStream.videoStreams.isNotEmpty)
_StreamOptionSelect(
label: Text(context.localized.video),
current: (mediaStream.videoStreams.first).prettyName,
itemBuilder: (context) => mediaStream.videoStreams
.map(
(e) => PopupMenuItem(
value: e,
child: Text(e.prettyName),
onTap: () {},
),
)
.toList(),
),
if (mediaStream.audioStreams.isNotEmpty)
_StreamOptionSelect(
label: Text(context.localized.audio),
current: mediaStream.currentAudioStream?.displayTitle ?? "",
itemBuilder: (context) => mediaStream.audioStreams
.map(
(e) => PopupMenuItem(
value: e,
padding: EdgeInsets.zero,
child: textWidget(context, selected: mediaStream.currentAudioStream == e, label: e.displayTitle),
onTap: () => onAudioIndexChanged?.call(e.index),
),
)
.toList(),
),
if (mediaStream.subStreams.isNotEmpty)
_StreamOptionSelect(
label: Text(context.localized.subtitles),
current: mediaStream.currentSubStream?.displayTitle ?? "",
itemBuilder: (context) => [SubStreamModel.no(), ...mediaStream.subStreams]
.map(
(e) => PopupMenuItem(
value: e,
padding: EdgeInsets.zero,
child: textWidget(context, selected: mediaStream.currentSubStream == e, label: e.displayTitle),
onTap: () => onSubIndexChanged?.call(e.index),
),
)
.toList(),
),
],
);
}
Widget textWidget(BuildContext context, {required bool selected, required String label}) {
return Container(
height: kMinInteractiveDimension,
width: double.maxFinite,
color: selected ? Theme.of(context).colorScheme.primary : null,
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
class _StreamOptionSelect<T> extends StatelessWidget {
final Text label;
final String current;
final List<PopupMenuEntry<T>> Function(BuildContext context) itemBuilder;
const _StreamOptionSelect({
required this.label,
required this.current,
required this.itemBuilder,
});
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.titleMedium;
const padding = EdgeInsets.all(6.0);
final itemList = itemBuilder(context);
return LabelTitleItem(
title: label,
content: Flexible(
child: PopupMenuButton(
tooltip: '',
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
enabled: itemList.length > 1,
itemBuilder: itemBuilder,
padding: padding,
child: Padding(
padding: padding,
child: Material(
textStyle: textStyle?.copyWith(
fontWeight: FontWeight.bold,
color: itemList.length > 1 ? Theme.of(context).colorScheme.primary : null),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Text(
current,
textAlign: TextAlign.start,
),
),
const SizedBox(width: 6),
if (itemList.length > 1)
Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,165 @@
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/util/humanize_duration.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class OverviewHeader extends ConsumerWidget {
final String name;
final EdgeInsets? padding;
final String? subTitle;
final String? originalTitle;
final Function()? onTitleClicked;
final int? productionYear;
final Duration? runTime;
final String? officialRating;
final double? communityRating;
final List<Studio> studios;
final List<GenreItems> genres;
final List<ExternalUrls>? externalUrls;
final List<Widget> actions;
const OverviewHeader({
required this.name,
this.padding,
this.subTitle,
this.originalTitle,
this.onTitleClicked,
this.productionYear,
this.runTime,
this.officialRating,
this.communityRating,
this.externalUrls,
this.genres = const [],
this.studios = const [],
this.actions = const [],
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final mainStyle = Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
);
final subStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
fontSize: 20,
);
return Padding(
padding: padding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 32),
if (subTitle == null)
Flexible(
child: SelectableText(
name,
style: mainStyle,
),
)
else ...{
Flexible(
child: SelectableText(
subTitle ?? "",
style: mainStyle,
),
),
Flexible(
child: Opacity(
opacity: 0.75,
child: Row(
children: [
Flexible(
child: SelectableText(
name,
style: subStyle,
onTap: onTitleClicked,
),
),
if (onTitleClicked != null)
IconButton(
onPressed: onTitleClicked,
icon: Transform.translate(offset: Offset(0, 1.5), child: Icon(Icons.read_more_rounded)))
],
),
),
),
},
if (name != originalTitle && originalTitle != null)
SelectableText(
originalTitle.toString(),
style: subStyle,
),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (productionYear != null)
SelectableText(
productionYear.toString(),
style: subStyle,
),
if (runTime != null && (runTime?.inSeconds ?? 0) > 1)
SelectableText(
runTime.humanize.toString(),
style: subStyle,
),
if (officialRating != null)
Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
child: SelectableText(
officialRating.toString(),
style: subStyle,
),
),
),
if (communityRating != null)
Row(
children: [
Icon(
Icons.star_rate_rounded,
color: Theme.of(context).colorScheme.primary,
),
Text(
communityRating?.toStringAsFixed(1) ?? "",
style: subStyle,
),
],
),
],
),
const SizedBox(height: 6),
if (studios.isNotEmpty)
Text(
"${context.localized.watchOn} ${studios.map((e) => e.name).first}",
style: subStyle?.copyWith(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 6),
if (externalUrls?.isNotEmpty ?? false)
ExternalUrlsRow(
urls: externalUrls,
),
const SizedBox(height: 6),
if (genres.isNotEmpty)
Genres(
genres: genres.take(10).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: actions.addPadding(
const EdgeInsets.symmetric(horizontal: 6),
),
),
],
),
);
}
}

View file

@ -0,0 +1,4 @@
export 'movie_detail_screen.dart';
export 'series_detail_screen.dart';
export 'person_detail_screen.dart';
export 'empty_item.dart';

View file

@ -0,0 +1,19 @@
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class EmptyItem extends ConsumerWidget {
final ItemBaseModel item;
const EmptyItem({required this.item, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return DetailScaffold(
label: "Empty",
content: (padding) =>
Center(child: Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet.")),
);
}
}

View file

@ -0,0 +1,176 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/screens/details_screens/components/overview_header.dart';
import 'package:fladder/screens/shared/media/components/media_play_button.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/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_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';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/shared/media/chapter_row.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:go_router/go_router.dart';
class EpisodeDetailScreen extends ConsumerStatefulWidget {
final ItemBaseModel item;
const EpisodeDetailScreen({required this.item, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ItemDetailScreenState();
}
class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
late final providerInstance = episodeDetailsProvider(widget.item.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(providerInstance);
final seasonDetails = details.series;
final episodeDetails = details.episode;
return DetailScaffold(
label: widget.item.name,
item: details.episode,
actions: (context) => details.episode?.generateActions(
context,
ref,
exclude: {
if (details.series == null) ItemActions.openShow,
ItemActions.details,
},
onDeleteSuccesFully: (item) {
if (context.mounted) {
context.pop();
}
},
),
onRefresh: () async => await ref.read(providerInstance.notifier).fetchDetails(widget.item),
backDrops: details.episode?.images ?? details.series?.images,
content: (padding) => seasonDetails != null && episodeDetails != null
? Padding(
padding: const EdgeInsets.only(bottom: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.35),
MediaHeader(
name: details.series?.name ?? "",
logo: seasonDetails.images?.logo,
),
OverviewHeader(
name: details.series?.name ?? "",
padding: padding,
subTitle: details.episode?.name,
originalTitle: details.series?.originalTitle,
onTitleClicked: () => details.series?.navigateTo(context),
productionYear: details.series?.overview.productionYear,
runTime: details.episode?.overview.runTime,
studios: details.series?.overview.studios ?? [],
genres: details.series?.overview.genreItems ?? [],
officialRating: details.series?.overview.parentalRating,
communityRating: details.series?.overview.communityRating,
externalUrls: details.series?.overview.externalUrls,
),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (episodeDetails.playAble)
MediaPlayButton(
item: episodeDetails,
onPressed: () async {
await details.episode.play(context, ref);
ref.read(providerInstance.notifier).fetchDetails(widget.item);
},
onLongPressed: () async {
await details.episode.play(context, ref, showPlaybackOption: true);
ref.read(providerInstance.notifier).fetchDetails(widget.item);
},
),
SelectableIconButton(
onPressed: () async {
await ref
.read(userProvider.notifier)
.setAsFavorite(!(episodeDetails.userData.isFavourite), episodeDetails.id);
},
selected: episodeDetails.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
),
SelectableIconButton(
onPressed: () async {
await ref
.read(userProvider.notifier)
.markAsPlayed(!(episodeDetails.userData.played), episodeDetails.id);
},
selected: episodeDetails.userData.played,
selectedIcon: IconsaxBold.tick_circle,
icon: IconsaxOutline.tick_circle,
),
].addPadding(const EdgeInsets.symmetric(horizontal: 6)),
).padding(padding),
if (details.episode?.mediaStreams != null)
Padding(
padding: padding,
child: MediaStreamInformation(
mediaStream: details.episode!.mediaStreams,
onSubIndexChanged: (index) {
ref.read(providerInstance.notifier).setSubIndex(index);
},
onAudioIndexChanged: (index) {
ref.read(providerInstance.notifier).setAudioIndex(index);
},
),
),
if (episodeDetails.overview.summary.isNotEmpty == true)
ExpandingOverview(
text: episodeDetails.overview.summary,
).padding(padding),
if (episodeDetails.chapters.isNotEmpty)
ChapterRow(
chapters: episodeDetails.chapters,
contentPadding: padding,
onPressed: (chapter) async {
await details.episode?.play(context, ref, startPosition: chapter.startPosition);
ref.read(providerInstance.notifier).fetchDetails(widget.item);
},
),
if (details.episodes.length > 1)
EpisodePosters(
contentPadding: padding,
label: context.localized
.moreFrom("${context.localized.season(1).toLowerCase()} ${episodeDetails.season}"),
onEpisodeTap: (action, episodeModel) {
if (episodeModel.id == episodeDetails.id) {
fladderSnackbar(context, title: context.localized.selectedWith(context.localized.episode(0)));
} else {
action();
}
},
playEpisode: (episode) => episode.play(
context,
ref,
),
episodes: details.episodes.where((element) => element.season == episodeDetails.season).toList(),
),
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
),
)
: Container(),
);
}
}

View file

@ -0,0 +1,59 @@
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/providers/items/folder_details_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.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';
import 'package:page_transition/page_transition.dart';
class FolderDetailScreen extends ConsumerWidget {
final ItemBaseModel item;
const FolderDetailScreen({required this.item, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final providerInstance = folderDetailsProvider(item.id);
final details = ref.watch(providerInstance);
return PullToRefresh(
child: Scaffold(
appBar: AppBar(
title: Text(
details?.name ?? "",
)),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: PosterGrid(
posters: details?.items ?? [],
onPressed: (action, item) async {
switch (item) {
case PhotoModel photoModel:
final photoItems = details?.items.whereType<PhotoModel>().toList();
await Navigator.of(context, rootNavigator: true).push(PageTransition(
child: PhotoViewerScreen(
items: photoItems,
indexOfSelected: photoItems?.indexOf(photoModel) ?? 0,
),
type: PageTransitionType.fade));
break;
default:
if (context.mounted) {
await item.navigateTo(context);
}
}
},
),
)
],
),
),
onRefresh: () async {
await ref.read(providerInstance.notifier).fetchDetails(item.id);
},
);
}
}

View file

@ -0,0 +1,164 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/item_base_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/overview_header.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/chapter_row.dart';
import 'package:fladder/screens/shared/media/components/media_play_button.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/people_row.dart';
import 'package:fladder/screens/shared/media/poster_row.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';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
class MovieDetailScreen extends ConsumerStatefulWidget {
final ItemBaseModel item;
const MovieDetailScreen({required this.item, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ItemDetailScreenState();
}
class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
late final providerInstance = movieDetailsProvider(widget.item.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(providerInstance);
return DetailScaffold(
label: widget.item.name,
item: details,
actions: (context) => details?.generateActions(
context,
ref,
exclude: {
ItemActions.play,
ItemActions.playFromStart,
ItemActions.details,
},
onDeleteSuccesFully: (item) {
if (context.mounted) {
context.pop();
}
},
),
onRefresh: () async => await ref.read(providerInstance.notifier).fetchDetails(widget.item),
backDrops: details?.images,
content: (padding) => details != null
? Padding(
padding: const EdgeInsets.only(bottom: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.25),
MediaHeader(
name: details.name,
logo: details.images?.logo,
),
OverviewHeader(
name: details.name,
padding: padding,
originalTitle: details.originalTitle,
productionYear: details.overview.productionYear,
runTime: details.overview.runTime,
genres: details.overview.genreItems,
studios: details.overview.studios,
officialRating: details.overview.parentalRating,
communityRating: details.overview.communityRating,
externalUrls: details.overview.externalUrls,
),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
MediaPlayButton(
item: details,
onLongPressed: () async {
await details.play(
context,
ref,
showPlaybackOption: true,
);
ref.read(providerInstance.notifier).fetchDetails(widget.item);
},
onPressed: () async {
await details.play(
context,
ref,
);
ref.read(providerInstance.notifier).fetchDetails(widget.item);
},
),
SelectableIconButton(
onPressed: () async {
await ref
.read(userProvider.notifier)
.setAsFavorite(!details.userData.isFavourite, details.id);
},
selected: details.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
),
SelectableIconButton(
onPressed: () async {
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
},
selected: details.userData.played,
selectedIcon: IconsaxBold.tick_circle,
icon: IconsaxOutline.tick_circle,
),
],
).padding(padding),
if (details.mediaStreams.isNotEmpty)
MediaStreamInformation(
onSubIndexChanged: (index) {
ref.read(providerInstance.notifier).setSubIndex(index);
},
onAudioIndexChanged: (index) {
ref.read(providerInstance.notifier).setAudioIndex(index);
},
mediaStream: details.mediaStreams,
).padding(padding),
if (details.overview.summary.isNotEmpty == true)
ExpandingOverview(
text: details.overview.summary,
).padding(padding),
if (details.chapters.isNotEmpty)
ChapterRow(
chapters: details.chapters,
contentPadding: padding,
onPressed: (chapter) {
details.play(
context,
ref,
startPosition: chapter.startPosition,
);
},
),
if (details.overview.people.isNotEmpty)
PeopleRow(
people: details.overview.people,
contentPadding: padding,
),
if (details.related.isNotEmpty)
PosterRow(posters: details.related, contentPadding: padding, label: "Related"),
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
),
)
: Container(),
);
}
}

View file

@ -0,0 +1,127 @@
import 'package:collection/collection.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/providers/items/person_details_provider.dart';
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/fladder_image.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class PersonDetailScreen extends ConsumerStatefulWidget {
final Person person;
const PersonDetailScreen({required this.person, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _PersonDetailScreenState();
}
class _PersonDetailScreenState extends ConsumerState<PersonDetailScreen> {
late final providerID = personDetailsProvider(widget.person.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(providerID);
return DetailScaffold(
label: details?.name ?? "",
onRefresh: () async {
await ref.read(providerID.notifier).fetchPerson(widget.person);
},
backDrops: [...?details?.movies, ...?details?.series].random().firstOrNull?.images,
content: (padding) => Column(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(height: MediaQuery.of(context).size.height / 6),
Padding(
padding: padding,
child: Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.spaceEvenly,
crossAxisAlignment: WrapCrossAlignment.center,
runSpacing: 32,
spacing: 32,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
width: AdaptiveLayout.of(context).layout == LayoutState.phone
? MediaQuery.of(context).size.width
: MediaQuery.of(context).size.width / 3.5,
child: AspectRatio(
aspectRatio: 0.70,
child: FladderImage(
fit: BoxFit.cover,
placeHolder: placeHolder(details?.name ?? ""),
image: details?.images?.primary,
),
),
),
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(details?.name ?? "", style: Theme.of(context).textTheme.displaySmall)),
const SizedBox(width: 15),
SelectableIconButton(
onPressed: () async => await ref
.read(userProvider.notifier)
.setAsFavorite(!(details?.userData.isFavourite ?? false), details?.id ?? ""),
selected: (details?.userData.isFavourite ?? false),
selectedIcon: Icons.favorite_rounded,
icon: Icons.favorite_border_rounded,
),
],
),
),
if (details?.dateOfBirth != null)
Text("Birthday: ${DateFormat.yMEd().format(details?.dateOfBirth ?? DateTime.now()).toString()}"),
if (details?.age != null) Text("Age: ${details?.age}"),
if (details?.birthPlace.isEmpty == false) Text("Born in ${details?.birthPlace.join(",")}"),
if (details?.overview.externalUrls?.isNotEmpty ?? false)
ExternalUrlsRow(
urls: details?.overview.externalUrls,
).padding(padding),
],
),
],
),
),
const SizedBox(height: 32),
if (details?.movies.isNotEmpty ?? false)
PosterRow(contentPadding: padding, posters: details?.movies ?? [], label: "Movies"),
if (details?.series.isNotEmpty ?? false)
PosterRow(contentPadding: padding, posters: details?.series ?? [], label: "Series")
],
),
);
}
Widget placeHolder(String name) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: FractionallySizedBox(
widthFactor: 0.4,
child: Card(
shape: const CircleBorder(),
child: Center(
child: Text(
name.getInitials(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
)),
),
),
);
}
}

View file

@ -0,0 +1,185 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/items/season_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/episode_details_list.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/people_row.dart';
import 'package:fladder/screens/shared/media/person_list_.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';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SeasonDetailScreen extends ConsumerStatefulWidget {
final ItemBaseModel item;
const SeasonDetailScreen({required this.item, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SeasonDetailScreenState();
}
class _SeasonDetailScreenState extends ConsumerState<SeasonDetailScreen> {
Set<EpisodeDetailsViewType> viewOptions = {EpisodeDetailsViewType.grid};
late final providerId = seasonDetailsProvider(widget.item.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(providerId);
return DetailScaffold(
label: details?.localizedName(context) ?? "",
item: details,
actions: (context) => details?.generateActions(context, ref, exclude: {
ItemActions.details,
}),
onRefresh: () async {
await ref.read(providerId.notifier).fetchDetails(widget.item.id);
},
backDrops: details?.parentImages,
content: (padding) => Padding(
padding: const EdgeInsets.only(bottom: 64),
child: details != null
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.35),
Wrap(
alignment: WrapAlignment.spaceAround,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
MediaHeader(
name: "${details.seriesName} - ${details.name}",
logo: details.parentImages?.logo,
),
OverviewHeader(
name: details.seriesName,
padding: padding,
subTitle: details.localizedName(context),
onTitleClicked: () => details.parentBaseModel.navigateTo(context),
originalTitle: details.seriesName,
productionYear: details.overview.productionYear,
runTime: details.overview.runTime,
studios: details.overview.studios,
officialRating: details.overview.parentalRating,
genres: details.overview.genreItems,
communityRating: details.overview.communityRating,
externalUrls: details.overview.externalUrls,
),
],
),
),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 300),
child: Card(child: FladderImage(image: details.getPosters?.primary))),
],
).padding(padding),
Row(
children: [
Expanded(
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SelectableIconButton(
onPressed: () async => await ref
.read(userProvider.notifier)
.setAsFavorite(!details.userData.isFavourite, details.id),
selected: details.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
),
SelectableIconButton(
onPressed: () async => await ref
.read(userProvider.notifier)
.markAsPlayed(!details.userData.played, details.id),
selected: details.userData.played,
selectedIcon: IconsaxBold.tick_circle,
icon: IconsaxOutline.tick_circle,
),
],
),
),
Row(
children: [
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(200)),
child: SegmentedButton(
style: ButtonStyle(
elevation: WidgetStatePropertyAll(5),
side: WidgetStatePropertyAll(BorderSide.none),
),
showSelectedIcon: true,
segments: EpisodeDetailsViewType.values
.map(
(e) => ButtonSegment(
value: e,
icon: Icon(e.icon),
label: SizedBox(
height: 50,
child: Center(
child: Text(
e.name.capitalize(),
),
)),
),
)
.toList(),
selected: viewOptions,
onSelectionChanged: (newOptions) {
setState(() {
viewOptions = newOptions;
});
},
),
),
],
),
],
).padding(padding),
if (details.overview.summary.isNotEmpty)
ExpandingOverview(
text: details.overview.summary,
).padding(padding),
if (details.overview.directors.isNotEmpty)
PersonList(
label: context.localized.director(2),
people: details.overview.directors,
).padding(padding),
if (details.overview.writers.isNotEmpty)
PersonList(label: context.localized.writer(2), people: details.overview.writers).padding(padding),
if (details.episodes.isNotEmpty)
EpisodeDetailsList(
viewType: viewOptions.first,
episodes: details.episodes,
padding: padding,
),
if (details.overview.people.isNotEmpty)
PeopleRow(
people: details.overview.people,
contentPadding: padding,
),
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
)
: null,
),
);
}
}

View file

@ -0,0 +1,165 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/screens/details_screens/components/overview_header.dart';
import 'package:fladder/screens/shared/media/components/media_play_button.dart';
import 'package:fladder/screens/shared/media/components/next_up_episode.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/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/items/series_details_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/shared/media/expanding_overview.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/list_padding.dart';
import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
import 'package:go_router/go_router.dart';
class SeriesDetailScreen extends ConsumerStatefulWidget {
final ItemBaseModel item;
const SeriesDetailScreen({required this.item, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SeriesDetailScreenState();
}
class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
late final providerId = seriesDetailsProvider(widget.item.id);
@override
Widget build(BuildContext context) {
final details = ref.watch(providerId);
return DetailScaffold(
label: details?.name ?? "",
item: details,
actions: (context) => details?.generateActions(
context,
ref,
exclude: {
ItemActions.play,
ItemActions.playFromStart,
ItemActions.details,
},
onDeleteSuccesFully: (item) {
if (context.mounted) {
context.pop();
}
},
),
onRefresh: () => ref.read(providerId.notifier).fetchDetails(widget.item),
backDrops: details?.images,
content: (padding) => details != null
? Padding(
padding: const EdgeInsets.only(bottom: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.35),
MediaHeader(
name: details.name,
logo: details.images?.logo,
),
OverviewHeader(
name: details.name,
padding: padding,
originalTitle: details.originalTitle,
productionYear: details.overview.productionYear,
runTime: details.overview.runTime,
studios: details.overview.studios,
officialRating: details.overview.parentalRating,
genres: details.overview.genreItems,
communityRating: details.overview.communityRating,
externalUrls: details.overview.externalUrls,
),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
MediaPlayButton(
item: details.nextUp,
onPressed: details.nextUp != null
? () async {
await details.nextUp.play(context, ref);
ref.read(providerId.notifier).fetchDetails(widget.item);
}
: null,
onLongPressed: details.nextUp != null
? () async {
await details.nextUp.play(context, ref, showPlaybackOption: true);
ref.read(providerId.notifier).fetchDetails(widget.item);
}
: null,
),
SelectableIconButton(
onPressed: () async {
await ref
.read(userProvider.notifier)
.setAsFavorite(!details.userData.isFavourite, details.id);
},
selected: details.userData.isFavourite,
selectedIcon: IconsaxBold.heart,
icon: IconsaxOutline.heart,
),
SelectableIconButton(
onPressed: () async {
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
},
selected: details.userData.played,
selectedIcon: IconsaxBold.tick_circle,
icon: IconsaxOutline.tick_circle,
),
],
).padding(padding),
if (details.nextUp != null)
NextUpEpisode(
nextEpisode: details.nextUp!,
onChanged: (episode) => ref.read(providerId.notifier).updateEpisodeInfo(episode),
).padding(padding),
if (details.overview.summary.isNotEmpty)
ExpandingOverview(
text: details.overview.summary,
).padding(padding),
if (details.availableEpisodes?.isNotEmpty ?? false)
EpisodePosters(
contentPadding: padding,
label: context.localized.episode(details.availableEpisodes?.length ?? 2),
playEpisode: (episode) async {
await episode.play(
context,
ref,
);
ref.read(providerId.notifier).fetchDetails(widget.item);
},
episodes: details.availableEpisodes ?? [],
),
if (details.seasons?.isNotEmpty ?? false)
SeasonsRow(
contentPadding: padding,
seasons: details.seasons,
onSeasonPressed: (season) => season.navigateTo(context),
),
if (details.overview.people.isNotEmpty)
PeopleRow(
people: details.overview.people,
contentPadding: padding,
),
if (details.related.isNotEmpty)
PosterRow(posters: details.related, contentPadding: padding, label: context.localized.related),
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
),
)
: Container(),
);
}
}