Init repo

This commit is contained in:
PartyDonut 2024-09-15 14:12:28 +02:00
commit 764b6034e3
566 changed files with 212335 additions and 0 deletions

View 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),
),
);
}
}

View 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,
),
),
),
),
),
);
}
}

View 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(),
),
);
}
}

View 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),
],
),
),
],
);
}
},
),
],
);
}
}

View 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,
),
],
),
),
),
),
)
}
],
),
),
),
);
}
}

View file

@ -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(),
);
}
}