From d2138da785ab6124e7988a833e61451a7a31287a Mon Sep 17 00:00:00 2001 From: PartyDonut <42371342+PartyDonut@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:21:47 +0100 Subject: [PATCH] feature: Details screen rework (#190) Co-authored-by: PartyDonut --- lib/l10n/app_en.arb | 3 +- lib/providers/crash_log_provider.dart | 4 +- .../details_screens/book_detail_screen.dart | 148 ++++++------ .../components/overview_header.dart | 222 +++++++++--------- .../episode_detail_screen.dart | 47 ++-- .../details_screens/movie_detail_screen.dart | 56 +++-- .../details_screens/season_detail_screen.dart | 69 ++---- .../details_screens/series_detail_screen.dart | 51 ++-- lib/screens/shared/detail_scaffold.dart | 58 ++++- lib/screens/shared/flat_button.dart | 3 + lib/screens/shared/media/chapter_row.dart | 6 +- .../shared/media/components/chip_button.dart | 26 +- .../shared/media/components/media_header.dart | 80 ++++--- .../media/components/media_play_button.dart | 10 +- .../media/components/next_up_episode.dart | 14 +- .../components/small_detail_widgets.dart | 2 + lib/screens/shared/media/external_urls.dart | 40 +++- lib/util/fladder_image.dart | 6 + lib/util/humanize_duration.dart | 2 +- lib/util/string_extensions.dart | 1 + lib/widgets/shared/trick_play_image.dart | 8 +- 21 files changed, 462 insertions(+), 394 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3238b0c..0ffe3ef 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1148,5 +1148,6 @@ "mediaSegmentRecap": "Recap", "mediaSegmentOutro": "Outro", "mediaSegmentIntro": "Intro", - "errorLogs": "Error logs" + "errorLogs": "Error logs", + "external": "External" } \ No newline at end of file diff --git a/lib/providers/crash_log_provider.dart b/lib/providers/crash_log_provider.dart index 54fa4d6..f5e917e 100644 --- a/lib/providers/crash_log_provider.dart +++ b/lib/providers/crash_log_provider.dart @@ -88,7 +88,7 @@ class CrashLogNotifier extends StateNotifier> { if (kDebugMode) { print('${rec.level.name}: ${rec.time}: ${rec.message}'); } - if (rec.level != Level.INFO) { + if (rec.level > Level.INFO) { state = [ErrorViewModel(rec: rec), ...state]; if (state.length >= maxLength) { state = state.sublist(0, maxLength); @@ -97,7 +97,7 @@ class CrashLogNotifier extends StateNotifier> { } 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) { print('${details.stack}'); diff --git a/lib/screens/details_screens/book_detail_screen.dart b/lib/screens/details_screens/book_detail_screen.dart index 2288566..286229c 100644 --- a/lib/screens/details_screens/book_detail_screen.dart +++ b/lib/screens/details_screens/book_detail_screen.dart @@ -5,14 +5,15 @@ import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/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/external_urls.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'; @@ -65,50 +66,34 @@ class _BookDetailScreenState extends ConsumerState { 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, + subTitle: details.book?.parentName ?? details.parentModel?.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, 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( @@ -116,14 +101,6 @@ class _BookDetailScreenState extends ConsumerState { 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), @@ -177,52 +154,61 @@ class _BookDetailScreenState extends ConsumerState { 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), + 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); - }) + ); + }, + ) + ], + ).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)), ), ) diff --git a/lib/screens/details_screens/components/overview_header.dart b/lib/screens/details_screens/components/overview_header.dart index 23c09d6..25b6a0f 100644 --- a/lib/screens/details_screens/components/overview_header.dart +++ b/lib/screens/details_screens/components/overview_header.dart @@ -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/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/external_urls.dart'; +import 'package:fladder/util/adaptive_layout.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 ImagesData? image; + final Widget? centerButtons; final EdgeInsets? padding; final String? subTitle; final String? originalTitle; @@ -19,10 +25,10 @@ class OverviewHeader extends ConsumerWidget { final double? communityRating; final List studios; final List genres; - final List? externalUrls; - final List actions; const OverviewHeader({ required this.name, + this.image, + this.centerButtons, this.padding, this.subTitle, this.originalTitle, @@ -31,134 +37,118 @@ class OverviewHeader extends ConsumerWidget { 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( + final mainStyle = Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ); - final subStyle = Theme.of(context).textTheme.titleMedium?.copyWith( - fontSize: 20, + final subStyle = Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 18, ); - 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, - ), + final fullHeight = + (MediaQuery.sizeOf(context).height - (MediaQuery.paddingOf(context).top + 150)).clamp(50, 1250).toDouble(); + + final crossAlignment = + AdaptiveLayout.of(context).layout != LayoutState.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center; + + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: fullHeight, + ), + child: Padding( + padding: padding ?? EdgeInsets.zero, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: crossAlignment, + mainAxisSize: MainAxisSize.min, + children: [ + MediaHeader( + name: name, + logo: image?.logo, + onTap: onTitleClicked, ), - 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: 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), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: crossAlignment, + children: [ + if (subTitle != null) + Flexible( child: SelectableText( - officialRating.toString(), - style: subStyle, + subTitle ?? "", + textAlign: TextAlign.center, + style: mainStyle, ), ), - ), - if (communityRating != null) - Row( + if (name.toLowerCase() != originalTitle?.toLowerCase() && originalTitle != null) + SelectableText( + 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: [ - Icon( - Icons.star_rate_rounded, - color: Theme.of(context).colorScheme.primary, - ), - Text( - communityRating?.toStringAsFixed(1) ?? "", - style: subStyle, - ), - ], + if (officialRating != null) + ChipButton( + label: officialRating.toString(), + ), + if (productionYear != null) + SelectableText( + 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, + )), ), - ], - ), - 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), + if (genres.isNotEmpty) + Genres( + genres: genres.take(6).toList(), + ), + ].addInBetween(const SizedBox(height: 10)), ), - 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), - ), - ), - ], + if (centerButtons != null) centerButtons!, + ].addInBetween(const SizedBox(height: 21)), + ), ), ); } diff --git a/lib/screens/details_screens/episode_detail_screen.dart b/lib/screens/details_screens/episode_detail_screen.dart index 17d0792..9d64f4c 100644 --- a/lib/screens/details_screens/episode_detail_screen.dart +++ b/lib/screens/details_screens/episode_detail_screen.dart @@ -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/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/components/media_play_button.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/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/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; @@ -41,6 +42,8 @@ class _ItemDetailScreenState extends ConsumerState { final details = ref.watch(providerInstance); final seasonDetails = details.series; final episodeDetails = details.episode; + final wrapAlignment = + AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; return DetailScaffold( label: widget.item.name, @@ -67,15 +70,24 @@ class _ItemDetailScreenState extends ConsumerState { 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 ?? "", + 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, - subTitle: details.episode?.name, + subTitle: details.episode?.detailedName(context), originalTitle: details.series?.originalTitle, onTitleClicked: () => details.series?.navigateTo(context), productionYear: details.series?.overview.productionYear, @@ -84,25 +96,13 @@ class _ItemDetailScreenState extends ConsumerState { 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, + alignment: wrapAlignment, 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 @@ -169,6 +169,13 @@ class _ItemDetailScreenState extends ConsumerState { ), 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)), ), ) diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart index 2452b1f..db815b3 100644 --- a/lib/screens/details_screens/movie_detail_screen.dart +++ b/lib/screens/details_screens/movie_detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:fladder/util/adaptive_layout.dart'; import 'package:flutter/material.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/shared/detail_scaffold.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/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/poster_row.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -37,6 +38,8 @@ class _ItemDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { final details = ref.watch(providerInstance); + final wrapAlignment = + AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; return DetailScaffold( label: widget.item.name, @@ -64,14 +67,28 @@ class _ItemDetailScreenState extends ConsumerState { 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, + image: details.images, 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, productionYear: details.overview.productionYear, runTime: details.overview.runTime, @@ -79,31 +96,13 @@ class _ItemDetailScreenState extends ConsumerState { studios: details.overview.studios, officialRating: details.overview.parentalRating, communityRating: details.overview.communityRating, - externalUrls: details.overview.externalUrls, ), Wrap( spacing: 8, runSpacing: 8, + alignment: wrapAlignment, 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 @@ -157,6 +156,13 @@ class _ItemDetailScreenState extends ConsumerState { ), if (details.related.isNotEmpty) 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)), ), ) diff --git a/lib/screens/details_screens/season_detail_screen.dart b/lib/screens/details_screens/season_detail_screen.dart index b6629f7..2a08dc4 100644 --- a/lib/screens/details_screens/season_detail_screen.dart +++ b/lib/screens/details_screens/season_detail_screen.dart @@ -9,12 +9,11 @@ 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/external_urls.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'; @@ -56,51 +55,20 @@ class _SeasonDetailScreenState extends ConsumerState { ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: MediaQuery.of(context).size.height * 0.25), - Wrap( - alignment: WrapAlignment.spaceAround, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: AspectRatio( - aspectRatio: 0.67, - child: Card( - child: FladderImage(image: details.getPosters?.primary), - ), - ), - ), - 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), + OverviewHeader( + name: details.seriesName, + image: details.parentImages, + 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, + ), Row( children: [ Expanded( @@ -191,6 +159,13 @@ class _SeasonDetailScreenState extends ConsumerState { people: details.overview.people, contentPadding: padding, ), + if (details.overview.externalUrls?.isNotEmpty == true) + Padding( + padding: padding, + child: ExternalUrlsRow( + urls: details.overview.externalUrls, + ), + ) ].addPadding(const EdgeInsets.symmetric(vertical: 16)), ) : null, diff --git a/lib/screens/details_screens/series_detail_screen.dart b/lib/screens/details_screens/series_detail_screen.dart index 33a5d7f..d08fce0 100644 --- a/lib/screens/details_screens/series_detail_screen.dart +++ b/lib/screens/details_screens/series_detail_screen.dart @@ -10,14 +10,15 @@ import 'package:fladder/providers/items/series_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/components/media_play_button.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/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/poster_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/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; @@ -41,6 +42,9 @@ class _SeriesDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { final details = ref.watch(providerId); + final wrapAlignment = + AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; + return DetailScaffold( label: details?.name ?? "", item: details, @@ -67,13 +71,24 @@ class _SeriesDetailScreenState extends ConsumerState { 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, + 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, originalTitle: details.originalTitle, productionYear: details.overview.productionYear, @@ -82,28 +97,13 @@ class _SeriesDetailScreenState extends ConsumerState { officialRating: details.overview.parentalRating, genres: details.overview.genreItems, communityRating: details.overview.communityRating, - externalUrls: details.overview.externalUrls, ), Wrap( spacing: 8, runSpacing: 8, + alignment: wrapAlignment, 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 @@ -159,6 +159,13 @@ class _SeriesDetailScreenState extends ConsumerState { ), if (details.related.isNotEmpty) 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)), ), ) diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index 754fe1e..32477c2 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -61,9 +61,11 @@ class _DetailScaffoldState extends ConsumerState { @override 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 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( onRefresh: () async { await widget.onRefresh?.call(); @@ -90,18 +92,54 @@ class _DetailScaffoldState extends ConsumerState { SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Stack( - alignment: Alignment.topCenter, children: [ SizedBox( - height: MediaQuery.of(context).size.height - 10, - width: MediaQuery.of(context).size.width, + height: maxHeight, + width: MediaQuery.sizeOf(context).width, child: FladderImage( 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( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + width: double.infinity, + height: maxHeight + 10, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -117,8 +155,8 @@ class _DetailScaffoldState extends ConsumerState { ), ), Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + height: MediaQuery.sizeOf(context).height, + width: MediaQuery.sizeOf(context).width, color: widget.backgroundColor, ), Padding( @@ -128,8 +166,8 @@ class _DetailScaffoldState extends ConsumerState { top: MediaQuery.of(context).padding.top + 50), child: ConstrainedBox( constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height, - maxWidth: MediaQuery.of(context).size.width, + minHeight: MediaQuery.sizeOf(context).height, + maxWidth: MediaQuery.sizeOf(context).width, ), child: widget.content(padding), ), diff --git a/lib/screens/shared/flat_button.dart b/lib/screens/shared/flat_button.dart index 5eaea82..633271f 100644 --- a/lib/screens/shared/flat_button.dart +++ b/lib/screens/shared/flat_button.dart @@ -13,6 +13,7 @@ class FlatButton extends ConsumerWidget { final BorderRadius? borderRadiusGeometry; final Color? splashColor; final double elevation; + final bool showFeedback; final Clip clipBehavior; const FlatButton({ this.child, @@ -23,6 +24,7 @@ class FlatButton extends ConsumerWidget { this.borderRadiusGeometry, this.splashColor, this.elevation = 0, + this.showFeedback = true, this.clipBehavior = Clip.none, super.key, }); @@ -46,6 +48,7 @@ class FlatButton extends ConsumerWidget { onSecondaryTapDown: onSecondaryTapDown, borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10), splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5), + hoverColor: showFeedback ? null : Colors.transparent, splashFactory: InkSparkle.splashFactory, ), ), diff --git a/lib/screens/shared/media/chapter_row.dart b/lib/screens/shared/media/chapter_row.dart index 754e80a..1e306da 100644 --- a/lib/screens/shared/media/chapter_row.dart +++ b/lib/screens/shared/media/chapter_row.dart @@ -1,4 +1,8 @@ +import 'package:flutter/material.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/screens/shared/flat_button.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/item_actions.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 { final List chapters; diff --git a/lib/screens/shared/media/components/chip_button.dart b/lib/screens/shared/media/components/chip_button.dart index e343cd7..13b5355 100644 --- a/lib/screens/shared/media/components/chip_button.dart +++ b/lib/screens/shared/media/components/chip_button.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; + class ChipButton extends ConsumerWidget { final String label; final Function()? onPressed; @@ -8,19 +11,20 @@ class ChipButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return TextButton( - onPressed: onPressed, - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.75), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: BorderSide.none, + return Card( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.15), + shadowColor: Colors.transparent, + child: FlatButton( + onTap: onPressed, + // ), + 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), - ), ); } } diff --git a/lib/screens/shared/media/components/media_header.dart b/lib/screens/shared/media/components/media_header.dart index cd040c5..48829e5 100644 --- a/lib/screens/shared/media/components/media_header.dart +++ b/lib/screens/shared/media/components/media_header.dart @@ -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_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/util/fladder_image.dart'; + class MediaHeader extends ConsumerWidget { final String name; final ImageData? logo; + final Function()? onTap; const MediaHeader({ required this.name, required this.logo, + this.onTap, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final maxWidth = - switch (AdaptiveLayout.layoutOf(context)) { LayoutState.desktop || LayoutState.tablet => 0.55, _ => 1 }; - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Material( - elevation: 30, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(150)), - shadowColor: Colors.black.withOpacity(0.35), - 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, - ), + final maxSize = 700.0; + final textWidget = Container( + height: 512, + alignment: Alignment.center, + child: SelectableText( + name, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontSize: 55, ), + ), + ); + + 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, + ), + ), + ], ), ), ), diff --git a/lib/screens/shared/media/components/media_play_button.dart b/lib/screens/shared/media/components/media_play_button.dart index ee65090..edba27a 100644 --- a/lib/screens/shared/media/components/media_play_button.dart +++ b/lib/screens/shared/media/components/media_play_button.dart @@ -1,8 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.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 { final ItemBaseModel? item; @@ -33,8 +35,8 @@ class MediaPlayButton extends ConsumerWidget { child: Text( item?.playButtonLabel(context) ?? "", maxLines: 2, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, color: textColor, ), ), diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index 2a2adf8..755262f 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -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/providers/sync_provider.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/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; -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/util/string_extensions.dart'; class NextUpEpisode extends ConsumerWidget { final EpisodeModel nextEpisode; @@ -17,6 +20,7 @@ class NextUpEpisode extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final alreadyPlayed = nextEpisode.userData.played; + final episodeSummary = nextEpisode.overview.summary.maxLength(limitTo: 250); return Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -53,7 +57,7 @@ class NextUpEpisode extends ConsumerWidget { const SizedBox(height: 16), if (nextEpisode.overview.summary.isNotEmpty) HtmlWidget( - nextEpisode.overview.summary, + episodeSummary, textStyle: Theme.of(context).textTheme.titleMedium, ), ], @@ -88,7 +92,7 @@ class NextUpEpisode extends ConsumerWidget { mediaStreams: nextEpisode.mediaStreams.copyWith(defaultSubStreamIndex: index))), ), if (nextEpisode.overview.summary.isNotEmpty) - HtmlWidget(nextEpisode.overview.summary, textStyle: Theme.of(context).textTheme.titleMedium), + HtmlWidget(episodeSummary, textStyle: Theme.of(context).textTheme.titleMedium), ], ), ), diff --git a/lib/screens/shared/media/components/small_detail_widgets.dart b/lib/screens/shared/media/components/small_detail_widgets.dart index ecdb4e9..926fdf7 100644 --- a/lib/screens/shared/media/components/small_detail_widgets.dart +++ b/lib/screens/shared/media/components/small_detail_widgets.dart @@ -82,6 +82,8 @@ class Genres extends StatelessWidget { return Wrap( runSpacing: 8, spacing: 8, + runAlignment: WrapAlignment.center, + alignment: WrapAlignment.center, children: genres .map( (genre) => ChipButton( diff --git a/lib/screens/shared/media/external_urls.dart b/lib/screens/shared/media/external_urls.dart index 719921b..359eb09 100644 --- a/lib/screens/shared/media/external_urls.dart +++ b/lib/screens/shared/media/external_urls.dart @@ -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_custom_tabs/flutter_custom_tabs.dart' as customtab; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart' as urllauncher; 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 { final List? urls; const ExternalUrlsRow({ @@ -15,16 +19,28 @@ class ExternalUrlsRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Wrap( - children: urls - ?.map( - (url) => TextButton( - onPressed: () => launchUrl(context, url.url), - child: Text(url.name), - ), - ) - .toList() ?? - [], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + StickyHeaderText( + label: context.localized.external, + ), + Transform.translate( + offset: const Offset(-12, 0), + child: Wrap( + children: urls + ?.map( + (url) => TextButton( + onPressed: () => launchUrl(context, url.url), + child: Text(url.name), + ), + ) + .toList() ?? + [], + ), + ), + ], ); } } diff --git a/lib/util/fladder_image.dart b/lib/util/fladder_image.dart index 0cf3f07..998345b 100644 --- a/lib/util/fladder_image.dart +++ b/lib/util/fladder_image.dart @@ -10,15 +10,19 @@ import 'package:fladder/providers/settings/client_settings_provider.dart'; class FladderImage extends ConsumerWidget { final ImageData? image; 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 BoxFit fit; + final AlignmentGeometry? alignment; final bool enableBlur; final bool blurOnly; const FladderImage({ required this.image, this.frameBuilder, + this.imageErrorBuilder, this.placeHolder, this.fit = BoxFit.cover, + this.alignment, this.enableBlur = false, this.blurOnly = false, super.key, @@ -50,7 +54,9 @@ class FladderImage extends ConsumerWidget { fit: fit, placeholderFit: fit, excludeFromSemantics: true, + alignment: alignment ?? Alignment.center, filterQuality: FilterQuality.high, + imageErrorBuilder: imageErrorBuilder, placeholderFilterQuality: FilterQuality.low, image: newImage.imageProvider, ) diff --git a/lib/util/humanize_duration.dart b/lib/util/humanize_duration.dart index dacecb6..64cad1d 100644 --- a/lib/util/humanize_duration.dart +++ b/lib/util/humanize_duration.dart @@ -6,7 +6,7 @@ extension DurationExtensions on Duration? { final duration = this!; 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 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(' '); return result.isNotEmpty ? result : null; } diff --git a/lib/util/string_extensions.dart b/lib/util/string_extensions.dart index 00863aa..34cf204 100644 --- a/lib/util/string_extensions.dart +++ b/lib/util/string_extensions.dart @@ -14,6 +14,7 @@ extension StringExtensions on String { } String maxLength({int limitTo = 75}) { + if (isEmpty) return this; if (length > limitTo) { return "${substring(0, limitTo.clamp(0, length))}..."; } else { diff --git a/lib/widgets/shared/trick_play_image.dart b/lib/widgets/shared/trick_play_image.dart index 92fa94c..cb81788 100644 --- a/lib/widgets/shared/trick_play_image.dart +++ b/lib/widgets/shared/trick_play_image.dart @@ -76,9 +76,11 @@ class _TrickPlayImageState extends ConsumerState { final Uint8List bytes = response.bodyBytes; final ui.Codec codec = await ui.instantiateImageCodec(bytes); final ui.FrameInfo frameInfo = await codec.getNextFrame(); - setState(() { - image = frameInfo.image; - }); + if (context.mounted) { + setState(() { + image = frameInfo.image; + }); + } } else { throw Exception('Failed to load network image'); }