mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-15 10:15:58 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
39
lib/screens/details_screens/components/label_title_item.dart
Normal file
39
lib/screens/details_screens/components/label_title_item.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LabelTitleItem extends ConsumerWidget {
|
||||
final Text? title;
|
||||
final String? label;
|
||||
final Widget? content;
|
||||
const LabelTitleItem({
|
||||
this.title,
|
||||
this.label,
|
||||
this.content,
|
||||
super.key,
|
||||
}) : assert(label != null || content != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: Material(
|
||||
color: Colors.transparent, textStyle: Theme.of(context).textTheme.titleMedium, child: title)),
|
||||
const SizedBox(width: 12),
|
||||
label != null
|
||||
? SelectableText(
|
||||
label!,
|
||||
)
|
||||
: content!,
|
||||
].whereNotNull().toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/items/media_streams_model.dart';
|
||||
import 'package:fladder/screens/details_screens/components/label_title_item.dart';
|
||||
|
||||
class MediaStreamInformation extends ConsumerWidget {
|
||||
final MediaStreamsModel mediaStream;
|
||||
final Function(int index)? onAudioIndexChanged;
|
||||
final Function(int index)? onSubIndexChanged;
|
||||
const MediaStreamInformation(
|
||||
{required this.mediaStream, this.onAudioIndexChanged, this.onSubIndexChanged, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mediaStream.videoStreams.isNotEmpty)
|
||||
_StreamOptionSelect(
|
||||
label: Text(context.localized.video),
|
||||
current: (mediaStream.videoStreams.first).prettyName,
|
||||
itemBuilder: (context) => mediaStream.videoStreams
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e.prettyName),
|
||||
onTap: () {},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (mediaStream.audioStreams.isNotEmpty)
|
||||
_StreamOptionSelect(
|
||||
label: Text(context.localized.audio),
|
||||
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => mediaStream.audioStreams
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: textWidget(context, selected: mediaStream.currentAudioStream == e, label: e.displayTitle),
|
||||
onTap: () => onAudioIndexChanged?.call(e.index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (mediaStream.subStreams.isNotEmpty)
|
||||
_StreamOptionSelect(
|
||||
label: Text(context.localized.subtitles),
|
||||
current: mediaStream.currentSubStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => [SubStreamModel.no(), ...mediaStream.subStreams]
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: textWidget(context, selected: mediaStream.currentSubStream == e, label: e.displayTitle),
|
||||
onTap: () => onSubIndexChanged?.call(e.index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget textWidget(BuildContext context, {required bool selected, required String label}) {
|
||||
return Container(
|
||||
height: kMinInteractiveDimension,
|
||||
width: double.maxFinite,
|
||||
color: selected ? Theme.of(context).colorScheme.primary : null,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StreamOptionSelect<T> extends StatelessWidget {
|
||||
final Text label;
|
||||
final String current;
|
||||
final List<PopupMenuEntry<T>> Function(BuildContext context) itemBuilder;
|
||||
const _StreamOptionSelect({
|
||||
required this.label,
|
||||
required this.current,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = Theme.of(context).textTheme.titleMedium;
|
||||
const padding = EdgeInsets.all(6.0);
|
||||
final itemList = itemBuilder(context);
|
||||
return LabelTitleItem(
|
||||
title: label,
|
||||
content: Flexible(
|
||||
child: PopupMenuButton(
|
||||
tooltip: '',
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
enabled: itemList.length > 1,
|
||||
itemBuilder: itemBuilder,
|
||||
padding: padding,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Material(
|
||||
textStyle: textStyle?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: itemList.length > 1 ? Theme.of(context).colorScheme.primary : null),
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
current,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (itemList.length > 1)
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/screens/details_screens/components/overview_header.dart
Normal file
165
lib/screens/details_screens/components/overview_header.dart
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class OverviewHeader extends ConsumerWidget {
|
||||
final String name;
|
||||
final EdgeInsets? padding;
|
||||
final String? subTitle;
|
||||
final String? originalTitle;
|
||||
final Function()? onTitleClicked;
|
||||
final int? productionYear;
|
||||
final Duration? runTime;
|
||||
final String? officialRating;
|
||||
final double? communityRating;
|
||||
final List<Studio> studios;
|
||||
final List<GenreItems> genres;
|
||||
final List<ExternalUrls>? externalUrls;
|
||||
final List<Widget> actions;
|
||||
const OverviewHeader({
|
||||
required this.name,
|
||||
this.padding,
|
||||
this.subTitle,
|
||||
this.originalTitle,
|
||||
this.onTitleClicked,
|
||||
this.productionYear,
|
||||
this.runTime,
|
||||
this.officialRating,
|
||||
this.communityRating,
|
||||
this.externalUrls,
|
||||
this.genres = const [],
|
||||
this.studios = const [],
|
||||
this.actions = const [],
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mainStyle = Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
final subStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontSize: 20,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
if (subTitle == null)
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
name,
|
||||
style: mainStyle,
|
||||
),
|
||||
)
|
||||
else ...{
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
subTitle ?? "",
|
||||
style: mainStyle,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
name,
|
||||
style: subStyle,
|
||||
onTap: onTitleClicked,
|
||||
),
|
||||
),
|
||||
if (onTitleClicked != null)
|
||||
IconButton(
|
||||
onPressed: onTitleClicked,
|
||||
icon: Transform.translate(offset: Offset(0, 1.5), child: Icon(Icons.read_more_rounded)))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
if (name != originalTitle && originalTitle != null)
|
||||
SelectableText(
|
||||
originalTitle.toString(),
|
||||
style: subStyle,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (productionYear != null)
|
||||
SelectableText(
|
||||
productionYear.toString(),
|
||||
style: subStyle,
|
||||
),
|
||||
if (runTime != null && (runTime?.inSeconds ?? 0) > 1)
|
||||
SelectableText(
|
||||
runTime.humanize.toString(),
|
||||
style: subStyle,
|
||||
),
|
||||
if (officialRating != null)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
|
||||
child: SelectableText(
|
||||
officialRating.toString(),
|
||||
style: subStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (communityRating != null)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star_rate_rounded,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
communityRating?.toStringAsFixed(1) ?? "",
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (studios.isNotEmpty)
|
||||
Text(
|
||||
"${context.localized.watchOn} ${studios.map((e) => e.name).first}",
|
||||
style: subStyle?.copyWith(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (externalUrls?.isNotEmpty ?? false)
|
||||
ExternalUrlsRow(
|
||||
urls: externalUrls,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (genres.isNotEmpty)
|
||||
Genres(
|
||||
genres: genres.take(10).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: actions.addPadding(
|
||||
const EdgeInsets.symmetric(horizontal: 6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue