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