diff --git a/lib/screens/files/file_detail.dart b/lib/screens/files/file_detail.dart index 7f985d99..37e9c8c1 100644 --- a/lib/screens/files/file_detail.dart +++ b/lib/screens/files/file_detail.dart @@ -1,28 +1,19 @@ -import 'dart:io'; - -import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gal/gal.dart'; import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/drive/file_references.dart'; -import 'package:island/pods/network.dart'; -import 'package:island/pods/drive/upload_tasks.dart'; -import 'package:island/models/drive_task.dart'; +import 'package:island/services/file_download.dart'; import 'package:island/services/responsive.dart'; import 'package:island/services/time.dart'; -import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/file_info_sheet.dart'; import 'package:island/widgets/content/file_viewer_contents.dart'; import 'package:island/widgets/content/sheet.dart'; -import 'package:path/path.dart' show extension; -import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; class FileDetailScreen extends HookConsumerWidget { @@ -155,7 +146,7 @@ class FileDetailScreen extends HookConsumerWidget { actions.add( IconButton( icon: Icon(Icons.save_alt), - onPressed: () async => _saveToGallery(ref), + onPressed: () => FileDownloadService(ref).saveToGallery(item), ), ); } @@ -166,7 +157,8 @@ class FileDetailScreen extends HookConsumerWidget { actions.add( IconButton( icon: Icon(Icons.save_alt), - onPressed: () async => _downloadFile(ref), + onPressed: () => + FileDownloadService(ref).downloadWithProgress(item), ), ); } @@ -199,80 +191,6 @@ class FileDetailScreen extends HookConsumerWidget { return actions; } - Future _saveToGallery(WidgetRef ref) async { - try { - showSnackBar('Saving image...'); - - final client = ref.read(apiClientProvider); - final tempDir = await getTemporaryDirectory(); - var extName = extension(item.name).trim(); - if (extName.isEmpty) { - extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg'; - } - final filePath = '${tempDir.path}/${item.id}.$extName'; - - await client.download( - '/drive/files/${item.id}', - filePath, - queryParameters: {'original': true}, - ); - if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { - await Gal.putImage(filePath, album: 'Solar Network'); - showSnackBar('Image saved to gallery'); - } else { - await FileSaver.instance.saveFile( - name: item.name.isEmpty ? '${item.id}.$extName' : item.name, - file: File(filePath), - ); - showSnackBar('Image saved to $filePath'); - } - } catch (e) { - showErrorAlert(e); - } - } - - Future _downloadFile(WidgetRef ref) async { - final taskNotifier = ref.read(uploadTasksProvider.notifier); - final taskId = taskNotifier.addLocalDownloadTask(item); - try { - showSnackBar('Downloading file...'); - - final client = ref.read(apiClientProvider); - final tempDir = await getTemporaryDirectory(); - var extName = extension(item.name).trim(); - if (extName.isEmpty) { - extName = item.mimeType?.split('/').lastOrNull ?? 'bin'; - } - final filePath = '${tempDir.path}/${item.id}.$extName'; - - await client.download( - '/drive/files/${item.id}', - filePath, - queryParameters: {'original': true}, - onReceiveProgress: (count, total) { - if (total > 0) { - taskNotifier.updateDownloadProgress(taskId, count, total); - taskNotifier.updateTransmissionProgress(taskId, count / total); - } - }, - ); - - await FileSaver.instance.saveFile( - name: item.name.isEmpty ? '${item.id}.$extName' : item.name, - file: File(filePath), - ); - taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed); - showSnackBar('File saved to downloads'); - } catch (e) { - taskNotifier.updateTaskStatus( - taskId, - DriveTaskStatus.failed, - errorMessage: e.toString(), - ); - showErrorAlert(e); - } - } - Widget _buildContent(BuildContext context, WidgetRef ref, String serverUrl) { final uri = '$serverUrl/drive/files/${item.id}'; diff --git a/lib/services/file_download.dart b/lib/services/file_download.dart new file mode 100644 index 00000000..b77a8360 --- /dev/null +++ b/lib/services/file_download.dart @@ -0,0 +1,128 @@ +import 'dart:io'; + +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/foundation.dart'; +import 'package:gal/gal.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/drive_task.dart'; +import 'package:island/models/file.dart'; +import 'package:island/pods/drive/upload_tasks.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class FileDownloadService { + final WidgetRef ref; + + FileDownloadService(this.ref); + + String _getFileExtension(SnCloudFile item) { + var extName = p.extension(item.name).trim(); + if (extName.isEmpty) { + extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg'; + } + return extName.replaceFirst('.', ''); + } + + String _getFileName(SnCloudFile item, String extName) { + return item.name.isEmpty ? '${item.id}.$extName' : item.name; + } + + Future _downloadToTemp(SnCloudFile item, String extName) async { + final client = ref.read(apiClientProvider); + final tempDir = await getTemporaryDirectory(); + final filePath = '${tempDir.path}/${item.id}.$extName'; + + await client.download( + '/drive/files/${item.id}', + filePath, + queryParameters: {'original': true}, + ); + + return filePath; + } + + Future saveToGallery(SnCloudFile item) async { + try { + showSnackBar('Saving image...'); + + final extName = _getFileExtension(item); + final filePath = await _downloadToTemp(item, extName); + + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + await Gal.putImage(filePath, album: 'Solar Network'); + showSnackBar('Image saved to gallery'); + } else { + await FileSaver.instance.saveFile( + name: _getFileName(item, extName), + file: File(filePath), + ); + showSnackBar('Image saved to downloads'); + } + } catch (e) { + showErrorAlert(e); + } + } + + Future downloadFile(SnCloudFile item) async { + try { + showSnackBar('Downloading file...'); + + final extName = _getFileExtension(item); + final filePath = await _downloadToTemp(item, extName); + + await FileSaver.instance.saveFile( + name: _getFileName(item, extName), + file: File(filePath), + ); + showSnackBar('File saved to downloads'); + } catch (e) { + showErrorAlert(e); + } + } + + Future downloadWithProgress( + SnCloudFile item, { + void Function(int received, int total)? onProgress, + }) async { + final taskNotifier = ref.read(uploadTasksProvider.notifier); + final taskId = taskNotifier.addLocalDownloadTask(item); + + try { + showSnackBar('Downloading file...'); + + final client = ref.read(apiClientProvider); + final extName = _getFileExtension(item); + final tempDir = await getTemporaryDirectory(); + final filePath = '${tempDir.path}/${item.id}.$extName'; + + await client.download( + '/drive/files/${item.id}', + filePath, + queryParameters: {'original': true}, + onReceiveProgress: (count, total) { + onProgress?.call(count, total); + if (total > 0) { + taskNotifier.updateDownloadProgress(taskId, count, total); + taskNotifier.updateTransmissionProgress(taskId, count / total); + } + }, + ); + + await FileSaver.instance.saveFile( + name: _getFileName(item, extName), + file: File(filePath), + ); + taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed); + showSnackBar('File saved to downloads'); + } catch (e) { + taskNotifier.updateTaskStatus( + taskId, + DriveTaskStatus.failed, + errorMessage: e.toString(), + ); + showErrorAlert(e); + } + } +} diff --git a/lib/widgets/content/cloud_file_lightbox.dart b/lib/widgets/content/cloud_file_lightbox.dart index 789c53ca..c7deb371 100644 --- a/lib/widgets/content/cloud_file_lightbox.dart +++ b/lib/widgets/content/cloud_file_lightbox.dart @@ -1,23 +1,16 @@ -import 'dart:io'; -import 'dart:math' as math; - import 'package:dismissible_page/dismissible_page.dart'; -import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gal/gal.dart'; -import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; -import 'package:island/pods/network.dart'; -import 'package:island/widgets/alert.dart'; +import 'package:island/services/file_download.dart'; +import 'package:island/widgets/content/file_action_button.dart'; import 'package:island/widgets/content/file_info_sheet.dart'; +import 'package:island/widgets/content/image_control_overlay.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:path/path.dart' show extension; -import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'cloud_files.dart'; @@ -39,42 +32,8 @@ class CloudFileLightbox extends HookConsumerWidget { final showOriginal = useState(false); - Future saveToGallery() async { - try { - // Show loading indicator - showSnackBar('Saving image...'); - - // Get the image URL - final client = ref.watch(apiClientProvider); - - // Create a temporary file to save the image - final tempDir = await getTemporaryDirectory(); - var extName = extension(item.name).trim(); - if (extName.isEmpty) { - extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg'; - } - final filePath = '${tempDir.path}/${item.id}.$extName'; - - await client.download( - '/drive/files/${item.id}', - filePath, - queryParameters: {'original': true}, - ); - if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { - // Save to gallery - await Gal.putImage(filePath, album: 'Solar Network'); - // Show success message - showSnackBar('Image saved to gallery'); - } else { - await FileSaver.instance.saveFile( - name: item.name.isEmpty ? '${item.id}.$extName' : item.name, - file: File(filePath), - ); - showSnackBar('Image saved to $filePath'); - } - } catch (e) { - showErrorAlert(e); - } + void saveToGallery() { + FileDownloadService(ref).saveToGallery(item); } void showInfoSheet() { @@ -86,10 +45,6 @@ class CloudFileLightbox extends HookConsumerWidget { ); } - final shadow = [ - Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)), - ]; - return DismissiblePage( isFullScreen: true, backgroundColor: Colors.transparent, @@ -128,15 +83,9 @@ class CloudFileLightbox extends HookConsumerWidget { Row( children: [ if (!kIsWeb) - IconButton( - icon: Icon( - Icons.save_alt, - color: Colors.white, - shadows: shadow, - ), - onPressed: () async { - saveToGallery(); - }, + FileActionButton.save( + onPressed: saveToGallery, + shadows: WhiteShadows.standard, ), IconButton( onPressed: () { @@ -145,103 +94,46 @@ class CloudFileLightbox extends HookConsumerWidget { icon: Icon( showOriginal.value ? Symbols.hd : Symbols.sd, color: Colors.white, - shadows: shadow, + shadows: WhiteShadows.standard, ), ), ], ), - IconButton( - icon: Icon(Icons.close, color: Colors.white, shadows: shadow), + FileActionButton.close( onPressed: () => Navigator.of(context).pop(), + shadows: WhiteShadows.standard, ), ], ), ), - // Rotation controls - Positioned( - bottom: MediaQuery.of(context).padding.bottom + 16, - left: 16, - right: 16, - child: Row( - children: [ - IconButton( - icon: Icon( - Icons.info_outline, - color: Colors.white, - shadows: shadow, - ), - onPressed: showInfoSheet, - ), - IconButton( - onPressed: () { - final router = GoRouter.of(context); - Navigator.of(context).pop(context); - Future(() { - router.pushNamed( - 'fileDetail', - pathParameters: {'id': item.id}, - extra: item, - ); - }); - }, - icon: Icon( - Icons.more_horiz, - color: Colors.white, - shadows: shadow, - ), - ), - Spacer(), - IconButton( - icon: Icon( - Icons.remove, - color: Colors.white, - shadows: shadow, - ), - onPressed: () { - photoViewController.scale = - (photoViewController.scale ?? 1) - 0.05; - }, - ), - IconButton( - icon: Icon(Icons.add, color: Colors.white, shadows: shadow), - onPressed: () { - photoViewController.scale = - (photoViewController.scale ?? 1) + 0.05; - }, - ), - const Gap(8), - IconButton( - icon: Icon( - Icons.rotate_left, - color: Colors.white, - shadows: shadow, - ), - onPressed: () { - rotation.value = (rotation.value - 1) % 4; - photoViewController.rotation = - rotation.value * -math.pi / 2; - }, - ), - IconButton( - icon: Icon( - Icons.rotate_right, - color: Colors.white, - shadows: [ - Shadow( - color: Colors.black54, - blurRadius: 5.0, - offset: Offset(1.0, 1.0), - ), - ], - ), - onPressed: () { - rotation.value = (rotation.value + 1) % 4; - photoViewController.rotation = - rotation.value * -math.pi / 2; - }, - ), - ], - ), + ImageControlOverlay( + photoViewController: photoViewController, + rotation: rotation, + showOriginal: showOriginal.value, + onToggleQuality: () { + showOriginal.value = !showOriginal.value; + }, + extraButtons: [ + FileActionButton.info( + onPressed: showInfoSheet, + shadows: WhiteShadows.standard, + ), + FileActionButton.more( + onPressed: () { + final router = GoRouter.of(context); + Navigator.of(context).pop(context); + Future(() { + router.pushNamed( + 'fileDetail', + pathParameters: {'id': item.id}, + extra: item, + ); + }); + }, + shadows: WhiteShadows.standard, + ), + ], + showExtraOnLeft: true, ), ], ), diff --git a/lib/widgets/content/file_action_button.dart b/lib/widgets/content/file_action_button.dart new file mode 100644 index 00000000..e07812c6 --- /dev/null +++ b/lib/widgets/content/file_action_button.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +enum FileActionType { save, info, more, close, custom } + +class FileActionButton extends StatelessWidget { + final FileActionType type; + final IconData? icon; + final VoidCallback onPressed; + final Color? color; + final List? shadows; + final String? tooltip; + + const FileActionButton({ + super.key, + required this.type, + required this.onPressed, + this.icon, + this.color, + this.shadows, + this.tooltip, + }); + + factory FileActionButton.save({ + Key? key, + required VoidCallback onPressed, + Color? color, + List? shadows, + }) { + return FileActionButton( + key: key, + type: FileActionType.save, + icon: Icons.save_alt, + onPressed: onPressed, + color: color ?? Colors.white, + shadows: shadows, + ); + } + + factory FileActionButton.info({ + Key? key, + required VoidCallback onPressed, + Color? color, + List? shadows, + }) { + return FileActionButton( + key: key, + type: FileActionType.info, + icon: Icons.info_outline, + onPressed: onPressed, + color: color ?? Colors.white, + shadows: shadows, + ); + } + + factory FileActionButton.more({ + Key? key, + required VoidCallback onPressed, + Color? color, + List? shadows, + }) { + return FileActionButton( + key: key, + type: FileActionType.more, + icon: Icons.more_horiz, + onPressed: onPressed, + color: color ?? Colors.white, + shadows: shadows, + ); + } + + factory FileActionButton.close({ + Key? key, + required VoidCallback onPressed, + Color? color, + List? shadows, + }) { + return FileActionButton( + key: key, + type: FileActionType.close, + icon: Icons.close, + onPressed: onPressed, + color: color ?? Colors.white, + shadows: shadows, + ); + } + + @override + Widget build(BuildContext context) { + final buttonIcon = icon ?? Icons.circle; + + final button = IconButton( + icon: Icon(buttonIcon, color: color, shadows: shadows), + onPressed: onPressed, + ); + + if (tooltip != null) { + return Tooltip(message: tooltip!, child: button); + } + + return button; + } +} + +class WhiteShadows { + static List get standard => [ + Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)), + ]; +} diff --git a/lib/widgets/content/file_viewer_contents.dart b/lib/widgets/content/file_viewer_contents.dart index bab95851..1507bccb 100644 --- a/lib/widgets/content/file_viewer_contents.dart +++ b/lib/widgets/content/file_viewer_contents.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; -import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -11,15 +10,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; +import 'package:island/services/file_download.dart'; import 'package:island/utils/format.dart'; -import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/audio.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/file_info_sheet.dart'; +import 'package:island/widgets/content/image_control_overlay.dart'; import 'package:island/widgets/content/video.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:path/path.dart' show extension; -import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; @@ -140,10 +138,6 @@ class ImageFileContent extends HookConsumerWidget { final rotation = useState(0); final showOriginal = useState(false); - final shadow = [ - Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)), - ]; - return Stack( children: [ Positioned.fill( @@ -156,8 +150,9 @@ class ImageFileContent extends HookConsumerWidget { if (delta != null && delta != 0) { final currentScale = photoViewController.scale ?? 1.0; // Adjust scale based on scroll direction (invert for natural zoom) - final newScale = - delta > 0 ? currentScale * 0.9 : currentScale * 1.1; + final newScale = delta > 0 + ? currentScale * 0.9 + : currentScale * 1.1; // Clamp scale to reasonable bounds final clampedScale = newScale.clamp(0.1, 10.0); photoViewController.scale = clampedScale; @@ -182,63 +177,13 @@ class ImageFileContent extends HookConsumerWidget { ), ), ), - // Controls overlay - Positioned( - bottom: MediaQuery.of(context).padding.bottom + 16, - left: 16, - right: 16, - child: Row( - children: [ - IconButton( - icon: Icon(Icons.remove, color: Colors.white, shadows: shadow), - onPressed: () { - photoViewController.scale = - (photoViewController.scale ?? 1) - 0.05; - }, - ), - IconButton( - icon: Icon(Icons.add, color: Colors.white, shadows: shadow), - onPressed: () { - photoViewController.scale = - (photoViewController.scale ?? 1) + 0.05; - }, - ), - const Gap(8), - IconButton( - icon: Icon( - Icons.rotate_left, - color: Colors.white, - shadows: shadow, - ), - onPressed: () { - rotation.value = (rotation.value - 1) % 4; - photoViewController.rotation = rotation.value * -math.pi / 2; - }, - ), - IconButton( - icon: Icon( - Icons.rotate_right, - color: Colors.white, - shadows: shadow, - ), - onPressed: () { - rotation.value = (rotation.value + 1) % 4; - photoViewController.rotation = rotation.value * -math.pi / 2; - }, - ), - const Spacer(), - IconButton( - onPressed: () { - showOriginal.value = !showOriginal.value; - }, - icon: Icon( - showOriginal.value ? Symbols.hd : Symbols.sd, - color: Colors.white, - shadows: shadow, - ), - ), - ], - ), + ImageControlOverlay( + photoViewController: photoViewController, + rotation: rotation, + showOriginal: showOriginal.value, + onToggleQuality: () { + showOriginal.value = !showOriginal.value; + }, ), ], ); @@ -253,10 +198,9 @@ class VideoFileContent extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var ratio = - item.fileMeta?['ratio'] is num - ? item.fileMeta!['ratio'].toDouble() - : 1.0; + var ratio = item.fileMeta?['ratio'] is num + ? item.fileMeta!['ratio'].toDouble() + : 1.0; if (ratio == 0) ratio = 1.0; return Center( @@ -294,34 +238,6 @@ class GenericFileContent extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - Future downloadFile() async { - try { - showSnackBar('Downloading file...'); - - final client = ref.read(apiClientProvider); - final tempDir = await getTemporaryDirectory(); - var extName = extension(item.name).trim(); - if (extName.isEmpty) { - extName = item.mimeType?.split('/').lastOrNull ?? 'bin'; - } - final filePath = '${tempDir.path}/${item.id}.$extName'; - - await client.download( - '/drive/files/${item.id}', - filePath, - queryParameters: {'original': true}, - ); - - await FileSaver.instance.saveFile( - name: item.name.isEmpty ? '${item.id}.$extName' : item.name, - file: File(filePath), - ); - showSnackBar('File saved to downloads'); - } catch (e) { - showErrorAlert(e); - } - } - return Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -354,7 +270,7 @@ class GenericFileContent extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ FilledButton.icon( - onPressed: downloadFile, + onPressed: () => FileDownloadService(ref).downloadFile(item), icon: const Icon(Symbols.download), label: Text('download').tr(), ), diff --git a/lib/widgets/content/image_control_overlay.dart b/lib/widgets/content/image_control_overlay.dart new file mode 100644 index 00000000..e4417c6e --- /dev/null +++ b/lib/widgets/content/image_control_overlay.dart @@ -0,0 +1,86 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class ImageControlOverlay extends HookWidget { + final PhotoViewController photoViewController; + final ValueNotifier rotation; + final bool showOriginal; + final VoidCallback onToggleQuality; + final List? extraButtons; + final bool showExtraOnLeft; + + const ImageControlOverlay({ + super.key, + required this.photoViewController, + required this.rotation, + required this.showOriginal, + required this.onToggleQuality, + this.extraButtons, + this.showExtraOnLeft = false, + }); + + @override + Widget build(BuildContext context) { + final shadow = [ + Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)), + ]; + + final controlButtons = [ + IconButton( + icon: Icon(Icons.remove, color: Colors.white, shadows: shadow), + onPressed: () { + photoViewController.scale = (photoViewController.scale ?? 1) - 0.05; + }, + ), + IconButton( + icon: Icon(Icons.add, color: Colors.white, shadows: shadow), + onPressed: () { + photoViewController.scale = (photoViewController.scale ?? 1) + 0.05; + }, + ), + const Gap(8), + IconButton( + icon: Icon(Icons.rotate_left, color: Colors.white, shadows: shadow), + onPressed: () { + rotation.value = (rotation.value - 1) % 4; + photoViewController.rotation = rotation.value * -math.pi / 2; + }, + ), + IconButton( + icon: Icon(Icons.rotate_right, color: Colors.white, shadows: shadow), + onPressed: () { + rotation.value = (rotation.value + 1) % 4; + photoViewController.rotation = rotation.value * -math.pi / 2; + }, + ), + ]; + + return Positioned( + bottom: MediaQuery.of(context).padding.bottom + 16, + left: 16, + right: 16, + child: Row( + children: showExtraOnLeft + ? [...?extraButtons, const Spacer(), ...controlButtons] + : [ + ...controlButtons, + const Spacer(), + IconButton( + onPressed: onToggleQuality, + icon: Icon( + showOriginal ? Symbols.hd : Symbols.sd, + color: Colors.white, + shadows: shadow, + ), + ), + ...?extraButtons, + ], + ), + ); + } +}