fix: Keyboard controls and translations (#79)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-10-26 14:00:15 +02:00 committed by GitHub
parent 7394077726
commit 1babf05834
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 796 additions and 745 deletions

View file

@ -1048,5 +1048,12 @@
}, },
"aboutCreatedBy": "Created by DonutWare", "aboutCreatedBy": "Created by DonutWare",
"aboutSocials": "Socials", "aboutSocials": "Socials",
"aboutLicenses": "Licenses" "aboutLicenses": "Licenses",
"subtitle": "Subtitle",
"subtitleConfiguration": "Subtitle configuration",
"off": "Off",
"screenBrightness": "Screen brightness",
"scale":"Scale",
"playBackSettings": "Playback Settings"
} }

View file

@ -1,14 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// ignore_for_file: public_member_api_docs, sort_constructors_first // ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/video_properties.dart'; import 'package:fladder/util/video_properties.dart';
class MediaStreamsModel { class MediaStreamsModel {
@ -233,6 +234,14 @@ class AudioStreamModel extends StreamModel {
); );
} }
String label(BuildContext context) {
if (index == -1) {
return context.localized.off;
} else {
return displayTitle;
}
}
String get title => String get title =>
[name, language, codec, channelLayout].whereNotNull().where((element) => element.isNotEmpty).join(' - '); [name, language, codec, channelLayout].whereNotNull().where((element) => element.isNotEmpty).join(' - ');
@ -283,6 +292,14 @@ class SubStreamModel extends StreamModel {
this.supportsExternalStream = false, this.supportsExternalStream = false,
}); });
String label(BuildContext context) {
if (index == -1) {
return context.localized.off;
} else {
return displayTitle;
}
}
factory SubStreamModel.fromMediaStream(dto.MediaStream stream, Ref ref) { factory SubStreamModel.fromMediaStream(dto.MediaStream stream, Ref ref) {
return SubStreamModel( return SubStreamModel(
name: stream.title ?? "", name: stream.title ?? "",

View file

@ -14,6 +14,7 @@ class MediaPlaybackModel {
final bool completed; final bool completed;
final bool errorPlaying; final bool errorPlaying;
final bool buffering; final bool buffering;
final bool fullScreen;
MediaPlaybackModel({ MediaPlaybackModel({
this.state = VideoPlayerState.disposed, this.state = VideoPlayerState.disposed,
this.playing = false, this.playing = false,
@ -24,6 +25,7 @@ class MediaPlaybackModel {
this.completed = false, this.completed = false,
this.errorPlaying = false, this.errorPlaying = false,
this.buffering = false, this.buffering = false,
this.fullScreen = false,
}); });
MediaPlaybackModel copyWith({ MediaPlaybackModel copyWith({
@ -36,6 +38,7 @@ class MediaPlaybackModel {
bool? completed, bool? completed,
bool? errorPlaying, bool? errorPlaying,
bool? buffering, bool? buffering,
bool? fullScreen,
}) { }) {
return MediaPlaybackModel( return MediaPlaybackModel(
state: state ?? this.state, state: state ?? this.state,
@ -47,6 +50,7 @@ class MediaPlaybackModel {
completed: completed ?? this.completed, completed: completed ?? this.completed,
errorPlaying: errorPlaying ?? this.errorPlaying, errorPlaying: errorPlaying ?? this.errorPlaying,
buffering: buffering ?? this.buffering, buffering: buffering ?? this.buffering,
fullScreen: fullScreen ?? this.fullScreen,
); );
} }
} }

View file

@ -1,5 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/book_model.dart';
import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/book_viewer_provider.dart';
import 'package:fladder/providers/items/book_details_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart';
@ -11,11 +18,6 @@ import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/throttler.dart'; import 'package:fladder/util/throttler.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class BookViewController { class BookViewController {
bool controlsVisible = true; bool controlsVisible = true;
@ -45,7 +47,6 @@ class BookViewerControls extends ConsumerStatefulWidget {
} }
class _BookViewerControlsState extends ConsumerState<BookViewerControls> { class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
final FocusNode focusNode = FocusNode();
final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130)); final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130));
final Duration pageAnimDuration = const Duration(milliseconds: 125); final Duration pageAnimDuration = const Duration(milliseconds: 125);
final Curve pageAnimCurve = Curves.easeInCubic; final Curve pageAnimCurve = Curves.easeInCubic;
@ -74,11 +75,12 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
viewController.visibilityChanged.addListener(() { viewController.visibilityChanged.addListener(() {
toggleControls(value: viewController.controlsVisible); toggleControls(value: viewController.controlsVisible);
}); });
ServicesBinding.instance.keyboard.addHandler(_onKey);
} }
@override @override
void dispose() { void dispose() {
super.dispose(); ServicesBinding.instance.keyboard.removeHandler(_onKey);
WakelockPlus.disable(); WakelockPlus.disable();
ScreenBrightness().resetScreenBrightness(); ScreenBrightness().resetScreenBrightness();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
@ -87,6 +89,36 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent,
)); ));
super.dispose();
}
bool _onKey(KeyEvent value) {
final bookViewerSettings = ref.read(bookViewerSettingsProvider);
if (value is KeyRepeatEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage();
return true;
}
}
if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.keyF) {
toggleControls();
return true;
}
}
return false;
} }
@override @override
@ -100,252 +132,232 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
final previousChapter = details.previousChapter(bookViewerDetails.book); final previousChapter = details.previousChapter(bookViewerDetails.book);
final nextChapter = details.nextChapter(bookViewerDetails.book); final nextChapter = details.nextChapter(bookViewerDetails.book);
if (AdaptiveLayout.of(context).isDesktop) {
FocusScope.of(context).requestFocus(focusNode);
}
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
child: KeyboardListener( child: Stack(
focusNode: focusNode, children: [
autofocus: AdaptiveLayout.of(context).isDesktop, IgnorePointer(
onKeyEvent: (value) { ignoring: !showControls,
if (value is KeyDownEvent) { child: AnimatedOpacity(
if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) { duration: const Duration(milliseconds: 500),
bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage(); opacity: showControls ? 1 : 0,
} child: Stack(
if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) { children: [
bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage(); Container(
} decoration: BoxDecoration(
if (value.logicalKey == LogicalKeyboardKey.space) { gradient: LinearGradient(
toggleControls(); begin: Alignment.topCenter,
} end: Alignment.bottomCenter,
} colors: [
}, overlayColor.withOpacity(1),
child: Stack( overlayColor.withOpacity(0.65),
children: [ overlayColor.withOpacity(0),
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,
),
)
],
),
const SizedBox(height: 16),
],
),
), ),
), ),
if (!bookViewerDetails.loading) ...{ child: Padding(
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8),
Align( child: Column(
alignment: Alignment.bottomCenter, mainAxisSize: MainAxisSize.min,
child: Container( children: [
decoration: BoxDecoration( if (AdaptiveLayout.of(context).isDesktop)
gradient: LinearGradient( const Flexible(
begin: Alignment.topCenter, child: DefaultTitleBar(
end: Alignment.bottomCenter, height: 50,
colors: [ brightness: Brightness.dark,
overlayColor.withOpacity(0),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(1),
],
), ),
), ),
child: Padding( Row(
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), crossAxisAlignment: CrossAxisAlignment.center,
child: Column( children: [
mainAxisSize: MainAxisSize.min, const BackButton(),
children: [ const SizedBox(
const SizedBox(height: 30), width: 16,
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),
),
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),
],
),
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),
)
],
),
],
), ),
), Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
),
)
],
), ),
), const SizedBox(height: 16),
} 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) ...{
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),
),
),
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),
],
),
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"),
],
),
),
),
)
},
],
),
),
),
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

@ -1,8 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:share_plus/share_plus.dart';
import 'package:square_progress_indicator/square_progress_indicator.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/settings/photo_view_settings_provider.dart'; import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
@ -11,14 +21,6 @@ import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/throttler.dart'; import 'package:fladder/util/throttler.dart';
import 'package:fladder/widgets/shared/elevated_icon.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart';
import 'package:fladder/widgets/shared/progress_floating_button.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:share_plus/share_plus.dart';
import 'package:square_progress_indicator/square_progress_indicator.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart';
class PhotoViewerControls extends ConsumerStatefulWidget { class PhotoViewerControls extends ConsumerStatefulWidget {
final EdgeInsets padding; final EdgeInsets padding;
@ -48,7 +50,6 @@ class PhotoViewerControls extends ConsumerStatefulWidget {
} }
class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with WindowListener { class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with WindowListener {
final FocusNode focusNode = FocusNode();
final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130)); final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130));
late int currentPage = widget.pageController.page?.round() ?? 0; late int currentPage = widget.pageController.page?.round() ?? 0;
double dragUpDelta = 0.0; double dragUpDelta = 0.0;
@ -69,13 +70,46 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
} }
} }
bool _onKey(KeyEvent value) {
if (value is KeyRepeatEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
throttler.run(() =>
widget.pageController.previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut));
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
throttler.run(
() => widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut));
return true;
}
}
if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
throttler.run(() =>
widget.pageController.previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut));
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
throttler.run(
() => widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut));
return true;
}
if (value.logicalKey == LogicalKeyboardKey.keyK) {
timerController.playPause();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.keyF) {
widget.toggleOverlay?.call(null);
return true;
}
}
return false;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.microtask(() => () {
if (AdaptiveLayout.of(context).isDesktop) focusNode.requestFocus();
});
windowManager.addListener(this); windowManager.addListener(this);
widget.pageController.addListener( widget.pageController.addListener(
() { () {
@ -83,10 +117,12 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
timerController.reset(); timerController.reset();
}, },
); );
ServicesBinding.instance.keyboard.addHandler(_onKey);
} }
@override @override
void onWindowMinimize() { void onWindowMinimize() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
timerController.cancel(); timerController.cancel();
super.onWindowMinimize(); super.onWindowMinimize();
} }
@ -100,8 +136,6 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (AdaptiveLayout.of(context).isDesktop) focusNode.requestFocus();
final gradient = [ final gradient = [
Colors.black.withOpacity(0.6), Colors.black.withOpacity(0.6),
Colors.black.withOpacity(0.3), Colors.black.withOpacity(0.3),
@ -109,217 +143,187 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
Colors.black.withOpacity(0.0), Colors.black.withOpacity(0.0),
]; ];
if (AdaptiveLayout.of(context).isDesktop) {
focusNode.requestFocus();
}
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
await WakelockPlus.disable(); await WakelockPlus.disable();
}, },
child: KeyboardListener( child: Stack(
focusNode: focusNode, children: [
autofocus: true, Align(
onKeyEvent: (value) { alignment: Alignment.topCenter,
if (value is KeyDownEvent) { widthFactor: 1,
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { child: Container(
throttler.run(() => widget.pageController decoration: BoxDecoration(
.previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); gradient: LinearGradient(
} begin: Alignment.topCenter,
if (value.logicalKey == LogicalKeyboardKey.arrowRight) { end: Alignment.bottomCenter,
throttler.run(() => colors: gradient,
widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut));
}
if (value.logicalKey == LogicalKeyboardKey.keyK) {
timerController.playPause();
}
if (value.logicalKey == LogicalKeyboardKey.space) {
widget.toggleOverlay?.call(null);
}
}
},
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: Padding(
child: Column( padding: EdgeInsets.only(top: widget.padding.top),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25), children: [
Padding( if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25),
padding: const EdgeInsets.symmetric(vertical: 12) Padding(
.add(EdgeInsets.only(left: padding.left, right: padding.right)), padding: const EdgeInsets.symmetric(vertical: 12)
child: Row( .add(EdgeInsets.only(left: padding.left, right: padding.right)),
mainAxisSize: MainAxisSize.max, child: Row(
children: [ mainAxisSize: MainAxisSize.max,
ElevatedIconButton( children: [
onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()), ElevatedIconButton(
icon: getBackIcon(context), onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()),
), icon: getBackIcon(context),
const SizedBox(width: 8), ),
Expanded( const SizedBox(width: 8),
child: Tooltip( Expanded(
message: widget.photo.name, child: Tooltip(
child: Text( message: widget.photo.name,
widget.photo.name, child: Text(
maxLines: 2, widget.photo.name,
style: Theme.of(context) maxLines: 2,
.textTheme style: Theme.of(context)
.titleMedium .textTheme
?.copyWith(fontWeight: FontWeight.bold, shadows: [ .titleMedium
BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)), ?.copyWith(fontWeight: FontWeight.bold, shadows: [
BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)), BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)),
BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)), 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( const SizedBox(width: 8),
children: [ Stack(
Positioned.fill( children: [
child: Container( Positioned.fill(
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onPrimary), borderRadius: BorderRadius.circular(8),
child: SquareProgressIndicator( color: Theme.of(context).colorScheme.onPrimary),
value: widget.currentIndex / (widget.itemCount - 1), child: SquareProgressIndicator(
borderRadius: 7, value: widget.currentIndex / (widget.itemCount - 1),
clockwise: false, borderRadius: 7,
color: Theme.of(context).colorScheme.primary, 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(
Padding( dimension: 16,
padding: const EdgeInsets.all(9), child: CircularProgressIndicator.adaptive(
child: Row( strokeCap: StrokeCap.round,
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,
),
), ),
].addInBetween(const SizedBox(width: 6)), ),
), ].addInBetween(const SizedBox(width: 6)),
), ),
Positioned.fill( ),
child: FlatButton( Positioned.fill(
borderRadiusGeometry: BorderRadius.circular(8), child: FlatButton(
onTap: () async { borderRadiusGeometry: BorderRadius.circular(8),
showDialog( onTap: () async {
context: context, showDialog(
builder: (context) => Dialog( context: context,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), builder: (context) => Dialog(
child: SizedBox( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
width: 125, child: SizedBox(
child: Padding( width: 125,
padding: const EdgeInsets.all(8.0), child: Padding(
child: Column( padding: const EdgeInsets.all(8.0),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Text( children: [
context.localized.goTo, Text(
style: Theme.of(context) context.localized.goTo,
.textTheme style: Theme.of(context)
.bodyLarge .textTheme
?.copyWith(fontWeight: FontWeight.bold), .bodyLarge
), ?.copyWith(fontWeight: FontWeight.bold),
const SizedBox(height: 5), ),
IntInputField( const SizedBox(height: 5),
controller: TextEditingController( IntInputField(
text: (widget.currentIndex + 1).toString()), controller:
onSubmitted: (value) { TextEditingController(text: (widget.currentIndex + 1).toString()),
final position = onSubmitted: (value) {
((value ?? 0) - 1).clamp(0, widget.itemCount - 1); final position = ((value ?? 0) - 1).clamp(0, widget.itemCount - 1);
widget.pageController.jumpToPage(position); widget.pageController.jumpToPage(position);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
], ],
),
), ),
), ),
), ),
); ),
}, );
), },
) ),
], )
), ],
const SizedBox(width: 12), ),
], const SizedBox(width: 12),
), ],
), ),
], ),
), ],
), ),
), ),
), ),
Align( ),
alignment: Alignment.bottomCenter, Align(
child: Container( alignment: Alignment.bottomCenter,
decoration: BoxDecoration( child: Container(
gradient: LinearGradient( decoration: BoxDecoration(
begin: Alignment.topCenter, gradient: LinearGradient(
end: Alignment.bottomCenter, begin: Alignment.topCenter,
colors: gradient.reversed.toList(), end: Alignment.bottomCenter,
), colors: gradient.reversed.toList(),
), ),
width: double.infinity, ),
child: Padding( width: double.infinity,
padding: EdgeInsets.only(bottom: widget.padding.bottom), child: Padding(
child: Column( padding: EdgeInsets.only(bottom: widget.padding.bottom),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Padding( children: [
padding: Padding(
const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)), padding: const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row( child: Row(
children: [ children: [
ElevatedIconButton( ElevatedIconButton(
onPressed: widget.openOptions, onPressed: widget.openOptions,
icon: IconsaxOutline.more_2, icon: IconsaxOutline.more_2,
), ),
const Spacer(), const Spacer(),
ElevatedIconButton( ElevatedIconButton(
onPressed: markAsFavourite, onPressed: markAsFavourite,
color: widget.photo.userData.isFavourite ? Colors.red : null, color: widget.photo.userData.isFavourite ? Colors.red : null,
icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart, icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart,
), ),
ProgressFloatingButton( ProgressFloatingButton(
controller: timerController, controller: timerController,
), ),
].addPadding(const EdgeInsets.symmetric(horizontal: 8)), ].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
), ),
) )
], ],
),
), ),
), ),
), ),
], ),
), ],
), ),
); );
} }

View file

@ -1,9 +1,11 @@
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/util/list_padding.dart';
class PassCodeInput extends ConsumerStatefulWidget { class PassCodeInput extends ConsumerStatefulWidget {
final ValueChanged<String> passCode; final ValueChanged<String> passCode;
const PassCodeInput({required this.passCode, super.key}); const PassCodeInput({required this.passCode, super.key});
@ -16,80 +18,90 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
final iconSize = 45.0; final iconSize = 45.0;
final passCodeLength = 4; final passCodeLength = 4;
var currentPasscode = ""; var currentPasscode = "";
final focusNode = FocusNode();
@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);
if (keyInt != null) {
addToPassCode(value.logicalKey.keyLabel);
return true;
}
if (value.logicalKey == LogicalKeyboardKey.backspace) {
backSpace();
return true;
}
}
return false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
focusNode.requestFocus(); return AlertDialog(
return KeyboardListener( scrollable: true,
focusNode: focusNode, content: Column(
autofocus: true, mainAxisSize: MainAxisSize.min,
onKeyEvent: (value) { crossAxisAlignment: CrossAxisAlignment.stretch,
if (value is KeyDownEvent) { children: [
final keyInt = int.tryParse(value.logicalKey.keyLabel); Row(
if (keyInt != null) { mainAxisSize: MainAxisSize.max,
addToPassCode(value.logicalKey.keyLabel); mainAxisAlignment: MainAxisAlignment.spaceBetween,
} children: List.generate(
if (value.logicalKey == LogicalKeyboardKey.backspace) { passCodeLength,
backSpace(); (index) => Expanded(
} child: Padding(
} padding: const EdgeInsets.symmetric(horizontal: 4),
}, child: SizedBox(
child: AlertDialog( height: iconSize * 1.2,
scrollable: true, width: iconSize * 1.2,
content: Column( child: Card(
mainAxisSize: MainAxisSize.min, child: Transform.translate(
crossAxisAlignment: CrossAxisAlignment.stretch, offset: const Offset(0, 5),
children: [ child: AnimatedFadeSize(
Row( child: Text(
mainAxisSize: MainAxisSize.max, currentPasscode.length > index ? "*" : "",
mainAxisAlignment: MainAxisAlignment.spaceBetween, style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
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(), ),
), ).toList(),
Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
backSpaceButton, children: [
passCodeNumber(0), backSpaceButton,
clearAllButton, passCodeNumber(0),
], clearAllButton,
) ],
].addPadding(const EdgeInsets.symmetric(vertical: 8)), )
), ].addPadding(const EdgeInsets.symmetric(vertical: 8)),
), ),
); );
} }

View file

@ -19,6 +19,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart';
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/enum_selection.dart';
@ -91,7 +92,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Flexible(flex: 1, child: Text("Screen Brightness")), Flexible(flex: 1, child: Text(context.localized.screenBrightness)),
Flexible( Flexible(
child: Row( child: Row(
children: [ children: [
@ -124,13 +125,13 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
), ),
), ),
SpacedListTile( SpacedListTile(
title: const Text("Subtitles"), title: Text(context.localized.subtitles),
content: Text(currentMediaStreams?.currentSubStream?.displayTitle ?? "Off"), content: Text(currentMediaStreams?.currentSubStream?.label(context) ?? context.localized.off),
onTap: currentMediaStreams?.subStreams.isNotEmpty == true ? () => showSubSelection(context) : null, onTap: currentMediaStreams?.subStreams.isNotEmpty == true ? () => showSubSelection(context) : null,
), ),
SpacedListTile( SpacedListTile(
title: const Text("Audio"), title: Text(context.localized.audio),
content: Text(currentMediaStreams?.currentAudioStream?.displayTitle ?? "Off"), content: Text(currentMediaStreams?.currentAudioStream?.label(context) ?? context.localized.off),
onTap: currentMediaStreams?.audioStreams.isNotEmpty == true ? () => showAudioSelection(context) : null, onTap: currentMediaStreams?.audioStreams.isNotEmpty == true ? () => showAudioSelection(context) : null,
), ),
ListTile( ListTile(
@ -139,7 +140,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
children: [ children: [
Flexible( Flexible(
child: EnumSelection( child: EnumSelection(
label: const Text("Scale"), label: Text(context.localized.scale),
current: videoSettings.videoFit.name.toUpperCaseSplit(), current: videoSettings.videoFit.name.toUpperCaseSplit(),
itemBuilder: (context) => BoxFit.values itemBuilder: (context) => BoxFit.values
.map((value) => PopupMenuItem( .map((value) => PopupMenuItem(
@ -159,9 +160,9 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
title: Row( title: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Expanded( Expanded(
flex: 3, flex: 3,
child: Text("Fill-screen"), child: Text(context.localized.videoScalingFill),
), ),
const Spacer(), const Spacer(),
Switch.adaptive( Switch.adaptive(
@ -235,7 +236,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
widget.minimizePlayer(); widget.minimizePlayer();
(this as EpisodeModel).parentBaseModel.navigateTo(context); (this as EpisodeModel).parentBaseModel.navigateTo(context);
}, },
title: const Text("Open show"), title: Text(context.localized.openShow),
), ),
ListTile( ListTile(
onTap: () async { onTap: () async {
@ -243,7 +244,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
widget.minimizePlayer(); widget.minimizePlayer();
await currentItem.navigateTo(context); await currentItem.navigateTo(context);
}, },
title: const Text("Show details"), title: Text(context.localized.showDetails),
), ),
if (currentItem.type != FladderItemType.boxset) if (currentItem.type != FladderItemType.boxset)
ListTile( ListTile(
@ -253,7 +254,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
context.refreshData(); context.refreshData();
} }
}, },
title: const Text("Add to collection"), title: Text(context.localized.addToCollection),
), ),
if (currentItem.type != FladderItemType.playlist) if (currentItem.type != FladderItemType.playlist)
ListTile( ListTile(
@ -263,7 +264,7 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
context.refreshData(); context.refreshData();
} }
}, },
title: const Text("Add to playlist"), title: Text(context.localized.addToPlaylist),
), ),
ListTile( ListTile(
onTap: () async { onTap: () async {
@ -280,14 +281,16 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
ref.read(playBackModel.notifier).update((state) => playbackModel); ref.read(playBackModel.notifier).update((state) => playbackModel);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
title: Text(currentItem.userData.isFavourite == true ? "Remove from favorites" : "Add to favourites"), title: Text(currentItem.userData.isFavourite == true
? context.localized.removeAsFavorite
: context.localized.addAsFavorite),
), ),
ListTile( ListTile(
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
showInfoScreen(context, currentItem); showInfoScreen(context, currentItem);
}, },
title: const Text('Media info'), title: Text(context.localized.info),
), ),
} }
], ],
@ -340,14 +343,14 @@ Future<void> showSubSelection(BuildContext context) {
contentPadding: const EdgeInsets.only(top: 8, bottom: 24), contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
title: Row( title: Row(
children: [ children: [
const Text("Subtitle"), Text(context.localized.subtitle),
const Spacer(), const Spacer(),
IconButton.outlined( IconButton.outlined(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
showSubtitleControls( showSubtitleControls(
context: context, context: context,
label: 'Subtitle configuration', label: context.localized.subtitleConfiguration,
); );
}, },
icon: const Icon(Icons.display_settings_rounded)) icon: const Icon(Icons.display_settings_rounded))
@ -357,7 +360,7 @@ Future<void> showSubSelection(BuildContext context) {
(index, subModel) { (index, subModel) {
final selected = playbackModel.mediaStreams?.defaultSubStreamIndex == subModel.index; final selected = playbackModel.mediaStreams?.defaultSubStreamIndex == subModel.index;
return ListTile( return ListTile(
title: Text(subModel.displayTitle), title: Text(subModel.label(context)),
tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null,
subtitle: subModel.language.isNotEmpty subtitle: subModel.language.isNotEmpty
? Opacity(opacity: 0.6, child: Text(subModel.language.capitalize())) ? Opacity(opacity: 0.6, child: Text(subModel.language.capitalize()))
@ -391,24 +394,14 @@ Future<void> showAudioSelection(BuildContext context) {
contentPadding: const EdgeInsets.only(top: 8, bottom: 24), contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
title: Row( title: Row(
children: [ children: [
const Text("Subtitle"), Text(context.localized.audio),
const Spacer(),
IconButton.outlined(
onPressed: () {
Navigator.pop(context);
showSubtitleControls(
context: context,
label: 'Subtitle configuration',
);
},
icon: const Icon(Icons.display_settings_rounded))
], ],
), ),
children: playbackModel?.audioStreams?.mapIndexed( children: playbackModel?.audioStreams?.mapIndexed(
(index, audioStream) { (index, audioStream) {
final selected = playbackModel.mediaStreams?.defaultAudioStreamIndex == audioStream.index; final selected = playbackModel.mediaStreams?.defaultAudioStreamIndex == audioStream.index;
return ListTile( return ListTile(
title: Text(audioStream.displayTitle), title: Text(audioStream.label(context)),
tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null,
subtitle: audioStream.language.isNotEmpty subtitle: audioStream.language.isNotEmpty
? Opacity(opacity: 0.6, child: Text(audioStream.language.capitalize())) ? Opacity(opacity: 0.6, child: Text(audioStream.language.capitalize()))

View file

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/widget_extensions.dart'; import 'package:fladder/util/widget_extensions.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> showSubtitleControls({ Future<void> showSubtitleControls({
required BuildContext context, required BuildContext context,
@ -18,7 +20,8 @@ Future<void> showSubtitleControls({
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: MediaQuery.sizeOf(context).width * 0.75), child: VideoSubtitleControls(label: label)), constraints: BoxConstraints(minWidth: MediaQuery.sizeOf(context).width * 0.75),
child: VideoSubtitleControls(label: label)),
), ),
); );
return; return;
@ -52,6 +55,8 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
final provider = ref.read(subtitleSettingsProvider.notifier); final provider = ref.read(subtitleSettingsProvider.notifier);
final controlsHidden = hideControls ? false : showPartial; final controlsHidden = hideControls ? false : showPartial;
return AnimatedContainer( return AnimatedContainer(
height: MediaQuery.sizeOf(context).width * 0.85,
width: MediaQuery.sizeOf(context).height * 0.7,
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@ -90,7 +95,9 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: subSettings != lastSettings ? () => provider.resetSettings(value: lastSettings) : null, onPressed: subSettings != lastSettings
? () => provider.resetSettings(value: lastSettings)
: null,
child: Text(context.localized.clearChanges), child: Text(context.localized.clearChanges),
), ),
const SizedBox(width: 32), const SizedBox(width: 32),
@ -105,15 +112,18 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
multiSelectionEnabled: false, multiSelectionEnabled: false,
segments: [ segments: [
ButtonSegment( ButtonSegment(
label: Text(context.localized.light, style: const TextStyle(fontWeight: FontWeight.w100)), label:
Text(context.localized.light, style: const TextStyle(fontWeight: FontWeight.w100)),
value: FontWeight.w100, value: FontWeight.w100,
), ),
ButtonSegment( ButtonSegment(
label: Text(context.localized.normal, style: const TextStyle(fontWeight: FontWeight.w500)), label:
Text(context.localized.normal, style: const TextStyle(fontWeight: FontWeight.w500)),
value: FontWeight.normal, value: FontWeight.normal,
), ),
ButtonSegment( ButtonSegment(
label: Text(context.localized.bold, style: const TextStyle(fontWeight: FontWeight.w900)), label:
Text(context.localized.bold, style: const TextStyle(fontWeight: FontWeight.w900)),
value: FontWeight.bold, value: FontWeight.bold,
), ),
], ],
@ -210,7 +220,8 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
const Icon(Icons.border_color_rounded), const Icon(Icons.border_color_rounded),
...[Colors.white, Colors.yellow, Colors.black, Colors.grey, Colors.transparent].map( ...[Colors.white, Colors.yellow, Colors.black, Colors.grey, Colors.transparent].map(
(e) => FlatButton( (e) => FlatButton(
onTap: () => provider.setOutlineColor(e == Colors.transparent ? e : e.withOpacity(0.85)), onTap: () =>
provider.setOutlineColor(e == Colors.transparent ? e : e.withOpacity(0.85)),
borderRadiusGeometry: BorderRadius.circular(5), borderRadiusGeometry: BorderRadius.circular(5),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( child: Container(
@ -290,7 +301,8 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
), ),
Text(context.localized.backgroundOpacity), Text(context.localized.backgroundOpacity),
], ],
).addVisiblity(activeKey == null ? controlsHidden : activeKey == const Key('backGroundOpacity')), ).addVisiblity(
activeKey == null ? controlsHidden : activeKey == const Key('backGroundOpacity')),
Column( Column(
children: [ children: [
Row( Row(

View file

@ -42,6 +42,11 @@ class DesktopControls extends ConsumerStatefulWidget {
} }
class _DesktopControlsState extends ConsumerState<DesktopControls> { class _DesktopControlsState extends ConsumerState<DesktopControls> {
late RestartableTimer timer = RestartableTimer(
const Duration(seconds: 5),
() => mounted ? toggleOverlay(value: false) : null,
);
final fadeDuration = const Duration(milliseconds: 350); final fadeDuration = const Duration(milliseconds: 350);
final focusNode = FocusNode(); final focusNode = FocusNode();
bool showOverlay = true; bool showOverlay = true;
@ -50,6 +55,72 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
late final double topPadding = MediaQuery.of(context).viewPadding.top; late final double topPadding = MediaQuery.of(context).viewPadding.top;
late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom; late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom;
bool _onKey(KeyEvent value) {
final introSkipModel = ref.read(playBackModel.select((value) => value?.introSkipModel));
final position = ref.read(mediaPlaybackProvider).position;
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
if (value is KeyRepeatEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
}
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
}
return true;
}
if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.keyS) {
if (showIntroSkipButton) {
skipIntro(introSkipModel);
} else if (showCreditSkipButton) {
skipCredits(introSkipModel);
}
focusNode.requestFocus();
}
if (value.logicalKey == LogicalKeyboardKey.escape) {
disableFullscreen();
}
if (value.logicalKey == LogicalKeyboardKey.space) {
ref.read(videoPlayerProvider).playOrPause();
}
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
seekBack(ref);
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
seekForward(ref);
}
if (value.logicalKey == LogicalKeyboardKey.keyF) {
toggleFullScreen(ref);
}
if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
}
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
}
return true;
}
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel)); final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel));
@ -66,121 +137,75 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
closePlayer(); closePlayer();
} }
}, },
child: KeyboardListener( child: GestureDetector(
focusNode: focusNode, onTap: () => toggleOverlay(),
autofocus: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer, child: MouseRegion(
onKeyEvent: (value) { cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
final position = ref.read(mediaPlaybackProvider).position; onEnter: (event) => toggleOverlay(value: true),
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; onExit: (event) => toggleOverlay(value: false),
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
if (value is KeyRepeatEvent) {} child: Stack(
if (value is KeyDownEvent) { children: [
if (value.logicalKey == LogicalKeyboardKey.keyS) { if (player != null)
if (showIntroSkipButton) { VideoSubtitles(
skipIntro(introSkipModel); key: const Key('subtitles'),
} else if (showCreditSkipButton) { controller: player,
skipCredits(introSkipModel); overlayed: showOverlay,
} ),
focusNode.requestFocus(); if (AdaptiveLayout.of(context).isDesktop)
} Consumer(builder: (context, ref, child) {
if (value.logicalKey == LogicalKeyboardKey.escape) { final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
disableFullscreen(); final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
} return playButton(playing, buffering);
if (value.logicalKey == LogicalKeyboardKey.space) { }),
ref.read(videoPlayerProvider).playOrPause(); IgnorePointer(
} ignoring: !showOverlay,
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { child: AnimatedOpacity(
seekBack(ref); duration: fadeDuration,
} opacity: showOverlay ? 1 : 0,
if (value.logicalKey == LogicalKeyboardKey.arrowRight) { child: Column(
seekForward(ref); children: [
} topButtons(context),
if (value.logicalKey == LogicalKeyboardKey.keyF) { const Spacer(),
toggleFullScreen(); bottomButtons(context),
} ],
if (AdaptiveLayout.of(context).isDesktop || kIsWeb) {
if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
}
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
}
}
focusNode.requestFocus();
}
},
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),
],
),
), ),
), ),
Consumer( ),
builder: (context, ref, child) { Consumer(
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); builder: (context, ref, child) {
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
return Stack( bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
children: [ return Stack(
if (showIntroSkipButton) children: [
Align( if (showIntroSkipButton)
alignment: Alignment.centerRight, Align(
child: Padding( alignment: Alignment.centerRight,
padding: const EdgeInsets.all(32), child: Padding(
child: IntroSkipButton( padding: const EdgeInsets.all(32),
isOverlayVisible: showOverlay, child: IntroSkipButton(
skipIntro: () => skipIntro(introSkipModel), isOverlayVisible: showOverlay,
), skipIntro: () => skipIntro(introSkipModel),
), ),
), ),
if (showCreditSkipButton) ),
Align( if (showCreditSkipButton)
alignment: Alignment.centerRight, Align(
child: Padding( alignment: Alignment.centerRight,
padding: const EdgeInsets.all(32), child: Padding(
child: CreditsSkipButton( padding: const EdgeInsets.all(32),
isOverlayVisible: showOverlay, child: CreditsSkipButton(
skipCredits: () => skipCredits(introSkipModel), isOverlayVisible: showOverlay,
), skipCredits: () => skipCredits(introSkipModel),
), ),
) ),
], )
); ],
}, );
), },
], ),
), ],
), ),
), ),
), ),
@ -317,11 +342,11 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
onPressed: () => showSubSelection(context), onPressed: () => showSubSelection(context),
icon: const Icon(IconsaxOutline.subtitle), icon: const Icon(IconsaxOutline.subtitle),
label: Text( label: Text(
ref ref.watch(playBackModel.select((value) {
.watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream)) final language = value?.mediaStreams?.currentSubStream?.language;
?.language return language?.isEmpty == true ? context.localized.off : language;
.capitalize() ?? }))?.capitalize() ??
"Off", "",
maxLines: 1, maxLines: 1,
), ),
), ),
@ -331,11 +356,11 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
onPressed: () => showAudioSelection(context), onPressed: () => showAudioSelection(context),
icon: const Icon(IconsaxOutline.audio_square), icon: const Icon(IconsaxOutline.audio_square),
label: Text( label: Text(
ref ref.watch(playBackModel.select((value) {
.watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream)) final language = value?.mediaStreams?.currentAudioStream?.language;
?.language return language?.isEmpty == true ? context.localized.off : language;
.capitalize() ?? }))?.capitalize() ??
"Off", "",
maxLines: 1, maxLines: 1,
), ),
), ),
@ -591,11 +616,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition));
} }
late RestartableTimer timer = RestartableTimer(
const Duration(seconds: 5),
() => mounted ? toggleOverlay(value: false) : null,
);
void toggleOverlay({bool? value}) { void toggleOverlay({bool? value}) {
if (showOverlay == (value ?? !showOverlay)) return; if (showOverlay == (value ?? !showOverlay)) return;
setState(() => showOverlay = (value ?? !showOverlay)); setState(() => showOverlay = (value ?? !showOverlay));
@ -652,9 +672,4 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} }
} }
} }
Future<void> toggleFullScreen() async {
final isFullScreen = await windowManager.isFullScreen();
await windowManager.setFullScreen(!isFullScreen);
}
} }

View file

@ -1,40 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class FullScreenButton extends StatefulWidget { import 'package:fladder/providers/video_player_provider.dart';
Future<void> toggleFullScreen(WidgetRef ref) async {
final isFullScreen = await windowManager.isFullScreen();
await windowManager.setFullScreen(!isFullScreen);
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen));
}
class FullScreenButton extends ConsumerWidget {
const FullScreenButton({super.key}); const FullScreenButton({super.key});
@override @override
State<FullScreenButton> createState() => _FullScreenButtonState(); Widget build(BuildContext context, WidgetRef ref) {
} final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen));
class _FullScreenButtonState extends State<FullScreenButton> {
bool isFullScreen = false;
@override
void initState() {
super.initState();
Future.microtask(checkFullScreen);
}
void checkFullScreen() async {
final fullScreen = await windowManager.isFullScreen();
setState(() {
isFullScreen = fullScreen;
});
}
@override
Widget build(BuildContext context) {
return IconButton( return IconButton(
onPressed: () async { onPressed: () => toggleFullScreen(ref),
await windowManager.setFullScreen(!isFullScreen);
checkFullScreen();
},
icon: Icon( icon: Icon(
isFullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4,
), ),
); );
} }

View file

@ -1,46 +1,34 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
class FullScreenButton extends StatefulWidget { import 'package:fladder/providers/video_player_provider.dart';
Future<void> toggleFullScreen(WidgetRef ref) async {
final isFullScreen = html.document.fullscreenElement != null;
if (isFullScreen) {
html.document.exitFullscreen();
//Wait for 1 second
await Future.delayed(const Duration(seconds: 1));
} else {
await html.document.documentElement?.requestFullscreen();
}
ref
.read(mediaPlaybackProvider.notifier)
.update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null));
}
class FullScreenButton extends ConsumerWidget {
const FullScreenButton({super.key}); const FullScreenButton({super.key});
@override @override
State<FullScreenButton> createState() => _FullScreenButtonState(); Widget build(BuildContext context, WidgetRef ref) {
} final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen));
class _FullScreenButtonState extends State<FullScreenButton> {
bool fullScreen = false;
@override
void initState() {
super.initState();
_updateFullScreenStatus();
}
void _updateFullScreenStatus() {
setState(() {
fullScreen = html.document.fullscreenElement != null;
});
}
void toggleFullScreen() async {
if (fullScreen) {
html.document.exitFullscreen();
//Wait for 1 second
await Future.delayed(const Duration(seconds: 1));
} else {
await html.document.documentElement?.requestFullscreen();
}
_updateFullScreenStatus();
}
@override
Widget build(BuildContext context) {
return IconButton( return IconButton(
onPressed: toggleFullScreen, onPressed: () => toggleFullScreen(ref),
icon: Icon( icon: Icon(
fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4,
), ),