mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 08:32:13 +00:00
Merge pull request #411 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- New: Tutorial on how to use zoom. - New: Manage storage view. - Improved: Media thumbnails for faster loading. - Fix: Some message where not marked as opened.
This commit is contained in:
commit
5fb51b20d7
66 changed files with 16645 additions and 1077 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,6 +10,9 @@
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
.swiftpm/
|
.swiftpm/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.13
|
||||||
|
|
||||||
|
- New: Tutorial on how to use zoom.
|
||||||
|
- New: Manage storage view.
|
||||||
|
- Improved: Media thumbnails for faster loading.
|
||||||
|
- Fix: Some message where not marked as opened.
|
||||||
|
|
||||||
## 0.2.12
|
## 0.2.12
|
||||||
|
|
||||||
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# twonly
|
# twonly
|
||||||
|
|
||||||
<a href="https://twonly.eu" rel="some text"><img src="docs/header.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
|
<a href="https://twonly.eu" rel="some text"><img src="metadata/en-US/images/featureGraphic.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
|
||||||
|
|
||||||
This repository contains the complete source code of the [twonly](https://twonly.eu) app. twonly is a replacement for Snapchat, but its purpose is not to replace instant messaging apps, as there are already [many fantastic alternatives](https://www.messenger-matrix.de/messenger-matrix-en.html) out there. It was started because I liked the basic features of Snapchat, such as opening with the camera, the easy-to-use image editor, and the focus on sending fun pictures to friends. But I was annoyed by Snapchat's forced AI chat, receiving random messages to follow strangers, and not knowing how my sent images/text messages were encrypted, if at all. I am also very critical of the direction in which the US is currently moving and therefore try to avoid US providers wherever possible.
|
This repository contains the complete source code of the [twonly](https://twonly.eu) app. twonly is a replacement for Snapchat, but its purpose is not to replace instant messaging apps, as there are already [many fantastic alternatives](https://www.messenger-matrix.de/messenger-matrix-en.html) out there. It was started because I liked the basic features of Snapchat, such as opening with the camera, the easy-to-use image editor, and the focus on sending fun pictures to friends. But I was annoyed by Snapchat's forced AI chat, receiving random messages to follow strangers, and not knowing how my sent images/text messages were encrypted, if at all. I am also very critical of the direction in which the US is currently moving and therefore try to avoid US providers wherever possible.
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/fonts/NotoColorEmoji.ttf
Normal file
BIN
assets/fonts/NotoColorEmoji.ttf
Normal file
Binary file not shown.
BIN
docs/header.png
BIN
docs/header.png
Binary file not shown.
|
Before Width: | Height: | Size: 800 KiB |
|
|
@ -36,16 +36,18 @@ import workmanager_apple
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
override func application(
|
||||||
|
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||||
|
) -> Bool {
|
||||||
|
|
||||||
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
|
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
|
||||||
if sharingIntent.hasSameSchemePrefix(url: url) {
|
if sharingIntent.hasSameSchemePrefix(url: url) {
|
||||||
return sharingIntent.application(app, open: url, options: options)
|
return sharingIntent.application(app, open: url, options: options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed url handling for other Flutter libraries like app_links
|
// Proceed url handling for other Flutter libraries like app_links
|
||||||
return super.application(app, open: url, options:options)
|
return super.application(app, open: url, options: options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
|
@ -58,7 +60,8 @@ import workmanager_apple
|
||||||
NSLog(
|
NSLog(
|
||||||
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
|
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
|
||||||
response.notification.request.content.userInfo)
|
response.notification.request.content.userInfo)
|
||||||
//...
|
super.userNotificationCenter(
|
||||||
|
center, didReceive: response, withCompletionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func userNotificationCenter(
|
override func userNotificationCenter(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,5 @@ class AppState {
|
||||||
static bool isInBackgroundTask = false;
|
static bool isInBackgroundTask = false;
|
||||||
static bool allowErrorTrackingViaSentry = false;
|
static bool allowErrorTrackingViaSentry = false;
|
||||||
static bool gotMessageFromServer = false;
|
static bool gotMessageFromServer = false;
|
||||||
static int latestAppVersionId = 115;
|
static int latestAppVersionId = 116;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
154
lib/main.dart
154
lib/main.dart
|
|
@ -1,10 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
@ -15,34 +11,24 @@ import 'package:twonly/core/frb_generated.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/callbacks/callbacks.dart';
|
import 'package:twonly/src/callbacks/callbacks.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
|
||||||
show getSignalSignedPreKeyStoreOld;
|
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
|
||||||
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
|
||||||
import 'package:twonly/src/providers/connection.provider.dart';
|
import 'package:twonly/src/providers/connection.provider.dart';
|
||||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||||
import 'package:twonly/src/providers/settings.provider.dart';
|
import 'package:twonly/src/providers/settings.provider.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
|
||||||
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||||
import 'package:twonly/src/services/backup.service.dart';
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/memories/memories.service.dart';
|
import 'package:twonly/src/services/memories/memories.service.dart';
|
||||||
|
import 'package:twonly/src/services/migrations.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
|
||||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
|
||||||
import 'package:twonly/src/utils/startup_guard.dart';
|
import 'package:twonly/src/utils/startup_guard.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
|
||||||
|
|
||||||
final _initMutex = Mutex();
|
final _initMutex = Mutex();
|
||||||
|
|
||||||
|
|
@ -168,144 +154,6 @@ void main() async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runMigrations() async {
|
|
||||||
if (userService.currentUser.appVersion < 90) {
|
|
||||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
|
||||||
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
|
|
||||||
await UserService.update((u) => u.appVersion = 90);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userService.currentUser.appVersion < 91) {
|
|
||||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
|
||||||
await makeMigrationToVersion91();
|
|
||||||
await UserService.update((u) => u.appVersion = 91);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userService.currentUser.appVersion < 109) {
|
|
||||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
|
||||||
for (final contact in contacts) {
|
|
||||||
if (contact.verified) {
|
|
||||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
|
||||||
contact.userId,
|
|
||||||
VerificationType.migratedFromOldVersion,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await UserService.update((u) {
|
|
||||||
u
|
|
||||||
..appVersion = 109
|
|
||||||
..skipSetupPages = true;
|
|
||||||
if (u.avatarSvg == null) {
|
|
||||||
u.currentSetupPage = SetupPages.profile.name;
|
|
||||||
} else {
|
|
||||||
u.currentSetupPage = SetupPages.shareYourFriends.name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (userService.currentUser.appVersion < 113) {
|
|
||||||
var migrationSuccess = true;
|
|
||||||
final signalIdentity = await SecureStorage.instance.read(
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
key: SecureStorageKeys.signalIdentity,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signalIdentity != null) {
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(signalIdentity);
|
|
||||||
final identity = SignalIdentity.fromJson(
|
|
||||||
decoded as Map<String, dynamic>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await RustKeyManager.importSignalIdentity(
|
|
||||||
identityKeyPairStructure: identity.identityKeyPairU8List,
|
|
||||||
registrationId: identity.registrationId,
|
|
||||||
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
|
|
||||||
);
|
|
||||||
Log.info('Importing signal identiy to the rust key manager');
|
|
||||||
|
|
||||||
// Clean up old keys after successful migration
|
|
||||||
await SecureStorage.instance.delete(
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
key: SecureStorageKeys.signalIdentity,
|
|
||||||
);
|
|
||||||
await SecureStorage.instance.delete(
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
key: SecureStorageKeys.signalSignedPreKey,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Failed to migrate signal identity: $e');
|
|
||||||
migrationSuccess = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (migrationSuccess) {
|
|
||||||
await UserService.update((u) {
|
|
||||||
u
|
|
||||||
..appVersion = 113
|
|
||||||
..canUseLoginTokenForAuth = false
|
|
||||||
// As usernames changes where not considered in the old version force users
|
|
||||||
// to reenter there passwords.
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
..twonlySafeBackup?.encryptionKey = []
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
..twonlySafeBackup?.backupId = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (userService.currentUser.appVersion < 114) {
|
|
||||||
final allMedia = await twonlyDB.mediaFilesDao
|
|
||||||
.select(twonlyDB.mediaFiles)
|
|
||||||
.get();
|
|
||||||
for (final media in allMedia) {
|
|
||||||
if (media.createdAtMonth == null) {
|
|
||||||
final monthStr = DateFormat('MMMM yyyy').format(media.createdAt);
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
media.mediaId,
|
|
||||||
MediaFilesCompanion(createdAtMonth: Value(monthStr)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await UserService.update((u) => u.appVersion = 114);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userService.currentUser.appVersion < 115) {
|
|
||||||
var migrationSuccess = true;
|
|
||||||
try {
|
|
||||||
final rustStore = await RustKeyManager.loadSignedPrekeys();
|
|
||||||
for (final entry in rustStore.entries) {
|
|
||||||
final companion = SignalSignedPreKeyStoresCompanion(
|
|
||||||
signedPreKeyId: Value(entry.key),
|
|
||||||
signedPreKey: Value(entry.value),
|
|
||||||
);
|
|
||||||
await twonlyDB
|
|
||||||
.into(twonlyDB.signalSignedPreKeyStores)
|
|
||||||
.insert(
|
|
||||||
companion,
|
|
||||||
mode: InsertMode.insertOrReplace,
|
|
||||||
);
|
|
||||||
await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Failed to migrate signed prekeys to Drift: $e');
|
|
||||||
migrationSuccess = false;
|
|
||||||
}
|
|
||||||
if (migrationSuccess) {
|
|
||||||
await UserService.update((u) => u.appVersion = 115);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
|
||||||
assert(
|
|
||||||
AppState.latestAppVersionId == 115,
|
|
||||||
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
AppState.latestAppVersionId == userService.currentUser.appVersion,
|
|
||||||
"Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> postStartupTasks() async {
|
Future<void> postStartupTasks() async {
|
||||||
Log.info('Post startup started.');
|
Log.info('Post startup started.');
|
||||||
unawaited(MemoriesService.prewarmCache());
|
unawaited(MemoriesService.prewarmCache());
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class Routes {
|
||||||
'/settings/privacy/user_discovery';
|
'/settings/privacy/user_discovery';
|
||||||
static const String settingsNotification = '/settings/notification';
|
static const String settingsNotification = '/settings/notification';
|
||||||
static const String settingsStorage = '/settings/storage_data';
|
static const String settingsStorage = '/settings/storage_data';
|
||||||
|
static const String settingsStorageManage = '/settings/storage_data/manage';
|
||||||
static const String settingsStorageImport = '/settings/storage_data/import';
|
static const String settingsStorageImport = '/settings/storage_data/import';
|
||||||
static const String settingsStorageExport = '/settings/storage_data/export';
|
static const String settingsStorageExport = '/settings/storage_data/export';
|
||||||
static const String settingsHelp = '/settings/help';
|
static const String settingsHelp = '/settings/help';
|
||||||
|
|
|
||||||
|
|
@ -114,16 +114,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
|
Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
|
||||||
return (select(mediaFiles)..where(
|
return (select(mediaFiles)..where(
|
||||||
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
|
(t) =>
|
||||||
))
|
t.stored.equals(true) &
|
||||||
.get();
|
(t.storedFileHash.isNull() |
|
||||||
}
|
t.hasCropAnalyzed.equals(false) |
|
||||||
|
(t.hasThumbnail.equals(false) &
|
||||||
Future<List<MediaFile>> getAllUnanalyzedStoredMediaFiles() async {
|
t.type.equals(MediaType.audio.name).not()) |
|
||||||
return (select(mediaFiles)..where(
|
t.sizeInBytes.isNull()),
|
||||||
(t) => t.stored.equals(true) & t.hasCropAnalyzed.equals(false),
|
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
@ -185,4 +184,17 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
final rows = await query.get();
|
final rows = await query.get();
|
||||||
return rows.map((row) => row.readTable(db.messages).messageId).toList();
|
return rows.map((row) => row.readTable(db.messages).messageId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<MediaType, int>> getStorageStats() async {
|
||||||
|
final rows = await select(mediaFiles).get();
|
||||||
|
final stats = <MediaType, int>{};
|
||||||
|
|
||||||
|
for (final row in rows) {
|
||||||
|
final type = row.type;
|
||||||
|
final size = row.sizeInBytes ?? 0;
|
||||||
|
stats[type] = (stats[type] ?? 0) + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -249,41 +249,49 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleMessagesOpened(
|
Future<void> handleMessagesOpened(
|
||||||
int contactId,
|
Value<int> contactId,
|
||||||
List<String> messageIds,
|
List<String> messageIds,
|
||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
) async {
|
) async {
|
||||||
await batch((batch) async {
|
try {
|
||||||
for (final messageId in messageIds) {
|
await twonlyDB.batch((batch) async {
|
||||||
batch.insert(
|
for (final messageId in messageIds) {
|
||||||
messageActions,
|
batch.insert(
|
||||||
MessageActionsCompanion(
|
messageActions,
|
||||||
messageId: Value(messageId),
|
MessageActionsCompanion(
|
||||||
contactId: Value(contactId),
|
messageId: Value(messageId),
|
||||||
type: const Value(MessageActionType.openedAt),
|
contactId: contactId,
|
||||||
actionAt: Value(timestamp),
|
type: const Value(MessageActionType.openedAt),
|
||||||
),
|
actionAt: Value(timestamp),
|
||||||
mode: InsertMode.insertOrReplace,
|
),
|
||||||
);
|
mode: InsertMode.insertOrReplace,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
for (final messageId in messageIds) {
|
for (final messageId in messageIds) {
|
||||||
|
try {
|
||||||
final isOpenedByAll = await haveAllMembers(
|
final isOpenedByAll = await haveAllMembers(
|
||||||
messageId,
|
messageId,
|
||||||
MessageActionType.openedAt,
|
MessageActionType.openedAt,
|
||||||
);
|
);
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
|
|
||||||
batch.update(
|
await (update(
|
||||||
twonlyDB.messages,
|
messages,
|
||||||
|
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
openedAt: Value(now),
|
openedAt: Value(now),
|
||||||
openedByAll: Value(isOpenedByAll ? now : null),
|
openedByAll: Value(isOpenedByAll ? now : null),
|
||||||
),
|
),
|
||||||
where: (tbl) => tbl.messageId.equals(messageId),
|
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleMessageAckByServer(
|
Future<void> handleMessageAckByServer(
|
||||||
|
|
@ -309,21 +317,27 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
String messageId,
|
String messageId,
|
||||||
MessageActionType action,
|
MessageActionType action,
|
||||||
) async {
|
) async {
|
||||||
final message = await twonlyDB.messagesDao
|
try {
|
||||||
.getMessageById(messageId)
|
final message = await twonlyDB.messagesDao
|
||||||
.getSingleOrNull();
|
.getMessageById(messageId)
|
||||||
if (message == null) return true;
|
.getSingleOrNull();
|
||||||
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
if (message == null) return true;
|
||||||
message.groupId,
|
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||||
);
|
message.groupId,
|
||||||
|
);
|
||||||
|
|
||||||
final actions =
|
final actions =
|
||||||
await (select(messageActions)..where(
|
await (select(messageActions)..where(
|
||||||
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
|
(t) =>
|
||||||
))
|
t.type.equals(action.name) & t.messageId.equals(messageId),
|
||||||
.get();
|
))
|
||||||
|
.get();
|
||||||
|
|
||||||
return members.length == actions.length;
|
return members.length == actions.length;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateMessageId(
|
Future<void> updateMessageId(
|
||||||
|
|
|
||||||
3019
lib/src/database/schemas/twonly_db/drift_schema_v16.json
Normal file
3019
lib/src/database/schemas/twonly_db/drift_schema_v16.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,14 +3,13 @@ import 'dart:convert';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
|
||||||
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
|
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
|
||||||
final storeSerialized = await SecureStorage.instance.read(
|
final storeSerialized = await SecureStorage.instance.read(
|
||||||
key: SecureStorageKeys.signalSignedPreKey,
|
key: 'signed_pre_key_store',
|
||||||
);
|
);
|
||||||
final store = HashMap<int, Uint8List>();
|
final store = HashMap<int, Uint8List>();
|
||||||
if (storeSerialized == null) {
|
if (storeSerialized == null) {
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,11 @@ class MediaFiles extends Table {
|
||||||
|
|
||||||
BlobColumn get storedFileHash => blob().nullable()();
|
BlobColumn get storedFileHash => blob().nullable()();
|
||||||
|
|
||||||
|
BoolColumn get hasThumbnail =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
IntColumn get sizeInBytes => integer().nullable()();
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
TextColumn get createdAtMonth => text().nullable()();
|
TextColumn get createdAtMonth => text().nullable()();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 15;
|
int get schemaVersion => 16;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
|
|
@ -211,6 +211,13 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
from14To15: (m, schema) async {
|
from14To15: (m, schema) async {
|
||||||
await m.createTable(schema.signalSignedPreKeyStores);
|
await m.createTable(schema.signalSignedPreKeyStores);
|
||||||
},
|
},
|
||||||
|
from15To16: (m, schema) async {
|
||||||
|
await m.addColumn(
|
||||||
|
schema.mediaFiles,
|
||||||
|
schema.mediaFiles.hasThumbnail,
|
||||||
|
);
|
||||||
|
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
|
||||||
|
},
|
||||||
)(m, from, to);
|
)(m, from, to);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2810,6 +2810,32 @@ class $MediaFilesTable extends MediaFiles
|
||||||
type: DriftSqlType.blob,
|
type: DriftSqlType.blob,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _hasThumbnailMeta = const VerificationMeta(
|
||||||
|
'hasThumbnail',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> hasThumbnail = GeneratedColumn<bool>(
|
||||||
|
'has_thumbnail',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("has_thumbnail" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _sizeInBytesMeta = const VerificationMeta(
|
||||||
|
'sizeInBytes',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> sizeInBytes = GeneratedColumn<int>(
|
||||||
|
'size_in_bytes',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||||
'createdAt',
|
'createdAt',
|
||||||
);
|
);
|
||||||
|
|
@ -2853,6 +2879,8 @@ class $MediaFilesTable extends MediaFiles
|
||||||
encryptionMac,
|
encryptionMac,
|
||||||
encryptionNonce,
|
encryptionNonce,
|
||||||
storedFileHash,
|
storedFileHash,
|
||||||
|
hasThumbnail,
|
||||||
|
sizeInBytes,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdAtMonth,
|
createdAtMonth,
|
||||||
];
|
];
|
||||||
|
|
@ -2987,6 +3015,24 @@ class $MediaFilesTable extends MediaFiles
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('has_thumbnail')) {
|
||||||
|
context.handle(
|
||||||
|
_hasThumbnailMeta,
|
||||||
|
hasThumbnail.isAcceptableOrUnknown(
|
||||||
|
data['has_thumbnail']!,
|
||||||
|
_hasThumbnailMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('size_in_bytes')) {
|
||||||
|
context.handle(
|
||||||
|
_sizeInBytesMeta,
|
||||||
|
sizeInBytes.isAcceptableOrUnknown(
|
||||||
|
data['size_in_bytes']!,
|
||||||
|
_sizeInBytesMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('created_at')) {
|
if (data.containsKey('created_at')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_createdAtMeta,
|
_createdAtMeta,
|
||||||
|
|
@ -3092,6 +3138,14 @@ class $MediaFilesTable extends MediaFiles
|
||||||
DriftSqlType.blob,
|
DriftSqlType.blob,
|
||||||
data['${effectivePrefix}stored_file_hash'],
|
data['${effectivePrefix}stored_file_hash'],
|
||||||
),
|
),
|
||||||
|
hasThumbnail: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}has_thumbnail'],
|
||||||
|
)!,
|
||||||
|
sizeInBytes: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}size_in_bytes'],
|
||||||
|
),
|
||||||
createdAt: attachedDatabase.typeMapping.read(
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}created_at'],
|
data['${effectivePrefix}created_at'],
|
||||||
|
|
@ -3147,6 +3201,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
final Uint8List? encryptionMac;
|
final Uint8List? encryptionMac;
|
||||||
final Uint8List? encryptionNonce;
|
final Uint8List? encryptionNonce;
|
||||||
final Uint8List? storedFileHash;
|
final Uint8List? storedFileHash;
|
||||||
|
final bool hasThumbnail;
|
||||||
|
final int? sizeInBytes;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final String? createdAtMonth;
|
final String? createdAtMonth;
|
||||||
const MediaFile({
|
const MediaFile({
|
||||||
|
|
@ -3168,6 +3224,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
this.encryptionMac,
|
this.encryptionMac,
|
||||||
this.encryptionNonce,
|
this.encryptionNonce,
|
||||||
this.storedFileHash,
|
this.storedFileHash,
|
||||||
|
required this.hasThumbnail,
|
||||||
|
this.sizeInBytes,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.createdAtMonth,
|
this.createdAtMonth,
|
||||||
});
|
});
|
||||||
|
|
@ -3228,6 +3286,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
if (!nullToAbsent || storedFileHash != null) {
|
if (!nullToAbsent || storedFileHash != null) {
|
||||||
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
|
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
|
||||||
}
|
}
|
||||||
|
map['has_thumbnail'] = Variable<bool>(hasThumbnail);
|
||||||
|
if (!nullToAbsent || sizeInBytes != null) {
|
||||||
|
map['size_in_bytes'] = Variable<int>(sizeInBytes);
|
||||||
|
}
|
||||||
map['created_at'] = Variable<DateTime>(createdAt);
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
if (!nullToAbsent || createdAtMonth != null) {
|
if (!nullToAbsent || createdAtMonth != null) {
|
||||||
map['created_at_month'] = Variable<String>(createdAtMonth);
|
map['created_at_month'] = Variable<String>(createdAtMonth);
|
||||||
|
|
@ -3278,6 +3340,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
storedFileHash: storedFileHash == null && nullToAbsent
|
storedFileHash: storedFileHash == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(storedFileHash),
|
: Value(storedFileHash),
|
||||||
|
hasThumbnail: Value(hasThumbnail),
|
||||||
|
sizeInBytes: sizeInBytes == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(sizeInBytes),
|
||||||
createdAt: Value(createdAt),
|
createdAt: Value(createdAt),
|
||||||
createdAtMonth: createdAtMonth == null && nullToAbsent
|
createdAtMonth: createdAtMonth == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
|
|
@ -3323,6 +3389,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
|
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
|
||||||
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
|
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
|
||||||
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
||||||
|
hasThumbnail: serializer.fromJson<bool>(json['hasThumbnail']),
|
||||||
|
sizeInBytes: serializer.fromJson<int?>(json['sizeInBytes']),
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
||||||
);
|
);
|
||||||
|
|
@ -3357,6 +3425,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
|
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
|
||||||
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
|
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
|
||||||
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
||||||
|
'hasThumbnail': serializer.toJson<bool>(hasThumbnail),
|
||||||
|
'sizeInBytes': serializer.toJson<int?>(sizeInBytes),
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
||||||
};
|
};
|
||||||
|
|
@ -3381,6 +3451,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||||
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
|
bool? hasThumbnail,
|
||||||
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
}) => MediaFile(
|
}) => MediaFile(
|
||||||
|
|
@ -3421,6 +3493,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
storedFileHash: storedFileHash.present
|
storedFileHash: storedFileHash.present
|
||||||
? storedFileHash.value
|
? storedFileHash.value
|
||||||
: this.storedFileHash,
|
: this.storedFileHash,
|
||||||
|
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||||
|
sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
createdAtMonth: createdAtMonth.present
|
createdAtMonth: createdAtMonth.present
|
||||||
? createdAtMonth.value
|
? createdAtMonth.value
|
||||||
|
|
@ -3476,6 +3550,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
storedFileHash: data.storedFileHash.present
|
storedFileHash: data.storedFileHash.present
|
||||||
? data.storedFileHash.value
|
? data.storedFileHash.value
|
||||||
: this.storedFileHash,
|
: this.storedFileHash,
|
||||||
|
hasThumbnail: data.hasThumbnail.present
|
||||||
|
? data.hasThumbnail.value
|
||||||
|
: this.hasThumbnail,
|
||||||
|
sizeInBytes: data.sizeInBytes.present
|
||||||
|
? data.sizeInBytes.value
|
||||||
|
: this.sizeInBytes,
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
createdAtMonth: data.createdAtMonth.present
|
createdAtMonth: data.createdAtMonth.present
|
||||||
? data.createdAtMonth.value
|
? data.createdAtMonth.value
|
||||||
|
|
@ -3504,6 +3584,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
..write('encryptionMac: $encryptionMac, ')
|
..write('encryptionMac: $encryptionMac, ')
|
||||||
..write('encryptionNonce: $encryptionNonce, ')
|
..write('encryptionNonce: $encryptionNonce, ')
|
||||||
..write('storedFileHash: $storedFileHash, ')
|
..write('storedFileHash: $storedFileHash, ')
|
||||||
|
..write('hasThumbnail: $hasThumbnail, ')
|
||||||
|
..write('sizeInBytes: $sizeInBytes, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('createdAtMonth: $createdAtMonth')
|
..write('createdAtMonth: $createdAtMonth')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
|
|
@ -3511,7 +3593,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hashAll([
|
||||||
mediaId,
|
mediaId,
|
||||||
type,
|
type,
|
||||||
uploadState,
|
uploadState,
|
||||||
|
|
@ -3530,9 +3612,11 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
$driftBlobEquality.hash(encryptionMac),
|
$driftBlobEquality.hash(encryptionMac),
|
||||||
$driftBlobEquality.hash(encryptionNonce),
|
$driftBlobEquality.hash(encryptionNonce),
|
||||||
$driftBlobEquality.hash(storedFileHash),
|
$driftBlobEquality.hash(storedFileHash),
|
||||||
|
hasThumbnail,
|
||||||
|
sizeInBytes,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdAtMonth,
|
createdAtMonth,
|
||||||
);
|
]);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|
@ -3561,6 +3645,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
other.storedFileHash,
|
other.storedFileHash,
|
||||||
this.storedFileHash,
|
this.storedFileHash,
|
||||||
) &&
|
) &&
|
||||||
|
other.hasThumbnail == this.hasThumbnail &&
|
||||||
|
other.sizeInBytes == this.sizeInBytes &&
|
||||||
other.createdAt == this.createdAt &&
|
other.createdAt == this.createdAt &&
|
||||||
other.createdAtMonth == this.createdAtMonth);
|
other.createdAtMonth == this.createdAtMonth);
|
||||||
}
|
}
|
||||||
|
|
@ -3584,6 +3670,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
final Value<Uint8List?> encryptionMac;
|
final Value<Uint8List?> encryptionMac;
|
||||||
final Value<Uint8List?> encryptionNonce;
|
final Value<Uint8List?> encryptionNonce;
|
||||||
final Value<Uint8List?> storedFileHash;
|
final Value<Uint8List?> storedFileHash;
|
||||||
|
final Value<bool> hasThumbnail;
|
||||||
|
final Value<int?> sizeInBytes;
|
||||||
final Value<DateTime> createdAt;
|
final Value<DateTime> createdAt;
|
||||||
final Value<String?> createdAtMonth;
|
final Value<String?> createdAtMonth;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
|
|
@ -3606,6 +3694,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.encryptionMac = const Value.absent(),
|
this.encryptionMac = const Value.absent(),
|
||||||
this.encryptionNonce = const Value.absent(),
|
this.encryptionNonce = const Value.absent(),
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
|
this.hasThumbnail = const Value.absent(),
|
||||||
|
this.sizeInBytes = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.createdAtMonth = const Value.absent(),
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
|
|
@ -3629,6 +3719,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.encryptionMac = const Value.absent(),
|
this.encryptionMac = const Value.absent(),
|
||||||
this.encryptionNonce = const Value.absent(),
|
this.encryptionNonce = const Value.absent(),
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
|
this.hasThumbnail = const Value.absent(),
|
||||||
|
this.sizeInBytes = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.createdAtMonth = const Value.absent(),
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
|
|
@ -3653,6 +3745,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Expression<Uint8List>? encryptionMac,
|
Expression<Uint8List>? encryptionMac,
|
||||||
Expression<Uint8List>? encryptionNonce,
|
Expression<Uint8List>? encryptionNonce,
|
||||||
Expression<Uint8List>? storedFileHash,
|
Expression<Uint8List>? storedFileHash,
|
||||||
|
Expression<bool>? hasThumbnail,
|
||||||
|
Expression<int>? sizeInBytes,
|
||||||
Expression<DateTime>? createdAt,
|
Expression<DateTime>? createdAt,
|
||||||
Expression<String>? createdAtMonth,
|
Expression<String>? createdAtMonth,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
|
|
@ -3680,6 +3774,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (encryptionMac != null) 'encryption_mac': encryptionMac,
|
if (encryptionMac != null) 'encryption_mac': encryptionMac,
|
||||||
if (encryptionNonce != null) 'encryption_nonce': encryptionNonce,
|
if (encryptionNonce != null) 'encryption_nonce': encryptionNonce,
|
||||||
if (storedFileHash != null) 'stored_file_hash': storedFileHash,
|
if (storedFileHash != null) 'stored_file_hash': storedFileHash,
|
||||||
|
if (hasThumbnail != null) 'has_thumbnail': hasThumbnail,
|
||||||
|
if (sizeInBytes != null) 'size_in_bytes': sizeInBytes,
|
||||||
if (createdAt != null) 'created_at': createdAt,
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
if (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
if (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
|
@ -3705,6 +3801,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Value<Uint8List?>? encryptionMac,
|
Value<Uint8List?>? encryptionMac,
|
||||||
Value<Uint8List?>? encryptionNonce,
|
Value<Uint8List?>? encryptionNonce,
|
||||||
Value<Uint8List?>? storedFileHash,
|
Value<Uint8List?>? storedFileHash,
|
||||||
|
Value<bool>? hasThumbnail,
|
||||||
|
Value<int?>? sizeInBytes,
|
||||||
Value<DateTime>? createdAt,
|
Value<DateTime>? createdAt,
|
||||||
Value<String?>? createdAtMonth,
|
Value<String?>? createdAtMonth,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
|
|
@ -3731,6 +3829,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
encryptionMac: encryptionMac ?? this.encryptionMac,
|
encryptionMac: encryptionMac ?? this.encryptionMac,
|
||||||
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
|
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
|
||||||
storedFileHash: storedFileHash ?? this.storedFileHash,
|
storedFileHash: storedFileHash ?? this.storedFileHash,
|
||||||
|
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||||
|
sizeInBytes: sizeInBytes ?? this.sizeInBytes,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
|
|
@ -3810,6 +3910,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (storedFileHash.present) {
|
if (storedFileHash.present) {
|
||||||
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash.value);
|
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash.value);
|
||||||
}
|
}
|
||||||
|
if (hasThumbnail.present) {
|
||||||
|
map['has_thumbnail'] = Variable<bool>(hasThumbnail.value);
|
||||||
|
}
|
||||||
|
if (sizeInBytes.present) {
|
||||||
|
map['size_in_bytes'] = Variable<int>(sizeInBytes.value);
|
||||||
|
}
|
||||||
if (createdAt.present) {
|
if (createdAt.present) {
|
||||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
}
|
}
|
||||||
|
|
@ -3843,6 +3949,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
..write('encryptionMac: $encryptionMac, ')
|
..write('encryptionMac: $encryptionMac, ')
|
||||||
..write('encryptionNonce: $encryptionNonce, ')
|
..write('encryptionNonce: $encryptionNonce, ')
|
||||||
..write('storedFileHash: $storedFileHash, ')
|
..write('storedFileHash: $storedFileHash, ')
|
||||||
|
..write('hasThumbnail: $hasThumbnail, ')
|
||||||
|
..write('sizeInBytes: $sizeInBytes, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('createdAtMonth: $createdAtMonth, ')
|
..write('createdAtMonth: $createdAtMonth, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
|
|
@ -15344,6 +15452,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
|
||||||
Value<Uint8List?> encryptionMac,
|
Value<Uint8List?> encryptionMac,
|
||||||
Value<Uint8List?> encryptionNonce,
|
Value<Uint8List?> encryptionNonce,
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
|
Value<bool> hasThumbnail,
|
||||||
|
Value<int?> sizeInBytes,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<String?> createdAtMonth,
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
|
|
@ -15368,6 +15478,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
|
||||||
Value<Uint8List?> encryptionMac,
|
Value<Uint8List?> encryptionMac,
|
||||||
Value<Uint8List?> encryptionNonce,
|
Value<Uint8List?> encryptionNonce,
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
|
Value<bool> hasThumbnail,
|
||||||
|
Value<int?> sizeInBytes,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<String?> createdAtMonth,
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
|
|
@ -15499,6 +15611,16 @@ class $$MediaFilesTableFilterComposer
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get hasThumbnail => $composableBuilder(
|
||||||
|
column: $table.hasThumbnail,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get sizeInBytes => $composableBuilder(
|
||||||
|
column: $table.sizeInBytes,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $table.createdAt,
|
column: $table.createdAt,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
|
|
@ -15634,6 +15756,16 @@ class $$MediaFilesTableOrderingComposer
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get hasThumbnail => $composableBuilder(
|
||||||
|
column: $table.hasThumbnail,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get sizeInBytes => $composableBuilder(
|
||||||
|
column: $table.sizeInBytes,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $table.createdAt,
|
column: $table.createdAt,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
|
@ -15741,6 +15873,16 @@ class $$MediaFilesTableAnnotationComposer
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get hasThumbnail => $composableBuilder(
|
||||||
|
column: $table.hasThumbnail,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get sizeInBytes => $composableBuilder(
|
||||||
|
column: $table.sizeInBytes,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedColumn<DateTime> get createdAt =>
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
|
@ -15821,6 +15963,8 @@ class $$MediaFilesTableTableManager
|
||||||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||||
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
|
Value<bool> hasThumbnail = const Value.absent(),
|
||||||
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
|
|
@ -15843,6 +15987,8 @@ class $$MediaFilesTableTableManager
|
||||||
encryptionMac: encryptionMac,
|
encryptionMac: encryptionMac,
|
||||||
encryptionNonce: encryptionNonce,
|
encryptionNonce: encryptionNonce,
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
|
hasThumbnail: hasThumbnail,
|
||||||
|
sizeInBytes: sizeInBytes,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
createdAtMonth: createdAtMonth,
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
|
|
@ -15867,6 +16013,8 @@ class $$MediaFilesTableTableManager
|
||||||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||||
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
|
Value<bool> hasThumbnail = const Value.absent(),
|
||||||
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
|
|
@ -15889,6 +16037,8 @@ class $$MediaFilesTableTableManager
|
||||||
encryptionMac: encryptionMac,
|
encryptionMac: encryptionMac,
|
||||||
encryptionNonce: encryptionNonce,
|
encryptionNonce: encryptionNonce,
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
|
hasThumbnail: hasThumbnail,
|
||||||
|
sizeInBytes: sizeInBytes,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
createdAtMonth: createdAtMonth,
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
|
|
|
||||||
|
|
@ -8032,6 +8032,519 @@ i1.GeneratedColumn<i2.Uint8List> _column_243(String aliasedName) =>
|
||||||
type: i1.DriftSqlType.blob,
|
type: i1.DriftSqlType.blob,
|
||||||
$customConstraints: 'NOT NULL',
|
$customConstraints: 'NOT NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema16 extends i0.VersionedSchema {
|
||||||
|
Schema16({required super.database}) : super(version: 16);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
contacts,
|
||||||
|
groups,
|
||||||
|
mediaFiles,
|
||||||
|
messages,
|
||||||
|
messageHistories,
|
||||||
|
reactions,
|
||||||
|
groupMembers,
|
||||||
|
receipts,
|
||||||
|
receivedReceipts,
|
||||||
|
signalIdentityKeyStores,
|
||||||
|
signalPreKeyStores,
|
||||||
|
signalSenderKeyStores,
|
||||||
|
signalSessionStores,
|
||||||
|
signalSignedPreKeyStores,
|
||||||
|
messageActions,
|
||||||
|
groupHistories,
|
||||||
|
keyVerifications,
|
||||||
|
verificationTokens,
|
||||||
|
userDiscoveryAnnouncedUsers,
|
||||||
|
userDiscoveryUserRelations,
|
||||||
|
userDiscoveryOtherPromotions,
|
||||||
|
userDiscoveryOwnPromotions,
|
||||||
|
userDiscoveryShares,
|
||||||
|
shortcuts,
|
||||||
|
shortcutMembers,
|
||||||
|
];
|
||||||
|
late final Shape39 contacts = Shape39(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'contacts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_106,
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_211,
|
||||||
|
_column_212,
|
||||||
|
_column_213,
|
||||||
|
_column_214,
|
||||||
|
_column_215,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape23 groups = Shape23(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'groups',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
_column_130,
|
||||||
|
_column_131,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_118,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
_column_138,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
_column_142,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape51 mediaFiles = Shape51(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'media_files',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(media_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
_column_146,
|
||||||
|
_column_147,
|
||||||
|
_column_148,
|
||||||
|
_column_149,
|
||||||
|
_column_239,
|
||||||
|
_column_240,
|
||||||
|
_column_207,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
_column_153,
|
||||||
|
_column_154,
|
||||||
|
_column_155,
|
||||||
|
_column_156,
|
||||||
|
_column_157,
|
||||||
|
_column_244,
|
||||||
|
_column_245,
|
||||||
|
_column_118,
|
||||||
|
_column_241,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 messages = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_144,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_165,
|
||||||
|
_column_153,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_169,
|
||||||
|
_column_118,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 messageHistories = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_161,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 reactions = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'reactions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
|
||||||
|
columns: [_column_174, _column_176, _column_177, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 groupMembers = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_178,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_209,
|
||||||
|
_column_210,
|
||||||
|
_column_181,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape37 receipts = Shape37(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
_column_208,
|
||||||
|
_column_187,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape30 receivedReceipts = Shape30(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'received_receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [_column_182, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape31 signalIdentityKeyStores = Shape31(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_identity_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_194, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 signalPreKeyStores = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
|
||||||
|
columns: [_column_195, _column_196, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 signalSenderKeyStores = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_sender_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
|
||||||
|
columns: [_column_197, _column_198],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape33 signalSessionStores = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_session_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_199, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 signalSignedPreKeyStores = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_signed_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'],
|
||||||
|
columns: [_column_242, _column_243, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape34 messageActions = Shape34(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_actions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
|
||||||
|
columns: [_column_174, _column_183, _column_144, _column_200],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 groupHistories = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_history_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_201,
|
||||||
|
_column_158,
|
||||||
|
_column_202,
|
||||||
|
_column_203,
|
||||||
|
_column_204,
|
||||||
|
_column_205,
|
||||||
|
_column_206,
|
||||||
|
_column_144,
|
||||||
|
_column_200,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape40 keyVerifications = Shape40(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'key_verifications',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_216, _column_183, _column_144, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 verificationTokens = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'verification_tokens',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_217, _column_218, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape42 userDiscoveryAnnouncedUsers = Shape42(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_announced_users',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_219,
|
||||||
|
_column_220,
|
||||||
|
_column_221,
|
||||||
|
_column_222,
|
||||||
|
_column_223,
|
||||||
|
_column_224,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 userDiscoveryUserRelations = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_user_relations',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
|
||||||
|
columns: [_column_225, _column_226, _column_227],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 userDiscoveryOtherPromotions = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_other_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_226,
|
||||||
|
_column_228,
|
||||||
|
_column_229,
|
||||||
|
_column_230,
|
||||||
|
_column_231,
|
||||||
|
_column_227,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 userDiscoveryOwnPromotions = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_own_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_232, _column_183, _column_233],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 userDiscoveryShares = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_shares',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_234, _column_235, _column_175],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 shortcuts = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcuts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_173, _column_236, _column_237],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 shortcutMembers = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcut_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'],
|
||||||
|
columns: [_column_238, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape51 extends i0.VersionedTable {
|
||||||
|
Shape51({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get mediaId =>
|
||||||
|
columnsByName['media_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get uploadState =>
|
||||||
|
columnsByName['upload_state']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get downloadState =>
|
||||||
|
columnsByName['download_state']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get requiresAuthentication =>
|
||||||
|
columnsByName['requires_authentication']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get stored =>
|
||||||
|
columnsByName['stored']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get isDraftMedia =>
|
||||||
|
columnsByName['is_draft_media']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get hasCropAnalyzed =>
|
||||||
|
columnsByName['has_crop_analyzed']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get preProgressingProcess =>
|
||||||
|
columnsByName['pre_progressing_process']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get reuploadRequestedBy =>
|
||||||
|
columnsByName['reupload_requested_by']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get displayLimitInMilliseconds =>
|
||||||
|
columnsByName['display_limit_in_milliseconds']!
|
||||||
|
as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get removeAudio =>
|
||||||
|
columnsByName['remove_audio']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get downloadToken =>
|
||||||
|
columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get encryptionKey =>
|
||||||
|
columnsByName['encryption_key']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get encryptionMac =>
|
||||||
|
columnsByName['encryption_mac']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get encryptionNonce =>
|
||||||
|
columnsByName['encryption_nonce']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get storedFileHash =>
|
||||||
|
columnsByName['stored_file_hash']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<int> get hasThumbnail =>
|
||||||
|
columnsByName['has_thumbnail']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get sizeInBytes =>
|
||||||
|
columnsByName['size_in_bytes']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get createdAtMonth =>
|
||||||
|
columnsByName['created_at_month']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_244(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'has_thumbnail',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 0 CHECK (has_thumbnail IN (0, 1))',
|
||||||
|
defaultValue: const i1.CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_245(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'size_in_bytes',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NULL',
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
|
@ -8047,6 +8560,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -8120,6 +8634,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from14To15(migrator, schema);
|
await from14To15(migrator, schema);
|
||||||
return 15;
|
return 15;
|
||||||
|
case 15:
|
||||||
|
final schema = Schema16(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from15To16(migrator, schema);
|
||||||
|
return 16;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -8141,6 +8660,7 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
|
@ -8157,5 +8677,6 @@ i1.OnUpgrade stepByStep({
|
||||||
from12To13: from12To13,
|
from12To13: from12To13,
|
||||||
from13To14: from13To14,
|
from13To14: from13To14,
|
||||||
from14To15: from14To15,
|
from14To15: from14To15,
|
||||||
|
from15To16: from15To16,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -98,16 +98,10 @@ abstract class AppLocalizations {
|
||||||
Locale('en'),
|
Locale('en'),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// No description provided for @registerTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Welcome to twonly!'**
|
|
||||||
String get registerTitle;
|
|
||||||
|
|
||||||
/// No description provided for @registerSlogan.
|
/// No description provided for @registerSlogan.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing'**
|
/// **'Stay in touch with friends privately and securely.'**
|
||||||
String get registerSlogan;
|
String get registerSlogan;
|
||||||
|
|
||||||
/// No description provided for @onboardingWelcomeTitle.
|
/// No description provided for @onboardingWelcomeTitle.
|
||||||
|
|
@ -179,7 +173,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @registerUsernameSlogan.
|
/// No description provided for @registerUsernameSlogan.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Please select a username so others can find you!'**
|
/// **'Your public username'**
|
||||||
String get registerUsernameSlogan;
|
String get registerUsernameSlogan;
|
||||||
|
|
||||||
/// No description provided for @registerUsernameDecoration.
|
/// No description provided for @registerUsernameDecoration.
|
||||||
|
|
@ -191,7 +185,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @registerUsernameLimits.
|
/// No description provided for @registerUsernameLimits.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Your username must be at least 3 characters long.'**
|
/// **'At least 3 characters.'**
|
||||||
String get registerUsernameLimits;
|
String get registerUsernameLimits;
|
||||||
|
|
||||||
/// No description provided for @registerProofOfWorkFailed.
|
/// No description provided for @registerProofOfWorkFailed.
|
||||||
|
|
@ -542,6 +536,36 @@ abstract class AppLocalizations {
|
||||||
/// **'When using WI-FI'**
|
/// **'When using WI-FI'**
|
||||||
String get settingsStorageDataAutoDownWifi;
|
String get settingsStorageDataAutoDownWifi;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageManageTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Manage storage'**
|
||||||
|
String get settingsStorageManageTitle;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageUsed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Storage used'**
|
||||||
|
String get settingsStorageUsed;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageImages.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Images'**
|
||||||
|
String get settingsStorageImages;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageVideos.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Videos'**
|
||||||
|
String get settingsStorageVideos;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageGifs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'GIFs'**
|
||||||
|
String get settingsStorageGifs;
|
||||||
|
|
||||||
/// No description provided for @settingsProfileCustomizeAvatar.
|
/// No description provided for @settingsProfileCustomizeAvatar.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1553,15 +1577,9 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @twonlySafeRecoverTitle.
|
/// No description provided for @twonlySafeRecoverTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Recovery'**
|
/// **'Restore backup'**
|
||||||
String get twonlySafeRecoverTitle;
|
String get twonlySafeRecoverTitle;
|
||||||
|
|
||||||
/// No description provided for @twonlySafeRecoverDesc.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'If you have created a backup with twonly Backup, you can restore it here.'**
|
|
||||||
String get twonlySafeRecoverDesc;
|
|
||||||
|
|
||||||
/// No description provided for @twonlySafeRecoverBtn.
|
/// No description provided for @twonlySafeRecoverBtn.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3188,12 +3206,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Emoji already used or invalid'**
|
/// **'Emoji already used or invalid'**
|
||||||
String get errorEmojiUsedOrInvalid;
|
String get errorEmojiUsedOrInvalid;
|
||||||
|
|
||||||
/// No description provided for @subscriptionPledgeTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Support independent privacy.'**
|
|
||||||
String get subscriptionPledgeTitle;
|
|
||||||
|
|
||||||
/// No description provided for @subscriptionPledgeSecureTitle.
|
/// No description provided for @subscriptionPledgeSecureTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3218,17 +3230,17 @@ abstract class AppLocalizations {
|
||||||
/// **'twonly will never show advertisements or sell your private data.'**
|
/// **'twonly will never show advertisements or sell your private data.'**
|
||||||
String get subscriptionPledgeNoAdsDesc;
|
String get subscriptionPledgeNoAdsDesc;
|
||||||
|
|
||||||
/// No description provided for @subscriptionPledgeFundedTitle.
|
/// No description provided for @subscriptionPledgeSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Independent and funded by Users'**
|
/// **'Zero ads. Total privacy.'**
|
||||||
String get subscriptionPledgeFundedTitle;
|
String get subscriptionPledgeSubtitle;
|
||||||
|
|
||||||
/// No description provided for @subscriptionPledgeFundedDesc.
|
/// No description provided for @dragToZoom.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'**
|
/// **'Drag to Zoom'**
|
||||||
String get subscriptionPledgeFundedDesc;
|
String get dragToZoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import 'app_localizations.dart';
|
||||||
class AppLocalizationsDe extends AppLocalizations {
|
class AppLocalizationsDe extends AppLocalizations {
|
||||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||||
|
|
||||||
@override
|
|
||||||
String get registerTitle => 'Willkommen bei twonly!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerSlogan =>
|
String get registerSlogan =>
|
||||||
'twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.';
|
'Privat und sicher mit Freunden in Kontakt bleiben.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
||||||
|
|
@ -55,15 +52,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan =>
|
String get registerUsernameSlogan => 'Dein öffentlicher Benutzername';
|
||||||
'Bitte wähle einen Benutzernamen, damit dich andere finden können!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameDecoration => 'Benutzername';
|
String get registerUsernameDecoration => 'Benutzername';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameLimits =>
|
String get registerUsernameLimits => 'Mindestens 3 Zeichen.';
|
||||||
'Der Benutzername muss mindestens 3 Zeichen lang sein.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerProofOfWorkFailed =>
|
String get registerProofOfWorkFailed =>
|
||||||
|
|
@ -249,6 +244,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN';
|
String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageManageTitle => 'Speicher verwalten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageUsed => 'Speicherplatz belegt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageImages => 'Bilder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageVideos => 'Videos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageGifs => 'GIFs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
||||||
|
|
||||||
|
|
@ -801,11 +811,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get backupChangePassword => 'Password ändern';
|
String get backupChangePassword => 'Password ändern';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get twonlySafeRecoverTitle => 'Recovery';
|
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get twonlySafeRecoverDesc =>
|
|
||||||
'Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get twonlySafeRecoverBtn => 'Backup wiederherstellen';
|
String get twonlySafeRecoverBtn => 'Backup wiederherstellen';
|
||||||
|
|
@ -1798,9 +1804,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get errorEmojiUsedOrInvalid =>
|
String get errorEmojiUsedOrInvalid =>
|
||||||
'Emoji wird bereits verwendet oder ist ungültig';
|
'Emoji wird bereits verwendet oder ist ungültig';
|
||||||
|
|
||||||
@override
|
|
||||||
String get subscriptionPledgeTitle => 'Unterstütze unabhängigen Datenschutz.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||||
|
|
||||||
|
|
@ -1816,10 +1819,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.';
|
'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeFundedTitle =>
|
String get subscriptionPledgeSubtitle => 'Keine Werbung. Volle Privatsphäre.';
|
||||||
'Unabhängig und durch Nutzer finanziert';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeFundedDesc =>
|
String get dragToZoom => 'Zum Zoomen ziehen';
|
||||||
'twonly wird rein durch Nutzer-Abonnements finanziert, um unsere Unabhängigkeit und die Zukunft von twonly zu sichern.';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import 'app_localizations.dart';
|
||||||
class AppLocalizationsEn extends AppLocalizations {
|
class AppLocalizationsEn extends AppLocalizations {
|
||||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
@override
|
|
||||||
String get registerTitle => 'Welcome to twonly!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerSlogan =>
|
String get registerSlogan =>
|
||||||
'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing';
|
'Stay in touch with friends privately and securely.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
||||||
|
|
@ -54,15 +51,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get onboardingGetStartedTitle => 'Let\'s go!';
|
String get onboardingGetStartedTitle => 'Let\'s go!';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan =>
|
String get registerUsernameSlogan => 'Your public username';
|
||||||
'Please select a username so others can find you!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameDecoration => 'Username';
|
String get registerUsernameDecoration => 'Username';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameLimits =>
|
String get registerUsernameLimits => 'At least 3 characters.';
|
||||||
'Your username must be at least 3 characters long.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerProofOfWorkFailed =>
|
String get registerProofOfWorkFailed =>
|
||||||
|
|
@ -245,6 +240,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataAutoDownWifi => 'When using WI-FI';
|
String get settingsStorageDataAutoDownWifi => 'When using WI-FI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageManageTitle => 'Manage storage';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageUsed => 'Storage used';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageImages => 'Images';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageVideos => 'Videos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageGifs => 'GIFs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
||||||
|
|
||||||
|
|
@ -795,11 +805,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get backupChangePassword => 'Change password';
|
String get backupChangePassword => 'Change password';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get twonlySafeRecoverTitle => 'Recovery';
|
String get twonlySafeRecoverTitle => 'Restore backup';
|
||||||
|
|
||||||
@override
|
|
||||||
String get twonlySafeRecoverDesc =>
|
|
||||||
'If you have created a backup with twonly Backup, you can restore it here.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get twonlySafeRecoverBtn => 'Restore backup';
|
String get twonlySafeRecoverBtn => 'Restore backup';
|
||||||
|
|
@ -1782,9 +1788,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
|
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
|
||||||
|
|
||||||
@override
|
|
||||||
String get subscriptionPledgeTitle => 'Support independent privacy.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||||
|
|
||||||
|
|
@ -1800,9 +1803,8 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'twonly will never show advertisements or sell your private data.';
|
'twonly will never show advertisements or sell your private data.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeFundedTitle => 'Independent and funded by Users';
|
String get subscriptionPledgeSubtitle => 'Zero ads. Total privacy.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subscriptionPledgeFundedDesc =>
|
String get dragToZoom => 'Drag to Zoom';
|
||||||
'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit f649128fd875a12f23518ff2641190cc129a9339
|
Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93
|
||||||
|
|
@ -165,6 +165,9 @@ class UserData {
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool skipSetupPages = false;
|
bool skipSetupPages = false;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool hasZoomed = false;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
|
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
|
||||||
..requestedAudioPermission =
|
..requestedAudioPermission =
|
||||||
json['requestedAudioPermission'] as bool? ?? false
|
json['requestedAudioPermission'] as bool? ?? false
|
||||||
|
..automaticallyMarkEqualMediaFilesAsOpened =
|
||||||
|
json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false
|
||||||
..videoStabilizationEnabled =
|
..videoStabilizationEnabled =
|
||||||
json['videoStabilizationEnabled'] as bool? ?? true
|
json['videoStabilizationEnabled'] as bool? ?? true
|
||||||
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
||||||
|
|
@ -100,7 +102,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String)
|
: DateTime.parse(json['lastUserStudyDataUpload'] as String)
|
||||||
..skipSetupPages = json['skipSetupPages'] as bool? ?? false;
|
..skipSetupPages = json['skipSetupPages'] as bool? ?? false
|
||||||
|
..hasZoomed = json['hasZoomed'] as bool? ?? false;
|
||||||
|
|
||||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'userId': instance.userId,
|
'userId': instance.userId,
|
||||||
|
|
@ -121,6 +124,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||||
'defaultShowTime': instance.defaultShowTime,
|
'defaultShowTime': instance.defaultShowTime,
|
||||||
'requestedAudioPermission': instance.requestedAudioPermission,
|
'requestedAudioPermission': instance.requestedAudioPermission,
|
||||||
|
'automaticallyMarkEqualMediaFilesAsOpened':
|
||||||
|
instance.automaticallyMarkEqualMediaFilesAsOpened,
|
||||||
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
|
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
|
||||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||||
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
|
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
|
||||||
|
|
@ -160,6 +165,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
?.toIso8601String(),
|
?.toIso8601String(),
|
||||||
'currentSetupPage': instance.currentSetupPage,
|
'currentSetupPage': instance.currentSetupPage,
|
||||||
'skipSetupPages': instance.skipSetupPages,
|
'skipSetupPages': instance.skipSetupPages,
|
||||||
|
'hasZoomed': instance.hasZoomed,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
|
import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart';
|
import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart';
|
import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/data_and_storage/manage_storage.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
||||||
|
|
@ -210,6 +211,10 @@ final routerProvider = GoRouter(
|
||||||
path: 'storage_data',
|
path: 'storage_data',
|
||||||
builder: (context, state) => const DataAndStorageView(),
|
builder: (context, state) => const DataAndStorageView(),
|
||||||
routes: [
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'manage',
|
||||||
|
builder: (context, state) => const ManageStorageView(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'import',
|
path: 'import',
|
||||||
builder: (context, state) => const ImportMediaView(),
|
builder: (context, state) => const ImportMediaView(),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
|
@ -426,7 +425,7 @@ class ApiService {
|
||||||
|
|
||||||
Future<bool> tryAuthenticateWithToken() async {
|
Future<bool> tryAuthenticateWithToken() async {
|
||||||
final apiAuthToken = await SecureStorage.instance.read(
|
final apiAuthToken = await SecureStorage.instance.read(
|
||||||
key: SecureStorageKeys.apiAuthToken,
|
key: 'api_auth_token',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (apiAuthToken != null) {
|
if (apiAuthToken != null) {
|
||||||
|
|
@ -464,7 +463,7 @@ class ApiService {
|
||||||
Log.info('Switch was successfully.');
|
Log.info('Switch was successfully.');
|
||||||
await UserService.update((u) => u.canUseLoginTokenForAuth = true);
|
await UserService.update((u) => u.canUseLoginTokenForAuth = true);
|
||||||
await SecureStorage.instance.delete(
|
await SecureStorage.instance.delete(
|
||||||
key: SecureStorageKeys.apiAuthToken,
|
key: 'api_auth_token',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -586,7 +585,7 @@ class ApiService {
|
||||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||||
|
|
||||||
await SecureStorage.instance.write(
|
await SecureStorage.instance.write(
|
||||||
key: SecureStorageKeys.apiAuthToken,
|
key: 'api_auth_token',
|
||||||
value: apiAuthTokenB64,
|
value: apiAuthTokenB64,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/utils.api.dart';
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
|
|
@ -14,7 +15,7 @@ Future<void> handleMessageUpdate(
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await twonlyDB.messagesDao.handleMessagesOpened(
|
await twonlyDB.messagesDao.handleMessagesOpened(
|
||||||
contactId,
|
Value(contactId),
|
||||||
messageUpdate.multipleTargetMessageIds,
|
messageUpdate.multipleTargetMessageIds,
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
||||||
|
|
@ -129,7 +128,7 @@ Future<Map<String, String>?> getAuthenticationHeader() async {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
final apiAuthTokenRaw = await SecureStorage.instance.read(
|
final apiAuthTokenRaw = await SecureStorage.instance.read(
|
||||||
key: SecureStorageKeys.apiAuthToken,
|
key: 'api_auth_token',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (apiAuthTokenRaw == null) {
|
if (apiAuthTokenRaw == null) {
|
||||||
|
|
|
||||||
|
|
@ -200,14 +200,24 @@ class MediaFileService {
|
||||||
Log.error('Could not create Thumbnail as stored media does not exists.');
|
Log.error('Could not create Thumbnail as stored media does not exists.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var success = false;
|
||||||
switch (mediaFile.type) {
|
switch (mediaFile.type) {
|
||||||
case MediaType.gif:
|
case MediaType.gif:
|
||||||
case MediaType.audio:
|
success = await createThumbnailsForGif(storedPath, thumbnailPath);
|
||||||
case MediaType.image:
|
case MediaType.image:
|
||||||
// all images are already compress..
|
success = await createThumbnailsForImage(storedPath, thumbnailPath);
|
||||||
break;
|
|
||||||
case MediaType.video:
|
case MediaType.video:
|
||||||
await createThumbnailsForVideo(storedPath, thumbnailPath);
|
success = await createThumbnailsForVideo(storedPath, thumbnailPath);
|
||||||
|
case MediaType.audio:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasThumbnail: Value(true)),
|
||||||
|
);
|
||||||
|
await updateFromDB();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +263,9 @@ class MediaFileService {
|
||||||
tempPath.existsSync();
|
tempPath.existsSync();
|
||||||
|
|
||||||
bool get imagePreviewAvailable =>
|
bool get imagePreviewAvailable =>
|
||||||
thumbnailPath.existsSync() || storedPath.existsSync();
|
mediaFile.hasThumbnail ||
|
||||||
|
thumbnailPath.existsSync() ||
|
||||||
|
storedPath.existsSync();
|
||||||
|
|
||||||
Future<void> storeMediaFile() async {
|
Future<void> storeMediaFile() async {
|
||||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||||
|
|
@ -284,10 +296,24 @@ class MediaFileService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
unawaited(createThumbnail());
|
unawaited(createThumbnail());
|
||||||
|
await calculateAndSaveSize();
|
||||||
await hashMediaFile();
|
await hashMediaFile();
|
||||||
// updateFromDb is done in hashStoredMedia()
|
// updateFromDb is done in hashStoredMedia()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> calculateAndSaveSize() async {
|
||||||
|
if (storedPath.existsSync()) {
|
||||||
|
final size = storedPath.lengthSync();
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
MediaFilesCompanion(
|
||||||
|
sizeInBytes: Value(size),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await updateFromDB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> hashMediaFile() async {
|
Future<void> hashMediaFile() async {
|
||||||
late final List<int> checksum;
|
late final List<int> checksum;
|
||||||
if (storedPath.existsSync()) {
|
if (storedPath.existsSync()) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:pro_video_editor/pro_video_editor.dart';
|
import 'package:pro_video_editor/pro_video_editor.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
Future<void> createThumbnailsForVideo(
|
Future<bool> createThumbnailsForVideo(
|
||||||
File sourceFile,
|
File sourceFile,
|
||||||
File destinationFile,
|
File destinationFile,
|
||||||
) async {
|
) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
if (destinationFile.existsSync()) {
|
if (destinationFile.existsSync()) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final images = await ProVideoEditor.instance.getThumbnails(
|
final images = await ProVideoEditor.instance.getThumbnails(
|
||||||
|
|
@ -28,11 +30,83 @@ Future<void> createThumbnailsForVideo(
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
destinationFile.writeAsBytesSync(images.first);
|
destinationFile.writeAsBytesSync(images.first);
|
||||||
Log.info(
|
Log.info(
|
||||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the video thumbnail.',
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Thumbnail creation failed for the video with exit code.',
|
'Thumbnail creation failed for the video.',
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> createThumbnailsForImage(
|
||||||
|
File sourceFile,
|
||||||
|
File destinationFile,
|
||||||
|
) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await FlutterImageCompress.compressAndGetFile(
|
||||||
|
sourceFile.absolute.path,
|
||||||
|
destinationFile.absolute.path,
|
||||||
|
minWidth: 300,
|
||||||
|
minHeight: 300,
|
||||||
|
quality: 100,
|
||||||
|
format: CompressFormat.webp,
|
||||||
|
);
|
||||||
|
stopwatch.stop();
|
||||||
|
Log.info(
|
||||||
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error creating image thumbnail: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> createThumbnailsForGif(
|
||||||
|
File sourceFile,
|
||||||
|
File destinationFile,
|
||||||
|
) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (destinationFile.existsSync()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For GIFs, we decode the first frame and save it as WebP
|
||||||
|
final bytes = sourceFile.readAsBytesSync();
|
||||||
|
final image = img.decodeGif(bytes);
|
||||||
|
if (image == null) {
|
||||||
|
Log.error('Could not decode GIF for thumbnail.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbnail = img.copyResize(
|
||||||
|
image,
|
||||||
|
width: image.width > image.height ? 400 : null,
|
||||||
|
height: image.height >= image.width ? 400 : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final pngBytes = img.encodePng(thumbnail);
|
||||||
|
final webp = await FlutterImageCompress.compressWithList(
|
||||||
|
pngBytes,
|
||||||
|
format: CompressFormat.webp,
|
||||||
|
quality: 85,
|
||||||
|
);
|
||||||
|
destinationFile.writeAsBytesSync(webp);
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
Log.info(
|
||||||
|
'It took ${stopwatch.elapsedMilliseconds}ms to create the GIF thumbnail.',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error creating GIF thumbnail: $e');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
|
@ -14,6 +15,7 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
class MemoriesState {
|
class MemoriesState {
|
||||||
const MemoriesState({
|
const MemoriesState({
|
||||||
required this.filesToMigrate,
|
required this.filesToMigrate,
|
||||||
|
required this.totalFilesToMigrate,
|
||||||
required this.galleryItems,
|
required this.galleryItems,
|
||||||
required this.months,
|
required this.months,
|
||||||
required this.orderedByMonth,
|
required this.orderedByMonth,
|
||||||
|
|
@ -21,16 +23,21 @@ class MemoriesState {
|
||||||
});
|
});
|
||||||
|
|
||||||
final int filesToMigrate;
|
final int filesToMigrate;
|
||||||
|
final int totalFilesToMigrate;
|
||||||
final List<MemoryItem> galleryItems;
|
final List<MemoryItem> galleryItems;
|
||||||
final List<String> months;
|
final List<String> months;
|
||||||
final Map<String, List<int>> orderedByMonth;
|
final Map<String, List<int>> orderedByMonth;
|
||||||
final Map<int, List<MemoryItem>> galleryItemsLastYears;
|
final Map<int, List<MemoryItem>> galleryItemsLastYears;
|
||||||
|
|
||||||
bool get isLoading => filesToMigrate > 0;
|
bool get isLoading => filesToMigrate > 0;
|
||||||
|
double get migrationProgress => totalFilesToMigrate > 0
|
||||||
|
? (totalFilesToMigrate - filesToMigrate) / totalFilesToMigrate
|
||||||
|
: 0;
|
||||||
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
|
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
|
||||||
|
|
||||||
MemoriesState copyWith({
|
MemoriesState copyWith({
|
||||||
int? filesToMigrate,
|
int? filesToMigrate,
|
||||||
|
int? totalFilesToMigrate,
|
||||||
List<MemoryItem>? galleryItems,
|
List<MemoryItem>? galleryItems,
|
||||||
List<String>? months,
|
List<String>? months,
|
||||||
Map<String, List<int>>? orderedByMonth,
|
Map<String, List<int>>? orderedByMonth,
|
||||||
|
|
@ -38,6 +45,7 @@ class MemoriesState {
|
||||||
}) {
|
}) {
|
||||||
return MemoriesState(
|
return MemoriesState(
|
||||||
filesToMigrate: filesToMigrate ?? this.filesToMigrate,
|
filesToMigrate: filesToMigrate ?? this.filesToMigrate,
|
||||||
|
totalFilesToMigrate: totalFilesToMigrate ?? this.totalFilesToMigrate,
|
||||||
galleryItems: galleryItems ?? this.galleryItems,
|
galleryItems: galleryItems ?? this.galleryItems,
|
||||||
months: months ?? this.months,
|
months: months ?? this.months,
|
||||||
orderedByMonth: orderedByMonth ?? this.orderedByMonth,
|
orderedByMonth: orderedByMonth ?? this.orderedByMonth,
|
||||||
|
|
@ -62,6 +70,7 @@ class MemoriesService {
|
||||||
|
|
||||||
MemoriesState _currentState = const MemoriesState(
|
MemoriesState _currentState = const MemoriesState(
|
||||||
filesToMigrate: 0,
|
filesToMigrate: 0,
|
||||||
|
totalFilesToMigrate: 0,
|
||||||
galleryItems: [],
|
galleryItems: [],
|
||||||
months: [],
|
months: [],
|
||||||
orderedByMonth: {},
|
orderedByMonth: {},
|
||||||
|
|
@ -88,14 +97,10 @@ class MemoriesService {
|
||||||
final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
|
final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
|
||||||
mediaIds,
|
mediaIds,
|
||||||
);
|
);
|
||||||
final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m};
|
|
||||||
|
|
||||||
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
||||||
final contactMap = {for (final c in allContacts) c.userId: c};
|
final contactMap = {for (final c in allContacts) c.userId: c};
|
||||||
|
final mediaIdToSender = <String, Contact?>{};
|
||||||
final now = clock.now();
|
|
||||||
final tempGalleryItems = <MemoryItem>[];
|
|
||||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
|
||||||
|
|
||||||
for (final itemJson in itemList) {
|
for (final itemJson in itemList) {
|
||||||
final map = itemJson as Map<String, dynamic>;
|
final map = itemJson as Map<String, dynamic>;
|
||||||
|
|
@ -103,64 +108,14 @@ class MemoriesService {
|
||||||
final senderUserId = map['senderUserId'] as int?;
|
final senderUserId = map['senderUserId'] as int?;
|
||||||
if (mediaId == null) continue;
|
if (mediaId == null) continue;
|
||||||
|
|
||||||
final mediaFile = mediaFileMap[mediaId];
|
mediaIdToSender[mediaId] = senderUserId != null
|
||||||
if (mediaFile == null) continue;
|
|
||||||
|
|
||||||
final mediaService = MediaFileService(mediaFile);
|
|
||||||
if (!mediaService.imagePreviewAvailable) continue;
|
|
||||||
|
|
||||||
final contact = senderUserId != null
|
|
||||||
? contactMap[senderUserId]
|
? contactMap[senderUserId]
|
||||||
: null;
|
: null;
|
||||||
final item = MemoryItem(
|
|
||||||
mediaService: mediaService,
|
|
||||||
messages: [],
|
|
||||||
sender: contact,
|
|
||||||
);
|
|
||||||
tempGalleryItems.add(item);
|
|
||||||
|
|
||||||
if (mediaFile.createdAt.month == now.month &&
|
|
||||||
mediaFile.createdAt.day == now.day) {
|
|
||||||
final diff = now.year - mediaFile.createdAt.year;
|
|
||||||
if (diff > 0) {
|
|
||||||
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final tempOrderedByMonth = <String, List<int>>{};
|
_cachedState = _computeState(
|
||||||
final tempMonths = <String>[];
|
mediaFiles: mediaFiles,
|
||||||
var lastMonth = '';
|
mediaIdToSender: mediaIdToSender,
|
||||||
|
|
||||||
for (var i = 0; i < tempGalleryItems.length; i++) {
|
|
||||||
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
|
||||||
final month =
|
|
||||||
mFile.createdAtMonth ??
|
|
||||||
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
|
||||||
if (lastMonth != month) {
|
|
||||||
lastMonth = month;
|
|
||||||
tempMonths.add(month);
|
|
||||||
}
|
|
||||||
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final list in tempGalleryItemsLastYears.values) {
|
|
||||||
list.sort(
|
|
||||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
|
||||||
a.mediaService.mediaFile.createdAt,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final sortedGalleryItemsLastYears =
|
|
||||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
|
||||||
|
|
||||||
_cachedState = MemoriesState(
|
|
||||||
filesToMigrate: 0,
|
|
||||||
galleryItems: tempGalleryItems,
|
|
||||||
months: tempMonths,
|
|
||||||
orderedByMonth: tempOrderedByMonth,
|
|
||||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -168,34 +123,124 @@ class MemoriesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static MemoriesState _computeState({
|
||||||
|
required List<MediaFile> mediaFiles,
|
||||||
|
required Map<String, Contact?> mediaIdToSender,
|
||||||
|
int filesToMigrate = 0,
|
||||||
|
}) {
|
||||||
|
final now = clock.now();
|
||||||
|
final tempGalleryItems = <MemoryItem>[];
|
||||||
|
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||||
|
|
||||||
|
for (final mediaFile in mediaFiles) {
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
if (!mediaService.imagePreviewAvailable) continue;
|
||||||
|
|
||||||
|
final senderContact = mediaIdToSender[mediaFile.mediaId];
|
||||||
|
final item = MemoryItem(
|
||||||
|
mediaService: mediaService,
|
||||||
|
messages: [],
|
||||||
|
sender: senderContact,
|
||||||
|
);
|
||||||
|
|
||||||
|
tempGalleryItems.add(item);
|
||||||
|
|
||||||
|
if (mediaFile.createdAt.month == now.month &&
|
||||||
|
mediaFile.createdAt.day == now.day) {
|
||||||
|
final diff = now.year - mediaFile.createdAt.year;
|
||||||
|
if (diff > 0) {
|
||||||
|
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort descending by creation date
|
||||||
|
tempGalleryItems.sort(
|
||||||
|
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||||
|
a.mediaService.mediaFile.createdAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tempOrderedByMonth = <String, List<int>>{};
|
||||||
|
final tempMonths = <String>[];
|
||||||
|
var lastMonth = '';
|
||||||
|
|
||||||
|
for (var i = 0; i < tempGalleryItems.length; i++) {
|
||||||
|
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
||||||
|
final month =
|
||||||
|
mFile.createdAtMonth ??
|
||||||
|
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
||||||
|
if (lastMonth != month) {
|
||||||
|
lastMonth = month;
|
||||||
|
tempMonths.add(month);
|
||||||
|
}
|
||||||
|
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final list in tempGalleryItemsLastYears.values) {
|
||||||
|
list.sort(
|
||||||
|
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||||
|
a.mediaService.mediaFile.createdAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortedGalleryItemsLastYears =
|
||||||
|
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
||||||
|
|
||||||
|
return MemoriesState(
|
||||||
|
filesToMigrate: filesToMigrate,
|
||||||
|
totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating.
|
||||||
|
galleryItems: tempGalleryItems,
|
||||||
|
months: tempMonths,
|
||||||
|
orderedByMonth: tempOrderedByMonth,
|
||||||
|
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initAsync() async {
|
Future<void> _initAsync() async {
|
||||||
try {
|
try {
|
||||||
// 1. Perform Inventory / Migration of non-hashed stored files
|
final pendingFiles = await twonlyDB.mediaFilesDao
|
||||||
final nonHashedFiles = await twonlyDB.mediaFilesDao
|
.getAllMediaFilesPendingMigration();
|
||||||
.getAllNonHashedStoredMediaFiles();
|
|
||||||
final unanalyzedFiles = await twonlyDB.mediaFilesDao
|
|
||||||
.getAllUnanalyzedStoredMediaFiles();
|
|
||||||
|
|
||||||
final totalToMigrate = nonHashedFiles.length + unanalyzedFiles.length;
|
if (pendingFiles.isNotEmpty) {
|
||||||
if (totalToMigrate > 0) {
|
_currentState = _currentState.copyWith(
|
||||||
_updateState(filesToMigrate: totalToMigrate);
|
filesToMigrate: pendingFiles.length,
|
||||||
|
totalFilesToMigrate: pendingFiles.length,
|
||||||
|
);
|
||||||
|
_notifyState();
|
||||||
|
|
||||||
for (final mediaFile in nonHashedFiles) {
|
for (final mediaFile in pendingFiles) {
|
||||||
final mediaService = MediaFileService(mediaFile);
|
final mediaService = MediaFileService(mediaFile);
|
||||||
await mediaService.hashMediaFile();
|
|
||||||
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
|
if (mediaService.mediaFile.storedFileHash == null) {
|
||||||
|
await mediaService.hashMediaFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mediaService.mediaFile.hasCropAnalyzed) {
|
||||||
|
await mediaService.cropTransparentBorders();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaService.mediaFile.sizeInBytes == null) {
|
||||||
|
await mediaService.calculateAndSaveSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mediaService.mediaFile.hasThumbnail) {
|
||||||
|
if (mediaService.thumbnailPath.existsSync()) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasThumbnail: Value(true)),
|
||||||
|
);
|
||||||
|
} else if (mediaFile.type != MediaType.audio) {
|
||||||
|
await mediaService.createThumbnail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_updateMigrationCount(_currentState.filesToMigrate - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final mediaFile in unanalyzedFiles) {
|
_updateMigrationCount(0);
|
||||||
final mediaService = MediaFileService(mediaFile);
|
|
||||||
await mediaService.cropTransparentBorders();
|
|
||||||
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateState(filesToMigrate: 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Subscribe to stored media files stream
|
|
||||||
await _dbSubscription?.cancel();
|
await _dbSubscription?.cancel();
|
||||||
_dbSubscription = twonlyDB.mediaFilesDao
|
_dbSubscription = twonlyDB.mediaFilesDao
|
||||||
.watchAllStoredMediaFiles()
|
.watchAllStoredMediaFiles()
|
||||||
|
|
@ -207,11 +252,6 @@ class MemoriesService {
|
||||||
|
|
||||||
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
|
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
|
||||||
try {
|
try {
|
||||||
final now = clock.now();
|
|
||||||
final tempGalleryItems = <MemoryItem>[];
|
|
||||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
|
||||||
|
|
||||||
// High-performance batch DB fetch for sender attribution via Messages table mapping
|
|
||||||
final mediaIds = mediaFiles.map((m) => m.mediaId).toList();
|
final mediaIds = mediaFiles.map((m) => m.mediaId).toList();
|
||||||
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
|
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
|
||||||
mediaIds,
|
mediaIds,
|
||||||
|
|
@ -230,82 +270,24 @@ class MemoriesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final mediaFile in mediaFiles) {
|
final newState = _computeState(
|
||||||
final mediaService = MediaFileService(mediaFile);
|
mediaFiles: mediaFiles,
|
||||||
if (!mediaService.imagePreviewAvailable) continue;
|
mediaIdToSender: mediaIdToSenderContact,
|
||||||
|
|
||||||
if (mediaService.mediaFile.type == MediaType.video) {
|
|
||||||
if (!mediaService.thumbnailPath.existsSync()) {
|
|
||||||
unawaited(mediaService.createThumbnail());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final senderContact = mediaIdToSenderContact[mediaFile.mediaId];
|
|
||||||
final item = MemoryItem(
|
|
||||||
mediaService: mediaService,
|
|
||||||
messages: [],
|
|
||||||
sender: senderContact,
|
|
||||||
);
|
|
||||||
|
|
||||||
tempGalleryItems.add(item);
|
|
||||||
|
|
||||||
if (mediaFile.createdAt.month == now.month &&
|
|
||||||
mediaFile.createdAt.day == now.day) {
|
|
||||||
final diff = now.year - mediaFile.createdAt.year;
|
|
||||||
if (diff > 0) {
|
|
||||||
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort descending by creation date
|
|
||||||
tempGalleryItems.sort(
|
|
||||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
|
||||||
a.mediaService.mediaFile.createdAt,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final tempOrderedByMonth = <String, List<int>>{};
|
|
||||||
final tempMonths = <String>[];
|
|
||||||
var lastMonth = '';
|
|
||||||
|
|
||||||
// High performance grouping leveraging pre-computed createdAtMonth column
|
|
||||||
for (var i = 0; i < tempGalleryItems.length; i++) {
|
|
||||||
final mFile = tempGalleryItems[i].mediaService.mediaFile;
|
|
||||||
final month =
|
|
||||||
mFile.createdAtMonth ??
|
|
||||||
DateFormat('MMMM yyyy').format(mFile.createdAt);
|
|
||||||
if (lastMonth != month) {
|
|
||||||
lastMonth = month;
|
|
||||||
tempMonths.add(month);
|
|
||||||
}
|
|
||||||
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final list in tempGalleryItemsLastYears.values) {
|
|
||||||
list.sort(
|
|
||||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
|
||||||
a.mediaService.mediaFile.createdAt,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final sortedGalleryItemsLastYears =
|
|
||||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
|
||||||
|
|
||||||
final newState = MemoriesState(
|
|
||||||
filesToMigrate: _currentState.filesToMigrate,
|
filesToMigrate: _currentState.filesToMigrate,
|
||||||
galleryItems: tempGalleryItems,
|
).copyWith(totalFilesToMigrate: _currentState.totalFilesToMigrate);
|
||||||
months: tempMonths,
|
|
||||||
orderedByMonth: tempOrderedByMonth,
|
for (final item in newState.galleryItems) {
|
||||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
if (!item.mediaService.mediaFile.hasThumbnail &&
|
||||||
);
|
item.mediaService.mediaFile.type != MediaType.audio) {
|
||||||
|
unawaited(item.mediaService.createThumbnail());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_cachedState = newState;
|
_cachedState = newState;
|
||||||
_updateStateWithObject(newState);
|
_updateState(newState);
|
||||||
|
|
||||||
// Persist to KeyValueStore cache asynchronously
|
// Persist to KeyValueStore cache asynchronously
|
||||||
final cacheList = tempGalleryItems
|
final cacheList = newState.galleryItems
|
||||||
.map(
|
.map(
|
||||||
(item) => {
|
(item) => {
|
||||||
'mediaId': item.mediaService.mediaFile.mediaId,
|
'mediaId': item.mediaService.mediaFile.mediaId,
|
||||||
|
|
@ -319,15 +301,17 @@ class MemoriesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateStateWithObject(MemoriesState newState) {
|
void _updateState(MemoriesState newState) {
|
||||||
_currentState = newState;
|
_currentState = newState;
|
||||||
if (!_stateController.isClosed) {
|
_notifyState();
|
||||||
_stateController.add(_currentState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateState({int? filesToMigrate}) {
|
void _updateMigrationCount(int filesToMigrate) {
|
||||||
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
|
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
|
||||||
|
_notifyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _notifyState() {
|
||||||
if (!_stateController.isClosed) {
|
if (!_stateController.isClosed) {
|
||||||
_stateController.add(_currentState);
|
_stateController.add(_currentState);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
lib/src/services/migrations.service.dart
Normal file
184
lib/src/services/migrations.service.dart
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
|
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
||||||
|
show getSignalSignedPreKeyStoreOld;
|
||||||
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
||||||
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
|
Future<void> runMigrations() async {
|
||||||
|
if (userService.currentUser.appVersion < 90) {
|
||||||
|
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||||
|
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
|
||||||
|
await UserService.update((u) => u.appVersion = 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.appVersion < 91) {
|
||||||
|
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||||
|
await makeMigrationToVersion91();
|
||||||
|
await UserService.update((u) => u.appVersion = 91);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.appVersion < 109) {
|
||||||
|
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.verified) {
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||||
|
contact.userId,
|
||||||
|
VerificationType.migratedFromOldVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..appVersion = 109
|
||||||
|
..skipSetupPages = true;
|
||||||
|
if (u.avatarSvg == null) {
|
||||||
|
u.currentSetupPage = SetupPages.profile.name;
|
||||||
|
} else {
|
||||||
|
u.currentSetupPage = SetupPages.shareYourFriends.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userService.currentUser.appVersion < 113) {
|
||||||
|
var migrationSuccess = true;
|
||||||
|
final signalIdentity = await SecureStorage.instance.read(
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
key: SecureStorageKeys.signalIdentity,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (signalIdentity != null) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(signalIdentity);
|
||||||
|
final identity = SignalIdentity.fromJson(
|
||||||
|
decoded as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await RustKeyManager.importSignalIdentity(
|
||||||
|
identityKeyPairStructure: identity.identityKeyPairU8List,
|
||||||
|
registrationId: identity.registrationId,
|
||||||
|
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
|
||||||
|
);
|
||||||
|
Log.info('Importing signal identify to the rust key manager');
|
||||||
|
|
||||||
|
// Clean up old keys after successful migration
|
||||||
|
await SecureStorage.instance.delete(
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
key: SecureStorageKeys.signalIdentity,
|
||||||
|
);
|
||||||
|
await SecureStorage.instance.delete(
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
key: SecureStorageKeys.signalSignedPreKey,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to migrate signal identity: $e');
|
||||||
|
migrationSuccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrationSuccess) {
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..appVersion = 113
|
||||||
|
..canUseLoginTokenForAuth = false
|
||||||
|
// As usernames changes where not considered in the old version force users
|
||||||
|
// to reenter there passwords.
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
..twonlySafeBackup?.encryptionKey = []
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
..twonlySafeBackup?.backupId = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userService.currentUser.appVersion < 114) {
|
||||||
|
final allMedia = await twonlyDB.mediaFilesDao
|
||||||
|
.select(twonlyDB.mediaFiles)
|
||||||
|
.get();
|
||||||
|
for (final media in allMedia) {
|
||||||
|
if (media.createdAtMonth == null) {
|
||||||
|
final monthStr = DateFormat('MMMM yyyy').format(media.createdAt);
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
media.mediaId,
|
||||||
|
MediaFilesCompanion(createdAtMonth: Value(monthStr)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UserService.update((u) => u.appVersion = 114);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.appVersion < 115) {
|
||||||
|
var migrationSuccess = true;
|
||||||
|
try {
|
||||||
|
final rustStore = await RustKeyManager.loadSignedPrekeys();
|
||||||
|
for (final entry in rustStore.entries) {
|
||||||
|
final companion = SignalSignedPreKeyStoresCompanion(
|
||||||
|
signedPreKeyId: Value(entry.key),
|
||||||
|
signedPreKey: Value(entry.value),
|
||||||
|
);
|
||||||
|
await twonlyDB
|
||||||
|
.into(twonlyDB.signalSignedPreKeyStores)
|
||||||
|
.insert(
|
||||||
|
companion,
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to migrate signed prekeys to Drift: $e');
|
||||||
|
migrationSuccess = false;
|
||||||
|
}
|
||||||
|
if (migrationSuccess) {
|
||||||
|
await UserService.update((u) => u.appVersion = 115);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.appVersion < 116) {
|
||||||
|
// Because of a Bug in the handleMessagesOpened function, some messages where not marked as opened. So use the logs,
|
||||||
|
// to mark the files as opened.
|
||||||
|
final logs = await loadLogFile();
|
||||||
|
final openedMessages = logs.split(
|
||||||
|
'messages.c2c.dart:12 > Opened message [',
|
||||||
|
);
|
||||||
|
for (final opened in openedMessages) {
|
||||||
|
final messageIds = opened.split(']');
|
||||||
|
if (messageIds.isNotEmpty) {
|
||||||
|
final now = clock.now();
|
||||||
|
for (final messageId in messageIds.first.split(',')) {
|
||||||
|
await (twonlyDB.update(
|
||||||
|
twonlyDB.messages,
|
||||||
|
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||||
|
MessagesCompanion(
|
||||||
|
openedAt: Value(now),
|
||||||
|
openedByAll: Value(now),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UserService.update((u) => u.appVersion = 116);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
assert(
|
||||||
|
AppState.latestAppVersionId == 116,
|
||||||
|
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
AppState.latestAppVersionId == userService.currentUser.appVersion,
|
||||||
|
"Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import 'dart:convert';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -38,7 +37,7 @@ class UserService {
|
||||||
|
|
||||||
// 2. If not found, try to load from SecureStorage (Migration path)
|
// 2. If not found, try to load from SecureStorage (Migration path)
|
||||||
final userDataJson = await SecureStorage.instance.read(
|
final userDataJson = await SecureStorage.instance.read(
|
||||||
key: SecureStorageKeys.userData,
|
key: 'userData',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userDataJson != null) {
|
if (userDataJson != null) {
|
||||||
|
|
|
||||||
342
lib/src/visual/components/draggable_scrollbar.comp.dart
Normal file
342
lib/src/visual/components/draggable_scrollbar.comp.dart
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
typedef TextLabelBuilder = Widget Function(String label);
|
||||||
|
|
||||||
|
class DraggableScrollbar extends StatefulWidget {
|
||||||
|
const DraggableScrollbar({
|
||||||
|
required this.controller,
|
||||||
|
required this.child,
|
||||||
|
this.labelBuilder,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final ScrollController controller;
|
||||||
|
final Widget child;
|
||||||
|
final String? Function(double scrollOffset)? labelBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DraggableScrollbar> createState() => _DraggableScrollbarState();
|
||||||
|
|
||||||
|
static const double labelThumbPadding = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0);
|
||||||
|
final ValueNotifier<double> _viewOffsetNotifier = ValueNotifier(0);
|
||||||
|
bool _isDragInProcess = false;
|
||||||
|
double _boundlessThumbOffset = 0;
|
||||||
|
|
||||||
|
late AnimationController _thumbAnimationController;
|
||||||
|
late CurvedAnimation _thumbAnimation;
|
||||||
|
late AnimationController _labelAnimationController;
|
||||||
|
late CurvedAnimation _labelAnimation;
|
||||||
|
Timer? _fadeoutTimer;
|
||||||
|
|
||||||
|
static const double thumbHeight = 60;
|
||||||
|
static const double thumbWidth = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_thumbAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
_thumbAnimation = CurvedAnimation(
|
||||||
|
parent: _thumbAnimationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
_labelAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
_labelAnimation = CurvedAnimation(
|
||||||
|
parent: _labelAnimationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_thumbOffsetNotifier.dispose();
|
||||||
|
_viewOffsetNotifier.dispose();
|
||||||
|
_thumbAnimation.dispose();
|
||||||
|
_thumbAnimationController.dispose();
|
||||||
|
_labelAnimation.dispose();
|
||||||
|
_labelAnimationController.dispose();
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollController get controller => widget.controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
_onScrollNotification(notification);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
RepaintBoundary(
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
// Scrollbar layer restricted to SafeArea
|
||||||
|
SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final maxThumbOffset = constraints.maxHeight - thumbHeight;
|
||||||
|
|
||||||
|
return ExcludeSemantics(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _thumbOffsetNotifier,
|
||||||
|
builder: (context, thumbOffset, child) {
|
||||||
|
final isDark =
|
||||||
|
Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final handleColor = isDark
|
||||||
|
? Colors.grey.shade900
|
||||||
|
: Colors.white;
|
||||||
|
final iconColor = isDark
|
||||||
|
? Colors.white70
|
||||||
|
: Colors.black54;
|
||||||
|
|
||||||
|
final label = widget.labelBuilder?.call(
|
||||||
|
_viewOffsetNotifier.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
alignment: AlignmentDirectional.topEnd,
|
||||||
|
padding: EdgeInsets.only(top: thumbOffset),
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onVerticalDragStart: (_) => _onVerticalDragStart(),
|
||||||
|
onVerticalDragUpdate: (details) =>
|
||||||
|
_onVerticalDragUpdate(
|
||||||
|
details.delta.dy,
|
||||||
|
maxThumbOffset,
|
||||||
|
),
|
||||||
|
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
|
||||||
|
child: SlideFadeTransition(
|
||||||
|
animation: _thumbAnimation,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (label != null && _isDragInProcess)
|
||||||
|
FadeTransition(
|
||||||
|
opacity: _labelAnimation,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _labelAnimation,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
right: DraggableScrollbar
|
||||||
|
.labelThumbPadding,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 6,
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
(isDark
|
||||||
|
? Colors.grey.shade900
|
||||||
|
: Colors.grey.shade200)
|
||||||
|
.withValues(alpha: 0.95),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
8,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(
|
||||||
|
alpha: 0.2,
|
||||||
|
),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: thumbWidth,
|
||||||
|
height: thumbHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: handleColor,
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white10
|
||||||
|
: Colors.black12,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
bottomLeft: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.angleUp,
|
||||||
|
size: 14,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.angleDown,
|
||||||
|
size: 14,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScrollNotification(ScrollNotification notification) {
|
||||||
|
final scrollMetrics = notification.metrics;
|
||||||
|
if (scrollMetrics.minScrollExtent >= scrollMetrics.maxScrollExtent) return;
|
||||||
|
|
||||||
|
_viewOffsetNotifier.value = scrollMetrics.pixels;
|
||||||
|
|
||||||
|
if (!_isDragInProcess) {
|
||||||
|
if (notification is ScrollUpdateNotification) {
|
||||||
|
// Find constraints and update thumb offset
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final renderBox = context.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox == null) return;
|
||||||
|
|
||||||
|
// Subtract SafeArea top/bottom
|
||||||
|
final padding = MediaQuery.paddingOf(context);
|
||||||
|
final availableHeight = renderBox.size.height - padding.vertical;
|
||||||
|
final maxThumbOffset = availableHeight - thumbHeight;
|
||||||
|
|
||||||
|
final scrollExtent =
|
||||||
|
scrollMetrics.pixels /
|
||||||
|
scrollMetrics.maxScrollExtent *
|
||||||
|
maxThumbOffset;
|
||||||
|
_thumbOffsetNotifier.value = scrollExtent.clamp(0.0, maxThumbOffset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification is ScrollUpdateNotification ||
|
||||||
|
notification is OverscrollNotification) {
|
||||||
|
_showThumb();
|
||||||
|
_scheduleFadeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalDragStart() {
|
||||||
|
_boundlessThumbOffset = _thumbOffsetNotifier.value;
|
||||||
|
_labelAnimationController.forward();
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
_showThumb();
|
||||||
|
setState(() => _isDragInProcess = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalDragUpdate(double deltaY, double maxThumbOffset) {
|
||||||
|
_showThumb();
|
||||||
|
if (_isDragInProcess && maxThumbOffset > 0) {
|
||||||
|
_boundlessThumbOffset += deltaY;
|
||||||
|
_thumbOffsetNotifier.value = _boundlessThumbOffset.clamp(
|
||||||
|
0.0,
|
||||||
|
maxThumbOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
final max = controller.position.maxScrollExtent;
|
||||||
|
final scrollOffset = (_thumbOffsetNotifier.value / maxThumbOffset) * max;
|
||||||
|
controller.jumpTo(
|
||||||
|
scrollOffset.clamp(0.0, controller.position.maxScrollExtent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalDragEnd() {
|
||||||
|
_scheduleFadeout();
|
||||||
|
setState(() => _isDragInProcess = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showThumb() {
|
||||||
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
|
_thumbAnimationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleFadeout() {
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
_fadeoutTimer = Timer(const Duration(milliseconds: 1500), () {
|
||||||
|
_thumbAnimationController.reverse();
|
||||||
|
_labelAnimationController.reverse();
|
||||||
|
_fadeoutTimer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SlideFadeTransition extends StatelessWidget {
|
||||||
|
const SlideFadeTransition({
|
||||||
|
required this.animation,
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final Animation<double> animation;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: animation.value,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,8 +53,12 @@ class EmojiPickerBottom extends StatelessWidget {
|
||||||
config: Config(
|
config: Config(
|
||||||
height: 400,
|
height: 400,
|
||||||
locale: Localizations.localeOf(context),
|
locale: Localizations.localeOf(context),
|
||||||
|
checkPlatformCompatibility: false,
|
||||||
emojiTextStyle: TextStyle(
|
emojiTextStyle: TextStyle(
|
||||||
fontSize: 24 * (Platform.isIOS ? 1.2 : 1),
|
fontSize: 24 * (Platform.isIOS ? 1.2 : 1),
|
||||||
|
fontFamilyFallback: Platform.isAndroid
|
||||||
|
? const ['NotoColorEmoji']
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
emojiViewConfig: EmojiViewConfig(
|
emojiViewConfig: EmojiViewConfig(
|
||||||
backgroundColor: context.color.surfaceContainer,
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
final ThemeData darkTheme = ThemeData.dark().copyWith(
|
final ThemeData darkTheme = () {
|
||||||
colorScheme: ColorScheme.fromSeed(
|
final base = ThemeData.dark().copyWith(
|
||||||
brightness: Brightness.dark,
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: const Color(0xFF57CC99),
|
brightness: Brightness.dark,
|
||||||
surface: const Color.fromARGB(255, 20, 18, 23),
|
seedColor: const Color(0xFF57CC99),
|
||||||
surfaceContainer: const Color.fromARGB(255, 45, 41, 54),
|
surface: const Color.fromARGB(255, 20, 18, 23),
|
||||||
surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45),
|
surfaceContainer: const Color.fromARGB(255, 45, 41, 54),
|
||||||
surfaceContainerHigh: const Color.fromARGB(255, 52, 48, 62),
|
surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45),
|
||||||
),
|
surfaceContainerHigh: const Color.fromARGB(255, 52, 48, 62),
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
),
|
||||||
border: OutlineInputBorder(),
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
),
|
border: OutlineInputBorder(),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
return base.copyWith(
|
||||||
|
textTheme: base.textTheme.apply(
|
||||||
|
fontFamily: Platform.isAndroid ? 'sans-serif' : null,
|
||||||
|
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
const primaryColor = Color(0xFF57CC99);
|
const primaryColor = Color(0xFF57CC99);
|
||||||
|
|
||||||
final ThemeData lightTheme = ThemeData(
|
final ThemeData lightTheme = () {
|
||||||
colorScheme: ColorScheme.fromSeed(
|
final base = ThemeData(
|
||||||
seedColor: primaryColor,
|
colorScheme: ColorScheme.fromSeed(
|
||||||
),
|
seedColor: primaryColor,
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
),
|
||||||
border: OutlineInputBorder(),
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
),
|
border: OutlineInputBorder(),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
return base.copyWith(
|
||||||
|
textTheme: base.textTheme.apply(
|
||||||
|
fontFamily: Platform.isAndroid ? 'sans-serif' : null,
|
||||||
|
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
|
||||||
final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom(
|
final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom(
|
||||||
backgroundColor: primaryColor,
|
backgroundColor: primaryColor,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
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/locator.dart';
|
||||||
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/zoom_tutorial_overlay.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/zoom_selector.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/zoom_selector.dart';
|
||||||
|
|
@ -136,25 +138,33 @@ class CameraBottomControls extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShutterButton() {
|
Widget _buildShutterButton() {
|
||||||
return GestureDetector(
|
return StreamBuilder(
|
||||||
onTap: onTakePicture,
|
stream: userService.onUserUpdated,
|
||||||
key: keyTriggerButton,
|
builder: (context, snapshot) {
|
||||||
child: Align(
|
return ZoomTutorialOverlay(
|
||||||
child: Container(
|
hasZoomed: userService.currentUser.hasZoomed,
|
||||||
height: 100,
|
child: GestureDetector(
|
||||||
width: 100,
|
onTap: onTakePicture,
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
key: keyTriggerButton,
|
||||||
padding: const EdgeInsets.all(2),
|
child: Align(
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
shape: BoxShape.circle,
|
height: 100,
|
||||||
border: Border.all(
|
width: 100,
|
||||||
width: 7,
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
color: isVideoRecording ? Colors.red : Colors.white,
|
padding: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
width: 7,
|
||||||
|
color: isVideoRecording ? Colors.red : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: mc.currentFilterType.preview,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: mc.currentFilterType.preview,
|
);
|
||||||
),
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class ZoomTutorialOverlay extends StatefulWidget {
|
||||||
|
const ZoomTutorialOverlay({
|
||||||
|
required this.child,
|
||||||
|
required this.hasZoomed,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final bool hasZoomed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ZoomTutorialOverlay> createState() => _ZoomTutorialOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZoomTutorialOverlayState extends State<ZoomTutorialOverlay>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _dragAnim;
|
||||||
|
late Animation<double> _opacityAnim;
|
||||||
|
late Animation<double> _scaleAnim;
|
||||||
|
late Animation<double> _textOpacityAnim;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 3500),
|
||||||
|
)..repeat();
|
||||||
|
|
||||||
|
_opacityAnim = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 1), weight: 10),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(1), weight: 70),
|
||||||
|
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0), weight: 10),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(0), weight: 10),
|
||||||
|
]).animate(_controller);
|
||||||
|
|
||||||
|
_textOpacityAnim = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(0), weight: 15),
|
||||||
|
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 1), weight: 15),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(1), weight: 50),
|
||||||
|
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0), weight: 15),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(0), weight: 5),
|
||||||
|
]).animate(_controller);
|
||||||
|
|
||||||
|
_scaleAnim = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 1.2,
|
||||||
|
end: 0.85,
|
||||||
|
).chain(CurveTween(curve: Curves.easeInQuad)),
|
||||||
|
weight: 20,
|
||||||
|
),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(0.85), weight: 80),
|
||||||
|
]).animate(_controller);
|
||||||
|
|
||||||
|
_dragAnim = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(0), weight: 35),
|
||||||
|
TweenSequenceItem(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: -75,
|
||||||
|
).chain(CurveTween(curve: Curves.easeInOutQuart)),
|
||||||
|
weight: 40,
|
||||||
|
),
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(-75), weight: 25),
|
||||||
|
]).animate(_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.hasZoomed) return widget.child;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
widget.child,
|
||||||
|
IgnorePointer(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: _dragAnim.value - 8,
|
||||||
|
right: 60,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _textOpacityAnim.value,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
context.lang.dragToZoom,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: _opacityAnim.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(0, _dragAnim.value),
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: _scaleAnim.value,
|
||||||
|
child: Container(
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.handPointer,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -441,6 +441,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
await mc.cameraController!.setZoomLevel(
|
await mc.cameraController!.setZoomLevel(
|
||||||
mc.selectedCameraDetails.scaleFactor,
|
mc.selectedCameraDetails.scaleFactor,
|
||||||
);
|
);
|
||||||
|
if (!userService.currentUser.hasZoomed) {
|
||||||
|
await UserService.update((u) => u.hasZoomed = true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pickImageFromGallery() async {
|
Future<void> pickImageFromGallery() async {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -242,7 +243,13 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
|
||||||
key: _boundaryKey,
|
key: _boundaryKey,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.emoji,
|
widget.emoji,
|
||||||
style: const TextStyle(fontSize: 94),
|
style: TextStyle(
|
||||||
|
fontSize: 94,
|
||||||
|
fontFamily: Platform.isAndroid ? 'sans-serif' : null,
|
||||||
|
fontFamilyFallback: Platform.isAndroid
|
||||||
|
? const ['NotoColorEmoji']
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -101,15 +101,16 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _getChatEntry(BorderRadius borderRadius, int reactionsForWidth) {
|
Widget? _getChatEntry(
|
||||||
|
BorderRadius borderRadius,
|
||||||
|
int reactionsForWidth,
|
||||||
|
BubbleInfo info,
|
||||||
|
) {
|
||||||
if (widget.message.type == MessageType.text.name) {
|
if (widget.message.type == MessageType.text.name) {
|
||||||
return ChatTextEntry(
|
return ChatTextEntry(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
nextMessage: widget.nextMessage,
|
|
||||||
prevMessage: widget.prevMessage,
|
|
||||||
userIdToContact: widget.userIdToContact,
|
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
minWidth: reactionsForWidth * 43,
|
info: info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,12 +119,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
if (mediaService!.mediaFile.type == MediaType.audio) {
|
if (mediaService!.mediaFile.type == MediaType.audio) {
|
||||||
return ChatAudioEntry(
|
return ChatAudioEntry(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
nextMessage: widget.nextMessage,
|
|
||||||
prevMessage: widget.prevMessage,
|
|
||||||
mediaService: mediaService!,
|
mediaService: mediaService!,
|
||||||
userIdToContact: widget.userIdToContact,
|
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
minWidth: reactionsForWidth * 43,
|
info: info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ChatMediaEntry(
|
return ChatMediaEntry(
|
||||||
|
|
@ -131,7 +129,8 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
group: widget.group,
|
group: widget.group,
|
||||||
mediaService: mediaService!,
|
mediaService: mediaService!,
|
||||||
galleryItems: widget.galleryItems,
|
galleryItems: widget.galleryItems,
|
||||||
minWidth: reactionsForWidth * 43,
|
borderRadius: borderRadius,
|
||||||
|
info: info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +167,15 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
.length;
|
.length;
|
||||||
if (reactionsForWidth > 4) reactionsForWidth = 4;
|
if (reactionsForWidth > 4) reactionsForWidth = 4;
|
||||||
|
|
||||||
|
final info = getBubbleInfo(
|
||||||
|
context,
|
||||||
|
widget.message,
|
||||||
|
widget.nextMessage,
|
||||||
|
widget.prevMessage,
|
||||||
|
widget.userIdToContact,
|
||||||
|
reactionsForWidth * 43.0,
|
||||||
|
);
|
||||||
|
|
||||||
Widget child = Stack(
|
Widget child = Stack(
|
||||||
// overflow: Overflow.visible,
|
// overflow: Overflow.visible,
|
||||||
// clipBehavior: Clip.none,
|
// clipBehavior: Clip.none,
|
||||||
|
|
@ -176,11 +184,8 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
if (widget.message.isDeletedFromSender)
|
if (widget.message.isDeletedFromSender)
|
||||||
ChatTextEntry(
|
ChatTextEntry(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
nextMessage: widget.nextMessage,
|
|
||||||
prevMessage: widget.prevMessage,
|
|
||||||
userIdToContact: widget.userIdToContact,
|
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
minWidth: reactionsForWidth * 43,
|
info: info,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -191,7 +196,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
mediaService: mediaService,
|
mediaService: mediaService,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
scrollToMessage: widget.scrollToMessage,
|
scrollToMessage: widget.scrollToMessage,
|
||||||
child: _getChatEntry(borderRadius, reactionsForWidth),
|
child: _getChatEntry(borderRadius, reactionsForWidth, info),
|
||||||
),
|
),
|
||||||
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
|
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,16 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/message_s
|
||||||
class ChatAudioEntry extends StatelessWidget {
|
class ChatAudioEntry extends StatelessWidget {
|
||||||
const ChatAudioEntry({
|
const ChatAudioEntry({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.nextMessage,
|
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
required this.prevMessage,
|
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
required this.userIdToContact,
|
required this.info,
|
||||||
required this.minWidth,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
final MediaFileService mediaService;
|
final MediaFileService mediaService;
|
||||||
final Message? nextMessage;
|
|
||||||
final Message? prevMessage;
|
|
||||||
final Map<int, Contact>? userIdToContact;
|
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final double minWidth;
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -36,64 +30,78 @@ class ChatAudioEntry extends StatelessWidget {
|
||||||
!mediaService.originalPath.existsSync()) {
|
!mediaService.originalPath.existsSync()) {
|
||||||
return Container(); // media file was purged
|
return Container(); // media file was purged
|
||||||
}
|
}
|
||||||
final info = getBubbleInfo(
|
|
||||||
context,
|
|
||||||
message,
|
|
||||||
nextMessage,
|
|
||||||
prevMessage,
|
|
||||||
userIdToContact,
|
|
||||||
minWidth,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return LayoutBuilder(
|
||||||
constraints: BoxConstraints(
|
builder: (context, constraints) {
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
final textWidth = measureTextWidth(info.text);
|
||||||
minWidth: 250,
|
const timeWidth = 60.0;
|
||||||
),
|
final isExpanded =
|
||||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
info.expanded ||
|
||||||
decoration: BoxDecoration(
|
(textWidth + timeWidth + 20 > constraints.maxWidth);
|
||||||
color: info.color,
|
final effectiveSpacerWidth =
|
||||||
borderRadius: borderRadius,
|
constraints.minWidth - textWidth - timeWidth;
|
||||||
),
|
final spacerWidth = effectiveSpacerWidth > 0
|
||||||
child: Column(
|
? effectiveSpacerWidth
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
: 0.0;
|
||||||
children: [
|
|
||||||
if (info.displayUserName != '')
|
return Container(
|
||||||
Text(
|
constraints: BoxConstraints(
|
||||||
info.displayUserName,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
textAlign: TextAlign.left,
|
minWidth: 250,
|
||||||
style: const TextStyle(
|
),
|
||||||
color: Colors.white,
|
padding: info.padding,
|
||||||
fontWeight: FontWeight.bold,
|
decoration: BoxDecoration(
|
||||||
),
|
color: info.color,
|
||||||
),
|
borderRadius: borderRadius,
|
||||||
Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (info.text != '')
|
if (info.displayUserName != '')
|
||||||
Expanded(
|
Text(
|
||||||
child: BetterText(text: info.text, textColor: info.textColor),
|
info.displayUserName,
|
||||||
)
|
textAlign: TextAlign.left,
|
||||||
else ...[
|
style: const TextStyle(
|
||||||
if (mediaService.mediaFile.downloadState ==
|
color: Colors.white,
|
||||||
DownloadState.ready ||
|
fontWeight: FontWeight.bold,
|
||||||
mediaService.mediaFile.downloadState == null)
|
),
|
||||||
mediaService.tempPath.existsSync()
|
),
|
||||||
? InChatAudioPlayer(
|
Row(
|
||||||
path: mediaService.tempPath.path,
|
mainAxisSize: MainAxisSize.min,
|
||||||
message: message,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
)
|
children: [
|
||||||
: Container()
|
if (isExpanded && info.text != '')
|
||||||
else
|
Expanded(
|
||||||
MessageSendStateIcon([message], [mediaService.mediaFile]),
|
child: BetterText(
|
||||||
],
|
text: info.text,
|
||||||
if (info.displayTime || message.modifiedAt != null)
|
textColor: info.textColor,
|
||||||
FriendlyMessageTime(message: message),
|
),
|
||||||
|
)
|
||||||
|
else if (info.text != '') ...[
|
||||||
|
BetterText(text: info.text, textColor: info.textColor),
|
||||||
|
SizedBox(width: spacerWidth),
|
||||||
|
] else ...[
|
||||||
|
if (mediaService.mediaFile.downloadState ==
|
||||||
|
DownloadState.ready ||
|
||||||
|
mediaService.mediaFile.downloadState == null)
|
||||||
|
mediaService.tempPath.existsSync()
|
||||||
|
? InChatAudioPlayer(
|
||||||
|
path: mediaService.tempPath.path,
|
||||||
|
message: message,
|
||||||
|
)
|
||||||
|
: Container()
|
||||||
|
else
|
||||||
|
MessageSendStateIcon([message], [mediaService.mediaFile]),
|
||||||
|
SizedBox(width: spacerWidth),
|
||||||
|
],
|
||||||
|
if (info.displayTime || message.modifiedAt != null)
|
||||||
|
FriendlyMessageTime(message: message),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,17 @@ class ChatMediaEntry extends StatefulWidget {
|
||||||
required this.group,
|
required this.group,
|
||||||
required this.galleryItems,
|
required this.galleryItems,
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
required this.minWidth,
|
required this.borderRadius,
|
||||||
|
required this.info,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
final double minWidth;
|
|
||||||
final Group group;
|
final Group group;
|
||||||
final List<MemoryItem> galleryItems;
|
final List<MemoryItem> galleryItems;
|
||||||
final MediaFileService mediaService;
|
final MediaFileService mediaService;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
||||||
|
|
@ -116,52 +118,34 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
var imageBorderRadius = BorderRadius.circular(12);
|
var imageBorderRadius = widget.borderRadius;
|
||||||
|
|
||||||
Widget additionalMessageData = Container();
|
Widget additionalMessageData = Container();
|
||||||
|
|
||||||
final addData = widget.message.additionalMessageData;
|
final addData = widget.message.additionalMessageData;
|
||||||
if (addData != null) {
|
if (addData != null) {
|
||||||
final info = getBubbleInfo(
|
|
||||||
context,
|
|
||||||
widget.message,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
final data = AdditionalMessageData.fromBuffer(addData);
|
final data = AdditionalMessageData.fromBuffer(addData);
|
||||||
if (data.hasLink() && widget.message.mediaStored) {
|
if (data.hasLink() && widget.message.mediaStored) {
|
||||||
imageBorderRadius = const BorderRadius.only(
|
imageBorderRadius = widget.borderRadius.copyWith(
|
||||||
topLeft: Radius.circular(12),
|
bottomLeft: const Radius.circular(5),
|
||||||
topRight: Radius.circular(12),
|
bottomRight: const Radius.circular(5),
|
||||||
bottomLeft: Radius.circular(5),
|
|
||||||
bottomRight: Radius.circular(5),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
additionalMessageData = Container(
|
additionalMessageData = Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(
|
padding: widget.info.padding,
|
||||||
left: 10,
|
|
||||||
top: 6,
|
|
||||||
bottom: 6,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: info.color,
|
color: widget.info.color,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: widget.borderRadius.copyWith(
|
||||||
topLeft: Radius.circular(5),
|
topLeft: const Radius.circular(5),
|
||||||
topRight: Radius.circular(12),
|
|
||||||
bottomLeft: Radius.circular(12),
|
|
||||||
bottomRight: Radius.circular(12),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BetterText(text: data.link, textColor: info.textColor),
|
BetterText(text: data.link, textColor: widget.info.textColor),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -178,7 +162,12 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
|
onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: (widget.minWidth > 150) ? widget.minWidth : 150,
|
width: (widget.info.minWidth > 150)
|
||||||
|
? widget.info.minWidth
|
||||||
|
: (widget.message.mediaStored &&
|
||||||
|
widget.mediaService.imagePreviewAvailable)
|
||||||
|
? 150
|
||||||
|
: null,
|
||||||
height:
|
height:
|
||||||
(widget.message.mediaStored &&
|
(widget.message.mediaStored &&
|
||||||
widget.mediaService.imagePreviewAvailable)
|
widget.mediaService.imagePreviewAvailable)
|
||||||
|
|
@ -195,6 +184,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
color: color,
|
color: color,
|
||||||
galleryItems: widget.galleryItems,
|
galleryItems: widget.galleryItems,
|
||||||
canBeReopened: _canBeReopened,
|
canBeReopened: _canBeReopened,
|
||||||
|
borderRadius: imageBorderRadius,
|
||||||
|
info: widget.info,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,14 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/f
|
||||||
class ChatTextEntry extends StatelessWidget {
|
class ChatTextEntry extends StatelessWidget {
|
||||||
const ChatTextEntry({
|
const ChatTextEntry({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.nextMessage,
|
|
||||||
required this.prevMessage,
|
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
required this.userIdToContact,
|
required this.info,
|
||||||
required this.minWidth,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
final Message? nextMessage;
|
|
||||||
final Message? prevMessage;
|
|
||||||
final Map<int, Contact>? userIdToContact;
|
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final double minWidth;
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -40,57 +34,66 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final info = getBubbleInfo(
|
return LayoutBuilder(
|
||||||
context,
|
builder: (context, constraints) {
|
||||||
message,
|
final textWidth = measureTextWidth(info.text);
|
||||||
nextMessage,
|
const timeWidth = 60.0;
|
||||||
prevMessage,
|
final isExpanded =
|
||||||
userIdToContact,
|
info.expanded ||
|
||||||
minWidth,
|
(textWidth + timeWidth + 20 > constraints.maxWidth);
|
||||||
);
|
final effectiveSpacerWidth =
|
||||||
|
constraints.minWidth - textWidth - timeWidth;
|
||||||
|
final spacerWidth = effectiveSpacerWidth > 0
|
||||||
|
? effectiveSpacerWidth
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
minWidth: minWidth,
|
minWidth: info.minWidth,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
padding: info.padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: info.color,
|
color: info.color,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
|
||||||
if (info.displayUserName != '')
|
|
||||||
Text(
|
|
||||||
info.displayUserName,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
if (info.expanded)
|
if (info.displayUserName != '')
|
||||||
Expanded(
|
Text(
|
||||||
child: BetterText(text: info.text, textColor: info.textColor),
|
info.displayUserName,
|
||||||
)
|
textAlign: TextAlign.left,
|
||||||
else ...[
|
style: const TextStyle(
|
||||||
BetterText(text: info.text, textColor: info.textColor),
|
color: Colors.white,
|
||||||
SizedBox(
|
fontWeight: FontWeight.bold,
|
||||||
width: info.spacerWidth,
|
),
|
||||||
),
|
),
|
||||||
],
|
Row(
|
||||||
if (info.displayTime || message.modifiedAt != null)
|
mainAxisSize: MainAxisSize.min,
|
||||||
FriendlyMessageTime(message: message),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (isExpanded)
|
||||||
|
Expanded(
|
||||||
|
child: BetterText(
|
||||||
|
text: info.text,
|
||||||
|
textColor: info.textColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
BetterText(text: info.text, textColor: info.textColor),
|
||||||
|
SizedBox(
|
||||||
|
width: spacerWidth,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (info.displayTime || message.modifiedAt != null)
|
||||||
|
FriendlyMessageTime(message: message),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class BubbleInfo {
|
||||||
late Color color;
|
late Color color;
|
||||||
late bool expanded;
|
late bool expanded;
|
||||||
late double spacerWidth;
|
late double spacerWidth;
|
||||||
|
late EdgeInsets padding;
|
||||||
|
late double minWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
BubbleInfo getBubbleInfo(
|
BubbleInfo getBubbleInfo(
|
||||||
|
|
@ -29,7 +31,11 @@ BubbleInfo getBubbleInfo(
|
||||||
..textColor = Colors.white
|
..textColor = Colors.white
|
||||||
..color = getMessageColor(message.senderId != null)
|
..color = getMessageColor(message.senderId != null)
|
||||||
..displayTime = !combineTextMessageWithNext(message, nextMessage)
|
..displayTime = !combineTextMessageWithNext(message, nextMessage)
|
||||||
..displayUserName = '';
|
..displayUserName = ''
|
||||||
|
..minWidth = minWidth
|
||||||
|
..padding = message.type == MessageType.media.name
|
||||||
|
? const EdgeInsets.symmetric(horizontal: 10, vertical: 2)
|
||||||
|
: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10);
|
||||||
|
|
||||||
if (message.senderId != null &&
|
if (message.senderId != null &&
|
||||||
userIdToContact != null &&
|
userIdToContact != null &&
|
||||||
|
|
@ -50,10 +56,11 @@ BubbleInfo getBubbleInfo(
|
||||||
info.spacerWidth = minWidth - measureTextWidth(info.text) - 53;
|
info.spacerWidth = minWidth - measureTextWidth(info.text) - 53;
|
||||||
if (info.spacerWidth < 0) info.spacerWidth = 0;
|
if (info.spacerWidth < 0) info.spacerWidth = 0;
|
||||||
|
|
||||||
info.expanded = false;
|
info
|
||||||
if (message.quotesMessageId == null) {
|
..expanded = false
|
||||||
info.color = getMessageColor(message.senderId != null);
|
..color = message.quotesMessageId != null
|
||||||
}
|
? Colors.transparent
|
||||||
|
: getMessageColor(message.senderId != null);
|
||||||
if (message.isDeletedFromSender) {
|
if (message.isDeletedFromSender) {
|
||||||
info
|
info
|
||||||
..color = context.color.surfaceBright
|
..color = context.color.surfaceBright
|
||||||
|
|
@ -85,17 +92,15 @@ double measureTextWidth(
|
||||||
}
|
}
|
||||||
|
|
||||||
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
||||||
if (nextMessage != null && nextMessage.content != null) {
|
if (nextMessage != null) {
|
||||||
if (nextMessage.senderId == message.senderId) {
|
if (nextMessage.senderId == message.senderId) {
|
||||||
if (nextMessage.type == MessageType.text.name &&
|
if (nextMessage.content == null ||
|
||||||
message.type == MessageType.text.name) {
|
!EmojiAnimationComp.supported(nextMessage.content!)) {
|
||||||
if (!EmojiAnimationComp.supported(nextMessage.content!)) {
|
final diff = nextMessage.createdAt
|
||||||
final diff = nextMessage.createdAt
|
.difference(message.createdAt)
|
||||||
.difference(message.createdAt)
|
.inMinutes;
|
||||||
.inMinutes;
|
if (diff <= 1) {
|
||||||
if (diff <= 1) {
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,46 +6,49 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class FriendlyMessageTime extends StatelessWidget {
|
class FriendlyMessageTime extends StatelessWidget {
|
||||||
const FriendlyMessageTime({required this.message, super.key});
|
const FriendlyMessageTime({
|
||||||
|
required this.message,
|
||||||
|
this.color,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
return Padding(
|
||||||
alignment: AlignmentGeometry.centerRight,
|
padding: const EdgeInsets.only(left: 6),
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.only(left: 6),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
if (message.modifiedAt != null && !message.isDeletedFromSender)
|
||||||
if (message.modifiedAt != null && !message.isDeletedFromSender)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(right: 5),
|
||||||
padding: const EdgeInsets.only(right: 5),
|
child: SizedBox(
|
||||||
child: SizedBox(
|
height: 10,
|
||||||
height: 10,
|
child: FaIcon(
|
||||||
child: FaIcon(
|
FontAwesomeIcons.pencil,
|
||||||
FontAwesomeIcons.pencil,
|
color: color ?? Colors.white.withAlpha(150),
|
||||||
color: Colors.white.withAlpha(150),
|
size: 10,
|
||||||
size: 10,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
friendlyTime(
|
|
||||||
context,
|
|
||||||
(message.modifiedAt != null)
|
|
||||||
? message.modifiedAt!
|
|
||||||
: message.createdAt,
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white.withAlpha(150),
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
Text(
|
||||||
),
|
friendlyTime(
|
||||||
|
context,
|
||||||
|
(message.modifiedAt != null)
|
||||||
|
? message.modifiedAt!
|
||||||
|
: message.createdAt,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: color ?? Colors.white.withAlpha(150),
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_send_state_icon.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||||
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||||
|
|
@ -17,6 +20,8 @@ class InChatMediaViewer extends StatefulWidget {
|
||||||
required this.color,
|
required this.color,
|
||||||
required this.galleryItems,
|
required this.galleryItems,
|
||||||
required this.canBeReopened,
|
required this.canBeReopened,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.info,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,6 +31,8 @@ class InChatMediaViewer extends StatefulWidget {
|
||||||
final List<MemoryItem> galleryItems;
|
final List<MemoryItem> galleryItems;
|
||||||
final Color color;
|
final Color color;
|
||||||
final bool canBeReopened;
|
final bool canBeReopened;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
||||||
|
|
@ -36,8 +43,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
int? galleryItemIndex;
|
int? galleryItemIndex;
|
||||||
StreamSubscription<Message?>? messageStream;
|
StreamSubscription<Message?>? messageStream;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
late final ValueNotifier<String?> _activeMediaIdNotifier =
|
late final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(
|
||||||
ValueNotifier(widget.message.mediaId);
|
widget.message.mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -46,14 +54,25 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
unawaited(initStream());
|
unawaited(initStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(InChatMediaViewer oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.message.mediaStored != oldWidget.message.mediaStored ||
|
||||||
|
widget.galleryItems != oldWidget.galleryItems) {
|
||||||
|
if (widget.message.mediaStored) {
|
||||||
|
unawaited(loadIndexAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadIndexAsync() async {
|
Future<void> loadIndexAsync() async {
|
||||||
if (!widget.message.mediaStored) return;
|
_timer?.cancel();
|
||||||
_timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
_timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
||||||
/// when the galleryItems are updated this widget is not reloaded
|
/// when the galleryItems are updated this widget is not reloaded
|
||||||
/// so using this timer as a workaround
|
/// so using this timer as a workaround
|
||||||
if (loadIndex()) {
|
if (loadIndex()) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
setState(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -135,21 +154,32 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
minHeight: 39,
|
minHeight: 39,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: widget.info.color.withValues(alpha: 0.3),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: widget.color,
|
color: widget.info.color.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: widget.borderRadius,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: widget.info.padding,
|
||||||
vertical: (widget.canBeReopened) ? 5 : 10.0,
|
child: Row(
|
||||||
horizontal: 4,
|
children: [
|
||||||
),
|
MessageSendStateIcon(
|
||||||
child: MessageSendStateIcon(
|
[widget.message],
|
||||||
[widget.message],
|
[widget.mediaService.mediaFile],
|
||||||
[widget.mediaService.mediaFile],
|
mainAxisAlignment: widget.message.senderId == null
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? MainAxisAlignment.end
|
||||||
canBeReopened: widget.canBeReopened,
|
: MainAxisAlignment.start,
|
||||||
|
canBeReopened: widget.canBeReopened,
|
||||||
|
),
|
||||||
|
if (widget.info.displayTime || widget.message.modifiedAt != null)
|
||||||
|
FriendlyMessageTime(
|
||||||
|
message: widget.message,
|
||||||
|
color: isDarkMode(context)
|
||||||
|
? Colors.white.withAlpha(100)
|
||||||
|
: Colors.black.withAlpha(100),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +190,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
),
|
),
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: widget.borderRadius,
|
||||||
),
|
),
|
||||||
child: galleryItemIndex != null
|
child: galleryItemIndex != null
|
||||||
? MemoriesThumbnailComp(
|
? MemoriesThumbnailComp(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -305,6 +306,10 @@ class _ParticlePainter extends CustomPainter {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24 * p.currentScale,
|
fontSize: 24 * p.currentScale,
|
||||||
color: Colors.black.withValues(alpha: p.opacity),
|
color: Colors.black.withValues(alpha: p.opacity),
|
||||||
|
fontFamily: Platform.isAndroid ? 'sans-serif' : null,
|
||||||
|
fontFamilyFallback: Platform.isAndroid
|
||||||
|
? const ['NotoColorEmoji']
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
textPainter
|
textPainter
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,8 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
||||||
Text(
|
Text(
|
||||||
'${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}',
|
'${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}',
|
||||||
),
|
),
|
||||||
|
if (userService.currentUser.isDeveloper)
|
||||||
|
Text('ID: ${widget.message.messageId}'),
|
||||||
if (messageHistory.isNotEmpty) ...[
|
if (messageHistory.isNotEmpty) ...[
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
Timer? _disableCameraTimer;
|
Timer? _disableCameraTimer;
|
||||||
|
|
||||||
final MainCameraController _mainCameraController = MainCameraController();
|
final MainCameraController _mainCameraController = MainCameraController();
|
||||||
final PageController _homeViewPageController = PageController(initialPage: 1);
|
late final PageController _homeViewPageController;
|
||||||
|
|
||||||
StreamSubscription<List<SharedFile>>? _intentStreamSub;
|
StreamSubscription<List<SharedFile>>? _intentStreamSub;
|
||||||
StreamSubscription<Uri>? _deepLinkSub;
|
StreamSubscription<Uri>? _deepLinkSub;
|
||||||
|
|
@ -53,12 +53,21 @@ class HomeViewState extends State<HomeView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
var initialPage = widget.initialPage;
|
||||||
|
if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) {
|
||||||
|
initialPage = 0;
|
||||||
|
}
|
||||||
|
_activePageIdx = initialPage;
|
||||||
|
_homeViewPageController = PageController(initialPage: initialPage);
|
||||||
|
|
||||||
_mainCameraController.setState = () {
|
_mainCameraController.setState = () {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
};
|
};
|
||||||
|
|
||||||
_homeViewPageIndexSub = streamHomeViewPageIndex.stream.listen((index) {
|
_homeViewPageIndexSub = streamHomeViewPageIndex.stream.listen((index) {
|
||||||
_homeViewPageController.jumpToPage(index);
|
if (_homeViewPageController.hasClients) {
|
||||||
|
_homeViewPageController.jumpToPage(index);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_activePageIdx = index;
|
_activePageIdx = index;
|
||||||
});
|
});
|
||||||
|
|
@ -286,15 +295,14 @@ class HomeViewState extends State<HomeView> {
|
||||||
bottomNavigationBar: AnimatedSize(
|
bottomNavigationBar: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
child: _isBottomNavVisible
|
child: (_activePageIdx != 2 || _isBottomNavVisible)
|
||||||
? BottomNavigationBar(
|
? BottomNavigationBar(
|
||||||
showSelectedLabels: false,
|
showSelectedLabels: false,
|
||||||
showUnselectedLabels: false,
|
showUnselectedLabels: false,
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.inverseSurface
|
).colorScheme.inverseSurface.withAlpha(150),
|
||||||
.withAlpha(150),
|
|
||||||
),
|
),
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(
|
||||||
color: Theme.of(context).colorScheme.inverseSurface,
|
color: Theme.of(context).colorScheme.inverseSurface,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/memories/memories.service.dart';
|
import 'package:twonly/src/services/memories/memories.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
|
||||||
import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart';
|
import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart';
|
import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart';
|
||||||
|
|
@ -292,29 +292,6 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final state = snapshot.data ?? _service.currentState;
|
final state = snapshot.data ?? _service.currentState;
|
||||||
|
|
||||||
if (state.isLoading) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ThreeRotatingDots(
|
|
||||||
size: 40,
|
|
||||||
color: context.color.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
context.lang.migrationOfMemories(state.filesToMigrate),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -371,99 +348,152 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
orderedByMonth = filteredOrdered;
|
orderedByMonth = filteredOrdered;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scrollbar(
|
return LayoutBuilder(
|
||||||
controller: _scrollController,
|
builder: (context, constraints) {
|
||||||
thickness: 12,
|
return DraggableScrollbar(
|
||||||
radius: const Radius.circular(6),
|
controller: _scrollController,
|
||||||
interactive: true,
|
labelBuilder: (offset) {
|
||||||
child: CustomScrollView(
|
final state = _service.currentState;
|
||||||
controller: _scrollController,
|
if (state.isEmpty) return null;
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
slivers: [
|
// Simple heuristic to find month by offset
|
||||||
SliverAppBar(
|
double currentOffset = 56;
|
||||||
title: const Text(
|
if (state.galleryItemsLastYears.isNotEmpty) {
|
||||||
'Memories',
|
currentOffset += 220;
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
}
|
||||||
),
|
|
||||||
floating: true,
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
snap: true,
|
final itemWidth = (screenWidth - 8) / 4;
|
||||||
elevation: 0,
|
final itemHeight = itemWidth * (16 / 9);
|
||||||
backgroundColor: context.color.surface,
|
final rowHeight = itemHeight + 2;
|
||||||
actions: [
|
|
||||||
IconButton(
|
for (final month in state.months) {
|
||||||
icon: Icon(
|
final indices = state.orderedByMonth[month]!;
|
||||||
_filterFavoritesOnly
|
final totalRows = (indices.length + 3) ~/ 4;
|
||||||
? Icons.favorite
|
final monthHeight = 44 + (totalRows * rowHeight);
|
||||||
: Icons.favorite_border,
|
|
||||||
color: _filterFavoritesOnly
|
if (offset < currentOffset + monthHeight) {
|
||||||
? Colors.redAccent
|
return month;
|
||||||
: null,
|
}
|
||||||
|
currentOffset += monthHeight;
|
||||||
|
}
|
||||||
|
return state.months.last;
|
||||||
|
},
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Memories',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
floating: true,
|
||||||
setState(() {
|
snap: true,
|
||||||
_filterFavoritesOnly = !_filterFavoritesOnly;
|
elevation: 0,
|
||||||
});
|
backgroundColor: context.color.surface,
|
||||||
},
|
actions: [
|
||||||
tooltip: _filterFavoritesOnly
|
if (state.isLoading)
|
||||||
? 'Show all'
|
Padding(
|
||||||
: 'Show favorites only',
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Tooltip(
|
||||||
|
message: context.lang.migrationOfMemories(
|
||||||
|
state.filesToMigrate,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: state.migrationProgress,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: context.color.primary,
|
||||||
|
backgroundColor: context.color.primary
|
||||||
|
.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_filterFavoritesOnly
|
||||||
|
? Icons.favorite
|
||||||
|
: Icons.favorite_border,
|
||||||
|
color: _filterFavoritesOnly
|
||||||
|
? Colors.redAccent
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_filterFavoritesOnly = !_filterFavoritesOnly;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _filterFavoritesOnly
|
||||||
|
? 'Show all'
|
||||||
|
: 'Show favorites only',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MemoriesFlashbackBannerComp(
|
||||||
|
lastYears: lastYears,
|
||||||
|
onOpenFlashback: (items, idx) =>
|
||||||
|
_openViewer(items, idx, isFlashback: true),
|
||||||
|
),
|
||||||
|
for (final month in months) ...[
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 12, 8, 6),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
month,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverGrid(
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
mainAxisSpacing: 2,
|
||||||
|
crossAxisSpacing: 2,
|
||||||
|
childAspectRatio: 9 / 16,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, idx) {
|
||||||
|
final globalIndex = orderedByMonth[month]![idx];
|
||||||
|
final item = state.galleryItems[globalIndex];
|
||||||
|
final mediaId =
|
||||||
|
item.mediaService.mediaFile.mediaId;
|
||||||
|
final isSelected = _selectedMediaIds.contains(
|
||||||
|
mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return MemoriesThumbnailComp(
|
||||||
|
galleryItem: item,
|
||||||
|
index: globalIndex,
|
||||||
|
selectionMode: _selectionMode,
|
||||||
|
isSelected: isSelected,
|
||||||
|
activeMediaIdNotifier: _activeMediaIdNotifier,
|
||||||
|
onLongPress: () => _onLongPressItem(mediaId),
|
||||||
|
onTap: () => _onTapItem(mediaId, globalIndex),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: orderedByMonth[month]!.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: 32),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
MemoriesFlashbackBannerComp(
|
},
|
||||||
lastYears: lastYears,
|
|
||||||
onOpenFlashback: (items, idx) =>
|
|
||||||
_openViewer(items, idx, isFlashback: true),
|
|
||||||
),
|
|
||||||
|
|
||||||
for (final month in months) ...[
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 12, 8, 6),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
month,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverGrid(
|
|
||||||
gridDelegate:
|
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 4,
|
|
||||||
mainAxisSpacing: 2,
|
|
||||||
crossAxisSpacing: 2,
|
|
||||||
childAspectRatio: 9 / 16,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, idx) {
|
|
||||||
final globalIndex = orderedByMonth[month]![idx];
|
|
||||||
final item = state.galleryItems[globalIndex];
|
|
||||||
final mediaId = item.mediaService.mediaFile.mediaId;
|
|
||||||
final isSelected = _selectedMediaIds.contains(
|
|
||||||
mediaId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return MemoriesThumbnailComp(
|
|
||||||
galleryItem: item,
|
|
||||||
index: globalIndex,
|
|
||||||
selectionMode: _selectionMode,
|
|
||||||
isSelected: isSelected,
|
|
||||||
activeMediaIdNotifier: _activeMediaIdNotifier,
|
|
||||||
onLongPress: () => _onLongPressItem(mediaId),
|
|
||||||
onTap: () => _onTapItem(mediaId, globalIndex),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: orderedByMonth[month]!.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: 32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
class LinkLogoAnimation extends StatefulWidget {
|
||||||
|
const LinkLogoAnimation({
|
||||||
|
super.key,
|
||||||
|
this.size = 130,
|
||||||
|
this.color = Colors.white,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkLogoAnimation> createState() => _LinkLogoAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkLogoAnimationState extends State<LinkLogoAnimation>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_rotation =
|
||||||
|
Tween<double>(
|
||||||
|
begin: -2.0 * (math.pi / 180.0),
|
||||||
|
end: 2.0 * (math.pi / 180.0),
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const originalViewportSize = 640.0;
|
||||||
|
|
||||||
|
const path1 =
|
||||||
|
'M451.5 160C434.9 160 418.8 164.5 404.7 172.7C388.9 156.7 370.5 143.3 350.2 133.2C378.4 109.2 414.3 96 451.5 96C537.9 96 608 166 608 252.5C608 294 591.5 333.8 562.2 363.1L491.1 434.2C461.8 463.5 422 480 380.5 480C294.1 480 224 410 224 323.5C224 322 224 320.5 224.1 319C224.6 301.3 239.3 287.4 257 287.9C274.7 288.4 288.6 303.1 288.1 320.8C288.1 321.7 288.1 322.6 288.1 323.4C288.1 374.5 329.5 415.9 380.6 415.9C405.1 415.9 428.6 406.2 446 388.8L517.1 317.7C534.4 300.4 544.2 276.8 544.2 252.3C544.2 201.2 502.8 159.8 451.7 159.8z';
|
||||||
|
const path2 =
|
||||||
|
'M307.2 237.3C305.3 236.5 303.4 235.4 301.7 234.2C289.1 227.7 274.7 224 259.6 224C235.1 224 211.6 233.7 194.2 251.1L123.1 322.2C105.8 339.5 96 363.1 96 387.6C96 438.7 137.4 480.1 188.5 480.1C205 480.1 221.1 475.7 235.2 467.5C251 483.5 269.4 496.9 289.8 507C261.6 530.9 225.8 544.2 188.5 544.2C102.1 544.2 32 474.2 32 387.7C32 346.2 48.5 306.4 77.8 277.1L148.9 206C178.2 176.7 218 160.2 259.5 160.2C346.1 160.2 416 230.8 416 317.1C416 318.4 416 319.7 416 321C415.6 338.7 400.9 352.6 383.2 352.2C365.5 351.8 351.6 337.1 352 319.4C352 318.6 352 317.9 352 317.1C352 283.4 334 253.8 307.2 237.5z';
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _rotation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Transform(
|
||||||
|
alignment: const Alignment(
|
||||||
|
(416 * 2 / originalViewportSize) - 1,
|
||||||
|
(288 * 2 / originalViewportSize) - 1,
|
||||||
|
),
|
||||||
|
transform: Matrix4.identity()..rotateZ(_rotation.value),
|
||||||
|
child: SvgPicture.string(
|
||||||
|
'<svg viewBox="0 0 640 640"><path d="$path1" fill="${_toHex(widget.color)}"/></svg>',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Transform(
|
||||||
|
alignment: const Alignment(
|
||||||
|
(224 * 2 / originalViewportSize) - 1,
|
||||||
|
(352 * 2 / originalViewportSize) - 1,
|
||||||
|
),
|
||||||
|
transform: Matrix4.identity()..rotateZ(-_rotation.value),
|
||||||
|
child: SvgPicture.string(
|
||||||
|
'<svg viewBox="0 0 640 640"><path d="$path2" fill="${_toHex(widget.color)}"/></svg>',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _toHex(Color color) {
|
||||||
|
return '#${color.toARGB32().toRadixString(16).substring(2)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class OnboardingWrapper extends StatelessWidget {
|
||||||
|
const OnboardingWrapper({
|
||||||
|
required this.children,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = isDarkMode(context);
|
||||||
|
final backgroundColor = isDark ? const Color(0xFF0F172A) : primaryColor;
|
||||||
|
final topBlobColor = isDark
|
||||||
|
? primaryColor.withValues(alpha: 0.15)
|
||||||
|
: Colors.white.withValues(alpha: 0.1);
|
||||||
|
final bottomBlobColor = isDark
|
||||||
|
? primaryColor.withValues(alpha: 0.08)
|
||||||
|
: Colors.black.withValues(alpha: 0.05);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: -100,
|
||||||
|
right: -100,
|
||||||
|
child: Container(
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: topBlobColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -50,
|
||||||
|
left: -50,
|
||||||
|
child: Container(
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: bottomBlobColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,9 @@ import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
||||||
|
|
||||||
class BackupRecoveryView extends StatefulWidget {
|
class BackupRecoveryView extends StatefulWidget {
|
||||||
const BackupRecoveryView({super.key});
|
const BackupRecoveryView({super.key});
|
||||||
|
|
@ -64,66 +66,128 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final isDark = isDarkMode(context);
|
||||||
appBar: AppBar(
|
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||||
title: Text('twonly Backup ${context.lang.twonlySafeRecoverTitle}'),
|
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
||||||
actions: [
|
|
||||||
IconButton(
|
return OnboardingWrapper(
|
||||||
onPressed: () async {
|
children: [
|
||||||
await showAlertDialog(
|
Row(
|
||||||
context,
|
|
||||||
'twonly Backup',
|
|
||||||
context.lang.backupTwonlySafeLongDesc,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
|
||||||
iconSize: 18,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsetsGeometry.symmetric(
|
|
||||||
vertical: 40,
|
|
||||||
horizontal: 40,
|
|
||||||
),
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
IconButton(
|
||||||
context.lang.twonlySafeRecoverDesc,
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
textAlign: TextAlign.center,
|
icon: const Icon(
|
||||||
),
|
Icons.arrow_back_ios_new_rounded,
|
||||||
const SizedBox(height: 30),
|
|
||||||
TextField(
|
|
||||||
controller: usernameCtrl,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
decoration: getInputDecoration(
|
|
||||||
context,
|
|
||||||
context.lang.registerUsernameDecoration,
|
|
||||||
),
|
),
|
||||||
|
color: Colors.white,
|
||||||
|
iconSize: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const Spacer(),
|
||||||
Stack(
|
IconButton(
|
||||||
children: [
|
onPressed: () async {
|
||||||
TextField(
|
await showAlertDialog(
|
||||||
controller: passwordCtrl,
|
context,
|
||||||
onChanged: (value) {
|
'twonly Backup',
|
||||||
setState(() {});
|
context.lang.backupTwonlySafeLongDesc,
|
||||||
},
|
);
|
||||||
style: const TextStyle(fontSize: 17),
|
},
|
||||||
obscureText: obscureText,
|
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||||
decoration: getInputDecoration(
|
color: Colors.white,
|
||||||
context,
|
iconSize: 20,
|
||||||
context.lang.password,
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
child: LinkLogoAnimation(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Text(
|
||||||
|
context.lang.twonlySafeRecoverTitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: isDark
|
||||||
|
? Colors.black.withValues(alpha: 0.3)
|
||||||
|
: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: usernameCtrl,
|
||||||
|
onChanged: (value) => setState(() {}),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.lang.registerUsernameDecoration,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: inputColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.alternate_email,
|
||||||
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
right: 0,
|
const SizedBox(height: 16),
|
||||||
top: 0,
|
TextField(
|
||||||
bottom: 0,
|
controller: passwordCtrl,
|
||||||
child: IconButton(
|
onChanged: (value) => setState(() {}),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
|
obscureText: obscureText,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.lang.password,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: inputColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
|
),
|
||||||
|
suffixIcon: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
obscureText = !obscureText;
|
obscureText = !obscureText;
|
||||||
|
|
@ -134,28 +198,46 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
? FontAwesomeIcons.eye
|
? FontAwesomeIcons.eye
|
||||||
: FontAwesomeIcons.eyeSlash,
|
: FontAwesomeIcons.eyeSlash,
|
||||||
size: 16,
|
size: 16,
|
||||||
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Center(
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
|
||||||
icon: isLoading
|
|
||||||
? const SizedBox(
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 1),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.lock_clock_rounded),
|
|
||||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 32),
|
||||||
],
|
FilledButton(
|
||||||
|
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size.fromHeight(60),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
context.lang.twonlySafeRecoverBtn,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// ignore_for_file: avoid_dynamic_calls
|
// ignore_for_file: avoid_dynamic_calls
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -16,7 +17,10 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/pow.dart';
|
import 'package:twonly/src/utils/pow.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
class RegisterView extends StatefulWidget {
|
class RegisterView extends StatefulWidget {
|
||||||
|
|
@ -134,7 +138,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
username: username,
|
username: username,
|
||||||
displayName: username,
|
displayName: username,
|
||||||
subscriptionPlan: 'Preview',
|
subscriptionPlan: 'Free',
|
||||||
currentSetupPage: SetupPages.profile.name,
|
currentSetupPage: SetupPages.profile.name,
|
||||||
)..appVersion = AppState.latestAppVersionId;
|
)..appVersion = AppState.latestAppVersionId;
|
||||||
|
|
||||||
|
|
@ -146,174 +150,199 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_registrationDisabled) {
|
final isDark = isDarkMode(context);
|
||||||
return Scaffold(
|
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||||
body: Padding(
|
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
||||||
padding: const EdgeInsets.all(10),
|
final sloganColor = isDark
|
||||||
child: Padding(
|
? Colors.white.withValues(alpha: 0.9)
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
: Colors.grey[800];
|
||||||
child: ListView(
|
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
||||||
children: [
|
|
||||||
const SizedBox(height: 50),
|
return OnboardingWrapper(
|
||||||
Text(
|
children: [
|
||||||
context.lang.registerTitle,
|
const SizedBox(height: 40),
|
||||||
textAlign: TextAlign.center,
|
Center(
|
||||||
style: const TextStyle(fontSize: 30),
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.all(20),
|
||||||
Padding(
|
child: const LinkLogoAnimation(),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
),
|
||||||
child: Text(
|
),
|
||||||
context.lang.registerSlogan,
|
const SizedBox(height: 16),
|
||||||
textAlign: TextAlign.center,
|
Padding(
|
||||||
style: const TextStyle(fontSize: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
),
|
child: Text(
|
||||||
),
|
context.lang.registerSlogan,
|
||||||
const SizedBox(height: 130),
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: isDark
|
||||||
|
? Colors.black.withValues(alpha: 0.3)
|
||||||
|
: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (_registrationDisabled) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
context.lang.registrationClosed,
|
context.lang.registrationClosed,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 48),
|
||||||
),
|
] else ...[
|
||||||
),
|
Text(
|
||||||
),
|
context.lang.registerUsernameSlogan,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InputDecoration getInputDecoration(String hintText) {
|
|
||||||
return InputDecoration(hintText: hintText, fillColor: Colors.grey[400]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text(''),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 50),
|
|
||||||
Text(
|
|
||||||
context.lang.registerTitle,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 30),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
|
||||||
child: Text(
|
|
||||||
context.lang.registerSlogan,
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 12),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: sloganColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 60),
|
TextField(
|
||||||
Center(
|
controller: usernameController,
|
||||||
child: Padding(
|
onChanged: (value) {
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
usernameController.text = value.toLowerCase();
|
||||||
child: Text(
|
usernameController.selection = TextSelection.fromPosition(
|
||||||
context.lang.registerUsernameSlogan,
|
TextPosition(
|
||||||
|
offset: usernameController.text.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isValidUserName = usernameController.text.length >= 3;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
inputFormatters: [
|
||||||
|
LengthLimitingTextInputFormatter(12),
|
||||||
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp('[a-z0-9A-Z._]'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.lang.registerUsernameDecoration,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: inputColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.alternate_email,
|
||||||
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showUserNameError &&
|
||||||
|
usernameController.text.length < 3) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.registerUsernameLimits,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
TextField(
|
|
||||||
controller: usernameController,
|
|
||||||
onChanged: (value) {
|
|
||||||
usernameController.text = value.toLowerCase();
|
|
||||||
usernameController.selection = TextSelection.fromPosition(
|
|
||||||
TextPosition(offset: usernameController.text.length),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_isValidUserName = usernameController.text.length >= 3;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
inputFormatters: [
|
|
||||||
LengthLimitingTextInputFormatter(12),
|
|
||||||
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
|
|
||||||
],
|
],
|
||||||
style: const TextStyle(fontSize: 17),
|
if (_showProofOfWorkError) ...[
|
||||||
decoration: getInputDecoration(
|
const SizedBox(height: 8),
|
||||||
context.lang.registerUsernameDecoration,
|
Text(
|
||||||
),
|
context.lang.registerProofOfWorkFailed,
|
||||||
),
|
style: const TextStyle(
|
||||||
const SizedBox(height: 10),
|
color: Colors.red,
|
||||||
Text(
|
fontSize: 13,
|
||||||
context.lang.registerUsernameLimits,
|
fontWeight: FontWeight.w500,
|
||||||
style: TextStyle(
|
),
|
||||||
color: _showUserNameError ? Colors.red : Colors.transparent,
|
textAlign: TextAlign.center,
|
||||||
fontSize: 12,
|
),
|
||||||
),
|
],
|
||||||
textAlign: TextAlign.center,
|
const SizedBox(height: 24),
|
||||||
),
|
FilledButton(
|
||||||
const SizedBox(height: 10),
|
onPressed: _isTryingToRegister ? null : createNewUser,
|
||||||
Text(
|
style: FilledButton.styleFrom(
|
||||||
context.lang.registerProofOfWorkFailed,
|
backgroundColor: primaryColor,
|
||||||
style: TextStyle(
|
foregroundColor: Colors.white,
|
||||||
color: _showProofOfWorkError
|
minimumSize: const Size.fromHeight(60),
|
||||||
? Colors.red
|
shape: RoundedRectangleBorder(
|
||||||
: Colors.transparent,
|
borderRadius: BorderRadius.circular(18),
|
||||||
fontSize: 12,
|
),
|
||||||
),
|
elevation: 0,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
),
|
child: _isTryingToRegister
|
||||||
const SizedBox(height: 10),
|
? const SizedBox(
|
||||||
Column(
|
width: 24,
|
||||||
children: [
|
height: 24,
|
||||||
FilledButton.icon(
|
child: CircularProgressIndicator(
|
||||||
icon: _isTryingToRegister
|
color: Colors.white,
|
||||||
? const SizedBox(
|
strokeWidth: 3,
|
||||||
width: 18,
|
),
|
||||||
height: 18,
|
)
|
||||||
child: CircularProgressIndicator(
|
: Text(
|
||||||
color: Colors.black,
|
context.lang.registerSubmitButton,
|
||||||
strokeWidth: 2,
|
style: const TextStyle(
|
||||||
),
|
fontSize: 18,
|
||||||
)
|
fontWeight: FontWeight.bold,
|
||||||
: const Icon(Icons.group),
|
),
|
||||||
onPressed: createNewUser,
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
vertical: 10,
|
|
||||||
horizontal: 30,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: _isTryingToRegister
|
const SizedBox(height: 16),
|
||||||
? WidgetStateProperty.all<MaterialColor>(
|
],
|
||||||
Colors.grey,
|
TextButton(
|
||||||
)
|
onPressed: () => context.push(
|
||||||
: null,
|
Routes.settingsBackupRecovery,
|
||||||
),
|
),
|
||||||
label: Text(
|
style: TextButton.styleFrom(
|
||||||
context.lang.registerSubmitButton,
|
minimumSize: const Size.fromHeight(50),
|
||||||
style: const TextStyle(fontSize: 17),
|
foregroundColor: secondaryButtonColor,
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Row(
|
child: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
context.lang.twonlySafeRecoverBtn,
|
||||||
children: [
|
style: const TextStyle(
|
||||||
OutlinedButton.icon(
|
fontSize: 15,
|
||||||
onPressed: () =>
|
fontWeight: FontWeight.w600,
|
||||||
context.push(Routes.settingsBackupRecovery),
|
|
||||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
)
|
)
|
||||||
: const Icon(Icons.lock_clock_rounded),
|
: const Icon(Icons.lock_clock_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
userService.currentUser.twonlySafeBackup == null
|
userService.currentUser.isBackupEnabled
|
||||||
? context.lang.backupEnableBackup
|
? context.lang.backupEnableBackup
|
||||||
: context.lang.backupChangePassword,
|
: context.lang.backupChangePassword,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
@ -64,6 +65,22 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
defaultAutoDownloadOptions;
|
defaultAutoDownloadOptions;
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
|
FutureBuilder<Map<MediaType, int>>(
|
||||||
|
future: twonlyDB.mediaFilesDao.getStorageStats(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final stats = snapshot.data ?? {};
|
||||||
|
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);
|
||||||
|
final sizeStr = formatBytes(totalBytes);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(context.lang.settingsStorageManageTitle),
|
||||||
|
subtitle: Text(sizeStr),
|
||||||
|
onTap: () => context.push(Routes.settingsStorageManage),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.settingsStorageDataStoreInGTitle),
|
title: Text(context.lang.settingsStorageDataStoreInGTitle),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class ManageStorageView extends StatefulWidget {
|
||||||
|
const ManageStorageView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ManageStorageView> createState() => _ManageStorageViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManageStorageViewState extends State<ManageStorageView> {
|
||||||
|
Map<MediaType, int> _stats = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadStats() async {
|
||||||
|
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_stats = stats;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final totalBytes = _stats.entries
|
||||||
|
.where((e) => e.key != MediaType.audio)
|
||||||
|
.fold<int>(0, (a, b) => a + b.value);
|
||||||
|
final imageBytes = _stats[MediaType.image] ?? 0;
|
||||||
|
final videoBytes = _stats[MediaType.video] ?? 0;
|
||||||
|
final gifBytes = _stats[MediaType.gif] ?? 0;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.settingsStorageManageTitle),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.settingsStorageUsed,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
formatBytes(totalBytes),
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
height: 24,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (totalBytes == 0) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final maxWidth = constraints.maxWidth;
|
||||||
|
final imageWidth = (imageBytes / totalBytes) * maxWidth;
|
||||||
|
final videoWidth = (videoBytes / totalBytes) * maxWidth;
|
||||||
|
final gifWidth = (gifBytes / totalBytes) * maxWidth;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (imageBytes > 0)
|
||||||
|
Container(width: imageWidth, color: Colors.blue),
|
||||||
|
if (videoBytes > 0)
|
||||||
|
Container(width: videoWidth, color: Colors.green),
|
||||||
|
if (gifBytes > 0)
|
||||||
|
Container(width: gifWidth, color: Colors.orange),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_StorageCategoryTile(
|
||||||
|
title: context.lang.settingsStorageImages,
|
||||||
|
size: formatBytes(imageBytes),
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
_StorageCategoryTile(
|
||||||
|
title: context.lang.settingsStorageVideos,
|
||||||
|
size: formatBytes(videoBytes),
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
_StorageCategoryTile(
|
||||||
|
title: context.lang.settingsStorageGifs,
|
||||||
|
size: formatBytes(gifBytes),
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StorageCategoryTile extends StatelessWidget {
|
||||||
|
const _StorageCategoryTile({
|
||||||
|
required this.title,
|
||||||
|
required this.size,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
final String title;
|
||||||
|
final String size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
size,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -216,6 +216,7 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
isDraftMedia: false,
|
isDraftMedia: false,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
hasCropAnalyzed: false,
|
hasCropAnalyzed: false,
|
||||||
|
hasThumbnail: false,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
);
|
);
|
||||||
final mediaService = MediaFileService(mediaFile);
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
context.lang.subscriptionPledgeTitle,
|
context.lang.subscriptionPledgeSubtitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: context.color.primary,
|
color: context.color.primary,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
|
|
@ -88,12 +88,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
desc: context.lang.subscriptionPledgeNoAdsDesc,
|
desc: context.lang.subscriptionPledgeNoAdsDesc,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_MissionRow(
|
|
||||||
icon: FontAwesomeIcons.heart,
|
|
||||||
title: context.lang.subscriptionPledgeFundedTitle,
|
|
||||||
desc: context.lang.subscriptionPledgeFundedDesc,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -145,7 +139,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
onPurchase: initAsync,
|
onPurchase: initAsync,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 30),
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.fileContract,
|
icon: FontAwesomeIcons.fileContract,
|
||||||
text: context.lang.termsOfService,
|
text: context.lang.termsOfService,
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 815 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 800 KiB |
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.2.12+121
|
version: 0.2.13+122
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
@ -212,3 +212,9 @@ flutter:
|
||||||
- assets/passwords/
|
- assets/passwords/
|
||||||
- CHANGELOG.md
|
- CHANGELOG.md
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
- family: NotoColorEmoji
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/NotoColorEmoji.ttf
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'schema_v12.dart' as v12;
|
||||||
import 'schema_v13.dart' as v13;
|
import 'schema_v13.dart' as v13;
|
||||||
import 'schema_v14.dart' as v14;
|
import 'schema_v14.dart' as v14;
|
||||||
import 'schema_v15.dart' as v15;
|
import 'schema_v15.dart' as v15;
|
||||||
|
import 'schema_v16.dart' as v16;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -54,6 +55,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v14.DatabaseAtV14(db);
|
return v14.DatabaseAtV14(db);
|
||||||
case 15:
|
case 15:
|
||||||
return v15.DatabaseAtV15(db);
|
return v15.DatabaseAtV15(db);
|
||||||
|
case 16:
|
||||||
|
return v16.DatabaseAtV16(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
|
|
@ -75,5 +78,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
13,
|
13,
|
||||||
14,
|
14,
|
||||||
15,
|
15,
|
||||||
|
16,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10584
test/drift/twonly_db/generated/schema_v16.dart
Normal file
10584
test/drift/twonly_db/generated/schema_v16.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pro_video_editor/core/platform/io/io_helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
|
|
@ -29,6 +30,9 @@ class LinkParserTest {
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('testing different urls', () async {
|
test('testing different urls', () async {
|
||||||
|
if (!Platform.isMacOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final testCases = [
|
final testCases = [
|
||||||
LinkParserTest(
|
LinkParserTest(
|
||||||
url: 'https://mastodon.social/@islieb/115883317936171927',
|
url: 'https://mastodon.social/@islieb/115883317936171927',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue