mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 16:28:40 +00:00
start with #190
This commit is contained in:
parent
4b84b3f20e
commit
eea77a6f08
14 changed files with 410 additions and 323 deletions
|
|
@ -81,11 +81,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Message>> getAllStoredMediaFiles() {
|
Stream<List<Message>> getAllStoredMediaFiles() {
|
||||||
return (select(messages)
|
return (select(messages)
|
||||||
..where((t) => t.mediaStored.equals(true))
|
..where((t) => t.mediaStored.equals(true))
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||||
.get();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
|
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
|
||||||
|
|
|
||||||
|
|
@ -232,5 +232,7 @@
|
||||||
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer",
|
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer",
|
||||||
"additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer",
|
"additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer",
|
||||||
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
||||||
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden."
|
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
||||||
|
"galleryDelete": "Datei löschen",
|
||||||
|
"galleryDetails": "Details anzeigen"
|
||||||
}
|
}
|
||||||
|
|
@ -390,5 +390,7 @@
|
||||||
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user",
|
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user",
|
||||||
"additionalUsersFreeTokens": "twonly-Codes für \"Free\" user",
|
"additionalUsersFreeTokens": "twonly-Codes für \"Free\" user",
|
||||||
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
|
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
|
||||||
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file."
|
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file.",
|
||||||
|
"galleryDelete": "Delete file",
|
||||||
|
"galleryDetails": "Show details"
|
||||||
}
|
}
|
||||||
|
|
@ -1408,6 +1408,18 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'**
|
/// **'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'**
|
||||||
String get planNotAllowed;
|
String get planNotAllowed;
|
||||||
|
|
||||||
|
/// No description provided for @galleryDelete.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete file'**
|
||||||
|
String get galleryDelete;
|
||||||
|
|
||||||
|
/// No description provided for @galleryDetails.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show details'**
|
||||||
|
String get galleryDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
|
|
||||||
|
|
@ -682,4 +682,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get planNotAllowed => 'In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.';
|
String get planNotAllowed => 'In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDelete => 'Datei löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDetails => 'Details anzeigen';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -682,4 +682,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get planNotAllowed => 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.';
|
String get planNotAllowed => 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDelete => 'Delete file';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDetails => 'Show details';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_message_ent
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
|
||||||
class ChatListEntry extends StatefulWidget {
|
class ChatListEntry extends StatefulWidget {
|
||||||
const ChatListEntry(
|
const ChatListEntry(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/providers/api/api.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:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.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/views/gallery/gallery_main_view.dart';
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
|
||||||
class ChatMediaEntry extends StatelessWidget {
|
class ChatMediaEntry extends StatelessWidget {
|
||||||
const ChatMediaEntry({
|
const ChatMediaEntry({
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ 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';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
import 'package:twonly/src/views/gallery/gallery_photo_view.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class InChatMediaViewer extends StatefulWidget {
|
class InChatMediaViewer extends StatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
import 'package:twonly/src/views/camera/camera_send_to_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:twonly/src/views/gallery/gallery_main_view.dart';
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
|
||||||
Color getMessageColor(Message message) {
|
Color getMessageColor(Message message) {
|
||||||
return (message.messageOtherId == null)
|
return (message.messageOtherId == null)
|
||||||
|
|
|
||||||
71
lib/src/views/gallery/gallery_item.dart
Normal file
71
lib/src/views/gallery/gallery_item.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
|
import 'package:twonly/src/providers/api/media_send.dart' as send;
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
|
|
||||||
|
class GalleryItem {
|
||||||
|
GalleryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.messages,
|
||||||
|
required this.date,
|
||||||
|
required this.mirrorVideo,
|
||||||
|
this.imagePath,
|
||||||
|
this.videoPath,
|
||||||
|
});
|
||||||
|
final String id;
|
||||||
|
final bool mirrorVideo;
|
||||||
|
final List<Message> messages;
|
||||||
|
final DateTime date;
|
||||||
|
final File? imagePath;
|
||||||
|
final File? videoPath;
|
||||||
|
|
||||||
|
static Future<Map<int, GalleryItem>> convertFromMessages(
|
||||||
|
List<Message> messages) async {
|
||||||
|
Map<int, GalleryItem> items = {};
|
||||||
|
for (final message in messages) {
|
||||||
|
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 {
|
||||||
|
if (message.mediaStored) {
|
||||||
|
/// media file was deleted, ... remove the file
|
||||||
|
twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
|
message.messageId, MessagesCompanion(mediaStored: Value(false)));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool mirrorVideo = false;
|
||||||
|
if (videoPath != null) {
|
||||||
|
MediaMessageContent content =
|
||||||
|
MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
|
||||||
|
mirrorVideo = content.mirrorVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
.putIfAbsent(
|
||||||
|
id,
|
||||||
|
() => GalleryItem(
|
||||||
|
id: id.toString(),
|
||||||
|
messages: [],
|
||||||
|
date: message.sendAt,
|
||||||
|
mirrorVideo: mirrorVideo,
|
||||||
|
imagePath: imagePath,
|
||||||
|
videoPath: videoPath))
|
||||||
|
.messages
|
||||||
|
.add(message);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/src/views/gallery/gallery_item_thumbnail.dart
Normal file
80
lib/src/views/gallery/gallery_item_thumbnail.dart
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,172 +1,13 @@
|
||||||
import 'dart:convert';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:twonly/src/model/json/message.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:flutter/material.dart';
|
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/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
import 'package:twonly/src/views/gallery/gallery_item_thumbnail.dart';
|
||||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
import 'package:twonly/src/views/gallery/gallery_photo_view.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,
|
|
||||||
required this.mirrorVideo,
|
|
||||||
this.imagePath,
|
|
||||||
this.videoPath,
|
|
||||||
});
|
|
||||||
final String id;
|
|
||||||
final bool mirrorVideo;
|
|
||||||
final List<Message> messages;
|
|
||||||
final DateTime date;
|
|
||||||
final File? imagePath;
|
|
||||||
final File? videoPath;
|
|
||||||
|
|
||||||
static Future<Map<int, GalleryItem>> convertFromMessages(
|
|
||||||
List<Message> messages) async {
|
|
||||||
Map<int, GalleryItem> items = {};
|
|
||||||
for (final message in messages) {
|
|
||||||
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 {
|
|
||||||
if (message.mediaStored) {
|
|
||||||
/// media file was deleted, ... remove the file
|
|
||||||
twonlyDatabase.messagesDao.updateMessageByMessageId(
|
|
||||||
message.messageId, MessagesCompanion(mediaStored: Value(false)));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool mirrorVideo = false;
|
|
||||||
if (videoPath != null) {
|
|
||||||
MediaMessageContent content =
|
|
||||||
MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
|
|
||||||
mirrorVideo = content.mirrorVideo;
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
.putIfAbsent(
|
|
||||||
id,
|
|
||||||
() => GalleryItem(
|
|
||||||
id: id.toString(),
|
|
||||||
messages: [],
|
|
||||||
date: message.sendAt,
|
|
||||||
mirrorVideo: mirrorVideo,
|
|
||||||
imagePath: imagePath,
|
|
||||||
videoPath: videoPath))
|
|
||||||
.messages
|
|
||||||
.add(message);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
class GalleryMainView extends StatefulWidget {
|
||||||
const GalleryMainView({super.key});
|
const GalleryMainView({super.key});
|
||||||
|
|
@ -181,6 +22,7 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
Map<String, List<int>> orderedByMonth = {};
|
Map<String, List<int>> orderedByMonth = {};
|
||||||
List<String> months = [];
|
List<String> months = [];
|
||||||
bool mounted = true;
|
bool mounted = true;
|
||||||
|
StreamSubscription<List<Message>>? messageSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -191,6 +33,7 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
|
messageSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,12 +73,12 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future initAsync() async {
|
Future initAsync() async {
|
||||||
List<Message> storedMediaFiles =
|
messageSub?.cancel();
|
||||||
await twonlyDatabase.messagesDao.getAllStoredMediaFiles();
|
Stream<List<Message>> msgStream =
|
||||||
|
twonlyDatabase.messagesDao.getAllStoredMediaFiles();
|
||||||
Map<int, GalleryItem> items =
|
|
||||||
await GalleryItem.convertFromMessages(storedMediaFiles);
|
|
||||||
|
|
||||||
|
messageSub = msgStream.listen((msgs) async {
|
||||||
|
Map<int, GalleryItem> items = await GalleryItem.convertFromMessages(msgs);
|
||||||
// Group items by month
|
// Group items by month
|
||||||
orderedByMonth = {};
|
orderedByMonth = {};
|
||||||
months = [];
|
months = [];
|
||||||
|
|
@ -254,6 +97,7 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -296,144 +140,17 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void open(BuildContext context, final int index) {
|
void open(BuildContext context, final int index) async {
|
||||||
Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => GalleryPhotoViewWrapper(
|
builder: (context) => GalleryPhotoViewWrapper(
|
||||||
galleryItems: galleryItems,
|
galleryItems: galleryItems,
|
||||||
// backgroundDecoration: const BoxDecoration(
|
|
||||||
// color: Colors.black,
|
|
||||||
// ),
|
|
||||||
initialIndex: index,
|
initialIndex: index,
|
||||||
scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal,
|
scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
initAsync();
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ShareImageEditorView(
|
|
||||||
videoFilePath:
|
|
||||||
widget.galleryItems[currentIndex].videoPath,
|
|
||||||
imageBytes: widget
|
|
||||||
.galleryItems[currentIndex].imagePath
|
|
||||||
?.readAsBytes(),
|
|
||||||
mirrorVideo: false,
|
|
||||||
useHighQuality: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
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!,
|
|
||||||
mirrorVideo: item.mirrorVideo,
|
|
||||||
),
|
|
||||||
// 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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
190
lib/src/views/gallery/gallery_photo_view.dart
Normal file
190
lib/src/views/gallery/gallery_photo_view.dart
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
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/providers/api/media_received.dart' as received;
|
||||||
|
import 'package:twonly/src/providers/api/media_send.dart' as send;
|
||||||
|
import 'package:twonly/src/utils/misc.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';
|
||||||
|
import 'package:twonly/src/views/components/video_player_wrapper.dart';
|
||||||
|
import 'package:twonly/src/views/gallery/gallery_item.dart';
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future deleteFile() async {
|
||||||
|
List<Message> messages = widget.galleryItems[currentIndex].messages;
|
||||||
|
bool confirmed = await showAlertDialog(
|
||||||
|
context, "Are you sure?", "The image will be irrevocably deleted.");
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
widget.galleryItems[currentIndex].imagePath?.deleteSync();
|
||||||
|
widget.galleryItems[currentIndex].videoPath?.deleteSync();
|
||||||
|
for (final message in messages) {
|
||||||
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
|
message.messageId,
|
||||||
|
MessagesCompanion(mediaStored: Value(false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.galleryItems.removeAt(currentIndex);
|
||||||
|
setState(() {});
|
||||||
|
await send.purgeSendMediaFiles();
|
||||||
|
await received.purgeReceivedMediaFiles();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ShareImageEditorView(
|
||||||
|
videoFilePath:
|
||||||
|
widget.galleryItems[currentIndex].videoPath,
|
||||||
|
imageBytes: widget
|
||||||
|
.galleryItems[currentIndex].imagePath
|
||||||
|
?.readAsBytes(),
|
||||||
|
mirrorVideo: false,
|
||||||
|
useHighQuality: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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: Stack(
|
||||||
|
children: [
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 5,
|
||||||
|
child: PopupMenuButton<String>(
|
||||||
|
onSelected: (String result) {
|
||||||
|
if (result == "delete") {
|
||||||
|
deleteFile();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<String>>[
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'delete',
|
||||||
|
child: Text(context.lang.galleryDelete),
|
||||||
|
),
|
||||||
|
// PopupMenuItem<String>(
|
||||||
|
// value: 'details',
|
||||||
|
// child: Text(context.lang.galleryDetails),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
|
||||||
|
final GalleryItem item = widget.galleryItems[index];
|
||||||
|
return item.videoPath != null
|
||||||
|
? PhotoViewGalleryPageOptions.customChild(
|
||||||
|
child: VideoPlayerWrapper(
|
||||||
|
videoPath: item.videoPath!,
|
||||||
|
mirrorVideo: item.mirrorVideo,
|
||||||
|
),
|
||||||
|
// 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue