mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
26
lib/screens/shared/media/components/chip_button.dart
Normal file
26
lib/screens/shared/media/components/chip_button.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ChipButton extends ConsumerWidget {
|
||||
final String label;
|
||||
final Function()? onPressed;
|
||||
const ChipButton({required this.label, this.onPressed, super.key});
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/screens/shared/media/components/media_header.dart
Normal file
53
lib/screens/shared/media/components/media_header.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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';
|
||||
|
||||
class MediaHeader extends ConsumerWidget {
|
||||
final String name;
|
||||
final ImageData? logo;
|
||||
const MediaHeader({
|
||||
required this.name,
|
||||
required this.logo,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/screens/shared/media/components/media_play_button.dart
Normal file
81
lib/screens/shared/media/components/media_play_button.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:ficonsax/ficonsax.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;
|
||||
final Function()? onPressed;
|
||||
final Function()? onLongPressed;
|
||||
const MediaPlayButton({
|
||||
required this.item,
|
||||
this.onPressed,
|
||||
this.onLongPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final resume = (item?.progress ?? 0) > 0;
|
||||
Widget buttonBuilder(bool resume, ButtonStyle? style, Color? textColor) {
|
||||
return ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPressed,
|
||||
style: style,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item?.playButtonLabel(context) ?? "",
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
IconsaxBold.play,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: onPressed != null
|
||||
? Stack(
|
||||
children: [
|
||||
buttonBuilder(resume, null, null),
|
||||
IgnorePointer(
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: (item?.progress ?? 0) / 100,
|
||||
child: buttonBuilder(
|
||||
resume,
|
||||
ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.primary),
|
||||
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary),
|
||||
),
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
key: UniqueKey(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/screens/shared/media/components/next_up_episode.dart
Normal file
103
lib/screens/shared/media/components/next_up_episode.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
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';
|
||||
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';
|
||||
|
||||
class NextUpEpisode extends ConsumerWidget {
|
||||
final EpisodeModel nextEpisode;
|
||||
final Function(EpisodeModel episode)? onChanged;
|
||||
const NextUpEpisode({required this.nextEpisode, this.onChanged, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final alreadyPlayed = nextEpisode.userData.played;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StickyHeaderText(
|
||||
label: alreadyPlayed ? context.localized.reWatch : context.localized.nextUp,
|
||||
),
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: SelectableText(
|
||||
"${context.localized.season(1)} ${nextEpisode.season} - ${context.localized.episode(1)} ${nextEpisode.episode}",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
nextEpisode.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(nextEpisode);
|
||||
if (constraints.maxWidth < 550) {
|
||||
return Column(
|
||||
children: [
|
||||
EpisodePoster(
|
||||
episode: nextEpisode,
|
||||
syncedItem: syncedItem,
|
||||
showLabel: false,
|
||||
onTap: () => nextEpisode.navigateTo(context),
|
||||
actions: const [],
|
||||
isCurrentEpisode: false,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (nextEpisode.overview.summary.isNotEmpty)
|
||||
HtmlWidget(
|
||||
nextEpisode.overview.summary,
|
||||
textStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: AdaptiveLayout.poster(context).gridRatio,
|
||||
maxWidth: MediaQuery.of(context).size.width / 2),
|
||||
child: EpisodePoster(
|
||||
episode: nextEpisode,
|
||||
syncedItem: syncedItem,
|
||||
showLabel: false,
|
||||
onTap: () => nextEpisode.navigateTo(context),
|
||||
actions: const [],
|
||||
isCurrentEpisode: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MediaStreamInformation(
|
||||
mediaStream: nextEpisode.mediaStreams,
|
||||
onAudioIndexChanged: (index) => onChanged?.call(nextEpisode.copyWith(
|
||||
mediaStreams: nextEpisode.mediaStreams.copyWith(defaultAudioStreamIndex: index))),
|
||||
onSubIndexChanged: (index) => onChanged?.call(nextEpisode.copyWith(
|
||||
mediaStreams: nextEpisode.mediaStreams.copyWith(defaultSubStreamIndex: index))),
|
||||
),
|
||||
if (nextEpisode.overview.summary.isNotEmpty)
|
||||
HtmlWidget(nextEpisode.overview.summary, textStyle: Theme.of(context).textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
428
lib/screens/shared/media/components/poster_image.dart
Normal file
428
lib/screens/shared/media/components/poster_image.dart
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/models/book_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/disable_keypad_focus.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/status_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class PosterImage extends ConsumerStatefulWidget {
|
||||
final ItemBaseModel poster;
|
||||
final bool heroTag;
|
||||
final bool? selected;
|
||||
final ValueChanged<bool>? playVideo;
|
||||
final bool inlineTitle;
|
||||
final Set<ItemActions> excludeActions;
|
||||
final List<ItemAction> otherActions;
|
||||
final Function(UserData? newData)? onUserDataChanged;
|
||||
final Function(ItemBaseModel newItem)? onItemUpdated;
|
||||
final Function(ItemBaseModel oldItem)? onItemRemoved;
|
||||
final Function(Function() action, ItemBaseModel item)? onPressed;
|
||||
const PosterImage({
|
||||
required this.poster,
|
||||
this.heroTag = false,
|
||||
this.selected,
|
||||
this.playVideo,
|
||||
this.inlineTitle = false,
|
||||
this.onItemUpdated,
|
||||
this.onItemRemoved,
|
||||
this.excludeActions = const {},
|
||||
this.otherActions = const [],
|
||||
this.onPressed,
|
||||
this.onUserDataChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _PosterImageState();
|
||||
}
|
||||
|
||||
class _PosterImageState extends ConsumerState<PosterImage> {
|
||||
late String currentTag = widget.heroTag == true ? widget.poster.id : UniqueKey().toString();
|
||||
bool hover = false;
|
||||
|
||||
Widget get placeHolder {
|
||||
return Center(
|
||||
child: Icon(widget.poster.type.icon),
|
||||
);
|
||||
}
|
||||
|
||||
void pressedWidget() async {
|
||||
if (widget.heroTag == false) {
|
||||
setState(() {
|
||||
currentTag = widget.poster.id;
|
||||
});
|
||||
}
|
||||
if (widget.onPressed != null) {
|
||||
widget.onPressed?.call(() async {
|
||||
await navigateToDetails();
|
||||
if (context.mounted) {
|
||||
context.refreshData();
|
||||
}
|
||||
}, widget.poster);
|
||||
} else {
|
||||
await navigateToDetails();
|
||||
if (context.mounted) {
|
||||
context.refreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigateToDetails() async {
|
||||
await widget.poster.navigateTo(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final poster = widget.poster;
|
||||
final padding = EdgeInsets.all(5);
|
||||
return Hero(
|
||||
tag: currentTag,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (event) => setState(() => hover = true),
|
||||
onExit: (event) => setState(() => hover = false),
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.2),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 1.0,
|
||||
color: Colors.white.withOpacity(0.10),
|
||||
),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FladderImage(
|
||||
image: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull,
|
||||
placeHolder: placeHolder,
|
||||
),
|
||||
if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.5),
|
||||
child: Text(
|
||||
context.localized.page((widget.poster as BookModel).currentPage),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.selected == true)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Text(
|
||||
widget.poster.name,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.poster.userData.isFavourite)
|
||||
Row(
|
||||
children: [
|
||||
StatusCard(
|
||||
color: Colors.red,
|
||||
child: Icon(
|
||||
IconsaxBold.heart,
|
||||
size: 21,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((poster.userData.progress > 0 && poster.userData.progress < 100) &&
|
||||
widget.poster.type != FladderItemType.book) ...{
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding),
|
||||
child: Card(
|
||||
color: Colors.transparent,
|
||||
elevation: 3,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 7.5,
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary.withOpacity(0.5),
|
||||
value: poster.userData.progress / 100,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
//Desktop overlay
|
||||
if (AdaptiveLayout.of(context).inputDevice != InputDevice.touch &&
|
||||
widget.poster.type != FladderItemType.person)
|
||||
AnimatedOpacity(
|
||||
opacity: hover ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
//Hover color overlay
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.55),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
)),
|
||||
//Poster Button
|
||||
Focus(
|
||||
onFocusChange: (value) => setState(() => hover = value),
|
||||
child: FlatButton(
|
||||
onTap: pressedWidget,
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
//Play Button
|
||||
if (widget.poster.playAble)
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: IconButton.filledTonal(
|
||||
onPressed: () => widget.playVideo?.call(false),
|
||||
icon: const Icon(
|
||||
IconsaxBold.play,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
tooltip: "Options",
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.popupMenuItems(useIcons: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: pressedWidget,
|
||||
onLongPress: () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: widget.poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.listTileItems(scrollContext, useIcons: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.poster.unWatched)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: StatusCard(
|
||||
color: Colors.amber,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.amber,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.inlineTitle)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
widget.poster.title.maxLength(limitTo: 25),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.bold, shadows: [
|
||||
BoxShadow(blurRadius: 8, spreadRadius: 16),
|
||||
BoxShadow(blurRadius: 2, spreadRadius: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: StatusCard(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: widget.poster.unPlayedItemCount != 0
|
||||
? Container(
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
child: Text(
|
||||
widget.poster.userData.unPlayedItemCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.overview.runTime != null &&
|
||||
((widget.poster is PhotoModel) &&
|
||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/screens/shared/media/components/chip_button.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
|
||||
class Ratings extends StatelessWidget {
|
||||
final double? communityRating;
|
||||
final String? officialRating;
|
||||
const Ratings({
|
||||
super.key,
|
||||
this.communityRating,
|
||||
this.officialRating,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (communityRating != null) ...{
|
||||
const Icon(
|
||||
Icons.star_rounded,
|
||||
color: Colors.yellow,
|
||||
),
|
||||
Text(
|
||||
communityRating?.toStringAsFixed(1) ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
},
|
||||
if (officialRating != null) ...{
|
||||
Card(
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
officialRating ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tags extends StatelessWidget {
|
||||
final List<String> tags;
|
||||
const Tags({
|
||||
super.key,
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: tags
|
||||
.map((tag) => ChipButton(
|
||||
onPressed: () {},
|
||||
label: tag.capitalize(),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Genres extends StatelessWidget {
|
||||
final List<GenreItems> genres;
|
||||
const Genres({
|
||||
super.key,
|
||||
required this.genres,
|
||||
this.details,
|
||||
});
|
||||
|
||||
final MovieModel? details;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: genres
|
||||
.map(
|
||||
(genre) => ChipButton(
|
||||
onPressed: null,
|
||||
label: genre.name.capitalize(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue