mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feature: More info playback state (#219)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
cf53f02d90
commit
f259151336
7 changed files with 112 additions and 186 deletions
|
|
@ -1167,5 +1167,6 @@
|
||||||
"tablet": "Tablet",
|
"tablet": "Tablet",
|
||||||
"desktop": "Desktop",
|
"desktop": "Desktop",
|
||||||
"layoutModeSingle": "Single",
|
"layoutModeSingle": "Single",
|
||||||
"layoutModeDual": "Dual"
|
"layoutModeDual": "Dual",
|
||||||
|
"copiedToClipboard": "Copied to clipboard"
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/error_log_model.dart';
|
import 'package:fladder/models/error_log_model.dart';
|
||||||
import 'package:fladder/providers/crash_log_provider.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/list_padding.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
|
|
@ -102,12 +101,7 @@ class CrashScreen extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () => context.copyToClipboard(e.clipBoard),
|
||||||
await Clipboard.setData(ClipboardData(text: e.clipBoard));
|
|
||||||
if (context.mounted) {
|
|
||||||
fladderSnackbar(context, title: "Copied to clipboard");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.copy_all_rounded),
|
icon: const Icon(Icons.copy_all_rounded),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/information_model.dart';
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/providers/items/information_provider.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/util/localization_helper.dart';
|
||||||
import 'package:fladder/widgets/shared/clickable_text.dart';
|
import 'package:fladder/widgets/shared/clickable_text.dart';
|
||||||
|
|
||||||
|
|
@ -79,12 +78,7 @@ class ItemInfoScreenState extends ConsumerState<ItemInfoScreen> {
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () => context.copyToClipboard(info.model.toString()),
|
||||||
await Clipboard.setData(ClipboardData(text: info.model.toString()));
|
|
||||||
if (context.mounted) {
|
|
||||||
fladderSnackbar(context, title: "Copied to clipboard");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.copy_all_rounded)),
|
icon: const Icon(Icons.copy_all_rounded)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -171,10 +165,7 @@ class ItemInfoScreenState extends ConsumerState<ItemInfoScreen> {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ClickableText(
|
child: ClickableText(
|
||||||
text: title,
|
text: title,
|
||||||
onTap: () async {
|
onTap: () => context.copyToClipboard(value),
|
||||||
await Clipboard.setData(ClipboardData(text: value));
|
|
||||||
fladderSnackbar(context, title: "Copied to clipboard");
|
|
||||||
},
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -211,10 +202,7 @@ class ItemInfoScreenState extends ConsumerState<ItemInfoScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () => context.copyToClipboard(InformationModel.mapToString(map)),
|
||||||
await Clipboard.setData(ClipboardData(text: InformationModel.mapToString(map)));
|
|
||||||
fladderSnackbar(context, title: "Copied to clipboard");
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.copy_all_rounded))
|
icon: const Icon(Icons.copy_all_rounded))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:chopper/chopper.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:chopper/chopper.dart';
|
||||||
|
|
||||||
void fladderSnackbar(
|
void fladderSnackbar(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
String title = "",
|
String title = "",
|
||||||
|
|
@ -34,156 +35,3 @@ void fladderSnackbarResponse(BuildContext context, Response? response, {String?
|
||||||
fladderSnackbar(context, title: altTitle);
|
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<Offset> _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<Offset>(
|
|
||||||
// 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,
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/playback/playback_model.dart';
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
import 'package:fladder/providers/session_info_provider.dart';
|
import 'package:fladder/providers/session_info_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_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/list_padding.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
|
|
@ -23,6 +29,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
||||||
final playbackModel = ref.watch(playBackModel);
|
final playbackModel = ref.watch(playBackModel);
|
||||||
final sessionInfo = ref.watch(sessionInfoProvider);
|
final sessionInfo = ref.watch(sessionInfoProvider);
|
||||||
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
|
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
|
||||||
|
final playbackState = ref.watch(videoPlayerProvider.select((value) => value.lastState));
|
||||||
return Dialog(
|
return Dialog(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
|
@ -42,12 +49,37 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)],
|
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)),
|
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
if (playbackState != null) _PlayerInformation(state: playbackState),
|
||||||
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Padding(
|
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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
17
lib/util/clipboard_helper.dart
Normal file
17
lib/util/clipboard_helper.dart
Normal file
|
|
@ -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<void> copyToClipboard(String value, {String? customMessage}) async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: value));
|
||||||
|
if (mounted) {
|
||||||
|
fladderSnackbar(
|
||||||
|
this,
|
||||||
|
title: customMessage ?? localized.copiedToClipboard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/shared/fladder_snackbar.dart';
|
||||||
import 'package:fladder/screens/syncing/sync_button.dart';
|
import 'package:fladder/screens/syncing/sync_button.dart';
|
||||||
import 'package:fladder/screens/syncing/sync_item_details.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/file_downloader.dart';
|
||||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
@ -223,12 +223,7 @@ extension ItemBaseModelExtensions on ItemBaseModel {
|
||||||
),
|
),
|
||||||
ItemActionButton(
|
ItemActionButton(
|
||||||
icon: const Icon(IconsaxOutline.link_21),
|
icon: const Icon(IconsaxOutline.link_21),
|
||||||
action: () async {
|
action: () => context.copyToClipboard(downloadUrl),
|
||||||
await Clipboard.setData(ClipboardData(text: downloadUrl));
|
|
||||||
if (context.mounted) {
|
|
||||||
fladderSnackbar(context, title: "Copied URL to clipboard");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: Text(context.localized.copyStreamUrl),
|
label: Text(context.localized.copyStreamUrl),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue