mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-17 11:16:33 -07:00
feature: Details screen rework (#190)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
473e817e0f
commit
d2138da785
21 changed files with 462 additions and 394 deletions
|
|
@ -1148,5 +1148,6 @@
|
||||||
"mediaSegmentRecap": "Recap",
|
"mediaSegmentRecap": "Recap",
|
||||||
"mediaSegmentOutro": "Outro",
|
"mediaSegmentOutro": "Outro",
|
||||||
"mediaSegmentIntro": "Intro",
|
"mediaSegmentIntro": "Intro",
|
||||||
"errorLogs": "Error logs"
|
"errorLogs": "Error logs",
|
||||||
|
"external": "External"
|
||||||
}
|
}
|
||||||
|
|
@ -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}');
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue