mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-14 09:46:01 -07:00
fix: Keyboard controls and translations (#79)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7394077726
commit
1babf05834
11 changed files with 796 additions and 745 deletions
|
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue