feature: Details screen rework (#190)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-12-27 15:21:47 +01:00 committed by GitHub
parent 473e817e0f
commit d2138da785
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 462 additions and 394 deletions

View file

@ -1148,5 +1148,6 @@
"mediaSegmentRecap": "Recap", "mediaSegmentRecap": "Recap",
"mediaSegmentOutro": "Outro", "mediaSegmentOutro": "Outro",
"mediaSegmentIntro": "Intro", "mediaSegmentIntro": "Intro",
"errorLogs": "Error logs" "errorLogs": "Error logs",
"external": "External"
} }

View file

@ -88,7 +88,7 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
if (kDebugMode) { if (kDebugMode) {
print('${rec.level.name}: ${rec.time}: ${rec.message}'); print('${rec.level.name}: ${rec.time}: ${rec.message}');
} }
if (rec.level != Level.INFO) { if (rec.level > Level.INFO) {
state = [ErrorViewModel(rec: rec), ...state]; state = [ErrorViewModel(rec: rec), ...state];
if (state.length >= maxLength) { if (state.length >= maxLength) {
state = state.sublist(0, maxLength); state = state.sublist(0, maxLength);
@ -97,7 +97,7 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
} }
void logFile(FlutterErrorDetails details) { void logFile(FlutterErrorDetails details) {
logger.severe('Flutter error: ${details.exception}', details.exception, details.stack!); logger.severe('Flutter error: ${details.exception}', details.exception, details.stack);
if (details.stack != null && kDebugMode) { if (details.stack != null && kDebugMode) {
print('${details.stack}'); print('${details.stack}');

View file

@ -5,14 +5,15 @@ import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/providers/items/book_details_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart';
import 'package:fladder/providers/user_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/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.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/components/media_play_button.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/poster_list_item.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/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
@ -65,50 +66,34 @@ class _BookDetailScreenState extends ConsumerState<BookDetailScreen> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ 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( Row(
children: [ 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( Flexible(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (details.nextUp != null) if (details.nextUp != null)
OverviewHeader( OverviewHeader(
subTitle: details.book!.parentName ?? details.parentModel?.name, subTitle: details.book?.parentName ?? details.parentModel?.name,
name: details.nextUp!.name, name: details.nextUp?.name ?? "",
image: ImagesData(
logo: details.book?.getPosters?.primary,
),
centerButtons: Builder(
builder: (context) {
//Wrapped so the correct context is used for refreshing the pages
return MediaPlayButton(
item: details.nextUp!,
onPressed: () async => details.nextUp.play(context, ref, provider: provider),
);
},
),
productionYear: details.nextUp!.overview.productionYear, productionYear: details.nextUp!.overview.productionYear,
runTime: details.nextUp!.overview.runTime, runTime: details.nextUp!.overview.runTime,
genres: details.nextUp!.overview.genreItems, genres: details.nextUp!.overview.genreItems,
studios: details.nextUp!.overview.studios, studios: details.nextUp!.overview.studios,
officialRating: details.nextUp!.overview.parentalRating, officialRating: details.nextUp!.overview.parentalRating,
communityRating: details.nextUp!.overview.communityRating, communityRating: details.nextUp!.overview.communityRating,
externalUrls: details.nextUp!.overview.externalUrls,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap( Wrap(
@ -116,14 +101,6 @@ class _BookDetailScreenState extends ConsumerState<BookDetailScreen> {
runSpacing: 8, runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ 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) if (details.parentModel != null)
SelectableIconButton( SelectableIconButton(
onPressed: () async => await details.parentModel?.navigateTo(context), onPressed: () async => await details.parentModel?.navigateTo(context),
@ -177,52 +154,61 @@ class _BookDetailScreenState extends ConsumerState<BookDetailScreen> {
text: details.nextUp!.overview.summary, text: details.nextUp!.overview.summary,
).padding(padding), ).padding(padding),
if (details.chapters.length > 1) if (details.chapters.length > 1)
Builder(builder: (context) { Builder(
final parentContext = context; builder: (context) {
return Column( final parentContext = context;
mainAxisSize: MainAxisSize.min, return Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text(context.localized.chapter(details.chapters.length), children: [
style: Theme.of(context).textTheme.titleLarge), Text(context.localized.chapter(details.chapters.length),
const Padding( style: Theme.of(context).textTheme.titleLarge),
padding: EdgeInsets.symmetric(vertical: 16), const Padding(
child: Divider(), padding: EdgeInsets.symmetric(vertical: 16),
), child: Divider(),
...details.chapters.map( ),
(e) { ...details.chapters.map(
final current = e == details.nextUp; (e) {
return Padding( final current = e == details.nextUp;
padding: const EdgeInsets.only(bottom: 2), return Padding(
child: Opacity( padding: const EdgeInsets.only(bottom: 2),
opacity: e.userData.played ? 0.65 : 1, child: Opacity(
child: Card( opacity: e.userData.played ? 0.65 : 1,
color: current ? Theme.of(context).colorScheme.surfaceContainerHighest : null, child: Card(
child: PosterListItem( color: current ? Theme.of(context).colorScheme.surfaceContainerHighest : null,
poster: e, child: PosterListItem(
onPressed: (action, item) => showBottomSheetPill( poster: e,
context: context, onPressed: (action, item) => showBottomSheetPill(
item: item, context: context,
content: (context, scrollController) => ListView( item: item,
shrinkWrap: true, content: (context, scrollController) => ListView(
controller: scrollController, shrinkWrap: true,
children: item controller: scrollController,
.generateActions( children: item
parentContext, .generateActions(
ref, parentContext,
) ref,
.listTileItems(context, useIcons: true), )
.listTileItems(context, useIcons: true),
),
), ),
), ),
), ),
), ),
), );
); },
}, )
) ],
], ).padding(padding);
).padding(padding); },
}) ),
if (details.nextUp?.overview.externalUrls?.isNotEmpty == true)
Padding(
padding: padding,
child: ExternalUrlsRow(
urls: details.nextUp?.overview.externalUrls,
),
)
].addPadding(const EdgeInsets.symmetric(vertical: 16)), ].addPadding(const EdgeInsets.symmetric(vertical: 16)),
), ),
) )

View file

@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/screens/shared/media/components/chip_button.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/components/small_detail_widgets.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/adaptive_layout.dart';
import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/humanize_duration.dart';
import 'package:fladder/util/list_padding.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 { class OverviewHeader extends ConsumerWidget {
final String name; final String name;
final ImagesData? image;
final Widget? centerButtons;
final EdgeInsets? padding; final EdgeInsets? padding;
final String? subTitle; final String? subTitle;
final String? originalTitle; final String? originalTitle;
@ -19,10 +25,10 @@ class OverviewHeader extends ConsumerWidget {
final double? communityRating; final double? communityRating;
final List<Studio> studios; final List<Studio> studios;
final List<GenreItems> genres; final List<GenreItems> genres;
final List<ExternalUrls>? externalUrls;
final List<Widget> actions;
const OverviewHeader({ const OverviewHeader({
required this.name, required this.name,
this.image,
this.centerButtons,
this.padding, this.padding,
this.subTitle, this.subTitle,
this.originalTitle, this.originalTitle,
@ -31,134 +37,118 @@ class OverviewHeader extends ConsumerWidget {
this.runTime, this.runTime,
this.officialRating, this.officialRating,
this.communityRating, this.communityRating,
this.externalUrls,
this.genres = const [], this.genres = const [],
this.studios = const [], this.studios = const [],
this.actions = const [],
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final mainStyle = Theme.of(context).textTheme.headlineMedium?.copyWith( final mainStyle = Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
); );
final subStyle = Theme.of(context).textTheme.titleMedium?.copyWith( final subStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 20, fontSize: 18,
); );
return Padding( final fullHeight =
padding: padding ?? EdgeInsets.zero, (MediaQuery.sizeOf(context).height - (MediaQuery.paddingOf(context).top + 150)).clamp(50, 1250).toDouble();
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, final crossAlignment =
mainAxisSize: MainAxisSize.min, AdaptiveLayout.of(context).layout != LayoutState.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center;
children: [
const SizedBox(height: 32), return ConstrainedBox(
if (subTitle == null) constraints: BoxConstraints(
Flexible( minHeight: fullHeight,
child: SelectableText( ),
name, child: Padding(
style: mainStyle, padding: padding ?? EdgeInsets.zero,
), child: Column(
) mainAxisAlignment: MainAxisAlignment.end,
else ...{ crossAxisAlignment: crossAlignment,
Flexible( mainAxisSize: MainAxisSize.min,
child: SelectableText( children: [
subTitle ?? "", MediaHeader(
style: mainStyle, name: name,
), logo: image?.logo,
onTap: onTitleClicked,
), ),
Flexible( Column(
child: Opacity( mainAxisSize: MainAxisSize.min,
opacity: 0.75, crossAxisAlignment: crossAlignment,
child: Row( children: [
children: [ if (subTitle != null)
Flexible( Flexible(
child: SelectableText(
name,
style: subStyle,
onTap: onTitleClicked,
),
),
if (onTitleClicked != null)
IconButton(
onPressed: onTitleClicked,
icon: Transform.translate(offset: const Offset(0, 1.5), child: const 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( child: SelectableText(
officialRating.toString(), subTitle ?? "",
style: subStyle, textAlign: TextAlign.center,
style: mainStyle,
), ),
), ),
), if (name.toLowerCase() != originalTitle?.toLowerCase() && originalTitle != null)
if (communityRating != null) SelectableText(
Row( originalTitle.toString(),
textAlign: TextAlign.center,
style: subStyle,
),
].addInBetween(const SizedBox(height: 4)),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: crossAlignment,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
direction: Axis.horizontal,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
Icon( if (officialRating != null)
Icons.star_rate_rounded, ChipButton(
color: Theme.of(context).colorScheme.primary, label: officialRating.toString(),
), ),
Text( if (productionYear != null)
communityRating?.toStringAsFixed(1) ?? "", SelectableText(
style: subStyle, productionYear.toString(),
), textAlign: TextAlign.center,
], style: subStyle,
),
if (runTime != null && (runTime?.inSeconds ?? 0) > 1)
SelectableText(
runTime.humanize.toString(),
textAlign: TextAlign.center,
style: subStyle,
),
if (communityRating != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.star_rate_rounded,
color: Theme.of(context).colorScheme.primary,
),
Text(
communityRating?.toStringAsFixed(1) ?? "",
style: subStyle,
),
],
),
].addInBetween(CircleAvatar(
radius: 3,
backgroundColor: Theme.of(context).colorScheme.onSurface,
)),
), ),
], if (genres.isNotEmpty)
), Genres(
const SizedBox(height: 6), genres: genres.take(6).toList(),
if (studios.isNotEmpty) ),
Text( ].addInBetween(const SizedBox(height: 10)),
"${context.localized.watchOn} ${studios.map((e) => e.name).first}",
style: subStyle?.copyWith(fontSize: 16, color: Colors.grey),
), ),
const SizedBox(height: 6), if (centerButtons != null) centerButtons!,
if (externalUrls?.isNotEmpty ?? false) ].addInBetween(const SizedBox(height: 21)),
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

@ -12,10 +12,11 @@ import 'package:fladder/screens/details_screens/components/overview_header.dart'
import 'package:fladder/screens/shared/detail_scaffold.dart'; import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/shared/media/chapter_row.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/components/media_play_button.dart'; import 'package:fladder/screens/shared/media/components/media_play_button.dart';
import 'package:fladder/screens/shared/media/episode_posters.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/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.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/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
@ -41,6 +42,8 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
final details = ref.watch(providerInstance); final details = ref.watch(providerInstance);
final seasonDetails = details.series; final seasonDetails = details.series;
final episodeDetails = details.episode; final episodeDetails = details.episode;
final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: widget.item.name, label: widget.item.name,
@ -67,15 +70,24 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.35),
MediaHeader(
name: details.series?.name ?? "",
logo: seasonDetails.images?.logo,
),
OverviewHeader( OverviewHeader(
name: details.series?.name ?? "", name: details.series?.name ?? "",
image: seasonDetails.images,
centerButtons: 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);
},
)
: null,
padding: padding, padding: padding,
subTitle: details.episode?.name, subTitle: details.episode?.detailedName(context),
originalTitle: details.series?.originalTitle, originalTitle: details.series?.originalTitle,
onTitleClicked: () => details.series?.navigateTo(context), onTitleClicked: () => details.series?.navigateTo(context),
productionYear: details.series?.overview.productionYear, productionYear: details.series?.overview.productionYear,
@ -84,25 +96,13 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
genres: details.series?.overview.genreItems ?? [], genres: details.series?.overview.genreItems ?? [],
officialRating: details.series?.overview.parentalRating, officialRating: details.series?.overview.parentalRating,
communityRating: details.series?.overview.communityRating, communityRating: details.series?.overview.communityRating,
externalUrls: details.series?.overview.externalUrls,
), ),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
alignment: wrapAlignment,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ 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( SelectableIconButton(
onPressed: () async { onPressed: () async {
await ref await ref
@ -169,6 +169,13 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
), ),
episodes: details.episodes.where((element) => element.season == episodeDetails.season).toList(), episodes: details.episodes.where((element) => element.season == episodeDetails.season).toList(),
), ),
if (details.series?.overview.externalUrls?.isNotEmpty == true)
Padding(
padding: padding,
child: ExternalUrlsRow(
urls: details.series?.overview.externalUrls,
),
)
].addPadding(const EdgeInsets.symmetric(vertical: 16)), ].addPadding(const EdgeInsets.symmetric(vertical: 16)),
), ),
) )

View file

@ -1,3 +1,4 @@
import 'package:fladder/util/adaptive_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -11,9 +12,9 @@ import 'package:fladder/screens/details_screens/components/media_stream_informat
import 'package:fladder/screens/details_screens/components/overview_header.dart'; import 'package:fladder/screens/details_screens/components/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.dart'; import 'package:fladder/screens/shared/detail_scaffold.dart';
import 'package:fladder/screens/shared/media/chapter_row.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/components/media_play_button.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/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.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/poster_row.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
@ -37,6 +38,8 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final details = ref.watch(providerInstance); final details = ref.watch(providerInstance);
final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: widget.item.name, label: widget.item.name,
@ -64,14 +67,28 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.25),
MediaHeader(
name: details.name,
logo: details.images?.logo,
),
OverviewHeader( OverviewHeader(
name: details.name, name: details.name,
image: details.images,
padding: padding, padding: padding,
centerButtons: 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);
},
),
originalTitle: details.originalTitle, originalTitle: details.originalTitle,
productionYear: details.overview.productionYear, productionYear: details.overview.productionYear,
runTime: details.overview.runTime, runTime: details.overview.runTime,
@ -79,31 +96,13 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
studios: details.overview.studios, studios: details.overview.studios,
officialRating: details.overview.parentalRating, officialRating: details.overview.parentalRating,
communityRating: details.overview.communityRating, communityRating: details.overview.communityRating,
externalUrls: details.overview.externalUrls,
), ),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
alignment: wrapAlignment,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ 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( SelectableIconButton(
onPressed: () async { onPressed: () async {
await ref await ref
@ -157,6 +156,13 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
), ),
if (details.related.isNotEmpty) if (details.related.isNotEmpty)
PosterRow(posters: details.related, contentPadding: padding, label: "Related"), PosterRow(posters: details.related, contentPadding: padding, label: "Related"),
if (details.overview.externalUrls?.isNotEmpty == true)
Padding(
padding: padding,
child: ExternalUrlsRow(
urls: details.overview.externalUrls,
),
)
].addPadding(const EdgeInsets.symmetric(vertical: 16)), ].addPadding(const EdgeInsets.symmetric(vertical: 16)),
), ),
) )

View file

@ -9,12 +9,11 @@ import 'package:fladder/providers/items/season_details_provider.dart';
import 'package:fladder/providers/user_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/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.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/episode_details_list.dart';
import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.dart'; import 'package:fladder/screens/shared/media/people_row.dart';
import 'package:fladder/screens/shared/media/person_list_.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -56,51 +55,20 @@ class _SeasonDetailScreenState extends ConsumerState<SeasonDetailScreen> {
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.25), OverviewHeader(
Wrap( name: details.seriesName,
alignment: WrapAlignment.spaceAround, image: details.parentImages,
runAlignment: WrapAlignment.center, padding: padding,
crossAxisAlignment: WrapCrossAlignment.center, subTitle: details.localizedName(context),
children: [ onTitleClicked: () => details.parentBaseModel.navigateTo(context),
ConstrainedBox( originalTitle: details.seriesName,
constraints: const BoxConstraints(maxWidth: 300), productionYear: details.overview.productionYear,
child: AspectRatio( runTime: details.overview.runTime,
aspectRatio: 0.67, studios: details.overview.studios,
child: Card( officialRating: details.overview.parentalRating,
child: FladderImage(image: details.getPosters?.primary), genres: details.overview.genreItems,
), communityRating: details.overview.communityRating,
), ),
),
ConstrainedBox(
constraints: const 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,
),
],
),
),
],
).padding(padding),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -191,6 +159,13 @@ class _SeasonDetailScreenState extends ConsumerState<SeasonDetailScreen> {
people: details.overview.people, people: details.overview.people,
contentPadding: padding, contentPadding: padding,
), ),
if (details.overview.externalUrls?.isNotEmpty == true)
Padding(
padding: padding,
child: ExternalUrlsRow(
urls: details.overview.externalUrls,
),
)
].addPadding(const EdgeInsets.symmetric(vertical: 16)), ].addPadding(const EdgeInsets.symmetric(vertical: 16)),
) )
: null, : null,

View file

@ -10,14 +10,15 @@ import 'package:fladder/providers/items/series_details_provider.dart';
import 'package:fladder/providers/user_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/overview_header.dart';
import 'package:fladder/screens/shared/detail_scaffold.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/components/media_play_button.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/screens/shared/media/components/next_up_episode.dart';
import 'package:fladder/screens/shared/media/episode_posters.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/expanding_overview.dart';
import 'package:fladder/screens/shared/media/external_urls.dart';
import 'package:fladder/screens/shared/media/people_row.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/poster_row.dart';
import 'package:fladder/screens/shared/media/season_row.dart'; import 'package:fladder/screens/shared/media/season_row.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.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/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
@ -41,6 +42,9 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final details = ref.watch(providerId); final details = ref.watch(providerId);
final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: details?.name ?? "", label: details?.name ?? "",
item: details, item: details,
@ -67,13 +71,24 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.35),
MediaHeader(
name: details.name,
logo: details.images?.logo,
),
OverviewHeader( OverviewHeader(
name: details.name, name: details.name,
image: details.images,
centerButtons: 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,
),
padding: padding, padding: padding,
originalTitle: details.originalTitle, originalTitle: details.originalTitle,
productionYear: details.overview.productionYear, productionYear: details.overview.productionYear,
@ -82,28 +97,13 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
officialRating: details.overview.parentalRating, officialRating: details.overview.parentalRating,
genres: details.overview.genreItems, genres: details.overview.genreItems,
communityRating: details.overview.communityRating, communityRating: details.overview.communityRating,
externalUrls: details.overview.externalUrls,
), ),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
alignment: wrapAlignment,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ 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( SelectableIconButton(
onPressed: () async { onPressed: () async {
await ref await ref
@ -159,6 +159,13 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
), ),
if (details.related.isNotEmpty) if (details.related.isNotEmpty)
PosterRow(posters: details.related, contentPadding: padding, label: context.localized.related), PosterRow(posters: details.related, contentPadding: padding, label: context.localized.related),
if (details.overview.externalUrls?.isNotEmpty == true)
Padding(
padding: padding,
child: ExternalUrlsRow(
urls: details.overview.externalUrls,
),
)
].addPadding(const EdgeInsets.symmetric(vertical: 16)), ].addPadding(const EdgeInsets.symmetric(vertical: 16)),
), ),
) )

View file

@ -61,9 +61,11 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final padding = EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 25); final padding = EdgeInsets.symmetric(horizontal: MediaQuery.sizeOf(context).width / 25);
final backGroundColor = Theme.of(context).colorScheme.surface.withOpacity(0.8); final backGroundColor = Theme.of(context).colorScheme.surface.withOpacity(0.8);
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
final minHeight = 450.0.clamp(0, MediaQuery.sizeOf(context).height).toDouble();
final maxHeight = MediaQuery.sizeOf(context).height - 10;
return PullToRefresh( return PullToRefresh(
onRefresh: () async { onRefresh: () async {
await widget.onRefresh?.call(); await widget.onRefresh?.call();
@ -90,18 +92,54 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
SingleChildScrollView( SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Stack( child: Stack(
alignment: Alignment.topCenter,
children: [ children: [
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height - 10, height: maxHeight,
width: MediaQuery.of(context).size.width, width: MediaQuery.sizeOf(context).width,
child: FladderImage( child: FladderImage(
image: backgroundImage, image: backgroundImage,
blurOnly: true,
), ),
), ),
if (backgroundImage != null)
Align(
alignment: Alignment.topCenter,
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Colors.white,
Colors.white,
Colors.white,
Colors.white,
Colors.white.withOpacity(0),
],
).createShader(bounds),
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: minHeight - 20,
maxHeight: maxHeight.clamp(minHeight, 2500) - 20,
),
child: FadeInImage(
placeholder: backgroundImage!.imageProvider,
placeholderColor: Colors.transparent,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
placeholderFit: BoxFit.cover,
excludeFromSemantics: true,
filterQuality: FilterQuality.high,
placeholderFilterQuality: FilterQuality.low,
image: backgroundImage!.imageProvider,
),
),
),
),
Container( Container(
height: MediaQuery.of(context).size.height, width: double.infinity,
width: MediaQuery.of(context).size.width, height: maxHeight + 10,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@ -117,8 +155,8 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
), ),
), ),
Container( Container(
height: MediaQuery.of(context).size.height, height: MediaQuery.sizeOf(context).height,
width: MediaQuery.of(context).size.width, width: MediaQuery.sizeOf(context).width,
color: widget.backgroundColor, color: widget.backgroundColor,
), ),
Padding( Padding(
@ -128,8 +166,8 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
top: MediaQuery.of(context).padding.top + 50), top: MediaQuery.of(context).padding.top + 50),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height, minHeight: MediaQuery.sizeOf(context).height,
maxWidth: MediaQuery.of(context).size.width, maxWidth: MediaQuery.sizeOf(context).width,
), ),
child: widget.content(padding), child: widget.content(padding),
), ),

View file

@ -13,6 +13,7 @@ class FlatButton extends ConsumerWidget {
final BorderRadius? borderRadiusGeometry; final BorderRadius? borderRadiusGeometry;
final Color? splashColor; final Color? splashColor;
final double elevation; final double elevation;
final bool showFeedback;
final Clip clipBehavior; final Clip clipBehavior;
const FlatButton({ const FlatButton({
this.child, this.child,
@ -23,6 +24,7 @@ class FlatButton extends ConsumerWidget {
this.borderRadiusGeometry, this.borderRadiusGeometry,
this.splashColor, this.splashColor,
this.elevation = 0, this.elevation = 0,
this.showFeedback = true,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
super.key, super.key,
}); });
@ -46,6 +48,7 @@ class FlatButton extends ConsumerWidget {
onSecondaryTapDown: onSecondaryTapDown, onSecondaryTapDown: onSecondaryTapDown,
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10), borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5), splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
hoverColor: showFeedback ? null : Colors.transparent,
splashFactory: InkSparkle.splashFactory, splashFactory: InkSparkle.splashFactory,
), ),
), ),

View file

@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
@ -8,8 +12,6 @@ import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/horizontal_list.dart'; import 'package:fladder/widgets/shared/horizontal_list.dart';
import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ChapterRow extends ConsumerWidget { class ChapterRow extends ConsumerWidget {
final List<Chapter> chapters; final List<Chapter> chapters;

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/screens/shared/flat_button.dart';
class ChipButton extends ConsumerWidget { class ChipButton extends ConsumerWidget {
final String label; final String label;
final Function()? onPressed; final Function()? onPressed;
@ -8,19 +11,20 @@ class ChipButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return TextButton( return Card(
onPressed: onPressed, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.15),
style: TextButton.styleFrom( shadowColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.75), child: FlatButton(
shape: RoundedRectangleBorder( onTap: onPressed,
borderRadius: BorderRadius.circular(10), // ),
side: BorderSide.none, child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
),
), ),
), ),
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
),
); );
} }
} }

View file

@ -1,50 +1,66 @@
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/util/fladder_image.dart';
class MediaHeader extends ConsumerWidget { class MediaHeader extends ConsumerWidget {
final String name; final String name;
final ImageData? logo; final ImageData? logo;
final Function()? onTap;
const MediaHeader({ const MediaHeader({
required this.name, required this.name,
required this.logo, required this.logo,
this.onTap,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final maxWidth = final maxSize = 700.0;
switch (AdaptiveLayout.layoutOf(context)) { LayoutState.desktop || LayoutState.tablet => 0.55, _ => 1 }; final textWidget = Container(
return Center( height: 512,
child: Padding( alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 32), child: SelectableText(
child: Material( name,
elevation: 30, textAlign: TextAlign.center,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(150)), style: Theme.of(context).textTheme.headlineLarge?.copyWith(
shadowColor: Colors.black.withOpacity(0.35), fontSize: 55,
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.2,
maxWidth: MediaQuery.sizeOf(context).width * maxWidth,
),
child: FladderImage(
image: logo,
enableBlur: true,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => Container(
color: Colors.red,
width: 512,
height: 512,
child: child,
),
placeHolder: const SizedBox(height: 0),
fit: BoxFit.contain,
),
), ),
),
);
return Center(
child: Material(
elevation: 30,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(150)),
shadowColor: Colors.black.withOpacity(0.3),
color: Colors.transparent,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (MediaQuery.sizeOf(context).height * 0.275).clamp(0, maxSize),
maxWidth: MediaQuery.sizeOf(context).width.clamp(0, maxSize),
),
child: Stack(
children: [
logo != null
? FladderImage(
image: logo,
enableBlur: true,
alignment: Alignment.bottomCenter,
imageErrorBuilder: (context, object, stack) => textWidget,
placeHolder: const SizedBox(height: 0),
fit: BoxFit.contain,
)
: textWidget,
if (onTap != null)
Positioned.fill(
child: GestureDetector(
onTap: onTap,
),
),
],
), ),
), ),
), ),

View file

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MediaPlayButton extends ConsumerWidget { class MediaPlayButton extends ConsumerWidget {
final ItemBaseModel? item; final ItemBaseModel? item;
@ -33,8 +35,8 @@ class MediaPlayButton extends ConsumerWidget {
child: Text( child: Text(
item?.playButtonLabel(context) ?? "", item?.playButtonLabel(context) ?? "",
maxLines: 2, maxLines: 2,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: textColor, color: textColor,
), ),
), ),

View file

@ -1,3 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
@ -5,9 +10,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sticky_header_text.dart'; import 'package:fladder/util/sticky_header_text.dart';
import 'package:flutter/material.dart'; import 'package:fladder/util/string_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
class NextUpEpisode extends ConsumerWidget { class NextUpEpisode extends ConsumerWidget {
final EpisodeModel nextEpisode; final EpisodeModel nextEpisode;
@ -17,6 +20,7 @@ class NextUpEpisode extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final alreadyPlayed = nextEpisode.userData.played; final alreadyPlayed = nextEpisode.userData.played;
final episodeSummary = nextEpisode.overview.summary.maxLength(limitTo: 250);
return Column( return Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -53,7 +57,7 @@ class NextUpEpisode extends ConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
if (nextEpisode.overview.summary.isNotEmpty) if (nextEpisode.overview.summary.isNotEmpty)
HtmlWidget( HtmlWidget(
nextEpisode.overview.summary, episodeSummary,
textStyle: Theme.of(context).textTheme.titleMedium, textStyle: Theme.of(context).textTheme.titleMedium,
), ),
], ],
@ -88,7 +92,7 @@ class NextUpEpisode extends ConsumerWidget {
mediaStreams: nextEpisode.mediaStreams.copyWith(defaultSubStreamIndex: index))), mediaStreams: nextEpisode.mediaStreams.copyWith(defaultSubStreamIndex: index))),
), ),
if (nextEpisode.overview.summary.isNotEmpty) if (nextEpisode.overview.summary.isNotEmpty)
HtmlWidget(nextEpisode.overview.summary, textStyle: Theme.of(context).textTheme.titleMedium), HtmlWidget(episodeSummary, textStyle: Theme.of(context).textTheme.titleMedium),
], ],
), ),
), ),

View file

@ -82,6 +82,8 @@ class Genres extends StatelessWidget {
return Wrap( return Wrap(
runSpacing: 8, runSpacing: 8,
spacing: 8, spacing: 8,
runAlignment: WrapAlignment.center,
alignment: WrapAlignment.center,
children: genres children: genres
.map( .map(
(genre) => ChipButton( (genre) => ChipButton(

View file

@ -1,11 +1,15 @@
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as customtab; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as customtab;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart' as urllauncher; import 'package:url_launcher/url_launcher.dart' as urllauncher;
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/sticky_header_text.dart';
class ExternalUrlsRow extends ConsumerWidget { class ExternalUrlsRow extends ConsumerWidget {
final List<ExternalUrls>? urls; final List<ExternalUrls>? urls;
const ExternalUrlsRow({ const ExternalUrlsRow({
@ -15,16 +19,28 @@ class ExternalUrlsRow extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Wrap( return Column(
children: urls crossAxisAlignment: CrossAxisAlignment.start,
?.map( mainAxisSize: MainAxisSize.min,
(url) => TextButton( children: [
onPressed: () => launchUrl(context, url.url), StickyHeaderText(
child: Text(url.name), label: context.localized.external,
), ),
) Transform.translate(
.toList() ?? offset: const Offset(-12, 0),
[], child: Wrap(
children: urls
?.map(
(url) => TextButton(
onPressed: () => launchUrl(context, url.url),
child: Text(url.name),
),
)
.toList() ??
[],
),
),
],
); );
} }
} }

View file

@ -10,15 +10,19 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
class FladderImage extends ConsumerWidget { class FladderImage extends ConsumerWidget {
final ImageData? image; final ImageData? image;
final Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded)? frameBuilder; final Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded)? frameBuilder;
final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder;
final Widget? placeHolder; final Widget? placeHolder;
final BoxFit fit; final BoxFit fit;
final AlignmentGeometry? alignment;
final bool enableBlur; final bool enableBlur;
final bool blurOnly; final bool blurOnly;
const FladderImage({ const FladderImage({
required this.image, required this.image,
this.frameBuilder, this.frameBuilder,
this.imageErrorBuilder,
this.placeHolder, this.placeHolder,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.alignment,
this.enableBlur = false, this.enableBlur = false,
this.blurOnly = false, this.blurOnly = false,
super.key, super.key,
@ -50,7 +54,9 @@ class FladderImage extends ConsumerWidget {
fit: fit, fit: fit,
placeholderFit: fit, placeholderFit: fit,
excludeFromSemantics: true, excludeFromSemantics: true,
alignment: alignment ?? Alignment.center,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
imageErrorBuilder: imageErrorBuilder,
placeholderFilterQuality: FilterQuality.low, placeholderFilterQuality: FilterQuality.low,
image: newImage.imageProvider, image: newImage.imageProvider,
) )

View file

@ -6,7 +6,7 @@ extension DurationExtensions on Duration? {
final duration = this!; final duration = this!;
final hours = duration.inHours != 0 ? '${duration.inHours.toString()}h' : null; final hours = duration.inHours != 0 ? '${duration.inHours.toString()}h' : null;
final minutes = duration.inMinutes % 60 != 0 ? '${duration.inMinutes % 60}m'.padLeft(3, '0') : null; final minutes = duration.inMinutes % 60 != 0 ? '${duration.inMinutes % 60}m'.padLeft(3, '0') : null;
final seconds = duration.inHours == 0 ? '${duration.inSeconds % 60}s'.padLeft(3, '0') : null; final seconds = duration.inSeconds % 60 != 0 ? '${duration.inSeconds % 60}s'.padLeft(3, '0') : null;
final result = [hours, minutes, seconds].whereNotNull().map((e) => e).join(' '); final result = [hours, minutes, seconds].whereNotNull().map((e) => e).join(' ');
return result.isNotEmpty ? result : null; return result.isNotEmpty ? result : null;
} }

View file

@ -14,6 +14,7 @@ extension StringExtensions on String {
} }
String maxLength({int limitTo = 75}) { String maxLength({int limitTo = 75}) {
if (isEmpty) return this;
if (length > limitTo) { if (length > limitTo) {
return "${substring(0, limitTo.clamp(0, length))}..."; return "${substring(0, limitTo.clamp(0, length))}...";
} else { } else {

View file

@ -76,9 +76,11 @@ class _TrickPlayImageState extends ConsumerState<TrickPlayImage> {
final Uint8List bytes = response.bodyBytes; final Uint8List bytes = response.bodyBytes;
final ui.Codec codec = await ui.instantiateImageCodec(bytes); final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frameInfo = await codec.getNextFrame(); final ui.FrameInfo frameInfo = await codec.getNextFrame();
setState(() { if (context.mounted) {
image = frameInfo.image; setState(() {
}); image = frameInfo.image;
});
}
} else { } else {
throw Exception('Failed to load network image'); throw Exception('Failed to load network image');
} }