|
| 1 | +import 'dart:io'; |
| 2 | +import 'dart:typed_data'; |
| 3 | + |
| 4 | +import 'package:file_picker/file_picker.dart'; |
1 | 5 | import 'package:flutter/material.dart'; |
2 | 6 | import 'package:flutter_bloc/flutter_bloc.dart'; |
| 7 | +import 'package:fpdart/fpdart.dart' show None, Some; |
3 | 8 | import 'package:go_router/go_router.dart'; |
4 | 9 | import 'package:smooth_sheets/smooth_sheets.dart'; |
5 | 10 | import 'package:tsdm_client/constants/layout.dart'; |
6 | 11 | import 'package:tsdm_client/extensions/build_context.dart'; |
| 12 | +import 'package:tsdm_client/extensions/string.dart'; |
7 | 13 | import 'package:tsdm_client/features/cache/bloc/image_cache_trigger_cubit.dart'; |
8 | 14 | import 'package:tsdm_client/i18n/strings.g.dart'; |
| 15 | +import 'package:tsdm_client/instance.dart'; |
9 | 16 | import 'package:tsdm_client/routes/screen_paths.dart'; |
| 17 | +import 'package:tsdm_client/shared/providers/image_cache_provider/image_cache_provider.dart'; |
| 18 | +import 'package:tsdm_client/shared/providers/image_cache_provider/models/models.dart'; |
10 | 19 | import 'package:tsdm_client/utils/clipboard.dart'; |
| 20 | +import 'package:tsdm_client/utils/platform.dart'; |
| 21 | +import 'package:tsdm_client/utils/show_toast.dart'; |
11 | 22 | import 'package:tsdm_client/widgets/network_indicator_image.dart'; |
12 | 23 |
|
13 | 24 | /// Show a bottom sheet with given [title] and build children |
@@ -150,6 +161,57 @@ Future<void> showImageActionBottomSheet({ |
150 | 161 | } |
151 | 162 | }, |
152 | 163 | ), |
| 164 | + ListTile( |
| 165 | + leading: const Icon(Icons.save_outlined), |
| 166 | + title: Text(tr.saveImage), |
| 167 | + onTap: () async { |
| 168 | + final imageProvider = getIt.get<ImageCacheProvider>(); |
| 169 | + switch (await imageProvider.getOrMakeCache(ImageCacheGeneralRequest(imageUrl))) { |
| 170 | + case None(): |
| 171 | + { |
| 172 | + if (context.mounted) { |
| 173 | + showSnackBar(context: context, message: tr.failedToSaveImage); |
| 174 | + } |
| 175 | + } |
| 176 | + case Some<Uint8List>(value: final data): |
| 177 | + { |
| 178 | + final fileName = |
| 179 | + imageUrl.tryParseAsUri()?.pathSegments.lastOrNull ?? |
| 180 | + 'tsdm_client_image_${DateTime.now().microsecondsSinceEpoch}.jpg'; |
| 181 | + if (isDesktop) { |
| 182 | + // On desktop platforms, `saveFiles` only return the selected path. |
| 183 | + final filePath = await FilePicker.platform.saveFile(dialogTitle: tr.saveImage, fileName: fileName); |
| 184 | + if (filePath == null) { |
| 185 | + return; |
| 186 | + } |
| 187 | + |
| 188 | + await File(filePath).writeAsBytes(data, flush: true); |
| 189 | + if (context.mounted) { |
| 190 | + context.pop(); |
| 191 | + showSnackBar( |
| 192 | + context: context, |
| 193 | + message: tr.imageSaved(filePath: filePath), |
| 194 | + ); |
| 195 | + } |
| 196 | + } else { |
| 197 | + // Mobile in one step. |
| 198 | + final filePath = await FilePicker.platform.saveFile( |
| 199 | + dialogTitle: tr.saveImage, |
| 200 | + fileName: fileName, |
| 201 | + bytes: data, |
| 202 | + ); |
| 203 | + if (filePath != null && context.mounted) { |
| 204 | + context.pop(); |
| 205 | + showSnackBar( |
| 206 | + context: context, |
| 207 | + message: tr.imageSaved(filePath: filePath), |
| 208 | + ); |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + }, |
| 214 | + ), |
153 | 215 | ], |
154 | 216 | ); |
155 | 217 | } |
0 commit comments