diff --git a/CHANGELOG.md b/CHANGELOG.md index 811b901..a9d3887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.0.86 + +- Allows to reopen send images (if send without time limit or enabled auth) +- Added support for front camera zoom +- Several bug fixes + ## 0.0.83 - Improved view of the diagnostic log diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 31210bc..2a6f815 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -508,6 +508,12 @@ abstract class AppLocalizations { /// **'Unpin'** String get contextMenuUnpin; + /// No description provided for @contextMenuViewAgain. + /// + /// In en, this message translates to: + /// **'View again'** + String get contextMenuViewAgain; + /// No description provided for @mediaViewerAuthReason. /// /// In en, this message translates to: @@ -2943,6 +2949,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Currently, group size is limited to {size} people!'** String groupSizeLimitError(Object size); + + /// No description provided for @authRequestReopenImage. + /// + /// In en, this message translates to: + /// **'You must authenticate to reopen the image.'** + String get authRequestReopenImage; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2543dd9..c328e26 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -234,6 +234,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contextMenuUnpin => 'Lösen'; + @override + String get contextMenuViewAgain => 'Nochmal anschauen'; + @override String get mediaViewerAuthReason => 'Bitte authentifiziere dich, um diesen twonly zu sehen!'; @@ -1642,4 +1645,8 @@ class AppLocalizationsDe extends AppLocalizations { String groupSizeLimitError(Object size) { return 'Derzeit ist die Gruppengröße auf $size Personen begrenzt!'; } + + @override + String get authRequestReopenImage => + 'Um das Bild erneut zu öffnen, musst du dich authentifizieren.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 42fe67c..13f17c7 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -232,6 +232,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contextMenuUnpin => 'Unpin'; + @override + String get contextMenuViewAgain => 'View again'; + @override String get mediaViewerAuthReason => 'Please authenticate to see this twonly!'; @@ -1630,4 +1633,8 @@ class AppLocalizationsEn extends AppLocalizations { String groupSizeLimitError(Object size) { return 'Currently, group size is limited to $size people!'; } + + @override + String get authRequestReopenImage => + 'You must authenticate to reopen the image.'; } diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index 46d4d9a..7b15afc 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -232,6 +232,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contextMenuUnpin => 'Unpin'; + @override + String get contextMenuViewAgain => 'View again'; + @override String get mediaViewerAuthReason => 'Please authenticate to see this twonly!'; @@ -1630,4 +1633,8 @@ class AppLocalizationsSv extends AppLocalizations { String groupSizeLimitError(Object size) { return 'Currently, group size is limited to $size people!'; } + + @override + String get authRequestReopenImage => + 'You must authenticate to reopen the image.'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index c1dc14a..20f3c2f 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit c1dc14a53ff2854389a50f4aaeb26946ff925367 +Subproject commit 20f3c2f0a49e4c9be452ecbc84d98054c92974e1 diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 22952cb..2901006 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -59,6 +59,7 @@ class MediaFileService { } else if (service.mediaFile.requiresAuthentication || service.mediaFile.displayLimitInMilliseconds != null) { // Message was opened by all persons, and they can not reopen the image. + // This branch will prevent to reach the next if condition, with would otherwise store the image for two days // delete = true; // do not overwrite a previous delete = false // this is just to make it easier to understand :) } else if (message.openedAt! diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 9600cd5..dadb1fd 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -436,7 +436,7 @@ class _CameraPreviewViewState extends State { CameraLensDirection.front; Future onPanUpdate(dynamic details) async { - if (isFront || details == null) { + if (details == null) { return; } if (mc.cameraController == null || @@ -603,9 +603,6 @@ class _CameraPreviewViewState extends State { bottomNavigation: Container(), child: GestureDetector( onPanStart: (details) async { - if (isFront) { - return; - } setState(() { _basePanY = details.localPosition.dy; _baseScaleFactor = mc.selectedCameraDetails.scaleFactor; @@ -721,12 +718,11 @@ class _CameraPreviewViewState extends State { children: [ if (mc.cameraController!.value.isInitialized && mc.selectedCameraDetails.isZoomAble && - !isFront && !_isVideoRecording) SizedBox( width: 120, child: CameraZoomButtons( - key: widget.key, + key: mc.zoomButtonKey, scaleFactor: mc.selectedCameraDetails.scaleFactor, updateScaleFactor: updateScaleFactor, selectCamera: mc.selectCamera, diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index c6543a7..a67d2fa 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -44,6 +44,7 @@ class MainCameraController { Map contactsVerified = {}; Map scannedNewProfiles = {}; String? scannedUrl; + GlobalKey zoomButtonKey = GlobalKey(); Future closeCamera() async { contactsVerified = {}; @@ -76,6 +77,7 @@ class MainCameraController { CameraLensDirection.back) { await cameraController?.startImageStream(_processCameraImage); } + zoomButtonKey = GlobalKey(); setState(); return cameraController; } @@ -89,10 +91,11 @@ class MainCameraController { try { await cameraController!.stopImageStream(); } catch (e) { - Log.warn(e); + // Log.warn(e); } - await cameraController!.dispose(); + final tmp = cameraController; cameraController = null; + await tmp!.dispose(); await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 2b20fae..85c57d4 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,19 +1,23 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ - required this.storeImageAsOriginal, required this.isLoading, required this.displayButtonLabel, required this.mediaService, + this.storeImageAsOriginal, super.key, }); - final Future Function() storeImageAsOriginal; + final Future Function()? storeImageAsOriginal; final bool displayButtonLabel; final MediaFileService mediaService; final bool isLoading; @@ -44,8 +48,32 @@ class SaveToGalleryButtonState extends State { _imageSaving = true; }); - await widget.storeImageAsOriginal(); - await widget.mediaService.storeMediaFile(); + if (widget.storeImageAsOriginal != null) { + await widget.storeImageAsOriginal!(); + } + + final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + type: Value(widget.mediaService.mediaFile.type), + createdAt: Value(clock.now()), + stored: const Value(true), + ), + ); + + if (newMediaFile != null) { + final newService = MediaFileService(newMediaFile); + + if (widget.mediaService.tempPath.existsSync()) { + widget.mediaService.tempPath.copySync( + newService.tempPath.path, + ); + } else if (widget.mediaService.originalPath.existsSync()) { + widget.mediaService.originalPath.copySync( + newService.originalPath.path, + ); + } + await newService.storeMediaFile(); + } setState(() { _imageSaved = true; diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index 058618e..e3f7032 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -62,8 +62,16 @@ class _CameraZoomButtonsState extends State { _wideCameraIndex = index; } - if (!showWideAngleZoom && Platform.isIOS && _wideCameraIndex != null) { + final isFront = widget.controller.description.lensDirection == + CameraLensDirection.front; + + if (!showWideAngleZoom && + Platform.isIOS && + _wideCameraIndex != null && + !isFront) { showWideAngleZoomIOS = true; + } else { + showWideAngleZoomIOS = false; } if (_isDisposed) return; setState(() {}); diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 1eed51c..28e3cb8 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -487,20 +486,6 @@ class _ShareImageEditorView extends State { } } - // In case the image was already stored, then rename the stored image. - if (mediaService.storedPath.existsSync()) { - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - type: Value(mediaService.mediaFile.type), - createdAt: Value(clock.now()), - stored: const Value(true), - ), - ); - if (mediaFile != null) { - mediaService.storedPath - .renameSync(MediaFileService(mediaFile).storedPath.path); - } - } return bytes; } diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 5f146e9..44e51e3 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -185,6 +185,7 @@ class _ChatListEntryState extends State { group: widget.group, onResponseTriggered: widget.onResponseTriggered!, galleryItems: widget.galleryItems, + mediaFileService: mediaService, child: Container( child: child, ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 5226345..62a9456 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -12,12 +12,14 @@ import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' as pb; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/context_menu.component.dart'; +import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ @@ -26,16 +28,55 @@ class MessageContextMenu extends StatelessWidget { required this.child, required this.onResponseTriggered, required this.galleryItems, + required this.mediaFileService, super.key, }); final Group group; final Widget child; final Message message; final List galleryItems; + final MediaFileService? mediaFileService; final VoidCallback onResponseTriggered; + Future reopenMediaFile(BuildContext context) async { + final isAuth = await authenticateUser( + context.lang.authRequestReopenImage, + force: false, + ); + + if (isAuth && context.mounted && mediaFileService != null) { + final galleryItems = [ + MemoryItem(mediaService: mediaFileService!, messages: []), + ]; + + await Navigator.push( + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( + galleryItems: galleryItems, + ), + ), + ); + } + } + @override Widget build(BuildContext context) { + var canBeOpenedAgain = false; + // in case this is a media send from this user... + if (mediaFileService != null && message.senderId == null) { + // and the media was send with unlimited display limit time and without auth required... + if (!mediaFileService!.mediaFile.requiresAuthentication && + mediaFileService!.mediaFile.displayLimitInMilliseconds == null) { + // and the temp media file still exists + if (mediaFileService!.tempPath.existsSync()) { + // the media file can be opened again... + canBeOpenedAgain = true; + } + } + } + return ContextMenu( items: [ if (!message.isDeletedFromSender) @@ -70,6 +111,12 @@ class MessageContextMenu extends StatelessWidget { }, icon: FontAwesomeIcons.faceLaugh, ), + if (canBeOpenedAgain) + ContextMenuItem( + title: context.lang.contextMenuViewAgain, + onTap: () => reopenMediaFile(context), + icon: FontAwesomeIcons.clockRotateLeft, + ), if (!message.isDeletedFromSender) ContextMenuItem( title: context.lang.reply, diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 13cea18..b0cfaf5 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; @@ -92,8 +93,36 @@ class _MemoriesPhotoSliderViewState extends State { } } + Future shareMediaFile() async { + final orgMediaService = widget.galleryItems[currentIndex].mediaService; + + final newMediaService = await initializeMediaUpload( + orgMediaService.mediaFile.type, + gUser.defaultShowTime, + ); + if (newMediaService == null) { + Log.error('Could not create new mediaFIle'); + return; + } + + orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + + if (!mounted) return; + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageEditorView( + mediaFileService: newMediaService, + sharedFromGallery: true, + ), + ), + ); + } + @override Widget build(BuildContext context) { + final orgMediaService = widget.galleryItems[currentIndex].mediaService; return Dismissible( key: key, direction: DismissDirection.vertical, @@ -117,36 +146,18 @@ class _MemoriesPhotoSliderViewState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + if (!orgMediaService.storedPath.existsSync()) + Padding( + padding: const EdgeInsets.only(right: 12), + child: SaveToGalleryButton( + isLoading: false, + displayButtonLabel: true, + mediaService: orgMediaService, + ), + ), FilledButton.icon( icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - final orgMediaService = - widget.galleryItems[currentIndex].mediaService; - - final newMediaService = await initializeMediaUpload( - orgMediaService.mediaFile.type, - gUser.defaultShowTime, - ); - if (newMediaService == null) { - Log.error('Could not create new mediaFIle'); - return; - } - - orgMediaService.storedPath - .copySync(newMediaService.originalPath.path); - - if (!context.mounted) return; - - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageEditorView( - mediaFileService: newMediaService, - sharedFromGallery: true, - ), - ), - ); - }, + onPressed: shareMediaFile, style: ButtonStyle( padding: WidgetStateProperty.all( const EdgeInsets.symmetric( @@ -217,10 +228,16 @@ class _MemoriesPhotoSliderViewState extends State { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { final item = widget.galleryItems[index]; + + var filePath = item.mediaService.storedPath; + if (!filePath.existsSync()) { + filePath = item.mediaService.tempPath; + } + return item.mediaService.mediaFile.type == MediaType.video ? PhotoViewGalleryPageOptions.customChild( child: VideoPlayerWrapper( - videoPath: item.mediaService.storedPath, + videoPath: filePath, ), // childSize: const Size(300, 300), initialScale: PhotoViewComputedScale.contained, @@ -231,7 +248,7 @@ class _MemoriesPhotoSliderViewState extends State { ), ) : PhotoViewGalleryPageOptions( - imageProvider: FileImage(item.mediaService.storedPath), + imageProvider: FileImage(filePath), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1,