mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 07:02:12 +00:00
Improved: Memories viewer redesigned
This commit is contained in:
parent
23004acbed
commit
a5fc97c648
34 changed files with 7144 additions and 729 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.12
|
||||||
|
|
||||||
|
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
|
||||||
|
|
||||||
## 0.2.11
|
## 0.2.11
|
||||||
|
|
||||||
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
- New: Create custom shortcuts to quickly share images with pre-selected groups
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ class AppEnvironment {
|
||||||
|
|
||||||
static bool _isInitialized = false;
|
static bool _isInitialized = false;
|
||||||
|
|
||||||
static bool _isInitialized = false;
|
|
||||||
|
|
||||||
// will be loaded in the main_camera_controller.dart
|
// will be loaded in the main_camera_controller.dart
|
||||||
static List<CameraDescription> cameras = [];
|
static List<CameraDescription> cameras = [];
|
||||||
|
|
||||||
|
|
@ -34,5 +32,6 @@ 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 = 113;
|
static int latestAppVersionId = 114;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.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';
|
||||||
|
|
@ -17,6 +19,7 @@ import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
||||||
show getSignalSignedPreKeyStoreOld;
|
show getSignalSignedPreKeyStoreOld;
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
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/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';
|
||||||
|
|
@ -28,6 +31,8 @@ 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/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.service.dart';
|
||||||
|
|
@ -247,9 +252,24 @@ Future<void> runMigrations() async {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 (kDebugMode) {
|
if (kDebugMode) {
|
||||||
assert(
|
assert(
|
||||||
AppState.latestAppVersionId == 113,
|
AppState.latestAppVersionId == 114,
|
||||||
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -261,6 +281,8 @@ Future<void> runMigrations() async {
|
||||||
|
|
||||||
Future<void> postStartupTasks() async {
|
Future<void> postStartupTasks() async {
|
||||||
Log.info('Post startup started.');
|
Log.info('Post startup started.');
|
||||||
|
unawaited(MemoriesService.prewarmCache());
|
||||||
|
|
||||||
// 1. Immediate background cleanup (Non-blocking for UI)
|
// 1. Immediate background cleanup (Non-blocking for UI)
|
||||||
await twonlyDB.messagesDao.purgeMessageTable();
|
await twonlyDB.messagesDao.purgeMessageTable();
|
||||||
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
|
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,13 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<MediaFile>> getAllUnanalyzedStoredMediaFiles() async {
|
||||||
|
return (select(mediaFiles)..where(
|
||||||
|
(t) => t.stored.equals(true) & t.hasCropAnalyzed.equals(false),
|
||||||
|
))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<MediaFile>> getAllMediaFilesPendingUpload() async {
|
Future<List<MediaFile>> getAllMediaFilesPendingUpload() async {
|
||||||
return (select(mediaFiles)..where(
|
return (select(mediaFiles)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
|
|
|
||||||
2939
lib/src/database/schemas/twonly_db/drift_schema_v14.json
Normal file
2939
lib/src/database/schemas/twonly_db/drift_schema_v14.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -50,6 +50,9 @@ class MediaFiles extends Table {
|
||||||
|
|
||||||
BoolColumn get stored => boolean().withDefault(const Constant(false))();
|
BoolColumn get stored => boolean().withDefault(const Constant(false))();
|
||||||
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
|
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get hasCropAnalyzed =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
IntColumn get preProgressingProcess => integer().nullable()();
|
IntColumn get preProgressingProcess => integer().nullable()();
|
||||||
|
|
||||||
|
|
@ -67,6 +70,8 @@ class MediaFiles extends Table {
|
||||||
BlobColumn get storedFileHash => blob().nullable()();
|
BlobColumn get storedFileHash => blob().nullable()();
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
TextColumn get createdAtMonth => text().nullable()();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {mediaId};
|
Set<Column> get primaryKey => {mediaId};
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 13;
|
int get schemaVersion => 14;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
|
|
@ -195,6 +195,17 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
await m.createTable(schema.shortcuts);
|
await m.createTable(schema.shortcuts);
|
||||||
await m.createTable(schema.shortcutMembers);
|
await m.createTable(schema.shortcutMembers);
|
||||||
},
|
},
|
||||||
|
from13To14: (m, schema) async {
|
||||||
|
await m.addColumn(
|
||||||
|
schema.mediaFiles,
|
||||||
|
schema.mediaFiles.createdAtMonth,
|
||||||
|
);
|
||||||
|
await m.addColumn(schema.mediaFiles, schema.mediaFiles.isFavorite);
|
||||||
|
await m.addColumn(
|
||||||
|
schema.mediaFiles,
|
||||||
|
schema.mediaFiles.hasCropAnalyzed,
|
||||||
|
);
|
||||||
|
},
|
||||||
)(m, from, to);
|
)(m, from, to);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2676,6 +2676,36 @@ class $MediaFilesTable extends MediaFiles
|
||||||
),
|
),
|
||||||
defaultValue: const Constant(false),
|
defaultValue: const Constant(false),
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _isFavoriteMeta = const VerificationMeta(
|
||||||
|
'isFavorite',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> isFavorite = GeneratedColumn<bool>(
|
||||||
|
'is_favorite',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_favorite" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _hasCropAnalyzedMeta = const VerificationMeta(
|
||||||
|
'hasCropAnalyzed',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> hasCropAnalyzed = GeneratedColumn<bool>(
|
||||||
|
'has_crop_analyzed',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("has_crop_analyzed" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
static const VerificationMeta _preProgressingProcessMeta =
|
static const VerificationMeta _preProgressingProcessMeta =
|
||||||
const VerificationMeta('preProgressingProcess');
|
const VerificationMeta('preProgressingProcess');
|
||||||
@override
|
@override
|
||||||
|
|
@ -2792,6 +2822,17 @@ class $MediaFilesTable extends MediaFiles
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
defaultValue: currentDateAndTime,
|
defaultValue: currentDateAndTime,
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _createdAtMonthMeta = const VerificationMeta(
|
||||||
|
'createdAtMonth',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> createdAtMonth = GeneratedColumn<String>(
|
||||||
|
'created_at_month',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
mediaId,
|
mediaId,
|
||||||
|
|
@ -2801,6 +2842,8 @@ class $MediaFilesTable extends MediaFiles
|
||||||
requiresAuthentication,
|
requiresAuthentication,
|
||||||
stored,
|
stored,
|
||||||
isDraftMedia,
|
isDraftMedia,
|
||||||
|
isFavorite,
|
||||||
|
hasCropAnalyzed,
|
||||||
preProgressingProcess,
|
preProgressingProcess,
|
||||||
reuploadRequestedBy,
|
reuploadRequestedBy,
|
||||||
displayLimitInMilliseconds,
|
displayLimitInMilliseconds,
|
||||||
|
|
@ -2811,6 +2854,7 @@ class $MediaFilesTable extends MediaFiles
|
||||||
encryptionNonce,
|
encryptionNonce,
|
||||||
storedFileHash,
|
storedFileHash,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
createdAtMonth,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
|
@ -2856,6 +2900,21 @@ class $MediaFilesTable extends MediaFiles
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('is_favorite')) {
|
||||||
|
context.handle(
|
||||||
|
_isFavoriteMeta,
|
||||||
|
isFavorite.isAcceptableOrUnknown(data['is_favorite']!, _isFavoriteMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('has_crop_analyzed')) {
|
||||||
|
context.handle(
|
||||||
|
_hasCropAnalyzedMeta,
|
||||||
|
hasCropAnalyzed.isAcceptableOrUnknown(
|
||||||
|
data['has_crop_analyzed']!,
|
||||||
|
_hasCropAnalyzedMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('pre_progressing_process')) {
|
if (data.containsKey('pre_progressing_process')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_preProgressingProcessMeta,
|
_preProgressingProcessMeta,
|
||||||
|
|
@ -2934,6 +2993,15 @@ class $MediaFilesTable extends MediaFiles
|
||||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('created_at_month')) {
|
||||||
|
context.handle(
|
||||||
|
_createdAtMonthMeta,
|
||||||
|
createdAtMonth.isAcceptableOrUnknown(
|
||||||
|
data['created_at_month']!,
|
||||||
|
_createdAtMonthMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2977,6 +3045,14 @@ class $MediaFilesTable extends MediaFiles
|
||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_draft_media'],
|
data['${effectivePrefix}is_draft_media'],
|
||||||
)!,
|
)!,
|
||||||
|
isFavorite: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_favorite'],
|
||||||
|
)!,
|
||||||
|
hasCropAnalyzed: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}has_crop_analyzed'],
|
||||||
|
)!,
|
||||||
preProgressingProcess: attachedDatabase.typeMapping.read(
|
preProgressingProcess: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}pre_progressing_process'],
|
data['${effectivePrefix}pre_progressing_process'],
|
||||||
|
|
@ -3020,6 +3096,10 @@ class $MediaFilesTable extends MediaFiles
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}created_at'],
|
data['${effectivePrefix}created_at'],
|
||||||
)!,
|
)!,
|
||||||
|
createdAtMonth: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}created_at_month'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3056,6 +3136,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
final bool requiresAuthentication;
|
final bool requiresAuthentication;
|
||||||
final bool stored;
|
final bool stored;
|
||||||
final bool isDraftMedia;
|
final bool isDraftMedia;
|
||||||
|
final bool isFavorite;
|
||||||
|
final bool hasCropAnalyzed;
|
||||||
final int? preProgressingProcess;
|
final int? preProgressingProcess;
|
||||||
final List<int>? reuploadRequestedBy;
|
final List<int>? reuploadRequestedBy;
|
||||||
final int? displayLimitInMilliseconds;
|
final int? displayLimitInMilliseconds;
|
||||||
|
|
@ -3066,6 +3148,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
final Uint8List? encryptionNonce;
|
final Uint8List? encryptionNonce;
|
||||||
final Uint8List? storedFileHash;
|
final Uint8List? storedFileHash;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
final String? createdAtMonth;
|
||||||
const MediaFile({
|
const MediaFile({
|
||||||
required this.mediaId,
|
required this.mediaId,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
|
@ -3074,6 +3157,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
required this.requiresAuthentication,
|
required this.requiresAuthentication,
|
||||||
required this.stored,
|
required this.stored,
|
||||||
required this.isDraftMedia,
|
required this.isDraftMedia,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.hasCropAnalyzed,
|
||||||
this.preProgressingProcess,
|
this.preProgressingProcess,
|
||||||
this.reuploadRequestedBy,
|
this.reuploadRequestedBy,
|
||||||
this.displayLimitInMilliseconds,
|
this.displayLimitInMilliseconds,
|
||||||
|
|
@ -3084,6 +3169,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
this.encryptionNonce,
|
this.encryptionNonce,
|
||||||
this.storedFileHash,
|
this.storedFileHash,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
this.createdAtMonth,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
|
@ -3107,6 +3193,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
|
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
|
||||||
map['stored'] = Variable<bool>(stored);
|
map['stored'] = Variable<bool>(stored);
|
||||||
map['is_draft_media'] = Variable<bool>(isDraftMedia);
|
map['is_draft_media'] = Variable<bool>(isDraftMedia);
|
||||||
|
map['is_favorite'] = Variable<bool>(isFavorite);
|
||||||
|
map['has_crop_analyzed'] = Variable<bool>(hasCropAnalyzed);
|
||||||
if (!nullToAbsent || preProgressingProcess != null) {
|
if (!nullToAbsent || preProgressingProcess != null) {
|
||||||
map['pre_progressing_process'] = Variable<int>(preProgressingProcess);
|
map['pre_progressing_process'] = Variable<int>(preProgressingProcess);
|
||||||
}
|
}
|
||||||
|
|
@ -3141,6 +3229,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
|
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
|
||||||
}
|
}
|
||||||
map['created_at'] = Variable<DateTime>(createdAt);
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
if (!nullToAbsent || createdAtMonth != null) {
|
||||||
|
map['created_at_month'] = Variable<String>(createdAtMonth);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3157,6 +3248,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
requiresAuthentication: Value(requiresAuthentication),
|
requiresAuthentication: Value(requiresAuthentication),
|
||||||
stored: Value(stored),
|
stored: Value(stored),
|
||||||
isDraftMedia: Value(isDraftMedia),
|
isDraftMedia: Value(isDraftMedia),
|
||||||
|
isFavorite: Value(isFavorite),
|
||||||
|
hasCropAnalyzed: Value(hasCropAnalyzed),
|
||||||
preProgressingProcess: preProgressingProcess == null && nullToAbsent
|
preProgressingProcess: preProgressingProcess == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(preProgressingProcess),
|
: Value(preProgressingProcess),
|
||||||
|
|
@ -3186,6 +3279,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(storedFileHash),
|
: Value(storedFileHash),
|
||||||
createdAt: Value(createdAt),
|
createdAt: Value(createdAt),
|
||||||
|
createdAtMonth: createdAtMonth == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(createdAtMonth),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3210,6 +3306,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
),
|
),
|
||||||
stored: serializer.fromJson<bool>(json['stored']),
|
stored: serializer.fromJson<bool>(json['stored']),
|
||||||
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
|
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
|
||||||
|
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||||
|
hasCropAnalyzed: serializer.fromJson<bool>(json['hasCropAnalyzed']),
|
||||||
preProgressingProcess: serializer.fromJson<int?>(
|
preProgressingProcess: serializer.fromJson<int?>(
|
||||||
json['preProgressingProcess'],
|
json['preProgressingProcess'],
|
||||||
),
|
),
|
||||||
|
|
@ -3226,6 +3324,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
|
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
|
||||||
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
|
@ -3245,6 +3344,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
|
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
|
||||||
'stored': serializer.toJson<bool>(stored),
|
'stored': serializer.toJson<bool>(stored),
|
||||||
'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
|
'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
|
||||||
|
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||||
|
'hasCropAnalyzed': serializer.toJson<bool>(hasCropAnalyzed),
|
||||||
'preProgressingProcess': serializer.toJson<int?>(preProgressingProcess),
|
'preProgressingProcess': serializer.toJson<int?>(preProgressingProcess),
|
||||||
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
|
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
|
||||||
'displayLimitInMilliseconds': serializer.toJson<int?>(
|
'displayLimitInMilliseconds': serializer.toJson<int?>(
|
||||||
|
|
@ -3257,6 +3358,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
|
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
|
||||||
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3268,6 +3370,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
bool? requiresAuthentication,
|
bool? requiresAuthentication,
|
||||||
bool? stored,
|
bool? stored,
|
||||||
bool? isDraftMedia,
|
bool? isDraftMedia,
|
||||||
|
bool? isFavorite,
|
||||||
|
bool? hasCropAnalyzed,
|
||||||
Value<int?> preProgressingProcess = const Value.absent(),
|
Value<int?> preProgressingProcess = const Value.absent(),
|
||||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||||
|
|
@ -3278,6 +3382,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
}) => MediaFile(
|
}) => MediaFile(
|
||||||
mediaId: mediaId ?? this.mediaId,
|
mediaId: mediaId ?? this.mediaId,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
|
@ -3289,6 +3394,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
requiresAuthentication ?? this.requiresAuthentication,
|
requiresAuthentication ?? this.requiresAuthentication,
|
||||||
stored: stored ?? this.stored,
|
stored: stored ?? this.stored,
|
||||||
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed,
|
||||||
preProgressingProcess: preProgressingProcess.present
|
preProgressingProcess: preProgressingProcess.present
|
||||||
? preProgressingProcess.value
|
? preProgressingProcess.value
|
||||||
: this.preProgressingProcess,
|
: this.preProgressingProcess,
|
||||||
|
|
@ -3315,6 +3422,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
? storedFileHash.value
|
? storedFileHash.value
|
||||||
: this.storedFileHash,
|
: this.storedFileHash,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
createdAtMonth: createdAtMonth.present
|
||||||
|
? createdAtMonth.value
|
||||||
|
: this.createdAtMonth,
|
||||||
);
|
);
|
||||||
MediaFile copyWithCompanion(MediaFilesCompanion data) {
|
MediaFile copyWithCompanion(MediaFilesCompanion data) {
|
||||||
return MediaFile(
|
return MediaFile(
|
||||||
|
|
@ -3333,6 +3443,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
isDraftMedia: data.isDraftMedia.present
|
isDraftMedia: data.isDraftMedia.present
|
||||||
? data.isDraftMedia.value
|
? data.isDraftMedia.value
|
||||||
: this.isDraftMedia,
|
: this.isDraftMedia,
|
||||||
|
isFavorite: data.isFavorite.present
|
||||||
|
? data.isFavorite.value
|
||||||
|
: this.isFavorite,
|
||||||
|
hasCropAnalyzed: data.hasCropAnalyzed.present
|
||||||
|
? data.hasCropAnalyzed.value
|
||||||
|
: this.hasCropAnalyzed,
|
||||||
preProgressingProcess: data.preProgressingProcess.present
|
preProgressingProcess: data.preProgressingProcess.present
|
||||||
? data.preProgressingProcess.value
|
? data.preProgressingProcess.value
|
||||||
: this.preProgressingProcess,
|
: this.preProgressingProcess,
|
||||||
|
|
@ -3361,6 +3477,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
? data.storedFileHash.value
|
? data.storedFileHash.value
|
||||||
: this.storedFileHash,
|
: this.storedFileHash,
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
createdAtMonth: data.createdAtMonth.present
|
||||||
|
? data.createdAtMonth.value
|
||||||
|
: this.createdAtMonth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3374,6 +3493,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
..write('requiresAuthentication: $requiresAuthentication, ')
|
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||||
..write('stored: $stored, ')
|
..write('stored: $stored, ')
|
||||||
..write('isDraftMedia: $isDraftMedia, ')
|
..write('isDraftMedia: $isDraftMedia, ')
|
||||||
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('hasCropAnalyzed: $hasCropAnalyzed, ')
|
||||||
..write('preProgressingProcess: $preProgressingProcess, ')
|
..write('preProgressingProcess: $preProgressingProcess, ')
|
||||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||||
|
|
@ -3383,7 +3504,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('createdAt: $createdAt')
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('createdAtMonth: $createdAtMonth')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -3397,6 +3519,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
requiresAuthentication,
|
requiresAuthentication,
|
||||||
stored,
|
stored,
|
||||||
isDraftMedia,
|
isDraftMedia,
|
||||||
|
isFavorite,
|
||||||
|
hasCropAnalyzed,
|
||||||
preProgressingProcess,
|
preProgressingProcess,
|
||||||
reuploadRequestedBy,
|
reuploadRequestedBy,
|
||||||
displayLimitInMilliseconds,
|
displayLimitInMilliseconds,
|
||||||
|
|
@ -3407,6 +3531,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
$driftBlobEquality.hash(encryptionNonce),
|
$driftBlobEquality.hash(encryptionNonce),
|
||||||
$driftBlobEquality.hash(storedFileHash),
|
$driftBlobEquality.hash(storedFileHash),
|
||||||
createdAt,
|
createdAt,
|
||||||
|
createdAtMonth,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
|
|
@ -3419,6 +3544,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
other.requiresAuthentication == this.requiresAuthentication &&
|
other.requiresAuthentication == this.requiresAuthentication &&
|
||||||
other.stored == this.stored &&
|
other.stored == this.stored &&
|
||||||
other.isDraftMedia == this.isDraftMedia &&
|
other.isDraftMedia == this.isDraftMedia &&
|
||||||
|
other.isFavorite == this.isFavorite &&
|
||||||
|
other.hasCropAnalyzed == this.hasCropAnalyzed &&
|
||||||
other.preProgressingProcess == this.preProgressingProcess &&
|
other.preProgressingProcess == this.preProgressingProcess &&
|
||||||
other.reuploadRequestedBy == this.reuploadRequestedBy &&
|
other.reuploadRequestedBy == this.reuploadRequestedBy &&
|
||||||
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
|
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
|
||||||
|
|
@ -3434,7 +3561,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
other.storedFileHash,
|
other.storedFileHash,
|
||||||
this.storedFileHash,
|
this.storedFileHash,
|
||||||
) &&
|
) &&
|
||||||
other.createdAt == this.createdAt);
|
other.createdAt == this.createdAt &&
|
||||||
|
other.createdAtMonth == this.createdAtMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
|
|
@ -3445,6 +3573,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
final Value<bool> requiresAuthentication;
|
final Value<bool> requiresAuthentication;
|
||||||
final Value<bool> stored;
|
final Value<bool> stored;
|
||||||
final Value<bool> isDraftMedia;
|
final Value<bool> isDraftMedia;
|
||||||
|
final Value<bool> isFavorite;
|
||||||
|
final Value<bool> hasCropAnalyzed;
|
||||||
final Value<int?> preProgressingProcess;
|
final Value<int?> preProgressingProcess;
|
||||||
final Value<List<int>?> reuploadRequestedBy;
|
final Value<List<int>?> reuploadRequestedBy;
|
||||||
final Value<int?> displayLimitInMilliseconds;
|
final Value<int?> displayLimitInMilliseconds;
|
||||||
|
|
@ -3455,6 +3585,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
final Value<Uint8List?> encryptionNonce;
|
final Value<Uint8List?> encryptionNonce;
|
||||||
final Value<Uint8List?> storedFileHash;
|
final Value<Uint8List?> storedFileHash;
|
||||||
final Value<DateTime> createdAt;
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<String?> createdAtMonth;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
const MediaFilesCompanion({
|
const MediaFilesCompanion({
|
||||||
this.mediaId = const Value.absent(),
|
this.mediaId = const Value.absent(),
|
||||||
|
|
@ -3464,6 +3595,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.requiresAuthentication = const Value.absent(),
|
this.requiresAuthentication = const Value.absent(),
|
||||||
this.stored = const Value.absent(),
|
this.stored = const Value.absent(),
|
||||||
this.isDraftMedia = const Value.absent(),
|
this.isDraftMedia = const Value.absent(),
|
||||||
|
this.isFavorite = const Value.absent(),
|
||||||
|
this.hasCropAnalyzed = const Value.absent(),
|
||||||
this.preProgressingProcess = const Value.absent(),
|
this.preProgressingProcess = const Value.absent(),
|
||||||
this.reuploadRequestedBy = const Value.absent(),
|
this.reuploadRequestedBy = const Value.absent(),
|
||||||
this.displayLimitInMilliseconds = const Value.absent(),
|
this.displayLimitInMilliseconds = const Value.absent(),
|
||||||
|
|
@ -3474,6 +3607,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.encryptionNonce = const Value.absent(),
|
this.encryptionNonce = const Value.absent(),
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
});
|
});
|
||||||
MediaFilesCompanion.insert({
|
MediaFilesCompanion.insert({
|
||||||
|
|
@ -3484,6 +3618,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.requiresAuthentication = const Value.absent(),
|
this.requiresAuthentication = const Value.absent(),
|
||||||
this.stored = const Value.absent(),
|
this.stored = const Value.absent(),
|
||||||
this.isDraftMedia = const Value.absent(),
|
this.isDraftMedia = const Value.absent(),
|
||||||
|
this.isFavorite = const Value.absent(),
|
||||||
|
this.hasCropAnalyzed = const Value.absent(),
|
||||||
this.preProgressingProcess = const Value.absent(),
|
this.preProgressingProcess = const Value.absent(),
|
||||||
this.reuploadRequestedBy = const Value.absent(),
|
this.reuploadRequestedBy = const Value.absent(),
|
||||||
this.displayLimitInMilliseconds = const Value.absent(),
|
this.displayLimitInMilliseconds = const Value.absent(),
|
||||||
|
|
@ -3494,6 +3630,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.encryptionNonce = const Value.absent(),
|
this.encryptionNonce = const Value.absent(),
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
}) : mediaId = Value(mediaId),
|
}) : mediaId = Value(mediaId),
|
||||||
type = Value(type);
|
type = Value(type);
|
||||||
|
|
@ -3505,6 +3642,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Expression<bool>? requiresAuthentication,
|
Expression<bool>? requiresAuthentication,
|
||||||
Expression<bool>? stored,
|
Expression<bool>? stored,
|
||||||
Expression<bool>? isDraftMedia,
|
Expression<bool>? isDraftMedia,
|
||||||
|
Expression<bool>? isFavorite,
|
||||||
|
Expression<bool>? hasCropAnalyzed,
|
||||||
Expression<int>? preProgressingProcess,
|
Expression<int>? preProgressingProcess,
|
||||||
Expression<String>? reuploadRequestedBy,
|
Expression<String>? reuploadRequestedBy,
|
||||||
Expression<int>? displayLimitInMilliseconds,
|
Expression<int>? displayLimitInMilliseconds,
|
||||||
|
|
@ -3515,6 +3654,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Expression<Uint8List>? encryptionNonce,
|
Expression<Uint8List>? encryptionNonce,
|
||||||
Expression<Uint8List>? storedFileHash,
|
Expression<Uint8List>? storedFileHash,
|
||||||
Expression<DateTime>? createdAt,
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<String>? createdAtMonth,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
|
|
@ -3526,6 +3666,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
'requires_authentication': requiresAuthentication,
|
'requires_authentication': requiresAuthentication,
|
||||||
if (stored != null) 'stored': stored,
|
if (stored != null) 'stored': stored,
|
||||||
if (isDraftMedia != null) 'is_draft_media': isDraftMedia,
|
if (isDraftMedia != null) 'is_draft_media': isDraftMedia,
|
||||||
|
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||||
|
if (hasCropAnalyzed != null) 'has_crop_analyzed': hasCropAnalyzed,
|
||||||
if (preProgressingProcess != null)
|
if (preProgressingProcess != null)
|
||||||
'pre_progressing_process': preProgressingProcess,
|
'pre_progressing_process': preProgressingProcess,
|
||||||
if (reuploadRequestedBy != null)
|
if (reuploadRequestedBy != null)
|
||||||
|
|
@ -3539,6 +3681,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
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 (createdAt != null) 'created_at': createdAt,
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -3551,6 +3694,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Value<bool>? requiresAuthentication,
|
Value<bool>? requiresAuthentication,
|
||||||
Value<bool>? stored,
|
Value<bool>? stored,
|
||||||
Value<bool>? isDraftMedia,
|
Value<bool>? isDraftMedia,
|
||||||
|
Value<bool>? isFavorite,
|
||||||
|
Value<bool>? hasCropAnalyzed,
|
||||||
Value<int?>? preProgressingProcess,
|
Value<int?>? preProgressingProcess,
|
||||||
Value<List<int>?>? reuploadRequestedBy,
|
Value<List<int>?>? reuploadRequestedBy,
|
||||||
Value<int?>? displayLimitInMilliseconds,
|
Value<int?>? displayLimitInMilliseconds,
|
||||||
|
|
@ -3561,6 +3706,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Value<Uint8List?>? encryptionNonce,
|
Value<Uint8List?>? encryptionNonce,
|
||||||
Value<Uint8List?>? storedFileHash,
|
Value<Uint8List?>? storedFileHash,
|
||||||
Value<DateTime>? createdAt,
|
Value<DateTime>? createdAt,
|
||||||
|
Value<String?>? createdAtMonth,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return MediaFilesCompanion(
|
return MediaFilesCompanion(
|
||||||
|
|
@ -3572,6 +3718,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
requiresAuthentication ?? this.requiresAuthentication,
|
requiresAuthentication ?? this.requiresAuthentication,
|
||||||
stored: stored ?? this.stored,
|
stored: stored ?? this.stored,
|
||||||
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed,
|
||||||
preProgressingProcess:
|
preProgressingProcess:
|
||||||
preProgressingProcess ?? this.preProgressingProcess,
|
preProgressingProcess ?? this.preProgressingProcess,
|
||||||
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
|
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
|
||||||
|
|
@ -3584,6 +3732,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
|
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
|
||||||
storedFileHash: storedFileHash ?? this.storedFileHash,
|
storedFileHash: storedFileHash ?? this.storedFileHash,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -3620,6 +3769,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (isDraftMedia.present) {
|
if (isDraftMedia.present) {
|
||||||
map['is_draft_media'] = Variable<bool>(isDraftMedia.value);
|
map['is_draft_media'] = Variable<bool>(isDraftMedia.value);
|
||||||
}
|
}
|
||||||
|
if (isFavorite.present) {
|
||||||
|
map['is_favorite'] = Variable<bool>(isFavorite.value);
|
||||||
|
}
|
||||||
|
if (hasCropAnalyzed.present) {
|
||||||
|
map['has_crop_analyzed'] = Variable<bool>(hasCropAnalyzed.value);
|
||||||
|
}
|
||||||
if (preProgressingProcess.present) {
|
if (preProgressingProcess.present) {
|
||||||
map['pre_progressing_process'] = Variable<int>(
|
map['pre_progressing_process'] = Variable<int>(
|
||||||
preProgressingProcess.value,
|
preProgressingProcess.value,
|
||||||
|
|
@ -3658,6 +3813,9 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (createdAt.present) {
|
if (createdAt.present) {
|
||||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
}
|
}
|
||||||
|
if (createdAtMonth.present) {
|
||||||
|
map['created_at_month'] = Variable<String>(createdAtMonth.value);
|
||||||
|
}
|
||||||
if (rowid.present) {
|
if (rowid.present) {
|
||||||
map['rowid'] = Variable<int>(rowid.value);
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
}
|
}
|
||||||
|
|
@ -3674,6 +3832,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
..write('requiresAuthentication: $requiresAuthentication, ')
|
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||||
..write('stored: $stored, ')
|
..write('stored: $stored, ')
|
||||||
..write('isDraftMedia: $isDraftMedia, ')
|
..write('isDraftMedia: $isDraftMedia, ')
|
||||||
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('hasCropAnalyzed: $hasCropAnalyzed, ')
|
||||||
..write('preProgressingProcess: $preProgressingProcess, ')
|
..write('preProgressingProcess: $preProgressingProcess, ')
|
||||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||||
|
|
@ -3684,6 +3844,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
..write('encryptionNonce: $encryptionNonce, ')
|
..write('encryptionNonce: $encryptionNonce, ')
|
||||||
..write('storedFileHash: $storedFileHash, ')
|
..write('storedFileHash: $storedFileHash, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('createdAtMonth: $createdAtMonth, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
@ -14890,6 +15051,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
|
||||||
Value<bool> requiresAuthentication,
|
Value<bool> requiresAuthentication,
|
||||||
Value<bool> stored,
|
Value<bool> stored,
|
||||||
Value<bool> isDraftMedia,
|
Value<bool> isDraftMedia,
|
||||||
|
Value<bool> isFavorite,
|
||||||
|
Value<bool> hasCropAnalyzed,
|
||||||
Value<int?> preProgressingProcess,
|
Value<int?> preProgressingProcess,
|
||||||
Value<List<int>?> reuploadRequestedBy,
|
Value<List<int>?> reuploadRequestedBy,
|
||||||
Value<int?> displayLimitInMilliseconds,
|
Value<int?> displayLimitInMilliseconds,
|
||||||
|
|
@ -14900,6 +15063,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
|
||||||
Value<Uint8List?> encryptionNonce,
|
Value<Uint8List?> encryptionNonce,
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
typedef $$MediaFilesTableUpdateCompanionBuilder =
|
typedef $$MediaFilesTableUpdateCompanionBuilder =
|
||||||
|
|
@ -14911,6 +15075,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
|
||||||
Value<bool> requiresAuthentication,
|
Value<bool> requiresAuthentication,
|
||||||
Value<bool> stored,
|
Value<bool> stored,
|
||||||
Value<bool> isDraftMedia,
|
Value<bool> isDraftMedia,
|
||||||
|
Value<bool> isFavorite,
|
||||||
|
Value<bool> hasCropAnalyzed,
|
||||||
Value<int?> preProgressingProcess,
|
Value<int?> preProgressingProcess,
|
||||||
Value<List<int>?> reuploadRequestedBy,
|
Value<List<int>?> reuploadRequestedBy,
|
||||||
Value<int?> displayLimitInMilliseconds,
|
Value<int?> displayLimitInMilliseconds,
|
||||||
|
|
@ -14921,6 +15087,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
|
||||||
Value<Uint8List?> encryptionNonce,
|
Value<Uint8List?> encryptionNonce,
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -14994,6 +15161,16 @@ class $$MediaFilesTableFilterComposer
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get hasCropAnalyzed => $composableBuilder(
|
||||||
|
column: $table.hasCropAnalyzed,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnFilters<int> get preProgressingProcess => $composableBuilder(
|
ColumnFilters<int> get preProgressingProcess => $composableBuilder(
|
||||||
column: $table.preProgressingProcess,
|
column: $table.preProgressingProcess,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
|
|
@ -15045,6 +15222,11 @@ class $$MediaFilesTableFilterComposer
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<String> get createdAtMonth => $composableBuilder(
|
||||||
|
column: $table.createdAtMonth,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
Expression<bool> messagesRefs(
|
Expression<bool> messagesRefs(
|
||||||
Expression<bool> Function($$MessagesTableFilterComposer f) f,
|
Expression<bool> Function($$MessagesTableFilterComposer f) f,
|
||||||
) {
|
) {
|
||||||
|
|
@ -15115,6 +15297,16 @@ class $$MediaFilesTableOrderingComposer
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get hasCropAnalyzed => $composableBuilder(
|
||||||
|
column: $table.hasCropAnalyzed,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnOrderings<int> get preProgressingProcess => $composableBuilder(
|
ColumnOrderings<int> get preProgressingProcess => $composableBuilder(
|
||||||
column: $table.preProgressingProcess,
|
column: $table.preProgressingProcess,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
|
@ -15164,6 +15356,11 @@ class $$MediaFilesTableOrderingComposer
|
||||||
column: $table.createdAt,
|
column: $table.createdAt,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<String> get createdAtMonth => $composableBuilder(
|
||||||
|
column: $table.createdAtMonth,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$MediaFilesTableAnnotationComposer
|
class $$MediaFilesTableAnnotationComposer
|
||||||
|
|
@ -15206,6 +15403,16 @@ class $$MediaFilesTableAnnotationComposer
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get hasCropAnalyzed => $composableBuilder(
|
||||||
|
column: $table.hasCropAnalyzed,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedColumn<int> get preProgressingProcess => $composableBuilder(
|
GeneratedColumn<int> get preProgressingProcess => $composableBuilder(
|
||||||
column: $table.preProgressingProcess,
|
column: $table.preProgressingProcess,
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
|
|
@ -15255,6 +15462,11 @@ class $$MediaFilesTableAnnotationComposer
|
||||||
GeneratedColumn<DateTime> get createdAt =>
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get createdAtMonth => $composableBuilder(
|
||||||
|
column: $table.createdAtMonth,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
Expression<T> messagesRefs<T extends Object>(
|
Expression<T> messagesRefs<T extends Object>(
|
||||||
Expression<T> Function($$MessagesTableAnnotationComposer a) f,
|
Expression<T> Function($$MessagesTableAnnotationComposer a) f,
|
||||||
) {
|
) {
|
||||||
|
|
@ -15316,6 +15528,8 @@ class $$MediaFilesTableTableManager
|
||||||
Value<bool> requiresAuthentication = const Value.absent(),
|
Value<bool> requiresAuthentication = const Value.absent(),
|
||||||
Value<bool> stored = const Value.absent(),
|
Value<bool> stored = const Value.absent(),
|
||||||
Value<bool> isDraftMedia = const Value.absent(),
|
Value<bool> isDraftMedia = const Value.absent(),
|
||||||
|
Value<bool> isFavorite = const Value.absent(),
|
||||||
|
Value<bool> hasCropAnalyzed = const Value.absent(),
|
||||||
Value<int?> preProgressingProcess = const Value.absent(),
|
Value<int?> preProgressingProcess = const Value.absent(),
|
||||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||||
|
|
@ -15326,6 +15540,7 @@ class $$MediaFilesTableTableManager
|
||||||
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<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => MediaFilesCompanion(
|
}) => MediaFilesCompanion(
|
||||||
mediaId: mediaId,
|
mediaId: mediaId,
|
||||||
|
|
@ -15335,6 +15550,8 @@ class $$MediaFilesTableTableManager
|
||||||
requiresAuthentication: requiresAuthentication,
|
requiresAuthentication: requiresAuthentication,
|
||||||
stored: stored,
|
stored: stored,
|
||||||
isDraftMedia: isDraftMedia,
|
isDraftMedia: isDraftMedia,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
hasCropAnalyzed: hasCropAnalyzed,
|
||||||
preProgressingProcess: preProgressingProcess,
|
preProgressingProcess: preProgressingProcess,
|
||||||
reuploadRequestedBy: reuploadRequestedBy,
|
reuploadRequestedBy: reuploadRequestedBy,
|
||||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||||
|
|
@ -15345,6 +15562,7 @@ class $$MediaFilesTableTableManager
|
||||||
encryptionNonce: encryptionNonce,
|
encryptionNonce: encryptionNonce,
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
|
|
@ -15356,6 +15574,8 @@ class $$MediaFilesTableTableManager
|
||||||
Value<bool> requiresAuthentication = const Value.absent(),
|
Value<bool> requiresAuthentication = const Value.absent(),
|
||||||
Value<bool> stored = const Value.absent(),
|
Value<bool> stored = const Value.absent(),
|
||||||
Value<bool> isDraftMedia = const Value.absent(),
|
Value<bool> isDraftMedia = const Value.absent(),
|
||||||
|
Value<bool> isFavorite = const Value.absent(),
|
||||||
|
Value<bool> hasCropAnalyzed = const Value.absent(),
|
||||||
Value<int?> preProgressingProcess = const Value.absent(),
|
Value<int?> preProgressingProcess = const Value.absent(),
|
||||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||||
|
|
@ -15366,6 +15586,7 @@ class $$MediaFilesTableTableManager
|
||||||
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<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => MediaFilesCompanion.insert(
|
}) => MediaFilesCompanion.insert(
|
||||||
mediaId: mediaId,
|
mediaId: mediaId,
|
||||||
|
|
@ -15375,6 +15596,8 @@ class $$MediaFilesTableTableManager
|
||||||
requiresAuthentication: requiresAuthentication,
|
requiresAuthentication: requiresAuthentication,
|
||||||
stored: stored,
|
stored: stored,
|
||||||
isDraftMedia: isDraftMedia,
|
isDraftMedia: isDraftMedia,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
hasCropAnalyzed: hasCropAnalyzed,
|
||||||
preProgressingProcess: preProgressingProcess,
|
preProgressingProcess: preProgressingProcess,
|
||||||
reuploadRequestedBy: reuploadRequestedBy,
|
reuploadRequestedBy: reuploadRequestedBy,
|
||||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||||
|
|
@ -15385,6 +15608,7 @@ class $$MediaFilesTableTableManager
|
||||||
encryptionNonce: encryptionNonce,
|
encryptionNonce: encryptionNonce,
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
|
|
|
||||||
|
|
@ -7056,6 +7056,511 @@ i1.GeneratedColumn<int> _column_238(String aliasedName) =>
|
||||||
type: i1.DriftSqlType.int,
|
type: i1.DriftSqlType.int,
|
||||||
$customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE',
|
$customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema14 extends i0.VersionedSchema {
|
||||||
|
Schema14({required super.database}) : super(version: 14);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
contacts,
|
||||||
|
groups,
|
||||||
|
mediaFiles,
|
||||||
|
messages,
|
||||||
|
messageHistories,
|
||||||
|
reactions,
|
||||||
|
groupMembers,
|
||||||
|
receipts,
|
||||||
|
receivedReceipts,
|
||||||
|
signalIdentityKeyStores,
|
||||||
|
signalPreKeyStores,
|
||||||
|
signalSenderKeyStores,
|
||||||
|
signalSessionStores,
|
||||||
|
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 Shape49 mediaFiles = Shape49(
|
||||||
|
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_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 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 Shape49 extends i0.VersionedTable {
|
||||||
|
Shape49({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 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_239(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'is_favorite',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))',
|
||||||
|
defaultValue: const i1.CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_240(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'has_crop_analyzed',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints:
|
||||||
|
'NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1))',
|
||||||
|
defaultValue: const i1.CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_241(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'created_at_month',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
$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,
|
||||||
|
|
@ -7069,6 +7574,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||||
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,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -7132,6 +7638,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from12To13(migrator, schema);
|
await from12To13(migrator, schema);
|
||||||
return 13;
|
return 13;
|
||||||
|
case 13:
|
||||||
|
final schema = Schema14(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from13To14(migrator, schema);
|
||||||
|
return 14;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -7151,6 +7662,7 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||||
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,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
|
@ -7165,5 +7677,6 @@ i1.OnUpgrade stepByStep({
|
||||||
from10To11: from10To11,
|
from10To11: from10To11,
|
||||||
from11To12: from11To12,
|
from11To12: from11To12,
|
||||||
from12To13: from12To13,
|
from12To13: from12To13,
|
||||||
|
from13To14: from13To14,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1178,12 +1178,6 @@ abstract class AppLocalizations {
|
||||||
/// **'The plan upgrade must be paid for annually, as the current plan is also billed annually.'**
|
/// **'The plan upgrade must be paid for annually, as the current plan is also billed annually.'**
|
||||||
String get errorPlanUpgradeNotYearly;
|
String get errorPlanUpgradeNotYearly;
|
||||||
|
|
||||||
/// No description provided for @upgradeToPaidPlan.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Upgrade to a paid plan.'**
|
|
||||||
String get upgradeToPaidPlan;
|
|
||||||
|
|
||||||
/// No description provided for @upgradeToPaidPlanButton.
|
/// No description provided for @upgradeToPaidPlanButton.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1322,12 +1316,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Delete file'**
|
/// **'Delete file'**
|
||||||
String get galleryDelete;
|
String get galleryDelete;
|
||||||
|
|
||||||
/// No description provided for @galleryDetails.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Show details'**
|
|
||||||
String get galleryDetails;
|
|
||||||
|
|
||||||
/// No description provided for @galleryExport.
|
/// No description provided for @galleryExport.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1340,6 +1328,36 @@ abstract class AppLocalizations {
|
||||||
/// **'Successfully saved in the Gallery.'**
|
/// **'Successfully saved in the Gallery.'**
|
||||||
String get galleryExportSuccess;
|
String get galleryExportSuccess;
|
||||||
|
|
||||||
|
/// No description provided for @gallerySelectAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select all'**
|
||||||
|
String get gallerySelectAll;
|
||||||
|
|
||||||
|
/// No description provided for @galleryDeselectAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Deselect all'**
|
||||||
|
String get galleryDeselectAll;
|
||||||
|
|
||||||
|
/// No description provided for @galleryFavorite.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Favorite'**
|
||||||
|
String get galleryFavorite;
|
||||||
|
|
||||||
|
/// No description provided for @galleryUnfavorite.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Unfavorite'**
|
||||||
|
String get galleryUnfavorite;
|
||||||
|
|
||||||
|
/// No description provided for @galleryCancel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cancel'**
|
||||||
|
String get galleryCancel;
|
||||||
|
|
||||||
/// No description provided for @memoriesEmpty.
|
/// No description provided for @memoriesEmpty.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3157,6 +3175,48 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'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.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Secure by Design'**
|
||||||
|
String get subscriptionPledgeSecureTitle;
|
||||||
|
|
||||||
|
/// No description provided for @subscriptionPledgeSecureDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your messages and shared moments are fully end-to-end encrypted.'**
|
||||||
|
String get subscriptionPledgeSecureDesc;
|
||||||
|
|
||||||
|
/// No description provided for @subscriptionPledgeNoAdsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No Ads or Data selling'**
|
||||||
|
String get subscriptionPledgeNoAdsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @subscriptionPledgeNoAdsDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'twonly will never show advertisements or sell your private data.'**
|
||||||
|
String get subscriptionPledgeNoAdsDesc;
|
||||||
|
|
||||||
|
/// No description provided for @subscriptionPledgeFundedTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Independent and funded by Users'**
|
||||||
|
String get subscriptionPledgeFundedTitle;
|
||||||
|
|
||||||
|
/// No description provided for @subscriptionPledgeFundedDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'**
|
||||||
|
String get subscriptionPledgeFundedDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -601,9 +601,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get errorPlanUpgradeNotYearly =>
|
String get errorPlanUpgradeNotYearly =>
|
||||||
'Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.';
|
'Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
||||||
return 'Auf $planId upgraden$sufix';
|
return 'Auf $planId upgraden$sufix';
|
||||||
|
|
@ -677,15 +674,27 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get galleryDelete => 'Datei löschen';
|
String get galleryDelete => 'Datei löschen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get galleryDetails => 'Details anzeigen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryExport => 'In Galerie exportieren';
|
String get galleryExport => 'In Galerie exportieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryExportSuccess => 'Erfolgreich in der Gallery gespeichert.';
|
String get galleryExportSuccess => 'Erfolgreich in der Gallery gespeichert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gallerySelectAll => 'Alle auswählen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDeselectAll => 'Auswahl aufheben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryFavorite => 'Als Favorit markieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryUnfavorite => 'Favorit entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryCancel => 'Abbrechen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get memoriesEmpty =>
|
String get memoriesEmpty =>
|
||||||
'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.';
|
'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.';
|
||||||
|
|
@ -1780,4 +1789,29 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
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
|
||||||
|
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeSecureDesc =>
|
||||||
|
'Deine Nachrichten und Bilder sind immer vollständig Ende-zu-Ende verschlüsselt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeNoAdsTitle => 'Keine Werbung oder Datenverkauf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeNoAdsDesc =>
|
||||||
|
'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeFundedTitle =>
|
||||||
|
'Unabhängig und durch Nutzer finanziert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeFundedDesc =>
|
||||||
|
'twonly wird rein durch Nutzer-Abonnements finanziert, um unsere Unabhängigkeit und die Zukunft von twonly zu sichern.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -595,9 +595,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get errorPlanUpgradeNotYearly =>
|
String get errorPlanUpgradeNotYearly =>
|
||||||
'The plan upgrade must be paid for annually, as the current plan is also billed annually.';
|
'The plan upgrade must be paid for annually, as the current plan is also billed annually.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get upgradeToPaidPlan => 'Upgrade to a paid plan.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
||||||
return 'Upgrade to $planId$sufix';
|
return 'Upgrade to $planId$sufix';
|
||||||
|
|
@ -671,15 +668,27 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get galleryDelete => 'Delete file';
|
String get galleryDelete => 'Delete file';
|
||||||
|
|
||||||
@override
|
|
||||||
String get galleryDetails => 'Show details';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryExport => 'Export to gallery';
|
String get galleryExport => 'Export to gallery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryExportSuccess => 'Successfully saved in the Gallery.';
|
String get galleryExportSuccess => 'Successfully saved in the Gallery.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gallerySelectAll => 'Select all';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryDeselectAll => 'Deselect all';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryFavorite => 'Favorite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryUnfavorite => 'Unfavorite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get galleryCancel => 'Cancel';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get memoriesEmpty =>
|
String get memoriesEmpty =>
|
||||||
'As soon as you save pictures or videos, they end up here in your memories.';
|
'As soon as you save pictures or videos, they end up here in your memories.';
|
||||||
|
|
@ -1764,4 +1773,28 @@ 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
|
||||||
|
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeSecureDesc =>
|
||||||
|
'Your messages and shared moments are fully end-to-end encrypted.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeNoAdsTitle => 'No Ads or Data selling';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeNoAdsDesc =>
|
||||||
|
'twonly will never show advertisements or sell your private data.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeFundedTitle => 'Independent and funded by Users';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subscriptionPledgeFundedDesc =>
|
||||||
|
'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9218abf0961c072edd2f8aa5035d06a331b853c6
|
Subproject commit 10b4bfedcc6c99e4ba0374b306610d297b9e2276
|
||||||
|
|
@ -5,9 +5,11 @@ class MemoryItem {
|
||||||
MemoryItem({
|
MemoryItem({
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
required this.messages,
|
required this.messages,
|
||||||
|
this.sender,
|
||||||
});
|
});
|
||||||
final List<Message> messages;
|
final List<Message> messages;
|
||||||
final MediaFileService mediaService;
|
final MediaFileService mediaService;
|
||||||
|
final Contact? sender;
|
||||||
|
|
||||||
static Future<Map<String, MemoryItem>> convertFromMessages(
|
static Future<Map<String, MemoryItem>> convertFromMessages(
|
||||||
List<Message> messages,
|
List<Message> messages,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
|
@ -372,4 +374,142 @@ class MediaFileService {
|
||||||
namePrefix: '.overlay',
|
namePrefix: '.overlay',
|
||||||
extensionParam: 'png',
|
extensionParam: 'png',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<void> cropTransparentBorders() async {
|
||||||
|
if (mediaFile.type != MediaType.image) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storedPath.existsSync()) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bytes = storedPath.readAsBytesSync();
|
||||||
|
final image = img.decodeImage(bytes);
|
||||||
|
if (image == null) {
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var minY = 0;
|
||||||
|
var maxY = image.height - 1;
|
||||||
|
var minX = 0;
|
||||||
|
var maxX = image.width - 1;
|
||||||
|
|
||||||
|
var found = false;
|
||||||
|
for (var y = 0; y < image.height; y++) {
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
if (image.getPixel(x, y).a > 10) {
|
||||||
|
minY = y;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = false;
|
||||||
|
for (var y = image.height - 1; y >= minY; y--) {
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
if (image.getPixel(x, y).a > 10) {
|
||||||
|
maxY = y;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = false;
|
||||||
|
for (var x = 0; x < image.width; x++) {
|
||||||
|
for (var y = minY; y <= maxY; y++) {
|
||||||
|
if (image.getPixel(x, y).a > 10) {
|
||||||
|
minX = x;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = false;
|
||||||
|
for (var x = image.width - 1; x >= minX; x--) {
|
||||||
|
for (var y = minY; y <= maxY; y++) {
|
||||||
|
if (image.getPixel(x, y).a > 10) {
|
||||||
|
maxX = x;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newWidth = maxX - minX + 1;
|
||||||
|
final newHeight = maxY - minY + 1;
|
||||||
|
|
||||||
|
if (minY > 0 ||
|
||||||
|
maxY < image.height - 1 ||
|
||||||
|
minX > 0 ||
|
||||||
|
maxX < image.width - 1) {
|
||||||
|
if (newWidth > 10 && newHeight > 10) {
|
||||||
|
final cropped = img.copyCrop(
|
||||||
|
image,
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight,
|
||||||
|
);
|
||||||
|
final pngBytes = img.encodePng(cropped);
|
||||||
|
final webpBytes = await FlutterImageCompress.compressWithList(
|
||||||
|
pngBytes,
|
||||||
|
format: CompressFormat.webp,
|
||||||
|
quality: 90,
|
||||||
|
);
|
||||||
|
storedPath.writeAsBytesSync(webpBytes);
|
||||||
|
|
||||||
|
if (thumbnailPath.existsSync()) {
|
||||||
|
thumbnailPath.deleteSync();
|
||||||
|
}
|
||||||
|
await createThumbnail();
|
||||||
|
final checksum = await sha256File(storedPath);
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
MediaFilesCompanion(
|
||||||
|
hasCropAnalyzed: const Value(true),
|
||||||
|
storedFileHash: Value(Uint8List.fromList(checksum)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await updateFromDB();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
);
|
||||||
|
await updateFromDB();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
|
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
|
||||||
|
);
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||||
|
);
|
||||||
|
await updateFromDB();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
332
lib/src/services/memories/memories.service.dart
Normal file
332
lib/src/services/memories/memories.service.dart
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
class MemoriesState {
|
||||||
|
const MemoriesState({
|
||||||
|
required this.filesToMigrate,
|
||||||
|
required this.galleryItems,
|
||||||
|
required this.months,
|
||||||
|
required this.orderedByMonth,
|
||||||
|
required this.galleryItemsLastYears,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int filesToMigrate;
|
||||||
|
final List<MemoryItem> galleryItems;
|
||||||
|
final List<String> months;
|
||||||
|
final Map<String, List<int>> orderedByMonth;
|
||||||
|
final Map<int, List<MemoryItem>> galleryItemsLastYears;
|
||||||
|
|
||||||
|
bool get isLoading => filesToMigrate > 0;
|
||||||
|
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
|
||||||
|
|
||||||
|
MemoriesState copyWith({
|
||||||
|
int? filesToMigrate,
|
||||||
|
List<MemoryItem>? galleryItems,
|
||||||
|
List<String>? months,
|
||||||
|
Map<String, List<int>>? orderedByMonth,
|
||||||
|
Map<int, List<MemoryItem>>? galleryItemsLastYears,
|
||||||
|
}) {
|
||||||
|
return MemoriesState(
|
||||||
|
filesToMigrate: filesToMigrate ?? this.filesToMigrate,
|
||||||
|
galleryItems: galleryItems ?? this.galleryItems,
|
||||||
|
months: months ?? this.months,
|
||||||
|
orderedByMonth: orderedByMonth ?? this.orderedByMonth,
|
||||||
|
galleryItemsLastYears:
|
||||||
|
galleryItemsLastYears ?? this.galleryItemsLastYears,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoriesService {
|
||||||
|
MemoriesService() {
|
||||||
|
if (_cachedState != null) {
|
||||||
|
_currentState = _cachedState!;
|
||||||
|
}
|
||||||
|
unawaited(_initAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
static MemoriesState? _cachedState;
|
||||||
|
|
||||||
|
final _stateController = StreamController<MemoriesState>.broadcast();
|
||||||
|
Stream<MemoriesState> get watchState => _stateController.stream;
|
||||||
|
|
||||||
|
MemoriesState _currentState = const MemoriesState(
|
||||||
|
filesToMigrate: 0,
|
||||||
|
galleryItems: [],
|
||||||
|
months: [],
|
||||||
|
orderedByMonth: {},
|
||||||
|
galleryItemsLastYears: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
MemoriesState get currentState => _currentState;
|
||||||
|
|
||||||
|
StreamSubscription<List<MediaFile>>? _dbSubscription;
|
||||||
|
|
||||||
|
/// Instantly pre-warms the gallery state from disk cache during app loading
|
||||||
|
static Future<void> prewarmCache() async {
|
||||||
|
try {
|
||||||
|
final data = await KeyValueStore.get('memories_cache');
|
||||||
|
if (data != null && data['items'] is List) {
|
||||||
|
final itemList = data['items'] as List;
|
||||||
|
if (itemList.isEmpty) return;
|
||||||
|
|
||||||
|
final mediaIds = itemList
|
||||||
|
.map((e) => (e as Map<String, dynamic>)['mediaId'] as String?)
|
||||||
|
.whereType<String>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final mediaFiles =
|
||||||
|
await twonlyDB.mediaFilesDao.getMediaFilesByIds(mediaIds);
|
||||||
|
final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m};
|
||||||
|
|
||||||
|
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
||||||
|
final contactMap = {for (final c in allContacts) c.userId: c};
|
||||||
|
|
||||||
|
final now = clock.now();
|
||||||
|
final tempGalleryItems = <MemoryItem>[];
|
||||||
|
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||||
|
|
||||||
|
for (final itemJson in itemList) {
|
||||||
|
final map = itemJson as Map<String, dynamic>;
|
||||||
|
final mediaId = map['mediaId'] as String?;
|
||||||
|
final senderUserId = map['senderUserId'] as int?;
|
||||||
|
if (mediaId == null) continue;
|
||||||
|
|
||||||
|
final mediaFile = mediaFileMap[mediaId];
|
||||||
|
if (mediaFile == null) continue;
|
||||||
|
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
if (!mediaService.imagePreviewAvailable) continue;
|
||||||
|
|
||||||
|
final contact =
|
||||||
|
senderUserId != null ? contactMap[senderUserId] : 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>>{};
|
||||||
|
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);
|
||||||
|
|
||||||
|
_cachedState = MemoriesState(
|
||||||
|
filesToMigrate: 0,
|
||||||
|
galleryItems: tempGalleryItems,
|
||||||
|
months: tempMonths,
|
||||||
|
orderedByMonth: tempOrderedByMonth,
|
||||||
|
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error prewarming memories cache: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initAsync() async {
|
||||||
|
try {
|
||||||
|
// 1. Perform Inventory / Migration of non-hashed stored files
|
||||||
|
final nonHashedFiles =
|
||||||
|
await twonlyDB.mediaFilesDao.getAllNonHashedStoredMediaFiles();
|
||||||
|
final unanalyzedFiles =
|
||||||
|
await twonlyDB.mediaFilesDao.getAllUnanalyzedStoredMediaFiles();
|
||||||
|
|
||||||
|
final totalToMigrate = nonHashedFiles.length + unanalyzedFiles.length;
|
||||||
|
if (totalToMigrate > 0) {
|
||||||
|
_updateState(filesToMigrate: totalToMigrate);
|
||||||
|
|
||||||
|
for (final mediaFile in nonHashedFiles) {
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
await mediaService.hashStoredMedia();
|
||||||
|
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final mediaFile in unanalyzedFiles) {
|
||||||
|
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();
|
||||||
|
_dbSubscription = twonlyDB.mediaFilesDao
|
||||||
|
.watchAllStoredMediaFiles()
|
||||||
|
.listen(_processMediaFilesStream);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error initializing MemoriesService: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
|
||||||
|
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 allMessages =
|
||||||
|
await twonlyDB.messagesDao.getMessagesByMediaIds(mediaIds);
|
||||||
|
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
||||||
|
|
||||||
|
final contactMap = {for (final c in allContacts) c.userId: c};
|
||||||
|
final mediaIdToSenderContact = <String, Contact>{};
|
||||||
|
|
||||||
|
for (final msg in allMessages) {
|
||||||
|
if (msg.mediaId != null && msg.senderId != null) {
|
||||||
|
final contact = contactMap[msg.senderId];
|
||||||
|
if (contact != null) {
|
||||||
|
mediaIdToSenderContact[msg.mediaId!] = contact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final mediaFile in mediaFiles) {
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
if (!mediaService.imagePreviewAvailable) continue;
|
||||||
|
|
||||||
|
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,
|
||||||
|
galleryItems: tempGalleryItems,
|
||||||
|
months: tempMonths,
|
||||||
|
orderedByMonth: tempOrderedByMonth,
|
||||||
|
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||||
|
);
|
||||||
|
|
||||||
|
_cachedState = newState;
|
||||||
|
_updateStateWithObject(newState);
|
||||||
|
|
||||||
|
// Persist to KeyValueStore cache asynchronously
|
||||||
|
final cacheList = tempGalleryItems
|
||||||
|
.map(
|
||||||
|
(item) => {
|
||||||
|
'mediaId': item.mediaService.mediaFile.mediaId,
|
||||||
|
'senderUserId': item.sender?.userId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
unawaited(KeyValueStore.put('memories_cache', {'items': cacheList}));
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error processing media files stream in MemoriesService: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateStateWithObject(MemoriesState newState) {
|
||||||
|
_currentState = newState;
|
||||||
|
if (!_stateController.isClosed) {
|
||||||
|
_stateController.add(_currentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateState({int? filesToMigrate}) {
|
||||||
|
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
|
||||||
|
if (!_stateController.isClosed) {
|
||||||
|
_stateController.add(_currentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_dbSubscription?.cancel();
|
||||||
|
_stateController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@ 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/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/shared/memory_item_slider.view.dart';
|
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/shared/memory_item_thumbnail.comp.dart';
|
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||||
|
|
||||||
class InChatMediaViewer extends StatefulWidget {
|
class InChatMediaViewer extends StatefulWidget {
|
||||||
const InChatMediaViewer({
|
const InChatMediaViewer({
|
||||||
|
|
@ -36,6 +36,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
int? galleryItemIndex;
|
int? galleryItemIndex;
|
||||||
StreamSubscription<Message?>? messageStream;
|
StreamSubscription<Message?>? messageStream;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
late final ValueNotifier<String?> _activeMediaIdNotifier =
|
||||||
|
ValueNotifier(widget.message.mediaId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -71,10 +73,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
messageStream?.cancel();
|
messageStream?.cancel();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
// videoController?.dispose();
|
_activeMediaIdNotifier.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initStream() async {
|
Future<void> initStream() async {
|
||||||
|
|
@ -99,14 +101,27 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
|
|
||||||
Future<void> onTap() async {
|
Future<void> onTap() async {
|
||||||
if (galleryItemIndex == null) return;
|
if (galleryItemIndex == null) return;
|
||||||
|
_activeMediaIdNotifier.value = widget.message.mediaId;
|
||||||
|
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
|
transitionDuration: const Duration(milliseconds: 350),
|
||||||
galleryItems: widget.galleryItems,
|
reverseTransitionDuration: const Duration(milliseconds: 350),
|
||||||
initialIndex: galleryItemIndex!,
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
),
|
return SynchronizedImageViewerScreen(
|
||||||
|
galleryItems: widget.galleryItems,
|
||||||
|
initialIndex: galleryItemIndex!,
|
||||||
|
activeMediaIdNotifier: _activeMediaIdNotifier,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -148,9 +163,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: galleryItemIndex != null
|
child: galleryItemIndex != null
|
||||||
? MemoriesItemThumbnailComp(
|
? MemoriesThumbnailComp(
|
||||||
galleryItem: widget.galleryItems[galleryItemIndex!],
|
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
activeMediaIdNotifier: _activeMediaIdNotifier,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
||||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
|
||||||
import 'package:twonly/src/visual/views/shared/memory_item_slider.view.dart';
|
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
|
||||||
|
|
||||||
class MessageContextMenu extends StatelessWidget {
|
class MessageContextMenu extends StatelessWidget {
|
||||||
const MessageContextMenu({
|
const MessageContextMenu({
|
||||||
|
|
@ -77,9 +77,22 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
|
transitionDuration: const Duration(milliseconds: 350),
|
||||||
galleryItems: galleryItems,
|
reverseTransitionDuration: const Duration(milliseconds: 350),
|
||||||
),
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
|
return SynchronizedImageViewerScreen(
|
||||||
|
galleryItems: galleryItems,
|
||||||
|
initialIndex: 0,
|
||||||
|
activeMediaIdNotifier:
|
||||||
|
ValueNotifier(mediaFileService!.mediaFile.mediaId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
int _activePageIdx = 1;
|
int _activePageIdx = 1;
|
||||||
double _offsetRatio = 0;
|
double _offsetRatio = 0;
|
||||||
double _offsetFromOne = 0;
|
double _offsetFromOne = 0;
|
||||||
|
bool _isBottomNavVisible = true;
|
||||||
Timer? _disableCameraTimer;
|
Timer? _disableCameraTimer;
|
||||||
|
|
||||||
final MainCameraController _mainCameraController = MainCameraController();
|
final MainCameraController _mainCameraController = MainCameraController();
|
||||||
|
|
@ -174,6 +175,29 @@ class HomeViewState extends State<HomeView> {
|
||||||
bool _onPageView(ScrollNotification notification) {
|
bool _onPageView(ScrollNotification notification) {
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
|
|
||||||
|
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
||||||
|
if (_activePageIdx == 2 &&
|
||||||
|
notification.metrics.pixels < 100 &&
|
||||||
|
!_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
} else if (notification is ScrollUpdateNotification) {
|
||||||
|
final delta = notification.scrollDelta ?? 0;
|
||||||
|
if (delta > 5 &&
|
||||||
|
_isBottomNavVisible &&
|
||||||
|
(_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = false;
|
||||||
|
});
|
||||||
|
} else if (delta < -5 && !_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
|
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0);
|
_offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0);
|
||||||
|
|
@ -259,39 +283,48 @@ class HomeViewState extends State<HomeView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: AnimatedSize(
|
||||||
showSelectedLabels: false,
|
duration: const Duration(milliseconds: 250),
|
||||||
showUnselectedLabels: false,
|
curve: Curves.easeInOut,
|
||||||
unselectedIconTheme: IconThemeData(
|
child: _isBottomNavVisible
|
||||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
|
? BottomNavigationBar(
|
||||||
),
|
showSelectedLabels: false,
|
||||||
selectedIconTheme: IconThemeData(
|
showUnselectedLabels: false,
|
||||||
color: Theme.of(context).colorScheme.inverseSurface,
|
unselectedIconTheme: IconThemeData(
|
||||||
),
|
color: Theme.of(context)
|
||||||
items: const [
|
.colorScheme
|
||||||
BottomNavigationBarItem(
|
.inverseSurface
|
||||||
icon: FaIcon(FontAwesomeIcons.solidComments),
|
.withAlpha(150),
|
||||||
label: '',
|
),
|
||||||
),
|
selectedIconTheme: IconThemeData(
|
||||||
BottomNavigationBarItem(
|
color: Theme.of(context).colorScheme.inverseSurface,
|
||||||
icon: FaIcon(FontAwesomeIcons.camera),
|
),
|
||||||
label: '',
|
items: const [
|
||||||
),
|
BottomNavigationBarItem(
|
||||||
BottomNavigationBarItem(
|
icon: FaIcon(FontAwesomeIcons.solidComments),
|
||||||
icon: FaIcon(FontAwesomeIcons.photoFilm),
|
label: '',
|
||||||
label: '',
|
),
|
||||||
),
|
BottomNavigationBarItem(
|
||||||
],
|
icon: FaIcon(FontAwesomeIcons.camera),
|
||||||
onTap: (index) async {
|
label: '',
|
||||||
_activePageIdx = index;
|
),
|
||||||
await _homeViewPageController.animateToPage(
|
BottomNavigationBarItem(
|
||||||
index,
|
icon: FaIcon(FontAwesomeIcons.photoFilm),
|
||||||
duration: const Duration(milliseconds: 100),
|
label: '',
|
||||||
curve: Curves.bounceIn,
|
),
|
||||||
);
|
],
|
||||||
if (mounted) setState(() {});
|
onTap: (index) async {
|
||||||
},
|
_activePageIdx = index;
|
||||||
currentIndex: _activePageIdx,
|
await _homeViewPageController.animateToPage(
|
||||||
|
index,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.bounceIn,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
currentIndex: _activePageIdx,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class MemoriesFlashbackBannerComp extends StatelessWidget {
|
||||||
|
const MemoriesFlashbackBannerComp({
|
||||||
|
required this.lastYears,
|
||||||
|
required this.onOpenFlashback,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<int, List<MemoryItem>> lastYears;
|
||||||
|
final void Function(List<MemoryItem> items, int index) onOpenFlashback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (lastYears.isEmpty) return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 150,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
itemCount: lastYears.length,
|
||||||
|
separatorBuilder: (context, _) => const SizedBox(width: 12),
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final entry = lastYears.entries.elementAt(idx);
|
||||||
|
final years = entry.key;
|
||||||
|
final items = entry.value;
|
||||||
|
|
||||||
|
var text = context.lang.memoriesAYearAgo;
|
||||||
|
if (years > 1) {
|
||||||
|
text = context.lang.memoriesXYearsAgo(years);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onOpenFlashback(items, 0),
|
||||||
|
child: Container(
|
||||||
|
width: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Image.file(
|
||||||
|
items.first.mediaService.storedPath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.center,
|
||||||
|
colors: [
|
||||||
|
Colors.black.withValues(alpha: 0.7),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 12,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/components/memory_transition_painter.dart';
|
||||||
|
|
||||||
|
class MemoriesThumbnailComp extends StatefulWidget {
|
||||||
|
const MemoriesThumbnailComp({
|
||||||
|
required this.galleryItem,
|
||||||
|
required this.onTap,
|
||||||
|
this.index = 0,
|
||||||
|
this.onLongPress,
|
||||||
|
this.selectionMode = false,
|
||||||
|
this.isSelected = false,
|
||||||
|
this.activeMediaIdNotifier,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final MemoryItem galleryItem;
|
||||||
|
final int index;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
final GestureLongPressCallback? onLongPress;
|
||||||
|
final bool selectionMode;
|
||||||
|
final bool isSelected;
|
||||||
|
final ValueNotifier<String?>? activeMediaIdNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MemoriesThumbnailComp> createState() => _MemoriesThumbnailCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> _alreadyAnimatedIds = {};
|
||||||
|
|
||||||
|
class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _scaleController;
|
||||||
|
late final Animation<double> _scaleAnimation;
|
||||||
|
late final Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
ImageProvider? _imageProvider;
|
||||||
|
ImageStream? _imageStream;
|
||||||
|
ImageInfo? _imageInfo;
|
||||||
|
late final ImageStreamListener _listener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scaleController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
);
|
||||||
|
_scaleAnimation = Tween<double>(begin: 0.94, end: 1).animate(
|
||||||
|
CurvedAnimation(parent: _scaleController, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
_slideAnimation =
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.125),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: _scaleController, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
final mediaId = widget.galleryItem.mediaService.mediaFile.mediaId;
|
||||||
|
final shouldAnimate =
|
||||||
|
widget.index < 20 && !_alreadyAnimatedIds.contains(mediaId);
|
||||||
|
|
||||||
|
if (shouldAnimate) {
|
||||||
|
_alreadyAnimatedIds.add(mediaId);
|
||||||
|
final delayMs = widget.index * 10;
|
||||||
|
if (delayMs > 0) {
|
||||||
|
Future.delayed(Duration(milliseconds: delayMs), () {
|
||||||
|
if (mounted) {
|
||||||
|
_scaleController.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_scaleController.forward();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_scaleController.value = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_listener = ImageStreamListener((info, _) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_imageInfo = info;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_resolveImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resolveImage() {
|
||||||
|
final media = widget.galleryItem.mediaService;
|
||||||
|
final hasThumbnail = media.thumbnailPath.existsSync();
|
||||||
|
final hasStored = media.storedPath.existsSync();
|
||||||
|
final isImageOrGif =
|
||||||
|
media.mediaFile.type == MediaType.image ||
|
||||||
|
media.mediaFile.type == MediaType.gif;
|
||||||
|
|
||||||
|
if (hasThumbnail) {
|
||||||
|
_imageProvider = FileImage(media.thumbnailPath);
|
||||||
|
} else if (hasStored && isImageOrGif) {
|
||||||
|
_imageProvider = FileImage(media.storedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_imageProvider != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final config = createLocalImageConfiguration(context);
|
||||||
|
_imageStream = _imageProvider!.resolve(config);
|
||||||
|
_imageStream!.addListener(_listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant MemoriesThumbnailComp oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.galleryItem.mediaService.mediaFile.mediaId !=
|
||||||
|
widget.galleryItem.mediaService.mediaFile.mediaId) {
|
||||||
|
_imageStream?.removeListener(_listener);
|
||||||
|
_imageInfo = null;
|
||||||
|
_resolveImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scaleController.dispose();
|
||||||
|
_imageStream?.removeListener(_listener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final media = widget.galleryItem.mediaService;
|
||||||
|
final isVideo = media.mediaFile.type == MediaType.video;
|
||||||
|
final cachedInfo = _imageInfo;
|
||||||
|
final mediaId = media.mediaFile.mediaId;
|
||||||
|
|
||||||
|
Widget buildHero(String tag) {
|
||||||
|
return Hero(
|
||||||
|
key: ValueKey(tag),
|
||||||
|
tag: tag,
|
||||||
|
transitionOnUserGestures: true,
|
||||||
|
flightShuttleBuilder: cachedInfo != null
|
||||||
|
? (
|
||||||
|
flightContext,
|
||||||
|
animation,
|
||||||
|
flightDirection,
|
||||||
|
fromHeroContext,
|
||||||
|
toHeroContext,
|
||||||
|
) {
|
||||||
|
return TransitionImage(
|
||||||
|
imageInfo: cachedInfo,
|
||||||
|
animation: animation,
|
||||||
|
thumbnailFit: BoxFit.cover,
|
||||||
|
viewerFit: BoxFit.contain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _scaleAnimation,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _scaleController,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.isSelected
|
||||||
|
? context.color.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
margin: EdgeInsets.all(widget.isSelected ? 4 : 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
widget.isSelected ? 12 : 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (cachedInfo != null)
|
||||||
|
RawImage(
|
||||||
|
image: cachedInfo.image,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
else if (_imageProvider != null)
|
||||||
|
Image(
|
||||||
|
image: _imageProvider!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ColoredBox(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.image,
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (isVideo)
|
||||||
|
const Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.circlePlay,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
shadows: [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (widget.selectionMode)
|
||||||
|
Positioned(
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.isSelected
|
||||||
|
? context.color.primary
|
||||||
|
: Colors.black38,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
Theme.of(context).brightness ==
|
||||||
|
Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget.isSelected
|
||||||
|
? const Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
: const SizedBox(width: 14, height: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (media.mediaFile.isFavorite)
|
||||||
|
const Positioned(
|
||||||
|
bottom: 6,
|
||||||
|
left: 6,
|
||||||
|
child: Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
size: 16,
|
||||||
|
shadows: [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
|
child: widget.activeMediaIdNotifier != null
|
||||||
|
? ValueListenableBuilder<String?>(
|
||||||
|
valueListenable: widget.activeMediaIdNotifier!,
|
||||||
|
builder: (context, activeId, _) {
|
||||||
|
final isActive = activeId == null || activeId == mediaId;
|
||||||
|
return buildHero(
|
||||||
|
isActive ? mediaId : '${mediaId}_grid_inactive',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: buildHero(mediaId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TransitionImage extends StatelessWidget {
|
||||||
|
const TransitionImage({
|
||||||
|
required this.imageInfo,
|
||||||
|
required this.animation,
|
||||||
|
required this.thumbnailFit,
|
||||||
|
required this.viewerFit,
|
||||||
|
this.background,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageInfo imageInfo;
|
||||||
|
final Animation<double> animation;
|
||||||
|
final BoxFit thumbnailFit;
|
||||||
|
final BoxFit viewerFit;
|
||||||
|
final Color? background;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) => CustomPaint(
|
||||||
|
painter: _TransitionImagePainter(
|
||||||
|
image: imageInfo.image,
|
||||||
|
scale: imageInfo.scale,
|
||||||
|
t: animation.value,
|
||||||
|
thumbnailFit: thumbnailFit,
|
||||||
|
viewerFit: viewerFit,
|
||||||
|
background: background,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransitionImagePainter extends CustomPainter {
|
||||||
|
const _TransitionImagePainter({
|
||||||
|
required this.image,
|
||||||
|
required this.scale,
|
||||||
|
required this.t,
|
||||||
|
required this.thumbnailFit,
|
||||||
|
required this.viewerFit,
|
||||||
|
required this.background,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ui.Image? image;
|
||||||
|
final double scale;
|
||||||
|
final double t;
|
||||||
|
final Color? background;
|
||||||
|
final BoxFit thumbnailFit;
|
||||||
|
final BoxFit viewerFit;
|
||||||
|
|
||||||
|
static final _paint = Paint()
|
||||||
|
..isAntiAlias = true
|
||||||
|
..filterQuality = FilterQuality.medium;
|
||||||
|
static const Alignment _alignment = Alignment.center;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final targetImage = image;
|
||||||
|
if (targetImage == null || size.isEmpty) return;
|
||||||
|
|
||||||
|
final outputSize = size;
|
||||||
|
final inputSize = Size(
|
||||||
|
targetImage.width.toDouble(),
|
||||||
|
targetImage.height.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate source/destination dimensions for both start and end layout states
|
||||||
|
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
|
||||||
|
final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size);
|
||||||
|
|
||||||
|
// Linearly interpolate intermediate source framing and canvas bounds simultaneously
|
||||||
|
final sourceSize =
|
||||||
|
Size.lerp(thumbnailSizes.source, viewerSizes.source, t)! * scale;
|
||||||
|
final destinationSize = Size.lerp(
|
||||||
|
thumbnailSizes.destination,
|
||||||
|
viewerSizes.destination,
|
||||||
|
t,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
|
||||||
|
final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0;
|
||||||
|
final dx = halfWidthDelta + _alignment.x * halfWidthDelta;
|
||||||
|
final dy = halfHeightDelta + _alignment.y * halfHeightDelta;
|
||||||
|
final destinationRect = Offset(dx, dy) & destinationSize;
|
||||||
|
|
||||||
|
final sourceRect = _alignment.inscribe(sourceSize, Offset.zero & inputSize);
|
||||||
|
|
||||||
|
if (background != null) {
|
||||||
|
canvas.drawRect(destinationRect.deflate(1), Paint()..color = background!);
|
||||||
|
}
|
||||||
|
canvas.drawImageRect(targetImage, sourceRect, destinationRect, _paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _TransitionImagePainter oldDelegate) {
|
||||||
|
return oldDelegate.t != t ||
|
||||||
|
oldDelegate.image != image ||
|
||||||
|
oldDelegate.scale != scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class MemoriesSelectionToolbarComp extends StatelessWidget {
|
||||||
|
const MemoriesSelectionToolbarComp({
|
||||||
|
required this.selectedCount,
|
||||||
|
required this.areAllSelected,
|
||||||
|
required this.areAllFav,
|
||||||
|
required this.onSelectAll,
|
||||||
|
required this.onExport,
|
||||||
|
required this.onFavorite,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.onClear,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int selectedCount;
|
||||||
|
final bool areAllSelected;
|
||||||
|
final bool areAllFav;
|
||||||
|
final VoidCallback onSelectAll;
|
||||||
|
final VoidCallback onExport;
|
||||||
|
final VoidCallback onFavorite;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
final VoidCallback onClear;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 24,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surface.withValues(alpha: 0.95),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(
|
||||||
|
color: context.color.primary.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$selectedCount',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
areAllSelected ? Icons.deselect : Icons.select_all,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
onPressed: onSelectAll,
|
||||||
|
tooltip: areAllSelected
|
||||||
|
? context.lang.galleryDeselectAll
|
||||||
|
: context.lang.gallerySelectAll,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: context.color.primary.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
foregroundColor: context.color.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.file_download_outlined, size: 22),
|
||||||
|
onPressed: onExport,
|
||||||
|
tooltip: context.lang.galleryExport,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: context.color.primary.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
foregroundColor: context.color.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
areAllFav ? Icons.favorite : Icons.favorite_border,
|
||||||
|
size: 22,
|
||||||
|
color: areAllFav ? Colors.redAccent : null,
|
||||||
|
),
|
||||||
|
onPressed: onFavorite,
|
||||||
|
tooltip: areAllFav
|
||||||
|
? context.lang.galleryUnfavorite
|
||||||
|
: context.lang.galleryFavorite,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: context.color.primary.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
foregroundColor: context.color.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 22),
|
||||||
|
onPressed: onDelete,
|
||||||
|
tooltip: context.lang.galleryDelete,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.redAccent.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.redAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 20),
|
||||||
|
onPressed: onClear,
|
||||||
|
tooltip: context.lang.galleryCancel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class SynchronizedViewerActionsToolbarComp extends StatelessWidget {
|
||||||
|
const SynchronizedViewerActionsToolbarComp({
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.onShare,
|
||||||
|
required this.onExport,
|
||||||
|
required this.onToggleFavorite,
|
||||||
|
required this.onDelete,
|
||||||
|
this.showStoreButton = false,
|
||||||
|
this.onStore,
|
||||||
|
this.isImageSaving = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isFavorite;
|
||||||
|
final VoidCallback onShare;
|
||||||
|
final VoidCallback onExport;
|
||||||
|
final VoidCallback onToggleFavorite;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
final bool showStoreButton;
|
||||||
|
final VoidCallback? onStore;
|
||||||
|
final bool isImageSaving;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 24,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (showStoreButton) ...[
|
||||||
|
IconButton(
|
||||||
|
icon: isImageSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const FaIcon(
|
||||||
|
FontAwesomeIcons.floppyDisk,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: isImageSaving ? null : onStore,
|
||||||
|
tooltip: 'Store media',
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.fileArrowDown,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 21,
|
||||||
|
),
|
||||||
|
onPressed: onExport,
|
||||||
|
tooltip: context.lang.galleryExport,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isFavorite ? Colors.redAccent : Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: onToggleFavorite,
|
||||||
|
tooltip: 'Favorite',
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete_outline,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: onDelete,
|
||||||
|
tooltip: context.lang.galleryDelete,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
|
color: primaryColor,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
onPressed: onShare,
|
||||||
|
tooltip: context.lang.shareImagedEditorSendImage,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
// ignore_for_file: parameter_assignments
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
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/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/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
import 'package:twonly/src/visual/views/shared/memory_item_slider.view.dart';
|
import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/shared/memory_item_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/synchronized_viewer.view.dart';
|
||||||
|
|
||||||
class MemoriesView extends StatefulWidget {
|
class MemoriesView extends StatefulWidget {
|
||||||
const MemoriesView({super.key});
|
const MemoriesView({super.key});
|
||||||
|
|
@ -23,254 +22,498 @@ class MemoriesView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemoriesViewState extends State<MemoriesView> {
|
class MemoriesViewState extends State<MemoriesView> {
|
||||||
int _filesToMigrate = 0;
|
late final MemoriesService _service;
|
||||||
List<MemoryItem> galleryItems = [];
|
final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(null);
|
||||||
Map<String, List<int>> orderedByMonth = {};
|
final ScrollController _scrollController = ScrollController();
|
||||||
List<String> months = [];
|
bool _isViewingFlashback = false;
|
||||||
StreamSubscription<List<MediaFile>>? messageSub;
|
|
||||||
|
|
||||||
final Map<int, List<MemoryItem>> _galleryItemsLastYears = {};
|
final Set<String> _selectedMediaIds = {};
|
||||||
|
bool _filterFavoritesOnly = false;
|
||||||
|
bool get _selectionMode => _selectedMediaIds.isNotEmpty;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
unawaited(initAsync());
|
_service = MemoriesService();
|
||||||
|
_activeMediaIdNotifier.addListener(_onActiveMediaChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
messageSub?.cancel();
|
_activeMediaIdNotifier.removeListener(_onActiveMediaChanged);
|
||||||
|
_scrollController.dispose();
|
||||||
|
_service.dispose();
|
||||||
|
_activeMediaIdNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
void _onActiveMediaChanged() {
|
||||||
final nonHashedFiles = await twonlyDB.mediaFilesDao
|
if (_isViewingFlashback) return;
|
||||||
.getAllNonHashedStoredMediaFiles();
|
final mediaId = _activeMediaIdNotifier.value;
|
||||||
if (nonHashedFiles.isNotEmpty) {
|
if (mediaId == null) return;
|
||||||
setState(() {
|
final state = _service.currentState;
|
||||||
_filesToMigrate = nonHashedFiles.length;
|
if (state.isEmpty) return;
|
||||||
});
|
|
||||||
for (final mediaFile in nonHashedFiles) {
|
final index = state.galleryItems.indexWhere(
|
||||||
final mediaService = MediaFileService(mediaFile);
|
(item) => item.mediaService.mediaFile.mediaId == mediaId,
|
||||||
await mediaService.hashStoredMedia();
|
);
|
||||||
setState(() {
|
if (index == -1) return;
|
||||||
_filesToMigrate -= 1;
|
|
||||||
});
|
double offset = 56;
|
||||||
}
|
if (state.galleryItemsLastYears.isNotEmpty) {
|
||||||
_filesToMigrate = 0;
|
offset += 220;
|
||||||
}
|
}
|
||||||
await messageSub?.cancel();
|
|
||||||
final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles();
|
|
||||||
|
|
||||||
messageSub = msgStream.listen((mediaFiles) async {
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
// Group items by month
|
final itemWidth = (screenWidth - 8) / 4;
|
||||||
orderedByMonth = {};
|
final itemHeight = itemWidth * (16 / 9);
|
||||||
months = [];
|
final rowHeight = itemHeight + 2;
|
||||||
var lastMonth = '';
|
|
||||||
galleryItems = [];
|
|
||||||
|
|
||||||
final now = clock.now();
|
for (final month in state.months) {
|
||||||
|
final indices = state.orderedByMonth[month]!;
|
||||||
|
offset += 44;
|
||||||
|
|
||||||
for (final mediaFile in mediaFiles) {
|
if (indices.contains(index)) {
|
||||||
final mediaService = MediaFileService(mediaFile);
|
final localIdx = indices.indexOf(index);
|
||||||
if (!mediaService.imagePreviewAvailable) continue;
|
final row = localIdx ~/ 4;
|
||||||
if (mediaService.mediaFile.type == MediaType.video) {
|
offset += row * rowHeight;
|
||||||
if (!mediaService.thumbnailPath.existsSync()) {
|
break;
|
||||||
await mediaService.createThumbnail();
|
} else {
|
||||||
}
|
final totalRows = (indices.length + 3) ~/ 4;
|
||||||
}
|
offset += totalRows * rowHeight;
|
||||||
final item = MemoryItem(
|
|
||||||
mediaService: mediaService,
|
|
||||||
messages: [],
|
|
||||||
);
|
|
||||||
galleryItems.add(item);
|
|
||||||
if (mediaFile.createdAt.month == now.month &&
|
|
||||||
mediaFile.createdAt.day == now.day) {
|
|
||||||
final diff = now.year - mediaFile.createdAt.year;
|
|
||||||
if (diff > 0) {
|
|
||||||
if (!_galleryItemsLastYears.containsKey(diff)) {
|
|
||||||
_galleryItemsLastYears[diff] = [];
|
|
||||||
}
|
|
||||||
_galleryItemsLastYears[diff]!.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
galleryItems.sort(
|
}
|
||||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
|
||||||
a.mediaService.mediaFile.createdAt,
|
if (_scrollController.hasClients) {
|
||||||
),
|
final targetOffset = (offset - 100).clamp(
|
||||||
|
0.0,
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
);
|
);
|
||||||
for (var i = 0; i < galleryItems.length; i++) {
|
_scrollController.jumpTo(targetOffset);
|
||||||
final month = DateFormat(
|
}
|
||||||
'MMMM yyyy',
|
}
|
||||||
).format(galleryItems[i].mediaService.mediaFile.createdAt);
|
|
||||||
if (lastMonth != month) {
|
Future<void> _openViewer(
|
||||||
lastMonth = month;
|
List<MemoryItem> items,
|
||||||
months.add(month);
|
int index, {
|
||||||
}
|
bool isFlashback = false,
|
||||||
orderedByMonth.putIfAbsent(month, () => []).add(i);
|
}) async {
|
||||||
}
|
if (isFlashback) {
|
||||||
if (mounted) {
|
_isViewingFlashback = true;
|
||||||
setState(() {});
|
}
|
||||||
|
_activeMediaIdNotifier.value = items[index].mediaService.mediaFile.mediaId;
|
||||||
|
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
PageRouteBuilder(
|
||||||
|
opaque: false,
|
||||||
|
transitionDuration: const Duration(milliseconds: 350),
|
||||||
|
reverseTransitionDuration: const Duration(milliseconds: 350),
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
|
return SynchronizedImageViewerScreen(
|
||||||
|
galleryItems: items,
|
||||||
|
initialIndex: index,
|
||||||
|
activeMediaIdNotifier: _activeMediaIdNotifier,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFlashback) {
|
||||||
|
_isViewingFlashback = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleSelection(String mediaId) {
|
||||||
|
setState(() {
|
||||||
|
if (_selectedMediaIds.contains(mediaId)) {
|
||||||
|
_selectedMediaIds.remove(mediaId);
|
||||||
|
} else {
|
||||||
|
_selectedMediaIds.add(mediaId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _onLongPressItem(String mediaId) {
|
||||||
Widget build(BuildContext context) {
|
setState(() {
|
||||||
Widget child = Center(
|
_selectedMediaIds.add(mediaId);
|
||||||
child: Text(
|
});
|
||||||
context.lang.memoriesEmpty,
|
}
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
void _onTapItem(String mediaId, int globalIndex) {
|
||||||
|
if (_selectionMode) {
|
||||||
|
_toggleSelection(mediaId);
|
||||||
|
} else {
|
||||||
|
final state = _service.currentState;
|
||||||
|
var targetItems = state.galleryItems;
|
||||||
|
var targetIndex = globalIndex;
|
||||||
|
|
||||||
|
if (_filterFavoritesOnly) {
|
||||||
|
targetItems = state.galleryItems
|
||||||
|
.where((e) => e.mediaService.mediaFile.isFavorite)
|
||||||
|
.toList();
|
||||||
|
targetIndex = targetItems.indexWhere(
|
||||||
|
(e) => e.mediaService.mediaFile.mediaId == mediaId,
|
||||||
|
);
|
||||||
|
if (targetIndex == -1) targetIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openViewer(targetItems, targetIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectAll() {
|
||||||
|
setState(() {
|
||||||
|
final items = _service.currentState.galleryItems;
|
||||||
|
final targetIds = <String>{};
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
if (_filterFavoritesOnly) {
|
||||||
|
if (item.mediaService.mediaFile.isFavorite) {
|
||||||
|
targetIds.add(item.mediaService.mediaFile.mediaId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetIds.add(item.mediaService.mediaFile.mediaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final areAllSelected = targetIds.every(_selectedMediaIds.contains);
|
||||||
|
|
||||||
|
if (areAllSelected) {
|
||||||
|
_selectedMediaIds.removeAll(targetIds);
|
||||||
|
} else {
|
||||||
|
_selectedMediaIds.addAll(targetIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _batchDelete() async {
|
||||||
|
final count = _selectedMediaIds.length;
|
||||||
|
final confirmed = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.deleteImageTitle,
|
||||||
|
context.lang.deleteImageBody,
|
||||||
);
|
);
|
||||||
if (_filesToMigrate > 0) {
|
|
||||||
child = Center(
|
if (!confirmed) return;
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
final items = _service.currentState.galleryItems;
|
||||||
children: [
|
for (final mediaId in _selectedMediaIds) {
|
||||||
ThreeRotatingDots(
|
final item = items
|
||||||
size: 40,
|
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
||||||
color: context.color.primary,
|
.firstOrNull;
|
||||||
),
|
if (item != null) {
|
||||||
const SizedBox(height: 10),
|
item.mediaService.fullMediaRemoval();
|
||||||
Text(
|
}
|
||||||
context.lang.migrationOfMemories(_filesToMigrate),
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||||
textAlign: TextAlign.center,
|
}
|
||||||
),
|
|
||||||
],
|
setState(_selectedMediaIds.clear);
|
||||||
),
|
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'Deleted $count items successfully',
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _batchExport() async {
|
||||||
|
final items = _service.currentState.galleryItems;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (final mediaId in _selectedMediaIds) {
|
||||||
|
final item = items
|
||||||
|
.where((e) => e.mediaService.mediaFile.mediaId == mediaId)
|
||||||
|
.firstOrNull;
|
||||||
|
if (item != null) {
|
||||||
|
final media = item.mediaService;
|
||||||
|
if (media.mediaFile.type == MediaType.video) {
|
||||||
|
await saveVideoToGallery(media.storedPath.path);
|
||||||
|
} else if (media.mediaFile.type == MediaType.image ||
|
||||||
|
media.mediaFile.type == MediaType.gif) {
|
||||||
|
final imageBytes = await media.storedPath.readAsBytes();
|
||||||
|
await saveImageToGallery(imageBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.galleryExportSuccess,
|
||||||
|
level: SnackbarLevel.success,
|
||||||
);
|
);
|
||||||
} else if (galleryItems.isNotEmpty) {
|
} catch (e) {
|
||||||
child = ListView.builder(
|
if (!mounted) return;
|
||||||
itemCount:
|
showSnackbar(context, e.toString());
|
||||||
(months.length * 2) + (_galleryItemsLastYears.isEmpty ? 0 : 1),
|
}
|
||||||
itemBuilder: (context, mIndex) {
|
}
|
||||||
if (_galleryItemsLastYears.isNotEmpty && mIndex == 0) {
|
|
||||||
return SizedBox(
|
Future<void> _batchFavorite() async {
|
||||||
height: 140,
|
final items = _service.currentState.galleryItems;
|
||||||
width: MediaQuery.sizeOf(context).width,
|
var favCount = 0;
|
||||||
child: ListView(
|
for (final item in items) {
|
||||||
scrollDirection: Axis.horizontal,
|
if (_selectedMediaIds.contains(item.mediaService.mediaFile.mediaId)) {
|
||||||
children: _galleryItemsLastYears.entries.map(
|
if (item.mediaService.mediaFile.isFavorite) {
|
||||||
(item) {
|
favCount++;
|
||||||
var text = context.lang.memoriesAYearAgo;
|
}
|
||||||
if (item.key > 1) {
|
}
|
||||||
text = context.lang.memoriesXYearsAgo(item.key);
|
}
|
||||||
}
|
final areAllFav =
|
||||||
return GestureDetector(
|
_selectedMediaIds.isNotEmpty && favCount == _selectedMediaIds.length;
|
||||||
onTap: () async {
|
final targetFav = !areAllFav;
|
||||||
await open(context, item.value, 0);
|
|
||||||
},
|
for (final mediaId in _selectedMediaIds) {
|
||||||
child: Container(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
decoration: BoxDecoration(
|
mediaId,
|
||||||
borderRadius: BorderRadius.circular(12),
|
MediaFilesCompanion(isFavorite: Value(targetFav)),
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
spreadRadius: -12,
|
|
||||||
blurRadius: 12,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
height: 150,
|
|
||||||
width: 120,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.file(
|
|
||||||
item.value.first.mediaService.storedPath,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 10,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 20,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
color: Color.fromARGB(122, 0, 0, 0),
|
|
||||||
blurRadius: 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_galleryItemsLastYears.isNotEmpty) {
|
|
||||||
mIndex -= 1;
|
|
||||||
}
|
|
||||||
if (mIndex.isEven) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(months[(mIndex ~/ 2)]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final index = (mIndex - 1) ~/ 2;
|
|
||||||
return GridView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const ClampingScrollPhysics(),
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 4,
|
|
||||||
childAspectRatio: 9 / 16,
|
|
||||||
),
|
|
||||||
itemCount: orderedByMonth[months[index]]!.length,
|
|
||||||
itemBuilder: (context, gIndex) {
|
|
||||||
final gaIndex = orderedByMonth[months[index]]![gIndex];
|
|
||||||
return MemoriesItemThumbnailComp(
|
|
||||||
galleryItem: galleryItems[gaIndex],
|
|
||||||
onTap: () async {
|
|
||||||
await open(context, galleryItems, gaIndex);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Memories')),
|
body: Stack(
|
||||||
body: Scrollbar(
|
fit: StackFit.expand,
|
||||||
child: child,
|
children: [
|
||||||
|
StreamBuilder<MemoriesState>(
|
||||||
|
initialData: _service.currentState,
|
||||||
|
stream: _service.watchState,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
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) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.photo_library_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
context.lang.memoriesEmpty,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var months = state.months;
|
||||||
|
var orderedByMonth = state.orderedByMonth;
|
||||||
|
final lastYears = state.galleryItemsLastYears;
|
||||||
|
|
||||||
|
if (_filterFavoritesOnly) {
|
||||||
|
final filteredOrdered = <String, List<int>>{};
|
||||||
|
final filteredMonths = <String>[];
|
||||||
|
|
||||||
|
for (final m in months) {
|
||||||
|
final indices = orderedByMonth[m] ?? [];
|
||||||
|
final favIndices = indices.where((idx) {
|
||||||
|
return state
|
||||||
|
.galleryItems[idx]
|
||||||
|
.mediaService
|
||||||
|
.mediaFile
|
||||||
|
.isFavorite;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (favIndices.isNotEmpty) {
|
||||||
|
filteredOrdered[m] = favIndices;
|
||||||
|
filteredMonths.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
months = filteredMonths;
|
||||||
|
orderedByMonth = filteredOrdered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
thickness: 12,
|
||||||
|
radius: const Radius.circular(6),
|
||||||
|
interactive: true,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Memories',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: context.color.surface,
|
||||||
|
actions: [
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
sliver: 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
if (_selectionMode)
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final items = _service.currentState.galleryItems;
|
||||||
|
var visibleCount = 0;
|
||||||
|
var favCount = 0;
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
final isFav = item.mediaService.mediaFile.isFavorite;
|
||||||
|
if (!_filterFavoritesOnly || isFav) {
|
||||||
|
visibleCount++;
|
||||||
|
}
|
||||||
|
if (_selectedMediaIds.contains(
|
||||||
|
item.mediaService.mediaFile.mediaId,
|
||||||
|
)) {
|
||||||
|
if (isFav) {
|
||||||
|
favCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final areAllSelected =
|
||||||
|
visibleCount > 0 &&
|
||||||
|
_selectedMediaIds.length >= visibleCount;
|
||||||
|
final areAllFav =
|
||||||
|
_selectedMediaIds.isNotEmpty &&
|
||||||
|
favCount == _selectedMediaIds.length;
|
||||||
|
|
||||||
|
return MemoriesSelectionToolbarComp(
|
||||||
|
selectedCount: _selectedMediaIds.length,
|
||||||
|
areAllSelected: areAllSelected,
|
||||||
|
areAllFav: areAllFav,
|
||||||
|
onSelectAll: _selectAll,
|
||||||
|
onExport: _batchExport,
|
||||||
|
onFavorite: _batchFavorite,
|
||||||
|
onDelete: _batchDelete,
|
||||||
|
onClear: () => setState(_selectedMediaIds.clear),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> open(
|
|
||||||
BuildContext context,
|
|
||||||
List<MemoryItem> galleryItems,
|
|
||||||
int index,
|
|
||||||
) async {
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
PageRouteBuilder(
|
|
||||||
opaque: false,
|
|
||||||
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
|
|
||||||
galleryItems: galleryItems,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
as bool?;
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
383
lib/src/visual/views/memories/synchronized_viewer.view.dart
Normal file
383
lib/src/visual/views/memories/synchronized_viewer.view.dart
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
import 'package:twonly/src/visual/helpers/video_player_file.helper.dart';
|
||||||
|
import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart';
|
||||||
|
|
||||||
|
class SynchronizedImageViewerScreen extends StatefulWidget {
|
||||||
|
const SynchronizedImageViewerScreen({
|
||||||
|
required this.galleryItems,
|
||||||
|
required this.initialIndex,
|
||||||
|
required this.activeMediaIdNotifier,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<MemoryItem> galleryItems;
|
||||||
|
final int initialIndex;
|
||||||
|
final ValueNotifier<String?> activeMediaIdNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SynchronizedImageViewerScreen> createState() =>
|
||||||
|
_SynchronizedImageViewerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SynchronizedImageViewerScreenState
|
||||||
|
extends State<SynchronizedImageViewerScreen> {
|
||||||
|
late PageController _verticalPager;
|
||||||
|
late PageController _horizontalPager;
|
||||||
|
late ValueNotifier<String> _currentlyViewedMediaIdNotifier;
|
||||||
|
final ValueNotifier<double> _backdropOpacityNotifier = ValueNotifier(1);
|
||||||
|
|
||||||
|
final Set<String> _favoritedMediaIds = {};
|
||||||
|
bool _isSaving = false;
|
||||||
|
final Set<String> _storedMediaIds = {};
|
||||||
|
|
||||||
|
late int _currentIndex;
|
||||||
|
bool _isZoomed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
|
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
final initialId =
|
||||||
|
widget.galleryItems[widget.initialIndex].mediaService.mediaFile.mediaId;
|
||||||
|
_currentlyViewedMediaIdNotifier = ValueNotifier(initialId);
|
||||||
|
|
||||||
|
_horizontalPager = PageController(initialPage: widget.initialIndex);
|
||||||
|
_verticalPager = PageController(initialPage: 1);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final item in widget.galleryItems) {
|
||||||
|
if (item.mediaService.mediaFile.isFavorite) {
|
||||||
|
_favoritedMediaIds.add(item.mediaService.mediaFile.mediaId);
|
||||||
|
}
|
||||||
|
if (item.mediaService.storedPath.existsSync()) {
|
||||||
|
_storedMediaIds.add(item.mediaService.mediaFile.mediaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _storeMediaFile() async {
|
||||||
|
final item = widget.galleryItems[_currentIndex];
|
||||||
|
final mediaId = item.mediaService.mediaFile.mediaId;
|
||||||
|
setState(() => _isSaving = true);
|
||||||
|
try {
|
||||||
|
await item.mediaService.storeMediaFile();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_storedMediaIds.add(mediaId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSaving = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleFavorite(String mediaId) async {
|
||||||
|
final wasFavorite = _favoritedMediaIds.contains(mediaId);
|
||||||
|
final isFavoriteNow = !wasFavorite;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (isFavoriteNow) {
|
||||||
|
_favoritedMediaIds.add(mediaId);
|
||||||
|
} else {
|
||||||
|
_favoritedMediaIds.remove(mediaId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaId,
|
||||||
|
MediaFilesCompanion(isFavorite: Value(isFavoriteNow)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restoreSystemUI() {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(
|
||||||
|
SystemUiMode.manual,
|
||||||
|
overlays: SystemUiOverlay.values,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_restoreSystemUI();
|
||||||
|
_verticalPager
|
||||||
|
..removeListener(_onVerticalScrollUpdated)
|
||||||
|
..dispose();
|
||||||
|
_horizontalPager.dispose();
|
||||||
|
_currentlyViewedMediaIdNotifier.dispose();
|
||||||
|
_backdropOpacityNotifier.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalScrollUpdated() {
|
||||||
|
if (!_verticalPager.hasClients) return;
|
||||||
|
final page = _verticalPager.page ?? 1.0;
|
||||||
|
|
||||||
|
// Map vertical dragging proximity directly to square-root backdrop opacities
|
||||||
|
final linearFraction = min(1, max(0, page)).toDouble();
|
||||||
|
_backdropOpacityNotifier.value = linearFraction * linearFraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPageSnapped(int index) {
|
||||||
|
if (index == 0) {
|
||||||
|
_triggerSynchronizedPop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _triggerSynchronizedPop() {
|
||||||
|
_restoreSystemUI();
|
||||||
|
final targetId = _currentlyViewedMediaIdNotifier.value;
|
||||||
|
|
||||||
|
if (widget.activeMediaIdNotifier.value != targetId) {
|
||||||
|
widget.activeMediaIdNotifier.value = targetId;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) Navigator.maybeOf(context)?.pop(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Navigator.maybeOf(context)?.pop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteFile() async {
|
||||||
|
final confirmed = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.deleteImageTitle,
|
||||||
|
context.lang.deleteImageBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
widget.galleryItems[_currentIndex].mediaService.fullMediaRemoval();
|
||||||
|
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
||||||
|
widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
|
widget.galleryItems.removeAt(_currentIndex);
|
||||||
|
|
||||||
|
if (widget.galleryItems.isEmpty) {
|
||||||
|
if (mounted) Navigator.pop(context, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentIndex >= widget.galleryItems.length) {
|
||||||
|
_currentIndex = widget.galleryItems.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newId =
|
||||||
|
widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId;
|
||||||
|
_currentlyViewedMediaIdNotifier.value = newId;
|
||||||
|
widget.activeMediaIdNotifier.value = newId;
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportFile() async {
|
||||||
|
final item = widget.galleryItems[_currentIndex].mediaService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (item.mediaFile.type == MediaType.video) {
|
||||||
|
await saveVideoToGallery(item.storedPath.path);
|
||||||
|
} else if (item.mediaFile.type == MediaType.image ||
|
||||||
|
item.mediaFile.type == MediaType.gif) {
|
||||||
|
final imageBytes = await item.storedPath.readAsBytes();
|
||||||
|
await saveImageToGallery(imageBytes);
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.galleryExportSuccess,
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
e.toString(),
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _shareMediaFile() async {
|
||||||
|
final orgMediaService = widget.galleryItems[_currentIndex].mediaService;
|
||||||
|
|
||||||
|
final newMediaService = await initializeMediaUpload(
|
||||||
|
orgMediaService.mediaFile.type,
|
||||||
|
userService.currentUser.defaultShowTime,
|
||||||
|
);
|
||||||
|
if (newMediaService == null) {
|
||||||
|
Log.error('Could not create new mediaFile');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgMediaService.storedPath.existsSync()) {
|
||||||
|
orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
|
||||||
|
} else if (orgMediaService.tempPath.existsSync()) {
|
||||||
|
orgMediaService.tempPath.copySync(newMediaService.originalPath.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
await context.navPush(
|
||||||
|
ShareImageEditorView(
|
||||||
|
mediaFileService: newMediaService,
|
||||||
|
sharedFromGallery: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.galleryItems.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final orgMediaService = widget.galleryItems[_currentIndex].mediaService;
|
||||||
|
final currentMediaId = orgMediaService.mediaFile.mediaId;
|
||||||
|
|
||||||
|
return PopScope<Object?>(
|
||||||
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
|
_restoreSystemUI();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _backdropOpacityNotifier,
|
||||||
|
builder: (context, opacity, child) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Colors.black.withValues(alpha: opacity),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: PageView(
|
||||||
|
controller: _verticalPager,
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: _isZoomed
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const BouncingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
|
),
|
||||||
|
onPageChanged: _onPageSnapped,
|
||||||
|
children: [
|
||||||
|
//Fully transparent dismissal trigger anchor
|
||||||
|
const SizedBox.expand(),
|
||||||
|
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
PageView.builder(
|
||||||
|
controller: _horizontalPager,
|
||||||
|
physics: _isZoomed
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const BouncingScrollPhysics(),
|
||||||
|
itemCount: widget.galleryItems.length,
|
||||||
|
onPageChanged: (idx) {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = idx;
|
||||||
|
});
|
||||||
|
final newMediaId = widget
|
||||||
|
.galleryItems[idx]
|
||||||
|
.mediaService
|
||||||
|
.mediaFile
|
||||||
|
.mediaId;
|
||||||
|
_currentlyViewedMediaIdNotifier.value = newMediaId;
|
||||||
|
widget.activeMediaIdNotifier.value = newMediaId;
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = widget.galleryItems[index];
|
||||||
|
final itemMediaId = item.mediaService.mediaFile.mediaId;
|
||||||
|
|
||||||
|
var filePath = item.mediaService.storedPath;
|
||||||
|
if (!filePath.existsSync()) {
|
||||||
|
filePath = item.mediaService.tempPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isVideo =
|
||||||
|
item.mediaService.mediaFile.type == MediaType.video;
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ValueListenableBuilder<String>(
|
||||||
|
valueListenable: _currentlyViewedMediaIdNotifier,
|
||||||
|
builder: (context, activeMediaId, childWidget) {
|
||||||
|
// Dynamically resolve Hero tags to prevent layout tree duplicate assertions
|
||||||
|
final isActiveTarget = activeMediaId == itemMediaId;
|
||||||
|
|
||||||
|
if (isActiveTarget) {
|
||||||
|
return Hero(
|
||||||
|
tag: itemMediaId,
|
||||||
|
transitionOnUserGestures: true,
|
||||||
|
child: childWidget!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return childWidget!;
|
||||||
|
},
|
||||||
|
child: !filePath.existsSync()
|
||||||
|
? const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.broken_image_outlined,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 64,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: isVideo
|
||||||
|
? VideoPlayerFileHelper(videoPath: filePath)
|
||||||
|
: PhotoView(
|
||||||
|
imageProvider: FileImage(filePath),
|
||||||
|
initialScale:
|
||||||
|
PhotoViewComputedScale.contained,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale:
|
||||||
|
PhotoViewComputedScale.covered * 4.1,
|
||||||
|
backgroundDecoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
scaleStateChangedCallback: (state) {
|
||||||
|
final zoomed =
|
||||||
|
state != PhotoViewScaleState.initial;
|
||||||
|
if (_isZoomed != zoomed) {
|
||||||
|
setState(() {
|
||||||
|
_isZoomed = zoomed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SynchronizedViewerActionsToolbarComp(
|
||||||
|
isFavorite: _favoritedMediaIds.contains(currentMediaId),
|
||||||
|
onShare: _shareMediaFile,
|
||||||
|
onExport: _exportFile,
|
||||||
|
onToggleFavorite: () => _toggleFavorite(currentMediaId),
|
||||||
|
onDelete: _deleteFile,
|
||||||
|
showStoreButton: !_storedMediaIds.contains(currentMediaId),
|
||||||
|
onStore: _storeMediaFile,
|
||||||
|
isImageSaving: _isSaving,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -82,20 +82,6 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
|
||||||
child: FloatingActionButton(
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
onPressed: () => UserService.update(
|
|
||||||
(u) => u.preSelectedEmojies = EmojiAnimationComp
|
|
||||||
.animatedIcons
|
|
||||||
.keys
|
|
||||||
.toList()
|
|
||||||
.sublist(0, 6),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.settings_backup_restore_rounded),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
@ -6,14 +7,18 @@ 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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:restart_app/restart_app.dart';
|
import 'package:restart_app/restart_app.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/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.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';
|
||||||
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/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart';
|
import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart';
|
||||||
|
|
||||||
|
|
@ -25,11 +30,224 @@ class DeveloperSettingsView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
|
bool _isGeneratingMockImages = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _generate1000MockImages() async {
|
||||||
|
if (_isGeneratingMockImages) return;
|
||||||
|
setState(() {
|
||||||
|
_isGeneratingMockImages = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final now = clock.now();
|
||||||
|
const groupId = 'mock_group_gallery';
|
||||||
|
|
||||||
|
// Ensure mock group exists
|
||||||
|
await twonlyDB.groupsDao.createNewGroup(
|
||||||
|
const GroupsCompanion(
|
||||||
|
groupId: Value(groupId),
|
||||||
|
groupName: Value('Mock Gallery Group'),
|
||||||
|
isDirectChat: Value(false),
|
||||||
|
joinedGroup: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const size = Size(360, 640);
|
||||||
|
|
||||||
|
// Batch database entries using cascades for extreme operational speed and clean linting
|
||||||
|
await twonlyDB.batch((batch) async {
|
||||||
|
for (var i = 0; i < 1000; i++) {
|
||||||
|
final mediaId = 'mock_gen_$i';
|
||||||
|
final authorIndex = i % 12;
|
||||||
|
final contactId = 9000000 + authorIndex;
|
||||||
|
|
||||||
|
late DateTime itemDate;
|
||||||
|
if (i < 200) {
|
||||||
|
// Spread over the last month
|
||||||
|
itemDate = now.subtract(Duration(minutes: i * 216));
|
||||||
|
} else if (i < 400) {
|
||||||
|
// Spread between 1 month and 1 year ago
|
||||||
|
final localI = i - 200;
|
||||||
|
itemDate = now.subtract(
|
||||||
|
Duration(days: 30, minutes: localI * 2412),
|
||||||
|
);
|
||||||
|
} else if (i < 600) {
|
||||||
|
// Around a year ago
|
||||||
|
final localI = i - 400;
|
||||||
|
itemDate = now.subtract(
|
||||||
|
Duration(days: 365, minutes: localI * 216),
|
||||||
|
);
|
||||||
|
} else if (i < 800) {
|
||||||
|
// Around three years ago
|
||||||
|
final localI = i - 600;
|
||||||
|
itemDate = now.subtract(
|
||||||
|
Duration(days: 1095, minutes: localI * 216),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Around four years ago
|
||||||
|
final localI = i - 800;
|
||||||
|
itemDate = now.subtract(
|
||||||
|
Duration(days: 1460, minutes: localI * 216),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
batch
|
||||||
|
..insert(
|
||||||
|
twonlyDB.contacts,
|
||||||
|
ContactsCompanion(
|
||||||
|
userId: Value(contactId),
|
||||||
|
username: Value('mock_user_$authorIndex'),
|
||||||
|
displayName: Value('Author $authorIndex'),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
)
|
||||||
|
..insert(
|
||||||
|
twonlyDB.mediaFiles,
|
||||||
|
MediaFilesCompanion(
|
||||||
|
mediaId: Value(mediaId),
|
||||||
|
type: const Value(MediaType.image),
|
||||||
|
stored: const Value(true),
|
||||||
|
createdAt: Value(itemDate),
|
||||||
|
createdAtMonth: Value(DateFormat('MMMM yyyy').format(itemDate)),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
)
|
||||||
|
..insert(
|
||||||
|
twonlyDB.messages,
|
||||||
|
MessagesCompanion(
|
||||||
|
messageId: Value('mock_msg_$i'),
|
||||||
|
groupId: const Value(groupId),
|
||||||
|
senderId: Value(contactId),
|
||||||
|
type: const Value('media'),
|
||||||
|
mediaId: Value(mediaId),
|
||||||
|
mediaStored: const Value(true),
|
||||||
|
openedAt: Value(now),
|
||||||
|
createdAt: Value(itemDate),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render custom vector avatars and background colors efficiently
|
||||||
|
for (var i = 0; i < 1000; i++) {
|
||||||
|
final mediaId = 'mock_gen_$i';
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
final hue = (i * 137.5) % 360;
|
||||||
|
final bgColor = HSLColor.fromAHSL(1, hue, 0.65, 0.45).toColor();
|
||||||
|
canvas.drawRect(Offset.zero & size, Paint()..color = bgColor);
|
||||||
|
|
||||||
|
// Avatar vector representation on it
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final avatarBgPaint = Paint()
|
||||||
|
..color = Colors.white.withValues(alpha: 0.25);
|
||||||
|
canvas.drawCircle(center, 120, avatarBgPaint);
|
||||||
|
|
||||||
|
final eyePaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
final mouthPaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 8
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
const eyeOffset = 35.0;
|
||||||
|
final eyeRadius = 12.0 + (i % 5) * 2;
|
||||||
|
canvas
|
||||||
|
..drawCircle(
|
||||||
|
center + const Offset(-eyeOffset, -20),
|
||||||
|
eyeRadius,
|
||||||
|
eyePaint,
|
||||||
|
)
|
||||||
|
..drawCircle(
|
||||||
|
center + const Offset(eyeOffset, -20),
|
||||||
|
eyeRadius,
|
||||||
|
eyePaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
final mouthRect = Rect.fromCenter(
|
||||||
|
center: center + const Offset(0, 20),
|
||||||
|
width: 60,
|
||||||
|
height: 40,
|
||||||
|
);
|
||||||
|
final startAngle = 0.2 + (i % 3) * 0.1;
|
||||||
|
final sweepAngle = 2.7 - (i % 3) * 0.2;
|
||||||
|
canvas.drawArc(mouthRect, startAngle, sweepAngle, false, mouthPaint);
|
||||||
|
|
||||||
|
final textSpan = TextSpan(
|
||||||
|
text: '#$i',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: textSpan,
|
||||||
|
textDirection: ui.TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset((size.width - textPainter.width) / 2, size.height - 80),
|
||||||
|
);
|
||||||
|
|
||||||
|
final picture = recorder.endRecording();
|
||||||
|
final img = await picture.toImage(
|
||||||
|
size.width.toInt(),
|
||||||
|
size.height.toInt(),
|
||||||
|
);
|
||||||
|
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
if (byteData != null) {
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
final mediaFile = MediaFile(
|
||||||
|
mediaId: mediaId,
|
||||||
|
type: MediaType.image,
|
||||||
|
stored: true,
|
||||||
|
requiresAuthentication: false,
|
||||||
|
isDraftMedia: false,
|
||||||
|
isFavorite: false,
|
||||||
|
hasCropAnalyzed: false,
|
||||||
|
createdAt: now,
|
||||||
|
);
|
||||||
|
final mediaService = MediaFileService(mediaFile);
|
||||||
|
|
||||||
|
if (!mediaService.storedPath.parent.existsSync()) {
|
||||||
|
mediaService.storedPath.parent.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
mediaService.storedPath.writeAsBytesSync(bytes);
|
||||||
|
mediaService.thumbnailPath.writeAsBytesSync(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'Successfully generated 1000 mock images!',
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(context, 'Error generating images: $e');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isGeneratingMockImages = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> toggleDeveloperSettings() async {
|
Future<void> toggleDeveloperSettings() async {
|
||||||
await UserService.update((u) => u.isDeveloper = !u.isDeveloper);
|
await UserService.update((u) => u.isDeveloper = !u.isDeveloper);
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +350,20 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
context.push(Routes.settingsDeveloperAutomatedTesting),
|
context.push(Routes.settingsDeveloperAutomatedTesting),
|
||||||
),
|
),
|
||||||
|
if (kDebugMode)
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Generate 1000 Mock Images'),
|
||||||
|
trailing: _isGeneratingMockImages
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: _isGeneratingMockImages
|
||||||
|
? null
|
||||||
|
: _generate1000MockImages,
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Reopen Setup'),
|
title: const Text('Reopen Setup'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|
|
||||||
|
|
@ -57,21 +57,16 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAdditionalUser() async {
|
Future<void> addAdditionalUser() async {
|
||||||
final selectedUserIds =
|
final selectedUserIds = await context.navPush(
|
||||||
await Navigator.push(
|
SelectAdditionalUsers(
|
||||||
context,
|
limit: _planLimit,
|
||||||
MaterialPageRoute(
|
alreadySelected:
|
||||||
builder: (context) => SelectAdditionalUsers(
|
ballance?.additionalAccounts
|
||||||
limit: _planLimit,
|
.map((e) => e.userId.toInt())
|
||||||
alreadySelected:
|
.toList() ??
|
||||||
ballance?.additionalAccounts
|
[],
|
||||||
.map((e) => e.userId.toInt())
|
),
|
||||||
.toList() ??
|
) as List<int>?;
|
||||||
[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
as List<int>?;
|
|
||||||
if (selectedUserIds == null) return;
|
if (selectedUserIds == null) return;
|
||||||
for (final selectedUserId in selectedUserIds) {
|
for (final selectedUserId in selectedUserIds) {
|
||||||
final res = await apiService.addAdditionalUser(Int64(selectedUserId));
|
final res = await apiService.addAdditionalUser(Int64(selectedUserId));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
// ignore_for_file: inference_failure_on_instance_creation
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -61,26 +59,69 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
if (currentPlan.name == SubscriptionPlan.Free.name)
|
||||||
padding: const EdgeInsets.all(32),
|
Padding(
|
||||||
child: Center(
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
|
||||||
child: Container(
|
child: Column(
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: context.color.primary,
|
const SizedBox(height: 20),
|
||||||
borderRadius: BorderRadius.circular(15),
|
Text(
|
||||||
),
|
context.lang.subscriptionPledgeTitle,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
textAlign: TextAlign.center,
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
currentPlan.name,
|
fontSize: 20,
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 32,
|
color: context.color.primary,
|
||||||
fontWeight: FontWeight.bold,
|
letterSpacing: 0.5,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
_MissionRow(
|
||||||
|
icon: FontAwesomeIcons.shieldHalved,
|
||||||
|
title: context.lang.subscriptionPledgeSecureTitle,
|
||||||
|
desc: context.lang.subscriptionPledgeSecureDesc,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_MissionRow(
|
||||||
|
icon: FontAwesomeIcons.userSecret,
|
||||||
|
title: context.lang.subscriptionPledgeNoAdsTitle,
|
||||||
|
desc: context.lang.subscriptionPledgeNoAdsDesc,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_MissionRow(
|
||||||
|
icon: FontAwesomeIcons.heart,
|
||||||
|
title: context.lang.subscriptionPledgeFundedTitle,
|
||||||
|
desc: context.lang.subscriptionPledgeFundedDesc,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.primary,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 3,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
currentPlan.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
if (additionalOwnerName != null)
|
if (additionalOwnerName != null)
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -95,16 +136,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
),
|
),
|
||||||
if (!isPayingUser(currentPlan) ||
|
if (!isPayingUser(currentPlan) ||
|
||||||
currentPlan == SubscriptionPlan.Tester) ...[
|
currentPlan == SubscriptionPlan.Tester) ...[
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(18),
|
|
||||||
child: Text(
|
|
||||||
context.lang.upgradeToPaidPlan,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PlanCard(
|
PlanCard(
|
||||||
plan: SubscriptionPlan.Pro,
|
plan: SubscriptionPlan.Pro,
|
||||||
onPurchase: initAsync,
|
onPurchase: initAsync,
|
||||||
|
|
@ -152,14 +183,9 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
text: context.lang.manageAdditionalUsers,
|
text: context.lang.manageAdditionalUsers,
|
||||||
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
|
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Navigator.push(
|
await context.navPush(
|
||||||
context,
|
AdditionalUsersView(
|
||||||
MaterialPageRoute(
|
ballance: ballance,
|
||||||
builder: (context) {
|
|
||||||
return AdditionalUsersView(
|
|
||||||
ballance: ballance,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await initAsync();
|
await initAsync();
|
||||||
|
|
@ -347,3 +373,47 @@ class _PlanCardState extends State<PlanCard> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MissionRow extends StatelessWidget {
|
||||||
|
const _MissionRow({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.desc,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String desc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
icon,
|
||||||
|
size: 24,
|
||||||
|
color: context.color.primary.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
desc,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,267 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:photo_view/photo_view.dart';
|
|
||||||
import 'package:photo_view/photo_view_gallery.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
|
||||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
|
||||||
import 'package:twonly/src/visual/helpers/video_player_file.helper.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/save_to_gallery.dart';
|
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
|
|
||||||
|
|
||||||
class MemoriesPhotoSliderView extends StatefulWidget {
|
|
||||||
MemoriesPhotoSliderView({
|
|
||||||
required this.galleryItems,
|
|
||||||
super.key,
|
|
||||||
this.loadingBuilder,
|
|
||||||
this.minScale,
|
|
||||||
this.maxScale,
|
|
||||||
this.initialIndex = 0,
|
|
||||||
this.scrollDirection = Axis.horizontal,
|
|
||||||
}) : pageController = PageController(initialPage: initialIndex);
|
|
||||||
|
|
||||||
final LoadingBuilder? loadingBuilder;
|
|
||||||
final dynamic minScale;
|
|
||||||
final dynamic maxScale;
|
|
||||||
final int initialIndex;
|
|
||||||
final PageController pageController;
|
|
||||||
final List<MemoryItem> galleryItems;
|
|
||||||
final Axis scrollDirection;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _MemoriesPhotoSliderViewState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|
||||||
late int currentIndex = widget.initialIndex;
|
|
||||||
final GlobalKey<State<StatefulWidget>> key = GlobalKey();
|
|
||||||
|
|
||||||
void onPageChanged(int index) {
|
|
||||||
setState(() {
|
|
||||||
currentIndex = index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteFile() async {
|
|
||||||
final confirmed = await showAlertDialog(
|
|
||||||
context,
|
|
||||||
context.lang.deleteImageTitle,
|
|
||||||
context.lang.deleteImageBody,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
widget.galleryItems[currentIndex].mediaService.fullMediaRemoval();
|
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(
|
|
||||||
widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId,
|
|
||||||
);
|
|
||||||
|
|
||||||
widget.galleryItems.removeAt(currentIndex);
|
|
||||||
setState(() {});
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> exportFile() async {
|
|
||||||
final item = widget.galleryItems[currentIndex].mediaService;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (item.mediaFile.type == MediaType.video) {
|
|
||||||
await saveVideoToGallery(item.storedPath.path);
|
|
||||||
} else if (item.mediaFile.type == MediaType.image ||
|
|
||||||
item.mediaFile.type == MediaType.gif) {
|
|
||||||
final imageBytes = await item.storedPath.readAsBytes();
|
|
||||||
await saveImageToGallery(imageBytes);
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
showSnackbar(
|
|
||||||
context,
|
|
||||||
context.lang.galleryExportSuccess,
|
|
||||||
level: SnackbarLevel.success,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
showSnackbar(
|
|
||||||
context,
|
|
||||||
e.toString(),
|
|
||||||
level: SnackbarLevel.success,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> shareMediaFile() async {
|
|
||||||
final orgMediaService = widget.galleryItems[currentIndex].mediaService;
|
|
||||||
|
|
||||||
final newMediaService = await initializeMediaUpload(
|
|
||||||
orgMediaService.mediaFile.type,
|
|
||||||
userService.currentUser.defaultShowTime,
|
|
||||||
);
|
|
||||||
if (newMediaService == null) {
|
|
||||||
Log.error('Could not create new mediaFIle');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orgMediaService.storedPath.existsSync()) {
|
|
||||||
orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
|
|
||||||
} else if (orgMediaService.tempPath.existsSync()) {
|
|
||||||
orgMediaService.tempPath.copySync(newMediaService.originalPath.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ShareImageEditorView(
|
|
||||||
mediaFileService: newMediaService,
|
|
||||||
sharedFromGallery: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final orgMediaService = widget.galleryItems[currentIndex].mediaService;
|
|
||||||
return Dismissible(
|
|
||||||
key: key,
|
|
||||||
direction: DismissDirection.vertical,
|
|
||||||
resizeDuration: null,
|
|
||||||
onDismissed: (d) {
|
|
||||||
Navigator.pop(context, false);
|
|
||||||
},
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: Colors.white.withAlpha(0),
|
|
||||||
body: Container(
|
|
||||||
color: context.color.surface,
|
|
||||||
constraints: BoxConstraints.expand(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
children: <Widget>[
|
|
||||||
MediaViewSizingHelper(
|
|
||||||
bottomNavigation: ColoredBox(
|
|
||||||
color: context.color.surface,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (!orgMediaService.storedPath.existsSync())
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 12),
|
|
||||||
child: SaveToGalleryButton(
|
|
||||||
isLoading: false,
|
|
||||||
displayButtonLabel: true,
|
|
||||||
mediaService: orgMediaService,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
|
||||||
onPressed: shareMediaFile,
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
vertical: 10,
|
|
||||||
horizontal: 30,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: WidgetStateProperty.all<Color>(
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
context.lang.shareImagedEditorSendImage,
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
PhotoViewGallery.builder(
|
|
||||||
scrollPhysics: const BouncingScrollPhysics(),
|
|
||||||
builder: _buildItem,
|
|
||||||
itemCount: widget.galleryItems.length,
|
|
||||||
loadingBuilder: widget.loadingBuilder,
|
|
||||||
pageController: widget.pageController,
|
|
||||||
onPageChanged: onPageChanged,
|
|
||||||
scrollDirection: widget.scrollDirection,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 5,
|
|
||||||
child: PopupMenuButton<String>(
|
|
||||||
onSelected: (result) async {
|
|
||||||
if (result == 'delete') {
|
|
||||||
await deleteFile();
|
|
||||||
}
|
|
||||||
if (result == 'export') {
|
|
||||||
await exportFile();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => <PopupMenuEntry<String>>[
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'delete',
|
|
||||||
child: Text(context.lang.galleryDelete),
|
|
||||||
),
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'export',
|
|
||||||
child: Text(context.lang.galleryExport),
|
|
||||||
),
|
|
||||||
// PopupMenuItem<String>(
|
|
||||||
// value: 'details',
|
|
||||||
// child: Text(context.lang.galleryDetails),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
|
|
||||||
final item = widget.galleryItems[index];
|
|
||||||
|
|
||||||
var filePath = item.mediaService.storedPath;
|
|
||||||
if (!filePath.existsSync()) {
|
|
||||||
filePath = item.mediaService.tempPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.mediaService.mediaFile.type == MediaType.video
|
|
||||||
? PhotoViewGalleryPageOptions.customChild(
|
|
||||||
child: VideoPlayerFileHelper(
|
|
||||||
videoPath: filePath,
|
|
||||||
),
|
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
|
||||||
minScale: PhotoViewComputedScale.contained,
|
|
||||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: item.mediaService.mediaFile.mediaId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: PhotoViewGalleryPageOptions(
|
|
||||||
imageProvider: FileImage(filePath),
|
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
|
||||||
minScale: PhotoViewComputedScale.contained,
|
|
||||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: item.mediaService.mediaFile.mediaId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
|
||||||
|
|
||||||
class MemoriesItemThumbnailComp extends StatefulWidget {
|
|
||||||
const MemoriesItemThumbnailComp({
|
|
||||||
required this.galleryItem,
|
|
||||||
required this.onTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final MemoryItem galleryItem;
|
|
||||||
final GestureTapCallback onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MemoriesItemThumbnailComp> createState() =>
|
|
||||||
_MemoriesItemThumbnailCompState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MemoriesItemThumbnailCompState extends State<MemoriesItemThumbnailComp> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
initAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
|
||||||
if (!widget.galleryItem.mediaService.thumbnailPath.existsSync()) {
|
|
||||||
if (widget.galleryItem.mediaService.storedPath.existsSync()) {
|
|
||||||
await widget.galleryItem.mediaService.createThumbnail();
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
String formatDuration(Duration duration) {
|
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
||||||
final twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
|
|
||||||
final twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
|
||||||
return '$twoDigitMinutes:$twoDigitSeconds';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final media = widget.galleryItem.mediaService;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: widget.onTap,
|
|
||||||
child: Hero(
|
|
||||||
tag: media.mediaFile.mediaId,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
if (media.thumbnailPath.existsSync())
|
|
||||||
Image.file(media.thumbnailPath)
|
|
||||||
else if (media.storedPath.existsSync() &&
|
|
||||||
media.mediaFile.type == MediaType.image ||
|
|
||||||
media.mediaFile.type == MediaType.gif)
|
|
||||||
Image.file(media.storedPath)
|
|
||||||
else
|
|
||||||
const Text('Media file removed.'),
|
|
||||||
if (widget.galleryItem.mediaService.mediaFile.type ==
|
|
||||||
MediaType.video)
|
|
||||||
const Positioned.fill(
|
|
||||||
child: Center(
|
|
||||||
child: FaIcon(FontAwesomeIcons.circlePlay),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'schema_v10.dart' as v10;
|
||||||
import 'schema_v11.dart' as v11;
|
import 'schema_v11.dart' as v11;
|
||||||
import 'schema_v12.dart' as v12;
|
import 'schema_v12.dart' as v12;
|
||||||
import 'schema_v13.dart' as v13;
|
import 'schema_v13.dart' as v13;
|
||||||
|
import 'schema_v14.dart' as v14;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -48,10 +49,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v12.DatabaseAtV12(db);
|
return v12.DatabaseAtV12(db);
|
||||||
case 13:
|
case 13:
|
||||||
return v13.DatabaseAtV13(db);
|
return v13.DatabaseAtV13(db);
|
||||||
|
case 14:
|
||||||
|
return v14.DatabaseAtV14(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue