mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 22:28:41 +00:00
fix #155
This commit is contained in:
parent
597ba72c1e
commit
30a3668eec
5 changed files with 145 additions and 46 deletions
|
|
@ -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) {
|
Uint8List getRandomUint8List(int length) {
|
||||||
final Random random = Random.secure();
|
final Random random = Random.secure();
|
||||||
final Uint8List randomBytes = Uint8List(length);
|
final Uint8List randomBytes = Uint8List(length);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
@ -7,12 +8,13 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
class SaveToGalleryButton extends StatefulWidget {
|
class SaveToGalleryButton extends StatefulWidget {
|
||||||
final Future<Uint8List?> Function() getMergedImage;
|
final Future<Uint8List?> Function() getMergedImage;
|
||||||
final String? sendNextMediaToUserName;
|
final String? sendNextMediaToUserName;
|
||||||
|
final XFile? videoFilePath;
|
||||||
|
|
||||||
const SaveToGalleryButton({
|
const SaveToGalleryButton(
|
||||||
super.key,
|
{super.key,
|
||||||
required this.getMergedImage,
|
required this.getMergedImage,
|
||||||
this.sendNextMediaToUserName,
|
this.sendNextMediaToUserName,
|
||||||
});
|
this.videoFilePath});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SaveToGalleryButton> createState() => SaveToGalleryButtonState();
|
State<SaveToGalleryButton> createState() => SaveToGalleryButtonState();
|
||||||
|
|
@ -37,15 +39,31 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_imageSaving = true;
|
_imageSaving = true;
|
||||||
});
|
});
|
||||||
Uint8List? imageBytes = await widget.getMergedImage();
|
|
||||||
if (imageBytes == null || !context.mounted) return;
|
String? res;
|
||||||
final res = await saveImageToGallery(imageBytes);
|
|
||||||
|
if (widget.videoFilePath != null) {
|
||||||
|
res = await saveVideoToGallery(widget.videoFilePath!.path);
|
||||||
|
} else {
|
||||||
|
Uint8List? imageBytes = await widget.getMergedImage();
|
||||||
|
if (imageBytes == null || !context.mounted) return;
|
||||||
|
res = await saveImageToGallery(imageBytes);
|
||||||
|
}
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_imageSaving = false;
|
|
||||||
_imageSaved = true;
|
_imageSaved = true;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(res),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_imageSaving = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
children: [
|
children: [
|
||||||
SaveToGalleryButton(
|
SaveToGalleryButton(
|
||||||
getMergedImage: getMergedImage,
|
getMergedImage: getMergedImage,
|
||||||
|
videoFilePath: widget.videoFilePath,
|
||||||
sendNextMediaToUserName: sendNextMediaToUserName,
|
sendNextMediaToUserName: sendNextMediaToUserName,
|
||||||
),
|
),
|
||||||
if (sendNextMediaToUserName != null) SizedBox(width: 10),
|
if (sendNextMediaToUserName != null) SizedBox(width: 10),
|
||||||
|
|
|
||||||
|
|
@ -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/views/chats/media_viewer_view.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/contact/contact_view.dart';
|
import 'package:twonly/src/views/contact/contact_view.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
InputDecoration inputTextMessageDeco(BuildContext context) {
|
class ChatMediaViewerFullScreen extends StatelessWidget {
|
||||||
return InputDecoration(
|
const ChatMediaViewerFullScreen({super.key, required this.message});
|
||||||
hintText: context.lang.chatListDetailInput,
|
final Message message;
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
@override
|
||||||
border: OutlineInputBorder(
|
Widget build(BuildContext context) {
|
||||||
borderRadius: BorderRadius.circular(20),
|
return Scaffold(
|
||||||
borderSide:
|
body: SafeArea(
|
||||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
|
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 {
|
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 Message message;
|
||||||
|
final bool isInFullscreen;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
||||||
|
|
@ -58,6 +55,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
File? image;
|
File? image;
|
||||||
File? video;
|
File? video;
|
||||||
bool isMounted = true;
|
bool isMounted = true;
|
||||||
|
bool mirrorVideo = false;
|
||||||
|
VideoPlayerController? videoController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -73,7 +72,26 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
isSend ? "send" : "received",
|
isSend ? "send" : "received",
|
||||||
);
|
);
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
final videoPath = File("$basePath.mp4");
|
||||||
final imagePath = File("$basePath.png");
|
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()) {
|
if (imagePath.existsSync()) {
|
||||||
setState(() {
|
setState(() {
|
||||||
image = imagePath;
|
image = imagePath;
|
||||||
|
|
@ -87,22 +105,41 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
|
videoController?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return GestureDetector(
|
||||||
children: [
|
onTap: () {
|
||||||
if (image != null) Image.file(image!),
|
if (widget.isInFullscreen) return;
|
||||||
if (image == null && video == null)
|
Navigator.push(
|
||||||
Padding(
|
context,
|
||||||
padding: const EdgeInsets.all(10.0),
|
MaterialPageRoute(builder: (context) {
|
||||||
child: MessageSendStateIcon(
|
return ChatMediaViewerFullScreen(message: widget.message);
|
||||||
[widget.message],
|
}),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
);
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
child: MessageSendStateIcon(
|
||||||
|
[widget.message],
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
|
|
||||||
// current image related
|
// current image related
|
||||||
Uint8List? imageBytes;
|
Uint8List? imageBytes;
|
||||||
|
String? videoPath;
|
||||||
VideoPlayerController? videoController;
|
VideoPlayerController? videoController;
|
||||||
|
|
||||||
DateTime? canBeSeenUntil;
|
DateTime? canBeSeenUntil;
|
||||||
|
|
@ -137,6 +138,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
imageSaved = false;
|
imageSaved = false;
|
||||||
mirrorVideo = false;
|
mirrorVideo = false;
|
||||||
progress = 0;
|
progress = 0;
|
||||||
|
videoPath = null;
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
isRealTwonly = false;
|
isRealTwonly = false;
|
||||||
showSendTextMessageInput = false;
|
showSendTextMessageInput = false;
|
||||||
|
|
@ -200,9 +202,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (content.isVideo) {
|
if (content.isVideo) {
|
||||||
final vidoePath = await getVideoPath(current.messageId);
|
final videoPathTmp = await getVideoPath(current.messageId);
|
||||||
if (vidoePath != null) {
|
if (videoPathTmp != null) {
|
||||||
videoController = VideoPlayerController.file(File(vidoePath.path));
|
videoController = VideoPlayerController.file(File(videoPathTmp.path));
|
||||||
videoController?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
videoController?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
||||||
videoController?.initialize().then((_) {
|
videoController?.initialize().then((_) {
|
||||||
videoController!.play();
|
videoController!.play();
|
||||||
|
|
@ -214,7 +216,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {
|
||||||
|
videoPath = videoPathTmp.path;
|
||||||
|
});
|
||||||
}).catchError((Object error) {
|
}).catchError((Object error) {
|
||||||
Logger("media_viewer_view.dart").shout(error);
|
Logger("media_viewer_view.dart").shout(error);
|
||||||
});
|
});
|
||||||
|
|
@ -307,8 +311,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
imageSaved = true;
|
imageSaved = true;
|
||||||
});
|
});
|
||||||
final user = await getUser();
|
final user = await getUser();
|
||||||
|
|
||||||
if (user != null && (user.storeMediaFilesInGallery ?? true)) {
|
if (user != null && (user.storeMediaFilesInGallery ?? true)) {
|
||||||
await saveImageToGallery(imageBytes!);
|
if (videoPath != null) {
|
||||||
|
await saveVideoToGallery(videoPath!);
|
||||||
|
} else {
|
||||||
|
await saveImageToGallery(imageBytes!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
imageSaving = false;
|
imageSaving = false;
|
||||||
|
|
@ -320,7 +329,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (maxShowTime == gMediaShowInfinite && videoController == null)
|
if (maxShowTime == gMediaShowInfinite)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
iconColor: imageSaved
|
iconColor: imageSaved
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue