diff --git a/CHANGELOG.md b/CHANGELOG.md index a0863f1f..5888736d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.3 + +- Fix: Multiple UI issues +- Fix: Camera initialization issue + ## 0.3.2 - Fix: Multiple smaller performance and UI issues diff --git a/lib/main.dart b/lib/main.dart index b2ca02c2..b1114ec1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,7 +33,7 @@ import 'package:twonly/src/utils/startup_guard.dart'; final _initMutex = Mutex(); -/// This function is used to initialized the absolute minimum so it +/// This function is used to initialize the absolute minimum so it /// can also be used by the backend without the UI was loaded. Future twonlyMinimumInitialization() async { Log.info('twonlyMinimumInitialization: called'); diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index ba87a485..e0b9c585 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -314,11 +314,15 @@ class MediaFileService { await tempPath.copy(storedPath.path); if (userService.currentUser.storeMediaFilesInGallery) { if (mediaFile.type == MediaType.video) { - await saveVideoToGallery(storedPath.path); + await saveVideoToGallery( + storedPath.path, + name: mediaFile.mediaId, + ); } else { await saveImageToGallery( storedPath.readAsBytesSync(), createdAt: mediaFile.createdAt, + name: mediaFile.mediaId, ); } } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b11db174..0ddc1fbe 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -11,6 +11,7 @@ import 'package:gal/gal.dart'; import 'package:image/image.dart' as img; import 'package:intl/intl.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:path/path.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; @@ -36,6 +37,7 @@ extension ShortCutsExtension on BuildContext { Future saveImageToGallery( Uint8List imageBytes, { DateTime? createdAt, + String? name, }) async { var bytesToProcess = imageBytes; @@ -75,7 +77,7 @@ Future saveImageToGallery( await Gal.requestAccess(toAlbum: true); } try { - await Gal.putImageBytes(jpgImages, album: 'twonly'); + await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image'); return null; } on GalException catch (e) { Log.error(e); @@ -83,17 +85,45 @@ Future saveImageToGallery( } } -Future saveVideoToGallery(String videoPath) async { +Future saveVideoToGallery( + String videoPath, { + String? name, +}) async { final hasAccess = await Gal.hasAccess(toAlbum: true); if (!hasAccess) { await Gal.requestAccess(toAlbum: true); } + + var pathToSave = videoPath; + File? tempFile; + try { - await Gal.putVideo(videoPath, album: 'twonly'); + if (name != null) { + final file = File(videoPath); + final extension = file.path.split('.').last; + final tempDir = Directory.systemTemp; + tempFile = File(join(tempDir.path, '$name.$extension')); + if (tempFile.existsSync()) { + try { + tempFile.deleteSync(); + } catch (_) {} + } + file.copySync(tempFile.path); + pathToSave = tempFile.path; + } + await Gal.putVideo(pathToSave, album: 'twonly'); return null; } on GalException catch (e) { Log.error(e); return e.type.message; + } finally { + if (tempFile != null && tempFile.existsSync()) { + try { + tempFile.deleteSync(); + } catch (e) { + Log.error('Failed to delete temp video file: $e'); + } + } } } diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 195dc05f..2ed4830b 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -310,11 +310,18 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien if (item != null) { final media = item.mediaService; if (media.mediaFile.type == MediaType.video) { - await saveVideoToGallery(media.storedPath.path); + await saveVideoToGallery( + media.storedPath.path, + name: media.mediaFile.mediaId, + ); } else if (media.mediaFile.type == MediaType.image || media.mediaFile.type == MediaType.gif) { final imageBytes = await media.storedPath.readAsBytes(); - await saveImageToGallery(imageBytes, createdAt: media.mediaFile.createdAt); + await saveImageToGallery( + imageBytes, + createdAt: media.mediaFile.createdAt, + name: media.mediaFile.mediaId, + ); } } setProgress((i + 1) / selectedList.length); diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index 61ce30bb..52e108e1 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -193,11 +193,18 @@ class _SynchronizedImageViewerScreenState try { if (item.mediaFile.type == MediaType.video) { - await saveVideoToGallery(item.storedPath.path); + await saveVideoToGallery( + item.storedPath.path, + name: item.mediaFile.mediaId, + ); } else if (item.mediaFile.type == MediaType.image || item.mediaFile.type == MediaType.gif) { final imageBytes = await item.storedPath.readAsBytes(); - await saveImageToGallery(imageBytes, createdAt: item.mediaFile.createdAt); + await saveImageToGallery( + imageBytes, + createdAt: item.mediaFile.createdAt, + name: item.mediaFile.mediaId, + ); } if (!mounted) return; showSnackbar( diff --git a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart index d2d9b96d..7b7b6848 100644 --- a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/android_photo_picker.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart' show ShortCutsExtension, sha256File; import 'package:twonly/src/visual/components/selectable_thumbnail.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; @@ -84,8 +85,8 @@ class _ImportFromGalleryViewState extends State { final hash = Uint8List.fromList(sha256.convert(bytes).bytes); - final exsits = await twonlyDB.mediaFilesDao.getMediaByHash(hash); - if (exsits.isNotEmpty) { + final exists = await twonlyDB.mediaFilesDao.getMediaByHash(hash); + if (exists.isNotEmpty) { duplicated += 1; continue; } @@ -345,8 +346,8 @@ class _ImportFromGalleryViewState extends State { final hash = Uint8List.fromList(await sha256File(file)); - final exsits = await twonlyDB.mediaFilesDao.getMediaByHash(hash); - if (exsits.isNotEmpty) { + final exists = await twonlyDB.mediaFilesDao.getMediaByHash(hash); + if (exists.isNotEmpty) { duplicated += 1; continue; } @@ -363,14 +364,50 @@ class _ImportFromGalleryViewState extends State { type = MediaType.image; } - final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( - MediaFilesCompanion( - type: Value(type), - createdAt: Value(createdAt), - storedFileHash: Value(hash), - stored: const Value(true), - ), - ); + MediaFile? mediaFile; + var isRestored = false; + + if (Platform.isIOS) { + try { + final assetName = await asset.titleAsync; + final dotIndex = assetName.lastIndexOf('.'); + final baseName = dotIndex != -1 ? assetName.substring(0, dotIndex) : assetName; + + final uuidRegex = RegExp( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', + ); + if (uuidRegex.hasMatch(baseName)) { + final existing = await twonlyDB.mediaFilesDao.getMediaFileById(baseName); + if (existing != null) { + final mediaService = MediaFileService(existing); + if (!mediaService.storedPath.existsSync()) { + await twonlyDB.mediaFilesDao.updateMedia( + baseName, + MediaFilesCompanion( + storedFileHash: Value(hash), + stored: const Value(true), + ), + ); + mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(baseName); + isRestored = true; + } + } + } + } catch (e) { + Log.error('Error checking iOS asset UUID name: $e'); + } + } + + if (!isRestored) { + mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( + MediaFilesCompanion( + type: Value(type), + createdAt: Value(createdAt), + storedFileHash: Value(hash), + stored: const Value(true), + ), + ); + } if (mediaFile != null) { final mediaService = MediaFileService(mediaFile);