From f2591513360ec8c6749c37f18093f306b54f6b54 Mon Sep 17 00:00:00 2001 From: PartyDonut <42371342+PartyDonut@users.noreply.github.com> Date: Sat, 8 Feb 2025 11:48:42 +0100 Subject: [PATCH] feature: More info playback state (#219) Co-authored-by: PartyDonut --- lib/l10n/app_en.arb | 3 +- lib/screens/crash_screen/crash_screen.dart | 10 +- lib/screens/metadata/info_screen.dart | 20 +-- lib/screens/shared/fladder_snackbar.dart | 156 +----------------- .../video_playback_information.dart | 83 ++++++++++ lib/util/clipboard_helper.dart | 17 ++ .../item_base_model_extensions.dart | 9 +- 7 files changed, 112 insertions(+), 186 deletions(-) create mode 100644 lib/util/clipboard_helper.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b91cb33..7570d94 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1167,5 +1167,6 @@ "tablet": "Tablet", "desktop": "Desktop", "layoutModeSingle": "Single", - "layoutModeDual": "Dual" + "layoutModeDual": "Dual", + "copiedToClipboard": "Copied to clipboard" } \ No newline at end of file diff --git a/lib/screens/crash_screen/crash_screen.dart b/lib/screens/crash_screen/crash_screen.dart index 3ebfa1e..ecee680 100644 --- a/lib/screens/crash_screen/crash_screen.dart +++ b/lib/screens/crash_screen/crash_screen.dart @@ -1,12 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/error_log_model.dart'; import 'package:fladder/providers/crash_log_provider.dart'; -import 'package:fladder/screens/shared/fladder_snackbar.dart'; +import 'package:fladder/util/clipboard_helper.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -102,12 +101,7 @@ class CrashScreen extends ConsumerWidget { ), ), IconButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: e.clipBoard)); - if (context.mounted) { - fladderSnackbar(context, title: "Copied to clipboard"); - } - }, + onPressed: () => context.copyToClipboard(e.clipBoard), icon: const Icon(Icons.copy_all_rounded), ), ], diff --git a/lib/screens/metadata/info_screen.dart b/lib/screens/metadata/info_screen.dart index df541db..de5b0e7 100644 --- a/lib/screens/metadata/info_screen.dart +++ b/lib/screens/metadata/info_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/information_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/items/information_provider.dart'; -import 'package:fladder/screens/shared/fladder_snackbar.dart'; +import 'package:fladder/util/clipboard_helper.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/clickable_text.dart'; @@ -79,12 +78,7 @@ class ItemInfoScreenState extends ConsumerState { const Spacer(), const SizedBox(width: 6), IconButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: info.model.toString())); - if (context.mounted) { - fladderSnackbar(context, title: "Copied to clipboard"); - } - }, + onPressed: () => context.copyToClipboard(info.model.toString()), icon: const Icon(Icons.copy_all_rounded)), const SizedBox(width: 6), IconButton( @@ -171,10 +165,7 @@ class ItemInfoScreenState extends ConsumerState { Flexible( child: ClickableText( text: title, - onTap: () async { - await Clipboard.setData(ClipboardData(text: value)); - fladderSnackbar(context, title: "Copied to clipboard"); - }, + onTap: () => context.copyToClipboard(value), style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -211,10 +202,7 @@ class ItemInfoScreenState extends ConsumerState { ), ), IconButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: InformationModel.mapToString(map))); - fladderSnackbar(context, title: "Copied to clipboard"); - }, + onPressed: () => context.copyToClipboard(InformationModel.mapToString(map)), icon: const Icon(Icons.copy_all_rounded)) ], ), diff --git a/lib/screens/shared/fladder_snackbar.dart b/lib/screens/shared/fladder_snackbar.dart index 113a199..812c295 100644 --- a/lib/screens/shared/fladder_snackbar.dart +++ b/lib/screens/shared/fladder_snackbar.dart @@ -1,6 +1,7 @@ -import 'package:chopper/chopper.dart'; import 'package:flutter/material.dart'; +import 'package:chopper/chopper.dart'; + void fladderSnackbar( BuildContext context, { String title = "", @@ -34,156 +35,3 @@ void fladderSnackbarResponse(BuildContext context, Response? response, {String? fladderSnackbar(context, title: altTitle); } } - -// void _showOverlay( -// BuildContext context, { -// required String title, -// Widget? leading, -// bool showCloseButton = false, -// bool permanent = false, -// Duration duration = const Duration(seconds: 3), -// }) { -// late OverlayEntry overlayEntry; - -// overlayEntry = OverlayEntry( -// builder: (context) => _OverlayAnimationWidget( -// title: title, -// leading: leading, -// showCloseButton: showCloseButton, -// permanent: permanent, -// duration: duration, -// overlayEntry: overlayEntry, -// ), -// ); - -// // Insert the overlay entry into the overlay -// Overlay.of(context).insert(overlayEntry); -// } - -// class _OverlayAnimationWidget extends StatefulWidget { -// final String title; -// final Widget? leading; -// final bool showCloseButton; -// final bool permanent; -// final Duration duration; -// final OverlayEntry overlayEntry; - -// _OverlayAnimationWidget({ -// required this.title, -// this.leading, -// this.showCloseButton = false, -// this.permanent = false, -// this.duration = const Duration(seconds: 3), -// required this.overlayEntry, -// }); - -// @override -// _OverlayAnimationWidgetState createState() => _OverlayAnimationWidgetState(); -// } - -// class _OverlayAnimationWidgetState extends State<_OverlayAnimationWidget> with SingleTickerProviderStateMixin { -// late AnimationController _controller; -// late Animation _offsetAnimation; - -// void remove() { -// // Optionally, you can use a Future.delayed to remove the overlay after a certain duration -// _controller.reverse(); -// // Remove the overlay entry after the animation completes -// Future.delayed(Duration(seconds: 1), () { -// widget.overlayEntry.remove(); -// }); -// } - -// @override -// void initState() { -// super.initState(); - -// _controller = AnimationController( -// vsync: this, -// duration: Duration(milliseconds: 250), -// ); - -// _offsetAnimation = Tween( -// begin: Offset(0.0, 1.5), -// end: Offset.zero, -// ).animate(CurvedAnimation( -// parent: _controller, -// curve: Curves.fastOutSlowIn, -// )); - -// // Start the animation -// _controller.forward(); - -// Future.delayed(widget.duration, () { -// if (!widget.permanent) { -// remove(); -// } -// }); -// } - -// @override -// void dispose() { -// _controller.dispose(); -// super.dispose(); -// } - -// @override -// Widget build(BuildContext context) { -// return Positioned( -// bottom: 10 + MediaQuery.of(context).padding.bottom, -// left: 25, -// right: 25, -// child: Dismissible( -// key: UniqueKey(), -// direction: DismissDirection.horizontal, -// confirmDismiss: (direction) async { -// remove(); -// return true; -// }, -// child: SlideTransition( -// position: _offsetAnimation, -// child: Card( -// elevation: 5, -// color: Colors.transparent, -// surfaceTintColor: Colors.transparent, -// child: Container( -// decoration: BoxDecoration( -// color: Theme.of(context).colorScheme.secondaryContainer, -// ), -// child: Padding( -// padding: const EdgeInsets.all(12.0), -// child: ConstrainedBox( -// constraints: BoxConstraints(minHeight: 45), -// child: Row( -// children: [ -// if (widget.leading != null) widget.leading!, -// Expanded( -// child: Text( -// widget.title, -// style: TextStyle( -// fontSize: 16, -// fontWeight: FontWeight.w400, -// color: Theme.of(context).colorScheme.onSecondaryContainer), -// ), -// ), -// const SizedBox(width: 6), -// if (widget.showCloseButton || widget.permanent) -// IconButton( -// onPressed: () => remove(), -// icon: Icon( -// IconsaxOutline.close_square, -// size: 28, -// color: Theme.of(context).colorScheme.onSecondaryContainer, -// ), -// ) -// ], -// ), -// ), -// ), -// ), -// ), -// ), -// ), -// ); -// } -// } diff --git a/lib/screens/video_player/components/video_playback_information.dart b/lib/screens/video_player/components/video_playback_information.dart index b38cce7..dbc74c5 100644 --- a/lib/screens/video_player/components/video_playback_information.dart +++ b/lib/screens/video_player/components/video_playback_information.dart @@ -1,12 +1,18 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; +import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/providers/session_info_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/util/clipboard_helper.dart'; +import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/wrappers/players/player_states.dart'; Future showVideoPlaybackInformation(BuildContext context) { return showDialog( @@ -23,6 +29,7 @@ class _VideoPlaybackInformation extends ConsumerWidget { final playbackModel = ref.watch(playBackModel); final sessionInfo = ref.watch(sessionInfoProvider); final backend = ref.read(videoPlayerProvider.select((value) => value.backend)); + final playbackState = ref.watch(videoPlayerProvider.select((value) => value.lastState)); return Dialog( child: Padding( padding: const EdgeInsets.all(12.0), @@ -42,12 +49,37 @@ class _VideoPlaybackInformation extends ConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('url: '), + const SizedBox(width: 8), + Flexible( + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: 3.0, + sigmaY: 3.0, + ), + child: Text( + playbackModel?.media?.url ?? "No url", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + IconButton.filled( + onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"), + icon: const Icon(IconsaxOutline.copy), + ) + ], ) ].addPadding(const EdgeInsets.symmetric(vertical: 3)), ), ), ), const Divider(), + if (playbackState != null) _PlayerInformation(state: playbackState), Text("Playback information", style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Padding( @@ -113,3 +145,54 @@ class _VideoPlaybackInformation extends ConsumerWidget { ); } } + +class _PlayerInformation extends StatelessWidget { + final PlayerState state; + const _PlayerInformation({ + required this.state, + }); + + @override + Widget build( + BuildContext context, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Player state", style: Theme.of(context).textTheme.titleMedium), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), + child: Opacity( + opacity: 0.80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('playing: '), Text(state.playing.toString())], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('buffering: '), Text(state.buffering.toString())], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('duration: '), Text(state.duration.humanize ?? "")], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('rate: '), Text(state.rate.toString())], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('volume: '), Text(state.volume.toString())], + ), + ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + ), + ), + ), + const Divider(), + ], + ); + } +} diff --git a/lib/util/clipboard_helper.dart b/lib/util/clipboard_helper.dart new file mode 100644 index 0000000..8238e45 --- /dev/null +++ b/lib/util/clipboard_helper.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:fladder/screens/shared/fladder_snackbar.dart'; +import 'package:fladder/util/localization_helper.dart'; + +extension ClipboardHelper on BuildContext { + Future copyToClipboard(String value, {String? customMessage}) async { + await Clipboard.setData(ClipboardData(text: value)); + if (mounted) { + fladderSnackbar( + this, + title: customMessage ?? localized.copiedToClipboard, + ); + } + } +} diff --git a/lib/util/item_base_model/item_base_model_extensions.dart b/lib/util/item_base_model/item_base_model_extensions.dart index 0c29c52..f189195 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,6 +20,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/syncing/sync_button.dart'; import 'package:fladder/screens/syncing/sync_item_details.dart'; +import 'package:fladder/util/clipboard_helper.dart'; import 'package:fladder/util/file_downloader.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -223,12 +223,7 @@ extension ItemBaseModelExtensions on ItemBaseModel { ), ItemActionButton( icon: const Icon(IconsaxOutline.link_21), - action: () async { - await Clipboard.setData(ClipboardData(text: downloadUrl)); - if (context.mounted) { - fladderSnackbar(context, title: "Copied URL to clipboard"); - } - }, + action: () => context.copyToClipboard(downloadUrl), label: Text(context.localized.copyStreamUrl), ) ],