♻️ Improve file viewing experience
This commit is contained in:
@@ -1,28 +1,19 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/drive/file_references.dart';
|
import 'package:island/pods/drive/file_references.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/services/file_download.dart';
|
||||||
import 'package:island/pods/drive/upload_tasks.dart';
|
|
||||||
import 'package:island/models/drive_task.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||||
import 'package:island/widgets/content/file_viewer_contents.dart';
|
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||||
import 'package:island/widgets/content/sheet.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';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class FileDetailScreen extends HookConsumerWidget {
|
class FileDetailScreen extends HookConsumerWidget {
|
||||||
@@ -155,7 +146,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.save_alt),
|
icon: Icon(Icons.save_alt),
|
||||||
onPressed: () async => _saveToGallery(ref),
|
onPressed: () => FileDownloadService(ref).saveToGallery(item),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -166,7 +157,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.save_alt),
|
icon: Icon(Icons.save_alt),
|
||||||
onPressed: () async => _downloadFile(ref),
|
onPressed: () =>
|
||||||
|
FileDownloadService(ref).downloadWithProgress(item),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,80 +191,6 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _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<void> _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) {
|
Widget _buildContent(BuildContext context, WidgetRef ref, String serverUrl) {
|
||||||
final uri = '$serverUrl/drive/files/${item.id}';
|
final uri = '$serverUrl/drive/files/${item.id}';
|
||||||
|
|
||||||
|
|||||||
128
lib/services/file_download.dart
Normal file
128
lib/services/file_download.dart
Normal file
@@ -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<String> _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<void> 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<void> 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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/services/file_download.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/content/file_action_button.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.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: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:photo_view/photo_view.dart';
|
||||||
|
|
||||||
import 'cloud_files.dart';
|
import 'cloud_files.dart';
|
||||||
@@ -39,42 +32,8 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
|
|
||||||
final showOriginal = useState(false);
|
final showOriginal = useState(false);
|
||||||
|
|
||||||
Future<void> saveToGallery() async {
|
void saveToGallery() {
|
||||||
try {
|
FileDownloadService(ref).saveToGallery(item);
|
||||||
// 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 showInfoSheet() {
|
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(
|
return DismissiblePage(
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -128,15 +83,9 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
IconButton(
|
FileActionButton.save(
|
||||||
icon: Icon(
|
onPressed: saveToGallery,
|
||||||
Icons.save_alt,
|
shadows: WhiteShadows.standard,
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
saveToGallery();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -145,34 +94,31 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
showOriginal.value ? Symbols.hd : Symbols.sd,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: shadow,
|
shadows: WhiteShadows.standard,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
FileActionButton.close(
|
||||||
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
shadows: WhiteShadows.standard,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Rotation controls
|
ImageControlOverlay(
|
||||||
Positioned(
|
photoViewController: photoViewController,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
rotation: rotation,
|
||||||
left: 16,
|
showOriginal: showOriginal.value,
|
||||||
right: 16,
|
onToggleQuality: () {
|
||||||
child: Row(
|
showOriginal.value = !showOriginal.value;
|
||||||
children: [
|
},
|
||||||
IconButton(
|
extraButtons: [
|
||||||
icon: Icon(
|
FileActionButton.info(
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: showInfoSheet,
|
onPressed: showInfoSheet,
|
||||||
|
shadows: WhiteShadows.standard,
|
||||||
),
|
),
|
||||||
IconButton(
|
FileActionButton.more(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
Navigator.of(context).pop(context);
|
Navigator.of(context).pop(context);
|
||||||
@@ -184,64 +130,10 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(
|
shadows: WhiteShadows.standard,
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
showExtraOnLeft: true,
|
||||||
onPressed: () {
|
|
||||||
rotation.value = (rotation.value + 1) % 4;
|
|
||||||
photoViewController.rotation =
|
|
||||||
rotation.value * -math.pi / 2;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
108
lib/widgets/content/file_action_button.dart
Normal file
108
lib/widgets/content/file_action_button.dart
Normal file
@@ -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<Shadow>? 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<Shadow>? 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<Shadow>? 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<Shadow>? 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<Shadow>? 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<Shadow> get standard => [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/services/file_download.dart';
|
||||||
import 'package:island/utils/format.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/audio.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.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:island/widgets/content/video.dart';
|
||||||
import 'package:material_symbols_icons/symbols.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:photo_view/photo_view.dart';
|
||||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||||
|
|
||||||
@@ -140,10 +138,6 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
final rotation = useState(0);
|
final rotation = useState(0);
|
||||||
final showOriginal = useState(false);
|
final showOriginal = useState(false);
|
||||||
|
|
||||||
final shadow = [
|
|
||||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -156,8 +150,9 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
if (delta != null && delta != 0) {
|
if (delta != null && delta != 0) {
|
||||||
final currentScale = photoViewController.scale ?? 1.0;
|
final currentScale = photoViewController.scale ?? 1.0;
|
||||||
// Adjust scale based on scroll direction (invert for natural zoom)
|
// Adjust scale based on scroll direction (invert for natural zoom)
|
||||||
final newScale =
|
final newScale = delta > 0
|
||||||
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
|
? currentScale * 0.9
|
||||||
|
: currentScale * 1.1;
|
||||||
// Clamp scale to reasonable bounds
|
// Clamp scale to reasonable bounds
|
||||||
final clampedScale = newScale.clamp(0.1, 10.0);
|
final clampedScale = newScale.clamp(0.1, 10.0);
|
||||||
photoViewController.scale = clampedScale;
|
photoViewController.scale = clampedScale;
|
||||||
@@ -182,63 +177,13 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Controls overlay
|
ImageControlOverlay(
|
||||||
Positioned(
|
photoViewController: photoViewController,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
rotation: rotation,
|
||||||
left: 16,
|
showOriginal: showOriginal.value,
|
||||||
right: 16,
|
onToggleQuality: () {
|
||||||
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;
|
showOriginal.value = !showOriginal.value;
|
||||||
},
|
},
|
||||||
icon: Icon(
|
|
||||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -253,8 +198,7 @@ class VideoFileContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var ratio =
|
var ratio = item.fileMeta?['ratio'] is num
|
||||||
item.fileMeta?['ratio'] is num
|
|
||||||
? item.fileMeta!['ratio'].toDouble()
|
? item.fileMeta!['ratio'].toDouble()
|
||||||
: 1.0;
|
: 1.0;
|
||||||
if (ratio == 0) ratio = 1.0;
|
if (ratio == 0) ratio = 1.0;
|
||||||
@@ -294,34 +238,6 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
Future<void> 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(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -354,7 +270,7 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: downloadFile,
|
onPressed: () => FileDownloadService(ref).downloadFile(item),
|
||||||
icon: const Icon(Symbols.download),
|
icon: const Icon(Symbols.download),
|
||||||
label: Text('download').tr(),
|
label: Text('download').tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
86
lib/widgets/content/image_control_overlay.dart
Normal file
86
lib/widgets/content/image_control_overlay.dart
Normal file
@@ -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<int> rotation;
|
||||||
|
final bool showOriginal;
|
||||||
|
final VoidCallback onToggleQuality;
|
||||||
|
final List<Widget>? 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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user