mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +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();
|
||||
}
|
||||
|
||||
Future<List<Message>> getAllStoredMediaFiles() {
|
||||
return (select(messages)
|
||||
..where((t) => t.mediaStored.equals(true))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
|
||||
return (select(messages)
|
||||
..where(
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ Future sendMediaFile(
|
|||
Uint8List imageBytes,
|
||||
bool isRealTwonly,
|
||||
int maxShowTime,
|
||||
XFile? videoFilePath,
|
||||
File? videoFilePath,
|
||||
bool? enableVideoAudio,
|
||||
bool mirrorVideo,
|
||||
) async {
|
||||
|
|
@ -75,7 +75,7 @@ Future sendMediaFile(
|
|||
if (mediaUploadId != null) {
|
||||
if (videoFilePath != null) {
|
||||
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 handleSingleMediaFile(mediaUploadId, imageBytes);
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
useHighQuality = user.useHighQuality!;
|
||||
}
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +156,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
}
|
||||
});
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
cameraId = sCameraId;
|
||||
});
|
||||
|
|
@ -239,7 +243,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
|
||||
Future<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? imageBytes, XFile? videoFilePath) async {
|
||||
Future<Uint8List?>? imageBytes, File? videoFilePath) async {
|
||||
bool? shoudReturn = await Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
|
|
@ -361,16 +365,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
isVideoRecording = false;
|
||||
sharePreviewIsShown = true;
|
||||
});
|
||||
File? videoPathFile;
|
||||
XFile? videoPath = await controller?.stopVideoRecording();
|
||||
if (videoPath != null) {
|
||||
if (Platform.isAndroid) {
|
||||
// see https://github.com/flutter/flutter/issues/148335
|
||||
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();
|
||||
if (await pushMediaEditor(null, videoPath)) {
|
||||
if (await pushMediaEditor(null, videoPathFile)) {
|
||||
return;
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'dart:typed_data';
|
||||
|
|
@ -8,7 +8,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
class SaveToGalleryButton extends StatefulWidget {
|
||||
final Future<Uint8List?> Function() getMergedImage;
|
||||
final String? sendNextMediaToUserName;
|
||||
final XFile? videoFilePath;
|
||||
final File? videoFilePath;
|
||||
|
||||
const SaveToGalleryButton(
|
||||
{super.key,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -43,7 +42,7 @@ class ShareImageEditorView extends StatefulWidget {
|
|||
required this.useHighQuality,
|
||||
});
|
||||
final Future<Uint8List?>? imageBytes;
|
||||
final XFile? videoFilePath;
|
||||
final File? videoFilePath;
|
||||
final Contact? sendTo;
|
||||
final bool mirrorVideo;
|
||||
final bool useHighQuality;
|
||||
|
|
@ -77,8 +76,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
setState(() {
|
||||
sendingOrLoadingImage = false;
|
||||
});
|
||||
videoController =
|
||||
VideoPlayerController.file(File(widget.videoFilePath!.path));
|
||||
videoController = VideoPlayerController.file(widget.videoFilePath!);
|
||||
videoController?.setLooping(true);
|
||||
videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
|
@ -32,7 +32,7 @@ class ShareImageView extends StatefulWidget {
|
|||
final bool isRealTwonly;
|
||||
final bool mirrorVideo;
|
||||
final int maxShowTime;
|
||||
final XFile? videoFilePath;
|
||||
final File? videoFilePath;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final bool? enableVideoAudio;
|
||||
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/components/animate_icon.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/database/twonly_database.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
|
|
@ -251,32 +250,14 @@ class ChatListEntry extends StatelessWidget {
|
|||
alignment: Alignment.centerRight,
|
||||
child: ClipRRect(
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
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/database/twonly_database.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:video_player/video_player.dart';
|
||||
|
||||
class ChatMediaViewerFullScreen extends StatelessWidget {
|
||||
const ChatMediaViewerFullScreen({super.key, required this.message});
|
||||
class ChatMediaViewerFullScreen extends StatefulWidget {
|
||||
const ChatMediaViewerFullScreen(
|
||||
{super.key, required this.message, required this.contact});
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: InChatMediaViewer(message: message, isInFullscreen: true),
|
||||
body: Container(
|
||||
child: MediaViewSizing(
|
||||
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 {
|
||||
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 Contact contact;
|
||||
final bool isInFullscreen;
|
||||
|
||||
@override
|
||||
|
|
@ -78,6 +162,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
videoController!.setVolume(0);
|
||||
}
|
||||
videoController!.play();
|
||||
videoController!.setLooping(true);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
|
|
@ -100,39 +185,29 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
videoController?.dispose();
|
||||
}
|
||||
|
||||
Future deleteFiles() async {
|
||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||
widget.message.messageId,
|
||||
MessagesCompanion(mediaStored: Value(false)),
|
||||
Future onTap() async {
|
||||
if (image == null && videoController == null) return;
|
||||
if (widget.isInFullscreen) return;
|
||||
bool? removed = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return ChatMediaViewerFullScreen(
|
||||
message: widget.message, contact: widget.contact);
|
||||
}),
|
||||
);
|
||||
await send.purgeSendMediaFiles();
|
||||
await received.purgeReceivedMediaFiles();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
|
||||
if (removed != null && removed) {
|
||||
image = null;
|
||||
videoController?.dispose();
|
||||
videoController = null;
|
||||
if (isMounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: (image == null && videoController == null)
|
||||
? 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(() {});
|
||||
}
|
||||
},
|
||||
onTap: onTap,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (image != null) Image.file(image!),
|
||||
|
|
@ -151,22 +226,6 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
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:twonly/src/views/components/user_context_menu.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 'chats/chat_list_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -72,8 +73,9 @@ class HomeViewState extends State<HomeView> {
|
|||
setState(() {});
|
||||
},
|
||||
children: [
|
||||
CameraPreviewViewPermission(),
|
||||
ChatListView(),
|
||||
CameraPreviewViewPermission(),
|
||||
GalleryMainView()
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
|
|
@ -85,12 +87,16 @@ class HomeViewState extends State<HomeView> {
|
|||
selectedIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.inverseSurface),
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
label: "",
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
||||
icon: FaIcon(FontAwesomeIcons.photoFilm),
|
||||
label: "",
|
||||
),
|
||||
],
|
||||
onTap: (int index) {
|
||||
activePageIdx = index;
|
||||
|
|
|
|||
|
|
@ -1268,6 +1268,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ dependencies:
|
|||
video_player: ^2.9.5
|
||||
video_compress: ^3.1.4
|
||||
share_plus: ^11.0.0
|
||||
photo_view: ^0.15.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in a new issue