mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 12:16:47 +00:00
parent
849102dd3b
commit
8ecae72d80
15 changed files with 184 additions and 59 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c1dc14a53ff2854389a50f4aaeb26946ff925367
|
||||
Subproject commit 20f3c2f0a49e4c9be452ecbc84d98054c92974e1
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -436,7 +436,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
CameraLensDirection.front;
|
||||
|
||||
Future<void> onPanUpdate(dynamic details) async {
|
||||
if (isFront || details == null) {
|
||||
if (details == null) {
|
||||
return;
|
||||
}
|
||||
if (mc.cameraController == null ||
|
||||
|
|
@ -603,9 +603,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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<CameraPreviewView> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class MainCameraController {
|
|||
Map<int, ScannedVerifiedContact> contactsVerified = {};
|
||||
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
||||
String? scannedUrl;
|
||||
GlobalKey zoomButtonKey = GlobalKey();
|
||||
|
||||
Future<void> 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Uint8List?> Function() storeImageAsOriginal;
|
||||
final Future<Uint8List?> Function()? storeImageAsOriginal;
|
||||
final bool displayButtonLabel;
|
||||
final MediaFileService mediaService;
|
||||
final bool isLoading;
|
||||
|
|
@ -44,8 +48,32 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
_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;
|
||||
|
|
|
|||
|
|
@ -62,8 +62,16 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
_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(() {});
|
||||
|
|
|
|||
|
|
@ -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<ShareImageEditorView> {
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
group: widget.group,
|
||||
onResponseTriggered: widget.onResponseTriggered!,
|
||||
galleryItems: widget.galleryItems,
|
||||
mediaFileService: mediaService,
|
||||
child: Container(
|
||||
child: child,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<MemoryItem> galleryItems;
|
||||
final MediaFileService? mediaFileService;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
Future<void> 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,
|
||||
|
|
|
|||
|
|
@ -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<MemoriesPhotoSliderView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> 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<MemoriesPhotoSliderView> {
|
|||
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<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
|
|
@ -217,10 +228,16 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
|
||||
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<MemoriesPhotoSliderView> {
|
|||
),
|
||||
)
|
||||
: PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(item.mediaService.storedPath),
|
||||
imageProvider: FileImage(filePath),
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue