mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-15 18:25:59 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
228
lib/screens/details_screens/book_detail_screen.dart
Normal file
228
lib/screens/details_screens/book_detail_screen.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/screens/details_screens/components/label_title_item.dart
Normal file
39
lib/screens/details_screens/components/label_title_item.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/screens/details_screens/components/overview_header.dart
Normal file
165
lib/screens/details_screens/components/overview_header.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
4
lib/screens/details_screens/details_screens.dart
Normal file
4
lib/screens/details_screens/details_screens.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export 'movie_detail_screen.dart';
|
||||
export 'series_detail_screen.dart';
|
||||
export 'person_detail_screen.dart';
|
||||
export 'empty_item.dart';
|
||||
19
lib/screens/details_screens/empty_item.dart
Normal file
19
lib/screens/details_screens/empty_item.dart
Normal 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.")),
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/screens/details_screens/episode_detail_screen.dart
Normal file
176
lib/screens/details_screens/episode_detail_screen.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/screens/details_screens/folder_detail_screen.dart
Normal file
59
lib/screens/details_screens/folder_detail_screen.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/screens/details_screens/movie_detail_screen.dart
Normal file
164
lib/screens/details_screens/movie_detail_screen.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
127
lib/screens/details_screens/person_detail_screen.dart
Normal file
127
lib/screens/details_screens/person_detail_screen.dart
Normal 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),
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
185
lib/screens/details_screens/season_detail_screen.dart
Normal file
185
lib/screens/details_screens/season_detail_screen.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/screens/details_screens/series_detail_screen.dart
Normal file
165
lib/screens/details_screens/series_detail_screen.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue