fix: Improve keyboard input handling (#102)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-02 08:40:39 +01:00 committed by GitHub
parent 2038847552
commit 76ac1aaa5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 584 additions and 571 deletions

View file

@ -16,6 +16,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/shared/default_titlebar.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/throttler.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
@ -75,12 +76,10 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
viewController.visibilityChanged.addListener(() {
toggleControls(value: viewController.controlsVisible);
});
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
WakelockPlus.disable();
ScreenBrightness().resetScreenBrightness();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
@ -134,230 +133,233 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
return MediaQuery.removePadding(
context: context,
child: Stack(
children: [
IgnorePointer(
ignoring: !showControls,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: showControls ? 1 : 0,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(1),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(0),
],
child: InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Stack(
children: [
IgnorePointer(
ignoring: !showControls,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: showControls ? 1 : 0,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(1),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(0),
],
),
),
),
child: Padding(
padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (AdaptiveLayout.of(context).isDesktop)
const Flexible(
child: DefaultTitleBar(
height: 50,
brightness: Brightness.dark,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const BackButton(),
const SizedBox(
width: 16,
),
Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
child: Padding(
padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (AdaptiveLayout.of(context).isDesktop)
const Flexible(
child: DefaultTitleBar(
height: 50,
brightness: Brightness.dark,
),
)
],
),
const SizedBox(height: 16),
],
),
),
),
if (!bookViewerDetails.loading) ...{
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(0),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(1),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const BackButton(),
const SizedBox(
width: 16,
),
Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
),
)
],
),
),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 30),
Row(
children: [
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter?.name != null
? "Load ${previousChapter?.name}"
: ""
: nextChapter?.name != null
? "Load ${nextChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter != null
? () async => await loadNextBook(previousChapter)
: null
: nextChapter != null
? () async => await loadNextBook(nextChapter)
: null,
icon: const Icon(IconsaxOutline.backward),
),
),
const SizedBox(width: 8),
Flexible(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(60),
const SizedBox(height: 16),
],
),
),
),
if (!bookViewerDetails.loading) ...{
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(0),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(1),
],
),
),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 30),
Row(
children: [
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter?.name != null
? "Load ${previousChapter?.name}"
: ""
: nextChapter?.name != null
? "Load ${nextChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter != null
? () async => await loadNextBook(previousChapter)
: null
: nextChapter != null
? () async => await loadNextBook(nextChapter)
: null,
icon: const Icon(IconsaxOutline.backward),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
if (bookViewerSettings.readDirection == ReadDirection.leftToRight)
...controls(currentPage, bookViewerSettings, bookViewerDetails)
else
...controls(currentPage, bookViewerSettings, bookViewerDetails)
.reversed,
],
),
const SizedBox(width: 8),
Flexible(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(60),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
if (bookViewerSettings.readDirection == ReadDirection.leftToRight)
...controls(currentPage, bookViewerSettings, bookViewerDetails)
else
...controls(currentPage, bookViewerSettings, bookViewerDetails)
.reversed,
],
),
),
),
),
),
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter?.name != null
? "Load ${nextChapter?.name}"
: ""
: previousChapter?.name != null
? "Load ${previousChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter != null
? () async => await loadNextBook(nextChapter)
: null
: previousChapter != null
? () async => await loadNextBook(previousChapter)
: null,
icon: const Icon(IconsaxOutline.forward),
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter?.name != null
? "Load ${nextChapter?.name}"
: ""
: previousChapter?.name != null
? "Load ${previousChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter != null
? () async => await loadNextBook(nextChapter)
: null
: previousChapter != null
? () async => await loadNextBook(previousChapter)
: null,
icon: const Icon(IconsaxOutline.forward),
),
),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Transform.flip(
flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft,
child: IconButton(
onPressed: () => widget.controller
.animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve),
icon: const Icon(IconsaxOutline.backward)),
),
IconButton(
onPressed: () {
showBookViewerSettings(context);
},
icon: const Icon(IconsaxOutline.setting_2),
),
IconButton(
onPressed: chapters.length > 1
? () {
showBookViewerChapters(
context,
widget.provider,
onPressed: (book) async {
Navigator.of(context).pop();
loadNextBook(book);
},
);
}
: () => fladderSnackbar(context, title: "No other chapters"),
icon: const Icon(IconsaxOutline.bookmark_2),
)
],
),
],
const SizedBox(width: 8),
],
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Transform.flip(
flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft,
child: IconButton(
onPressed: () => widget.controller
.animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve),
icon: const Icon(IconsaxOutline.backward)),
),
IconButton(
onPressed: () {
showBookViewerSettings(context);
},
icon: const Icon(IconsaxOutline.setting_2),
),
IconButton(
onPressed: chapters.length > 1
? () {
showBookViewerChapters(
context,
widget.provider,
onPressed: (book) async {
Navigator.of(context).pop();
loadNextBook(book);
},
);
}
: () => fladderSnackbar(context, title: "No other chapters"),
icon: const Icon(IconsaxOutline.bookmark_2),
)
],
),
],
),
),
),
),
),
} else
const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.menu_book_rounded),
SizedBox(width: 8),
Text("Unable to load book"),
],
} else
const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.menu_book_rounded),
SizedBox(width: 8),
Text("Unable to load book"),
],
),
),
),
),
)
},
],
),
),
),
if (bookViewerDetails.loading)
Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (bookViewerDetails.book != null) ...{
Flexible(
child: Text("Loading ${bookViewerDetails.book?.name}",
style: Theme.of(context).textTheme.titleMedium),
),
const SizedBox(width: 16),
},
const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
],
),
)
},
],
),
),
)
],
),
if (bookViewerDetails.loading)
Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (bookViewerDetails.book != null) ...{
Flexible(
child: Text("Loading ${bookViewerDetails.book?.name}",
style: Theme.of(context).textTheme.titleMedium),
),
const SizedBox(width: 16),
},
const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
],
),
),
),
)
],
),
),
);
}

View file

@ -16,6 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/throttler.dart';
@ -99,7 +100,7 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
timerController.playPause();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.keyF) {
if (value.logicalKey == LogicalKeyboardKey.space) {
widget.toggleOverlay?.call(null);
return true;
}
@ -117,12 +118,10 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
timerController.reset();
},
);
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void onWindowMinimize() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
timerController.cancel();
super.onWindowMinimize();
}
@ -145,192 +144,197 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
final padding = MediaQuery.of(context).padding;
return PopScope(
onPopInvokedWithResult: (didPop, result) async {
await WakelockPlus.disable();
},
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
widthFactor: 1,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: gradient,
onPopInvokedWithResult: (didPop, result) async => await WakelockPlus.disable(),
child: InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
widthFactor: 1,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: gradient,
),
),
),
child: Padding(
padding: EdgeInsets.only(top: widget.padding.top),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12)
.add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ElevatedIconButton(
onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()),
icon: getBackIcon(context),
),
const SizedBox(width: 8),
Expanded(
child: Tooltip(
message: widget.photo.name,
child: Text(
widget.photo.name,
maxLines: 2,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold, shadows: [
BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)),
BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)),
BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)),
]),
child: Padding(
padding: EdgeInsets.only(top: widget.padding.top),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12)
.add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ElevatedIconButton(
onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()),
icon: getBackIcon(context),
),
const SizedBox(width: 8),
Expanded(
child: Tooltip(
message: widget.photo.name,
child: Text(
widget.photo.name,
maxLines: 2,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold, shadows: [
BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)),
BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)),
BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)),
]),
),
),
),
),
const SizedBox(width: 8),
Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.onPrimary),
child: SquareProgressIndicator(
value: widget.currentIndex / (widget.itemCount - 1),
borderRadius: 7,
clockwise: false,
color: Theme.of(context).colorScheme.primary,
const SizedBox(width: 8),
Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.onPrimary),
child: SquareProgressIndicator(
value: widget.currentIndex / (widget.itemCount - 1),
borderRadius: 7,
clockwise: false,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
Padding(
padding: const EdgeInsets.all(9),
child: Row(
children: [
Text(
"${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ",
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
if (widget.loadingMoreItems)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
),
Padding(
padding: const EdgeInsets.all(9),
child: Row(
children: [
Text(
"${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ",
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
),
].addInBetween(const SizedBox(width: 6)),
if (widget.loadingMoreItems)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
),
),
].addInBetween(const SizedBox(width: 6)),
),
),
),
Positioned.fill(
child: FlatButton(
borderRadiusGeometry: BorderRadius.circular(8),
onTap: () async {
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: SizedBox(
width: 125,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.goTo,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 5),
IntInputField(
controller:
TextEditingController(text: (widget.currentIndex + 1).toString()),
onSubmitted: (value) {
final position = ((value ?? 0) - 1).clamp(0, widget.itemCount - 1);
widget.pageController.jumpToPage(position);
Navigator.of(context).pop();
},
),
],
Positioned.fill(
child: FlatButton(
borderRadiusGeometry: BorderRadius.circular(8),
onTap: () async {
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: SizedBox(
width: 125,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.goTo,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 5),
IntInputField(
controller: TextEditingController(
text: (widget.currentIndex + 1).toString()),
onSubmitted: (value) {
final position =
((value ?? 0) - 1).clamp(0, widget.itemCount - 1);
widget.pageController.jumpToPage(position);
Navigator.of(context).pop();
},
),
],
),
),
),
),
),
);
},
),
)
],
),
const SizedBox(width: 12),
],
);
},
),
)
],
),
const SizedBox(width: 12),
],
),
),
),
],
],
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: gradient.reversed.toList(),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: gradient.reversed.toList(),
),
),
),
width: double.infinity,
child: Padding(
padding: EdgeInsets.only(bottom: widget.padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row(
children: [
ElevatedIconButton(
onPressed: widget.openOptions,
icon: IconsaxOutline.more_2,
),
const Spacer(),
ElevatedIconButton(
onPressed: markAsFavourite,
color: widget.photo.userData.isFavourite ? Colors.red : null,
icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart,
),
ProgressFloatingButton(
controller: timerController,
onLongPress: (duration) {
if (duration != null) {
ref
.read(photoViewSettingsProvider.notifier)
.update((state) => state.copyWith(timer: duration));
}
},
),
].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
),
)
],
width: double.infinity,
child: Padding(
padding: EdgeInsets.only(bottom: widget.padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row(
children: [
ElevatedIconButton(
onPressed: widget.openOptions,
icon: IconsaxOutline.more_2,
),
const Spacer(),
ElevatedIconButton(
onPressed: markAsFavourite,
color: widget.photo.userData.isFavourite ? Colors.red : null,
icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart,
),
ProgressFloatingButton(
controller: timerController,
onLongPress: (duration) {
if (duration != null) {
ref
.read(photoViewSettingsProvider.notifier)
.update((state) => state.copyWith(timer: duration));
}
},
),
].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
),
)
],
),
),
),
),
),
],
],
),
),
);
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';
class PassCodeInput extends ConsumerStatefulWidget {
@ -19,18 +20,6 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
final passCodeLength = 4;
var currentPasscode = "";
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
bool _onKey(KeyEvent value) {
if (value is KeyDownEvent) {
final keyInt = int.tryParse(value.logicalKey.keyLabel);
@ -48,60 +37,63 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
@override
Widget build(BuildContext context) {
return AlertDialog(
scrollable: true,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
passCodeLength,
(index) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: SizedBox(
height: iconSize * 1.2,
width: iconSize * 1.2,
child: Card(
child: Transform.translate(
offset: const Offset(0, 5),
child: AnimatedFadeSize(
child: Text(
currentPasscode.length > index ? "*" : "",
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
return InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: AlertDialog(
scrollable: true,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
passCodeLength,
(index) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: SizedBox(
height: iconSize * 1.2,
width: iconSize * 1.2,
child: Card(
child: Transform.translate(
offset: const Offset(0, 5),
child: AnimatedFadeSize(
child: Text(
currentPasscode.length > index ? "*" : "",
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
),
),
),
),
),
),
),
),
).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
backSpaceButton,
passCodeNumber(0),
clearAllButton,
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 8)),
).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
backSpaceButton,
passCodeNumber(0),
clearAllButton,
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 8)),
),
),
);
}

View file

@ -5,6 +5,7 @@ import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart';
class VideoPlayerSeekIndicator extends ConsumerStatefulWidget {
@ -20,18 +21,6 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
bool visible = false;
int seekPosition = 0;
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
void onSeekEnd() {
setState(() {
visible = false;
@ -46,7 +35,7 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
void onSeekStart(int value) {
if (timer == null) {
timer = RestartableTimer(const Duration(seconds: 2), () => onSeekEnd());
timer = RestartableTimer(const Duration(milliseconds: 500), () => onSeekEnd());
setState(() {
seekPosition = 0;
});
@ -85,28 +74,32 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: (visible && seekPosition != 0) ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
seekPosition > 0
? "+$seekPosition ${context.localized.seconds(seekPosition)}"
: "$seekPosition ${context.localized.seconds(seekPosition)}",
style: Theme.of(context).textTheme.bodyMedium,
)
],
return InputHandler(
autoFocus: true,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: (visible && seekPosition != 0) ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
seekPosition > 0
? "+$seekPosition ${context.localized.seconds(seekPosition)}"
: "$seekPosition ${context.localized.seconds(seekPosition)}",
style: Theme.of(context).textTheme.bodyMedium,
)
],
),
),
),
),

View file

@ -10,10 +10,10 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
class VideoSubtitles extends ConsumerStatefulWidget {
final VideoController controller;
final bool overlayed;
final bool overLayed;
const VideoSubtitles({
required this.controller,
this.overlayed = false,
this.overLayed = false,
super.key,
});
@ -56,7 +56,7 @@ class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
return SubtitleText(
subModel: settings,
padding: padding,
offset: (widget.overlayed ? 0.5 : settings.verticalOffset),
offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
text: text,
);
}

View file

@ -29,6 +29,7 @@ import 'package:fladder/screens/video_player/components/video_subtitles.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
@ -49,7 +50,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
);
final fadeDuration = const Duration(milliseconds: 350);
final focusNode = FocusNode();
bool showOverlay = true;
bool wasPlaying = false;
@ -79,7 +79,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} else if (showCreditSkipButton) {
skipCredits(introSkipModel);
}
focusNode.requestFocus();
}
if (value.logicalKey == LogicalKeyboardKey.escape) {
disableFullscreen();
@ -103,105 +102,93 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
return false;
}
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
timer.reset();
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
@override
Widget build(BuildContext context) {
final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel));
final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
if (AdaptiveLayout.of(context).isDesktop) {
focusNode.requestFocus();
}
return Listener(
onPointerSignal: (event) => resetTimer(),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
closePlayer();
}
},
child: GestureDetector(
onTap: () => toggleOverlay(),
child: MouseRegion(
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
onEnter: (event) => toggleOverlay(value: true),
onExit: (event) => toggleOverlay(value: false),
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack(
children: [
if (player != null)
VideoSubtitles(
key: const Key('subtitles'),
controller: player,
overlayed: showOverlay,
),
if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
return playButton(playing, buffering);
}),
IgnorePointer(
ignoring: !showOverlay,
child: AnimatedOpacity(
duration: fadeDuration,
opacity: showOverlay ? 1 : 0,
child: Column(
children: [
topButtons(context),
const Spacer(),
bottomButtons(context),
],
return InputHandler(
autoFocus: false,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Listener(
onPointerSignal: (event) => resetTimer(),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
closePlayer();
}
},
child: GestureDetector(
onTap: () => toggleOverlay(),
child: MouseRegion(
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
onEnter: (event) => toggleOverlay(value: true),
onExit: (event) => toggleOverlay(value: false),
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack(
children: [
if (player != null)
VideoSubtitles(
key: const Key('subtitles'),
controller: player,
overLayed: showOverlay,
),
if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
return playButton(playing, buffering);
}),
IgnorePointer(
ignoring: !showOverlay,
child: AnimatedOpacity(
duration: fadeDuration,
opacity: showOverlay ? 1 : 0,
child: Column(
children: [
topButtons(context),
const Spacer(),
bottomButtons(context),
],
),
),
),
),
const VideoPlayerSeekIndicator(),
Consumer(
builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
return Stack(
children: [
if (showIntroSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: IntroSkipButton(
isOverlayVisible: showOverlay,
skipIntro: () => skipIntro(introSkipModel),
const VideoPlayerSeekIndicator(),
Consumer(
builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
return Stack(
children: [
if (showIntroSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: IntroSkipButton(
isOverlayVisible: showOverlay,
skipIntro: () => skipIntro(introSkipModel),
),
),
),
),
if (showCreditSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: CreditsSkipButton(
isOverlayVisible: showOverlay,
skipCredits: () => skipCredits(introSkipModel),
if (showCreditSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: CreditsSkipButton(
isOverlayVisible: showOverlay,
skipCredits: () => skipCredits(introSkipModel),
),
),
),
)
],
);
},
),
],
)
],
);
},
),
],
),
),
),
),

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class InputHandler extends StatefulWidget {
final bool autoFocus;
final KeyEventResult Function(FocusNode node, KeyEvent event)? onKeyEvent;
final Widget child;
const InputHandler({
required this.child,
this.autoFocus = true,
this.onKeyEvent,
super.key,
});
@override
State<InputHandler> createState() => _InputHandlerState();
}
class _InputHandlerState extends State<InputHandler> {
final focusNode = FocusNode();
@override
Widget build(BuildContext context) {
return Focus(
autofocus: widget.autoFocus,
focusNode: focusNode,
onFocusChange: (value) {
if (!focusNode.hasFocus) {
focusNode.requestFocus();
}
},
onKeyEvent: widget.onKeyEvent,
child: widget.child,
);
}
}