This commit is contained in:
otsmr 2025-05-11 13:12:47 +02:00
parent 597ba72c1e
commit 30a3668eec
5 changed files with 145 additions and 46 deletions

View file

@ -56,6 +56,19 @@ Future<String?> saveImageToGallery(Uint8List imageBytes) async {
}
}
Future<String?> saveVideoToGallery(String videoPath) async {
final hasAccess = await Gal.hasAccess();
if (!hasAccess) {
await Gal.requestAccess();
}
try {
await Gal.putVideo(videoPath);
return null;
} on GalException catch (e) {
return e.type.message;
}
}
Uint8List getRandomUint8List(int length) {
final Random random = Random.secure();
final Uint8List randomBytes = Uint8List(length);

View file

@ -1,3 +1,4 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'dart:typed_data';
@ -7,12 +8,13 @@ import 'package:twonly/src/utils/misc.dart';
class SaveToGalleryButton extends StatefulWidget {
final Future<Uint8List?> Function() getMergedImage;
final String? sendNextMediaToUserName;
final XFile? videoFilePath;
const SaveToGalleryButton({
super.key,
const SaveToGalleryButton(
{super.key,
required this.getMergedImage,
this.sendNextMediaToUserName,
});
this.videoFilePath});
@override
State<SaveToGalleryButton> createState() => SaveToGalleryButtonState();
@ -37,15 +39,31 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
setState(() {
_imageSaving = true;
});
String? res;
if (widget.videoFilePath != null) {
res = await saveVideoToGallery(widget.videoFilePath!.path);
} else {
Uint8List? imageBytes = await widget.getMergedImage();
if (imageBytes == null || !context.mounted) return;
final res = await saveImageToGallery(imageBytes);
res = await saveImageToGallery(imageBytes);
}
if (res == null) {
setState(() {
_imageSaving = false;
_imageSaved = true;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(res),
duration: Duration(seconds: 3),
),
);
}
setState(() {
_imageSaving = false;
});
},
child: Row(
children: [

View file

@ -501,6 +501,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
children: [
SaveToGalleryButton(
getMergedImage: getMergedImage,
videoFilePath: widget.videoFilePath,
sendNextMediaToUserName: sendNextMediaToUserName,
),
if (sendNextMediaToUserName != null) SizedBox(width: 10),

View file

@ -23,32 +23,29 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/media_viewer_view.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart';
import 'package:video_player/video_player.dart';
InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
class ChatMediaViewerFullScreen extends StatelessWidget {
const ChatMediaViewerFullScreen({super.key, required this.message});
final Message message;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: InChatMediaViewer(message: message, isInFullscreen: true),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide: BorderSide(color: Colors.grey, width: 2.0),
),
);
}
}
class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer({super.key, required this.message});
const InChatMediaViewer(
{super.key, required this.message, this.isInFullscreen = false});
final Message message;
final bool isInFullscreen;
@override
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
@ -58,6 +55,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
File? image;
File? video;
bool isMounted = true;
bool mirrorVideo = false;
VideoPlayerController? videoController;
@override
void initState() {
@ -73,7 +72,26 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
isSend ? "send" : "received",
);
if (!isMounted) return;
final videoPath = File("$basePath.mp4");
final imagePath = File("$basePath.png");
if (videoPath.existsSync() && widget.message.contentJson != null) {
MessageContent? content = MessageContent.fromJson(
MessageKind.media, jsonDecode(widget.message.contentJson!));
if (content is MediaMessageContent) {
mirrorVideo = content.mirrorVideo;
}
videoController = VideoPlayerController.file(videoPath);
videoController?.initialize().then((_) {
if (!widget.isInFullscreen) {
videoController!.setVolume(0);
}
videoController!.play();
});
setState(() {
image = imagePath;
});
}
if (imagePath.existsSync()) {
setState(() {
image = imagePath;
@ -87,13 +105,31 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
void dispose() {
super.dispose();
isMounted = false;
videoController?.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
return GestureDetector(
onTap: () {
if (widget.isInFullscreen) return;
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ChatMediaViewerFullScreen(message: widget.message);
}),
);
},
child: Stack(
children: [
if (image != null) Image.file(image!),
if (videoController != null)
Positioned.fill(
child: Transform.flip(
flipX: mirrorVideo,
child: VideoPlayer(videoController!),
),
),
if (image == null && video == null)
Padding(
padding: const EdgeInsets.all(10.0),
@ -103,6 +139,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
),
)
],
),
);
}
}
@ -650,3 +687,24 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
);
}
}
InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide: BorderSide(color: Colors.grey, width: 2.0),
),
);
}

View file

@ -41,6 +41,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
// current image related
Uint8List? imageBytes;
String? videoPath;
VideoPlayerController? videoController;
DateTime? canBeSeenUntil;
@ -137,6 +138,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
imageSaved = false;
mirrorVideo = false;
progress = 0;
videoPath = null;
isDownloading = false;
isRealTwonly = false;
showSendTextMessageInput = false;
@ -200,9 +202,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
);
if (content.isVideo) {
final vidoePath = await getVideoPath(current.messageId);
if (vidoePath != null) {
videoController = VideoPlayerController.file(File(vidoePath.path));
final videoPathTmp = await getVideoPath(current.messageId);
if (videoPathTmp != null) {
videoController = VideoPlayerController.file(File(videoPathTmp.path));
videoController?.setLooping(content.maxShowTime == gMediaShowInfinite);
videoController?.initialize().then((_) {
videoController!.play();
@ -214,7 +216,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
});
}
setState(() {});
setState(() {
videoPath = videoPathTmp.path;
});
}).catchError((Object error) {
Logger("media_viewer_view.dart").shout(error);
});
@ -307,9 +311,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
imageSaved = true;
});
final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery ?? true)) {
if (videoPath != null) {
await saveVideoToGallery(videoPath!);
} else {
await saveImageToGallery(imageBytes!);
}
}
setState(() {
imageSaving = false;
});
@ -320,7 +329,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (maxShowTime == gMediaShowInfinite && videoController == null)
if (maxShowTime == gMediaShowInfinite)
OutlinedButton(
style: OutlinedButton.styleFrom(
iconColor: imageSaved