mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +00:00
fix #176
This commit is contained in:
parent
7a300456a3
commit
cedf20500c
13 changed files with 574 additions and 87 deletions
|
|
@ -81,6 +81,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Message>> getAllStoredMediaFiles() {
|
||||||
|
return (select(messages)
|
||||||
|
..where((t) => t.mediaStored.equals(true))
|
||||||
|
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
|
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
|
||||||
return (select(messages)
|
return (select(messages)
|
||||||
..where(
|
..where(
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ Future sendMediaFile(
|
||||||
Uint8List imageBytes,
|
Uint8List imageBytes,
|
||||||
bool isRealTwonly,
|
bool isRealTwonly,
|
||||||
int maxShowTime,
|
int maxShowTime,
|
||||||
XFile? videoFilePath,
|
File? videoFilePath,
|
||||||
bool? enableVideoAudio,
|
bool? enableVideoAudio,
|
||||||
bool mirrorVideo,
|
bool mirrorVideo,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -75,7 +75,7 @@ Future sendMediaFile(
|
||||||
if (mediaUploadId != null) {
|
if (mediaUploadId != null) {
|
||||||
if (videoFilePath != null) {
|
if (videoFilePath != null) {
|
||||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||||
await File(videoFilePath.path).rename("$basePath.orginal.mp4");
|
await videoFilePath.rename("$basePath.orginal.mp4");
|
||||||
}
|
}
|
||||||
await writeMediaFile(mediaUploadId, "orginal.png", imageBytes);
|
await writeMediaFile(mediaUploadId, "orginal.png", imageBytes);
|
||||||
await handleSingleMediaFile(mediaUploadId, imageBytes);
|
await handleSingleMediaFile(mediaUploadId, imageBytes);
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
useHighQuality = user.useHighQuality!;
|
useHighQuality = user.useHighQuality!;
|
||||||
}
|
}
|
||||||
hasAudioPermission = await Permission.microphone.isGranted;
|
hasAudioPermission = await Permission.microphone.isGranted;
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +156,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
cameraId = sCameraId;
|
cameraId = sCameraId;
|
||||||
});
|
});
|
||||||
|
|
@ -239,7 +243,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> pushMediaEditor(
|
Future<bool> pushMediaEditor(
|
||||||
Future<Uint8List?>? imageBytes, XFile? videoFilePath) async {
|
Future<Uint8List?>? imageBytes, File? videoFilePath) async {
|
||||||
bool? shoudReturn = await Navigator.push(
|
bool? shoudReturn = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
|
|
@ -361,16 +365,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
isVideoRecording = false;
|
isVideoRecording = false;
|
||||||
sharePreviewIsShown = true;
|
sharePreviewIsShown = true;
|
||||||
});
|
});
|
||||||
|
File? videoPathFile;
|
||||||
XFile? videoPath = await controller?.stopVideoRecording();
|
XFile? videoPath = await controller?.stopVideoRecording();
|
||||||
if (videoPath != null) {
|
if (videoPath != null) {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// see https://github.com/flutter/flutter/issues/148335
|
// see https://github.com/flutter/flutter/issues/148335
|
||||||
await File(videoPath.path).rename("${videoPath.path}.mp4");
|
await File(videoPath.path).rename("${videoPath.path}.mp4");
|
||||||
videoPath = XFile("${videoPath.path}.mp4");
|
videoPathFile = File("${videoPath.path}.mp4");
|
||||||
|
} else {
|
||||||
|
videoPathFile = File(videoPath.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await controller?.pausePreview();
|
await controller?.pausePreview();
|
||||||
if (await pushMediaEditor(null, videoPath)) {
|
if (await pushMediaEditor(null, videoPathFile)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} on CameraException catch (e) {
|
} on CameraException catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:camera/camera.dart';
|
import 'dart:io';
|
||||||
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';
|
||||||
|
|
@ -8,7 +8,7 @@ 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;
|
final File? videoFilePath;
|
||||||
|
|
||||||
const SaveToGalleryButton(
|
const SaveToGalleryButton(
|
||||||
{super.key,
|
{super.key,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
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 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -43,7 +42,7 @@ class ShareImageEditorView extends StatefulWidget {
|
||||||
required this.useHighQuality,
|
required this.useHighQuality,
|
||||||
});
|
});
|
||||||
final Future<Uint8List?>? imageBytes;
|
final Future<Uint8List?>? imageBytes;
|
||||||
final XFile? videoFilePath;
|
final File? videoFilePath;
|
||||||
final Contact? sendTo;
|
final Contact? sendTo;
|
||||||
final bool mirrorVideo;
|
final bool mirrorVideo;
|
||||||
final bool useHighQuality;
|
final bool useHighQuality;
|
||||||
|
|
@ -77,8 +76,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingOrLoadingImage = false;
|
sendingOrLoadingImage = false;
|
||||||
});
|
});
|
||||||
videoController =
|
videoController = VideoPlayerController.file(widget.videoFilePath!);
|
||||||
VideoPlayerController.file(File(widget.videoFilePath!.path));
|
|
||||||
videoController?.setLooping(true);
|
videoController?.setLooping(true);
|
||||||
videoController?.initialize().then((_) {
|
videoController?.initialize().then((_) {
|
||||||
videoController!.play();
|
videoController!.play();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
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 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -32,7 +32,7 @@ class ShareImageView extends StatefulWidget {
|
||||||
final bool isRealTwonly;
|
final bool isRealTwonly;
|
||||||
final bool mirrorVideo;
|
final bool mirrorVideo;
|
||||||
final int maxShowTime;
|
final int maxShowTime;
|
||||||
final XFile? videoFilePath;
|
final File? videoFilePath;
|
||||||
final HashSet<int> selectedUserIds;
|
final HashSet<int> selectedUserIds;
|
||||||
final bool? enableVideoAudio;
|
final bool? enableVideoAudio;
|
||||||
final Function(int, bool) updateStatus;
|
final Function(int, bool) updateStatus;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||||
import 'package:twonly/src/views/chats/components/in_chat_media_viewer.dart';
|
import 'package:twonly/src/views/chats/components/in_chat_media_viewer.dart';
|
||||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
import 'package:twonly/src/views/components/better_text.dart';
|
import 'package:twonly/src/views/components/better_text.dart';
|
||||||
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
|
||||||
import 'package:twonly/src/views/chats/components/sliding_response.dart';
|
import 'package:twonly/src/views/chats/components/sliding_response.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
|
|
@ -251,32 +250,14 @@ class ChatListEntry extends StatelessWidget {
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: InChatMediaViewer(message: message),
|
child: InChatMediaViewer(
|
||||||
|
message: message,
|
||||||
|
contact: contact,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (message.kind == MessageKind.storedMediaFile) {
|
|
||||||
child = Container(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
width: 150,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
getMessageColorFromType(TextMessageContent(text: ""), context),
|
|
||||||
width: 1.0,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: MessageSendStateIcon(
|
|
||||||
[message],
|
|
||||||
mainAxisAlignment:
|
|
||||||
right ? MainAxisAlignment.center : MainAxisAlignment.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/providers/api/media_send.dart' as send;
|
import 'package:twonly/src/providers/api/media_send.dart' as send;
|
||||||
|
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||||
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
|
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||||
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
|
|
@ -13,26 +16,107 @@ import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/providers/api/media_received.dart' as received;
|
import 'package:twonly/src/providers/api/media_received.dart' as received;
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class ChatMediaViewerFullScreen extends StatelessWidget {
|
class ChatMediaViewerFullScreen extends StatefulWidget {
|
||||||
const ChatMediaViewerFullScreen({super.key, required this.message});
|
const ChatMediaViewerFullScreen(
|
||||||
|
{super.key, required this.message, required this.contact});
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatMediaViewerFullScreen> createState() =>
|
||||||
|
_ChatMediaViewerFullScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> {
|
||||||
|
bool hideMediaFile = false;
|
||||||
|
|
||||||
|
Future deleteFiles(context) async {
|
||||||
|
bool confirmed = await showAlertDialog(
|
||||||
|
context, "Are you sure?", "The image will be irrevocably deleted.");
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
|
widget.message.messageId,
|
||||||
|
MessagesCompanion(mediaStored: Value(false)),
|
||||||
|
);
|
||||||
|
await send.purgeSendMediaFiles();
|
||||||
|
await received.purgeReceivedMediaFiles();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: Container(
|
||||||
child: Center(
|
child: MediaViewSizing(
|
||||||
child: InChatMediaViewer(message: message, isInFullscreen: true),
|
bottomNavigation: Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
IconButton.outlined(
|
||||||
|
onPressed: () {
|
||||||
|
deleteFiles(context);
|
||||||
|
},
|
||||||
|
icon: FaIcon(FontAwesomeIcons.trashCan),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton.filled(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.camera),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
hideMediaFile = true;
|
||||||
|
});
|
||||||
|
await Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return CameraSendToView(widget.contact);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
setState(() {
|
||||||
|
hideMediaFile = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.all<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: (hideMediaFile)
|
||||||
|
? Container()
|
||||||
|
: InChatMediaViewer(
|
||||||
|
message: widget.message,
|
||||||
|
contact: widget.contact,
|
||||||
|
isInFullscreen: true,
|
||||||
|
),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InChatMediaViewer extends StatefulWidget {
|
class InChatMediaViewer extends StatefulWidget {
|
||||||
const InChatMediaViewer(
|
const InChatMediaViewer(
|
||||||
{super.key, required this.message, this.isInFullscreen = false});
|
{super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.contact,
|
||||||
|
this.isInFullscreen = false});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final Contact contact;
|
||||||
final bool isInFullscreen;
|
final bool isInFullscreen;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -78,6 +162,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
videoController!.setVolume(0);
|
videoController!.setVolume(0);
|
||||||
}
|
}
|
||||||
videoController!.play();
|
videoController!.play();
|
||||||
|
videoController!.setLooping(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -100,39 +185,29 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
videoController?.dispose();
|
videoController?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future deleteFiles() async {
|
Future onTap() async {
|
||||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
if (image == null && videoController == null) return;
|
||||||
widget.message.messageId,
|
if (widget.isInFullscreen) return;
|
||||||
MessagesCompanion(mediaStored: Value(false)),
|
bool? removed = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) {
|
||||||
|
return ChatMediaViewerFullScreen(
|
||||||
|
message: widget.message, contact: widget.contact);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
await send.purgeSendMediaFiles();
|
|
||||||
await received.purgeReceivedMediaFiles();
|
if (removed != null && removed) {
|
||||||
if (context.mounted) {
|
image = null;
|
||||||
Navigator.pop(context, true);
|
videoController?.dispose();
|
||||||
|
videoController = null;
|
||||||
|
if (isMounted) setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: (image == null && videoController == null)
|
onTap: onTap,
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
if (widget.isInFullscreen) return;
|
|
||||||
bool? removed = await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) {
|
|
||||||
return ChatMediaViewerFullScreen(message: widget.message);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (removed != null && removed) {
|
|
||||||
image = null;
|
|
||||||
videoController?.dispose();
|
|
||||||
videoController = null;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (image != null) Image.file(image!),
|
if (image != null) Image.file(image!),
|
||||||
|
|
@ -151,22 +226,6 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.isInFullscreen)
|
|
||||||
Positioned(
|
|
||||||
bottom: 10,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: deleteFiles,
|
|
||||||
icon: FaIcon(FontAwesomeIcons.trashCan),
|
|
||||||
label: Text("Delete media file"),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
46
lib/src/views/components/video_player_wrapper.dart
Normal file
46
lib/src/views/components/video_player_wrapper.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
class VideoPlayerWrapper extends StatefulWidget {
|
||||||
|
final File videoPath;
|
||||||
|
|
||||||
|
const VideoPlayerWrapper({super.key, required this.videoPath});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
|
||||||
|
late VideoPlayerController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = VideoPlayerController.file(widget.videoPath)
|
||||||
|
..initialize().then((_) {
|
||||||
|
setState(() {
|
||||||
|
_controller.play(); // Auto-play the video
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: _controller.value.isInitialized
|
||||||
|
? AspectRatio(
|
||||||
|
aspectRatio: _controller.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_controller),
|
||||||
|
)
|
||||||
|
: CircularProgressIndicator(), // Show loading indicator while initializing
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
374
lib/src/views/gallery/gallery_main_view.dart
Normal file
374
lib/src/views/gallery/gallery_main_view.dart
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:twonly/src/providers/api/media_send.dart' as send;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||||
|
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||||
|
import 'package:twonly/src/views/components/video_player_wrapper.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
class GalleryItem {
|
||||||
|
GalleryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.messages,
|
||||||
|
required this.date,
|
||||||
|
this.imagePath,
|
||||||
|
this.videoPath,
|
||||||
|
});
|
||||||
|
final int id;
|
||||||
|
final List<Message> messages;
|
||||||
|
final DateTime date;
|
||||||
|
final File? imagePath;
|
||||||
|
final File? videoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryItemGrid {
|
||||||
|
GalleryItemGrid({
|
||||||
|
this.galleryItemIndex,
|
||||||
|
this.month,
|
||||||
|
this.hide,
|
||||||
|
});
|
||||||
|
final int? galleryItemIndex;
|
||||||
|
final String? month;
|
||||||
|
final bool? hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryItemThumbnail extends StatefulWidget {
|
||||||
|
const GalleryItemThumbnail({
|
||||||
|
super.key,
|
||||||
|
required this.galleryItem,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final GalleryItem galleryItem;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GalleryItemThumbnail> createState() => _GalleryItemThumbnailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GalleryItemThumbnailState extends State<GalleryItemThumbnail> {
|
||||||
|
VideoPlayerController? _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.galleryItem.videoPath != null) {
|
||||||
|
_controller = VideoPlayerController.file(widget.galleryItem.videoPath!)
|
||||||
|
..initialize().then((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatDuration(Duration duration) {
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
|
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
|
||||||
|
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
||||||
|
return "$twoDigitMinutes:$twoDigitSeconds";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: Hero(
|
||||||
|
tag: widget.galleryItem.id,
|
||||||
|
child: (widget.galleryItem.imagePath != null)
|
||||||
|
? Image.file(widget.galleryItem.imagePath!)
|
||||||
|
: Stack(
|
||||||
|
children: [
|
||||||
|
if (_controller != null && _controller!.value.isInitialized)
|
||||||
|
Positioned.fill(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: _controller!.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_controller!),
|
||||||
|
)),
|
||||||
|
if (_controller != null && _controller!.value.isInitialized)
|
||||||
|
Positioned(
|
||||||
|
bottom: 5,
|
||||||
|
right: 5,
|
||||||
|
child: Text(
|
||||||
|
_controller!.value.isInitialized
|
||||||
|
? formatDuration(_controller!.value.duration)
|
||||||
|
: '...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryMainView extends StatefulWidget {
|
||||||
|
const GalleryMainView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GalleryMainView> createState() => GalleryMainViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
|
bool verticalGallery = false;
|
||||||
|
List<GalleryItem> galleryItems = [];
|
||||||
|
Map<String, List<int>> orderedByMonth = {};
|
||||||
|
List<String> months = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future initAsync() async {
|
||||||
|
List<Message> storedMediaFiles =
|
||||||
|
await twonlyDatabase.messagesDao.getAllStoredMediaFiles();
|
||||||
|
|
||||||
|
Map<int, GalleryItem> items = {};
|
||||||
|
for (final message in storedMediaFiles) {
|
||||||
|
bool isSend = message.messageOtherId == null;
|
||||||
|
int id = message.mediaUploadId ?? message.messageId;
|
||||||
|
final basePath = await send.getMediaFilePath(
|
||||||
|
isSend ? message.mediaUploadId! : message.messageId,
|
||||||
|
isSend ? "send" : "received",
|
||||||
|
);
|
||||||
|
File? imagePath;
|
||||||
|
File? videoPath;
|
||||||
|
if (await File("$basePath.mp4").exists()) {
|
||||||
|
videoPath = File("$basePath.mp4");
|
||||||
|
} else if (await File("$basePath.png").exists()) {
|
||||||
|
imagePath = File("$basePath.png");
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items
|
||||||
|
.putIfAbsent(
|
||||||
|
id,
|
||||||
|
() => GalleryItem(
|
||||||
|
id: id,
|
||||||
|
messages: [],
|
||||||
|
date: message.sendAt,
|
||||||
|
imagePath: imagePath,
|
||||||
|
videoPath: videoPath))
|
||||||
|
.messages
|
||||||
|
.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group items by month
|
||||||
|
orderedByMonth = {};
|
||||||
|
months = [];
|
||||||
|
|
||||||
|
String lastMonth = "";
|
||||||
|
galleryItems = items.values.toList();
|
||||||
|
for (var i = 0; i < galleryItems.length; i++) {
|
||||||
|
String month = DateFormat('MMMM yyyy').format(galleryItems[i].date);
|
||||||
|
if (lastMonth != month) {
|
||||||
|
lastMonth = month;
|
||||||
|
months.add(month);
|
||||||
|
}
|
||||||
|
orderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('Memories')),
|
||||||
|
body: Scrollbar(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: (months.length * 2),
|
||||||
|
itemBuilder: (context, mIndex) {
|
||||||
|
if (mIndex % 2 == 0) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(months[(mIndex / 2).toInt()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
int index = ((mIndex - 1) / 2).toInt();
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
childAspectRatio: 9 / 16,
|
||||||
|
),
|
||||||
|
itemCount: orderedByMonth[months[index]]!.length,
|
||||||
|
itemBuilder: (context, gIndex) {
|
||||||
|
int gaIndex = orderedByMonth[months[index]]![gIndex];
|
||||||
|
return GalleryItemThumbnail(
|
||||||
|
galleryItem: galleryItems[gaIndex],
|
||||||
|
onTap: () {
|
||||||
|
open(context, gaIndex);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void open(BuildContext context, final int index) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => GalleryPhotoViewWrapper(
|
||||||
|
galleryItems: galleryItems,
|
||||||
|
backgroundDecoration: const BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
initialIndex: index,
|
||||||
|
scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryPhotoViewWrapper extends StatefulWidget {
|
||||||
|
GalleryPhotoViewWrapper({
|
||||||
|
super.key,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.backgroundDecoration,
|
||||||
|
this.minScale,
|
||||||
|
this.maxScale,
|
||||||
|
this.initialIndex = 0,
|
||||||
|
required this.galleryItems,
|
||||||
|
this.scrollDirection = Axis.horizontal,
|
||||||
|
}) : pageController = PageController(initialPage: initialIndex);
|
||||||
|
|
||||||
|
final LoadingBuilder? loadingBuilder;
|
||||||
|
final BoxDecoration? backgroundDecoration;
|
||||||
|
final dynamic minScale;
|
||||||
|
final dynamic maxScale;
|
||||||
|
final int initialIndex;
|
||||||
|
final PageController pageController;
|
||||||
|
final List<GalleryItem> galleryItems;
|
||||||
|
final Axis scrollDirection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _GalleryPhotoViewWrapperState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GalleryPhotoViewWrapperState extends State<GalleryPhotoViewWrapper> {
|
||||||
|
late int currentIndex = widget.initialIndex;
|
||||||
|
|
||||||
|
void onPageChanged(int index) {
|
||||||
|
setState(() {
|
||||||
|
currentIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: widget.backgroundDecoration,
|
||||||
|
constraints: BoxConstraints.expand(
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
children: <Widget>[
|
||||||
|
MediaViewSizing(
|
||||||
|
bottomNavigation: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FilledButton.icon(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
PageRouteBuilder(
|
||||||
|
opaque: false,
|
||||||
|
pageBuilder: (context, a1, a2) =>
|
||||||
|
ShareImageEditorView(
|
||||||
|
videoFilePath:
|
||||||
|
widget.galleryItems[currentIndex].videoPath,
|
||||||
|
imageBytes: widget
|
||||||
|
.galleryItems[currentIndex].imagePath
|
||||||
|
?.readAsBytes(),
|
||||||
|
mirrorVideo: false,
|
||||||
|
useHighQuality: true,
|
||||||
|
),
|
||||||
|
transitionsBuilder:
|
||||||
|
(context, animation, secondaryAnimation, child) {
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
transitionDuration: Duration.zero,
|
||||||
|
reverseTransitionDuration: Duration.zero,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.all<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
)),
|
||||||
|
label: Text(
|
||||||
|
context.lang.shareImagedEditorSendImage,
|
||||||
|
style: TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: PhotoViewGallery.builder(
|
||||||
|
scrollPhysics: const BouncingScrollPhysics(),
|
||||||
|
builder: _buildItem,
|
||||||
|
itemCount: widget.galleryItems.length,
|
||||||
|
loadingBuilder: widget.loadingBuilder,
|
||||||
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
|
pageController: widget.pageController,
|
||||||
|
onPageChanged: onPageChanged,
|
||||||
|
scrollDirection: widget.scrollDirection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
|
||||||
|
final GalleryItem item = widget.galleryItems[index];
|
||||||
|
return item.videoPath != null
|
||||||
|
? PhotoViewGalleryPageOptions.customChild(
|
||||||
|
child: VideoPlayerWrapper(videoPath: item.videoPath!),
|
||||||
|
// childSize: const Size(300, 300),
|
||||||
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
|
||||||
|
)
|
||||||
|
: PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: FileImage(item.imagePath!),
|
||||||
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:pie_menu/pie_menu.dart';
|
import 'package:pie_menu/pie_menu.dart';
|
||||||
import 'package:twonly/src/views/components/user_context_menu.dart';
|
import 'package:twonly/src/views/components/user_context_menu.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
|
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
|
||||||
import 'camera/camera_preview_view.dart';
|
import 'camera/camera_preview_view.dart';
|
||||||
import 'chats/chat_list_view.dart';
|
import 'chats/chat_list_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -72,8 +73,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
CameraPreviewViewPermission(),
|
|
||||||
ChatListView(),
|
ChatListView(),
|
||||||
|
CameraPreviewViewPermission(),
|
||||||
|
GalleryMainView()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
|
@ -85,12 +87,16 @@ class HomeViewState extends State<HomeView> {
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(
|
||||||
color: Theme.of(context).colorScheme.inverseSurface),
|
color: Theme.of(context).colorScheme.inverseSurface),
|
||||||
items: [
|
items: [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.camera),
|
icon: FaIcon(FontAwesomeIcons.camera),
|
||||||
label: "",
|
label: "",
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
icon: FaIcon(FontAwesomeIcons.photoFilm),
|
||||||
|
label: "",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onTap: (int index) {
|
onTap: (int index) {
|
||||||
activePageIdx = index;
|
activePageIdx = index;
|
||||||
|
|
|
||||||
|
|
@ -1268,6 +1268,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
pie_menu:
|
pie_menu:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ dependencies:
|
||||||
video_player: ^2.9.5
|
video_player: ^2.9.5
|
||||||
video_compress: ^3.1.4
|
video_compress: ^3.1.4
|
||||||
share_plus: ^11.0.0
|
share_plus: ^11.0.0
|
||||||
|
photo_view: ^0.15.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue