mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-10 07:50:28 -07:00
fix: Long press "play" button with dpad navigation
This commit is contained in:
parent
fd35ffb004
commit
3ce0ed6dbc
9 changed files with 188 additions and 121 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/video_stream_model.dart';
|
import 'package:fladder/models/video_stream_model.dart';
|
||||||
|
import 'package:fladder/util/focus_provider.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
Future<PlaybackType?> showPlaybackTypeSelection({
|
Future<PlaybackType?> showPlaybackTypeSelection({
|
||||||
|
|
@ -12,14 +15,12 @@ Future<PlaybackType?> showPlaybackTypeSelection({
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: false,
|
useSafeArea: false,
|
||||||
builder: (context) => Dialog(
|
builder: (context) => PlaybackDialogue(
|
||||||
child: PlaybackDialogue(
|
options: options,
|
||||||
options: options,
|
onClose: (type) {
|
||||||
onClose: (type) {
|
playbackType = type;
|
||||||
playbackType = type;
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return playbackType;
|
return playbackType;
|
||||||
|
|
@ -32,26 +33,46 @@ class PlaybackDialogue extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Dialog(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
|
Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
|
||||||
context.localized.playbackType,
|
child: Text(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
context.localized.playbackType,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const Divider(),
|
||||||
const Divider(),
|
...options.mapIndexed(
|
||||||
...options.map((type) => ListTile(
|
(index, type) => Padding(
|
||||||
title: Text(type.name(context)),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
leading: Icon(type.icon),
|
child: FocusButton(
|
||||||
onTap: () {
|
autoFocus: index == 0,
|
||||||
onClose(type);
|
onTap: () {
|
||||||
},
|
onClose(type);
|
||||||
))
|
},
|
||||||
],
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Icon(type.icon),
|
||||||
|
Text(type.name(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,12 @@ class _BookDetailScreenState extends ConsumerState<BookDetailScreen> {
|
||||||
//Wrapped so the correct context is used for refreshing the pages
|
//Wrapped so the correct context is used for refreshing the pages
|
||||||
return MediaPlayButton(
|
return MediaPlayButton(
|
||||||
item: details.nextUp!,
|
item: details.nextUp!,
|
||||||
onPressed: () async => details.nextUp.play(context, ref, provider: provider),
|
onPressed: (restart) async => details.nextUp.play(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
provider: provider,
|
||||||
|
currentPage: restart ? 0 : null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,21 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
|
||||||
playButton: episodeDetails.playAble
|
playButton: episodeDetails.playAble
|
||||||
? MediaPlayButton(
|
? MediaPlayButton(
|
||||||
item: episodeDetails,
|
item: episodeDetails,
|
||||||
onPressed: () async {
|
onPressed: (restart) async {
|
||||||
await details.episode.play(context, ref);
|
await details.episode.play(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
|
);
|
||||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||||
},
|
},
|
||||||
onLongPressed: () async {
|
onLongPressed: (restart) async {
|
||||||
await details.episode.play(context, ref, showPlaybackOption: true);
|
await details.episode.play(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
showPlaybackOption: true,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
|
);
|
||||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -75,18 +75,20 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
|
||||||
padding: padding,
|
padding: padding,
|
||||||
playButton: MediaPlayButton(
|
playButton: MediaPlayButton(
|
||||||
item: details,
|
item: details,
|
||||||
onLongPressed: () async {
|
onLongPressed: (restart) async {
|
||||||
await details.play(
|
await details.play(
|
||||||
context,
|
context,
|
||||||
ref,
|
ref,
|
||||||
showPlaybackOption: true,
|
showPlaybackOption: true,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
);
|
);
|
||||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||||
},
|
},
|
||||||
onPressed: () async {
|
onPressed: (restart) async {
|
||||||
await details.play(
|
await details.play(
|
||||||
context,
|
context,
|
||||||
ref,
|
ref,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
);
|
);
|
||||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -76,21 +76,28 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||||
OverviewHeader(
|
OverviewHeader(
|
||||||
name: details.name,
|
name: details.name,
|
||||||
image: details.images,
|
image: details.images,
|
||||||
playButton: MediaPlayButton(
|
playButton: details.nextUp != null
|
||||||
item: details.nextUp,
|
? MediaPlayButton(
|
||||||
onPressed: details.nextUp != null
|
item: details.nextUp,
|
||||||
? () async {
|
onPressed: (restart) async {
|
||||||
await details.nextUp.play(context, ref);
|
await details.nextUp.play(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
|
);
|
||||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||||
}
|
},
|
||||||
: null,
|
onLongPressed: (restart) async {
|
||||||
onLongPressed: details.nextUp != null
|
await details.nextUp.play(
|
||||||
? () async {
|
context,
|
||||||
await details.nextUp.play(context, ref, showPlaybackOption: true);
|
ref,
|
||||||
|
showPlaybackOption: true,
|
||||||
|
startPosition: restart ? Duration.zero : null,
|
||||||
|
);
|
||||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||||
}
|
},
|
||||||
: null,
|
)
|
||||||
),
|
: null,
|
||||||
centerButtons: Wrap(
|
centerButtons: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ 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:fladder/theme.dart';
|
import 'package:fladder/theme.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
|
import 'package:fladder/util/focus_provider.dart';
|
||||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||||
|
|
||||||
class MediaPlayButton extends ConsumerWidget {
|
class MediaPlayButton extends ConsumerWidget {
|
||||||
final ItemBaseModel? item;
|
final ItemBaseModel? item;
|
||||||
final VoidCallback? onPressed;
|
final Function(bool restart)? onPressed;
|
||||||
final VoidCallback? onLongPressed;
|
final Function(bool restart)? onLongPressed;
|
||||||
|
|
||||||
const MediaPlayButton({
|
const MediaPlayButton({
|
||||||
required this.item,
|
required this.item,
|
||||||
|
|
@ -26,17 +27,7 @@ class MediaPlayButton extends ConsumerWidget {
|
||||||
final progress = (item?.progress ?? 0) / 100.0;
|
final progress = (item?.progress ?? 0) / 100.0;
|
||||||
final padding = 3.0;
|
final padding = 3.0;
|
||||||
final radius = FladderTheme.smallShape.borderRadius.subtract(BorderRadius.circular(padding));
|
final radius = FladderTheme.smallShape.borderRadius.subtract(BorderRadius.circular(padding));
|
||||||
final buttonState = WidgetStateProperty.resolveWith(
|
final theme = Theme.of(context);
|
||||||
(states) {
|
|
||||||
return BorderSide(
|
|
||||||
width: 2,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer
|
|
||||||
.withValues(alpha: states.contains(WidgetState.focused) ? 0.9 : 0.0),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget buttonTitle(Color contentColor) {
|
Widget buttonTitle(Color contentColor) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -50,10 +41,10 @@ class MediaPlayButton extends ConsumerWidget {
|
||||||
item?.playButtonLabel(context) ?? "",
|
item?.playButtonLabel(context) ?? "",
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: contentColor,
|
color: contentColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
|
@ -70,54 +61,79 @@ class MediaPlayButton extends ConsumerWidget {
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: onPressed == null
|
child: onPressed == null
|
||||||
? const SizedBox.shrink(key: ValueKey('empty'))
|
? const SizedBox.shrink(key: ValueKey('empty'))
|
||||||
: TextButton(
|
: Row(
|
||||||
onPressed: onPressed,
|
spacing: 2,
|
||||||
onLongPress: onLongPressed,
|
children: [
|
||||||
autofocus: AdaptiveLayout.inputDeviceOf(context) == InputDevice.dPad,
|
FocusButton(
|
||||||
style: ButtonStyle(
|
onTap: () => onPressed?.call(false),
|
||||||
side: buttonState,
|
onLongPress: () => onLongPressed?.call(false),
|
||||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
autoFocus: AdaptiveLayout.inputDeviceOf(context) == InputDevice.dPad,
|
||||||
),
|
darkOverlay: false,
|
||||||
onFocusChange: (value) {
|
onFocusChanged: (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
context.ensureVisible(
|
context.ensureVisible(
|
||||||
alignment: 1.0,
|
alignment: 1.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(padding),
|
padding: EdgeInsets.all(padding),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Progress background
|
// Progress background
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
color: theme.colorScheme.primaryContainer,
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
// Button content
|
|
||||||
buttonTitle(Theme.of(context).colorScheme.onPrimaryContainer),
|
|
||||||
Positioned.fill(
|
|
||||||
child: ClipRect(
|
|
||||||
clipper: _ProgressClipper(
|
|
||||||
progress,
|
|
||||||
),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
borderRadius: radius,
|
|
||||||
),
|
),
|
||||||
child: buttonTitle(Theme.of(context).colorScheme.onPrimary),
|
),
|
||||||
|
// Button content
|
||||||
|
buttonTitle(theme.colorScheme.onPrimaryContainer),
|
||||||
|
Positioned.fill(
|
||||||
|
child: ClipRect(
|
||||||
|
clipper: _ProgressClipper(
|
||||||
|
progress,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
borderRadius: radius,
|
||||||
|
),
|
||||||
|
child: buttonTitle(theme.colorScheme.onPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (progress != 0)
|
||||||
|
FocusButton(
|
||||||
|
onTap: () => onPressed?.call(true),
|
||||||
|
onLongPress: () => onLongPressed?.call(true),
|
||||||
|
onFocusChanged: (value) {
|
||||||
|
if (value) {
|
||||||
|
context.ensureVisible(
|
||||||
|
alignment: 1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Card(
|
||||||
|
color: theme.colorScheme.primaryContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
IconsaxPlusBold.refresh,
|
||||||
|
size: 29,
|
||||||
|
color: theme.colorScheme.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,20 +87,22 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final episode = episodes[index];
|
final episode = episodes[index];
|
||||||
final isCurrentEpisode = index == indexOfCurrent;
|
final isCurrentEpisode = index == indexOfCurrent;
|
||||||
|
final tag = UniqueKey();
|
||||||
return EpisodePoster(
|
return EpisodePoster(
|
||||||
episode: episode,
|
episode: episode,
|
||||||
|
heroTag: tag,
|
||||||
blur: allPlayed ? false : indexOfCurrent < index,
|
blur: allPlayed ? false : indexOfCurrent < index,
|
||||||
onTap: widget.onEpisodeTap != null
|
onTap: widget.onEpisodeTap != null
|
||||||
? () {
|
? () {
|
||||||
widget.onEpisodeTap?.call(
|
widget.onEpisodeTap?.call(
|
||||||
() {
|
() {
|
||||||
episode.navigateTo(context);
|
episode.navigateTo(context, tag: tag);
|
||||||
},
|
},
|
||||||
episode,
|
episode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: () {
|
: () {
|
||||||
episode.navigateTo(context);
|
episode.navigateTo(context, tag: tag);
|
||||||
},
|
},
|
||||||
onLongPress: () async {
|
onLongPress: () async {
|
||||||
await showBottomSheetPill(
|
await showBottomSheetPill(
|
||||||
|
|
@ -134,6 +136,7 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
final bool blur;
|
final bool blur;
|
||||||
final List<ItemAction> actions;
|
final List<ItemAction> actions;
|
||||||
final bool isCurrentEpisode;
|
final bool isCurrentEpisode;
|
||||||
|
final Object? heroTag;
|
||||||
|
|
||||||
const EpisodePoster({
|
const EpisodePoster({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -144,6 +147,7 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
this.blur = false,
|
this.blur = false,
|
||||||
required this.actions,
|
required this.actions,
|
||||||
required this.isCurrentEpisode,
|
required this.isCurrentEpisode,
|
||||||
|
this.heroTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -176,15 +180,18 @@ class EpisodePoster extends ConsumerWidget {
|
||||||
await showMenu(
|
await showMenu(
|
||||||
context: context, position: position, items: actions.popupMenuItems(useIcons: true));
|
context: context, position: position, items: actions.popupMenuItems(useIcons: true));
|
||||||
},
|
},
|
||||||
child: FladderImage(
|
child: Hero(
|
||||||
image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary,
|
tag: heroTag ?? UniqueKey(),
|
||||||
placeHolder: placeHolder,
|
child: FladderImage(
|
||||||
blurOnly: !episodeAvailable
|
image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary,
|
||||||
? true
|
placeHolder: placeHolder,
|
||||||
: ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes))
|
blurOnly: !episodeAvailable
|
||||||
? blur
|
? true
|
||||||
: false,
|
: ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes))
|
||||||
decodeHeight: 250,
|
? blur
|
||||||
|
: false,
|
||||||
|
decodeHeight: 250,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
overlays: [
|
overlays: [
|
||||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer && actions.isNotEmpty)
|
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer && actions.isNotEmpty)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class FladderTheme {
|
||||||
final buttonState = WidgetStateProperty.resolveWith(
|
final buttonState = WidgetStateProperty.resolveWith(
|
||||||
(states) {
|
(states) {
|
||||||
return BorderSide(
|
return BorderSide(
|
||||||
width: 2,
|
width: 3,
|
||||||
color: scheme?.onPrimaryContainer.withValues(alpha: states.contains(WidgetState.focused) ? 0.9 : 0.0) ??
|
color: scheme?.onPrimaryContainer.withValues(alpha: states.contains(WidgetState.focused) ? 0.9 : 0.0) ??
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ class FocusButtonState extends State<FocusButton> {
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primaryContainer
|
.primaryContainer
|
||||||
.withValues(alpha: widget.darkOverlay ? 0.1 : 0),
|
.withValues(alpha: widget.darkOverlay ? 0.1 : 0),
|
||||||
border: Border.all(width: 4, color: Theme.of(context).colorScheme.onPrimaryContainer),
|
border: Border.all(width: 3, color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue