add shortcuts

This commit is contained in:
otsmr 2026-05-13 03:30:13 +02:00
parent ba06126a3c
commit f2b27e19f2
25 changed files with 5948 additions and 24 deletions

View file

@ -2,6 +2,7 @@
## 0.2.11 ## 0.2.11
- New: Create custom shortcuts to quickly share images with pre-selected groups
- New: Seamless recovery for iOS reinstallations - New: Seamless recovery for iOS reinstallations
- Improved: Redesigned snackbar notifications - Improved: Redesigned snackbar notifications
- Improved: New backup mechanism to allow larger backup files - Improved: New backup mechanism to allow larger backup files

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,83 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'shortcuts.dao.g.dart';
@DriftAccessor(
tables: [
Shortcuts,
ShortcutMembers,
],
)
class ShortcutsDao extends DatabaseAccessor<TwonlyDB> with _$ShortcutsDaoMixin {
ShortcutsDao(super.db);
Stream<List<Shortcut>> watchAllShortcuts() {
return (select(shortcuts)..orderBy([
(t) =>
OrderingTerm(expression: t.usageCounter, mode: OrderingMode.desc),
]))
.watch();
}
Future<Shortcut?> getShortcutByEmoji(String emoji) {
return (select(
shortcuts,
)..where((t) => t.emoji.equals(emoji))).getSingleOrNull();
}
Future<void> createShortcut(String emoji) async {
try {
await into(shortcuts).insert(
ShortcutsCompanion.insert(emoji: emoji),
);
// ignore: empty_catches
} catch (e) {}
}
Future<void> addShortcutMembers(int shortcutId, List<String> groupIds) async {
await batch((b) {
b.insertAll(
shortcutMembers,
groupIds.map(
(gId) => ShortcutMembersCompanion.insert(
shortcutId: shortcutId,
groupId: gId,
),
),
);
});
}
Future<List<ShortcutMember>> getShortcutMembers(int shortcutId) {
return (select(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).get();
}
Future<void> incrementUsage(int shortcutId) async {
await customStatement(
'UPDATE shortcuts SET usage_counter = usage_counter + 1 WHERE id = ?',
[shortcutId],
);
// Notify updates to trigger streams
notifyUpdates({TableUpdate.onTable(shortcuts, kind: UpdateKind.update)});
}
Future<void> updateShortcut(int shortcutId, String emoji) async {
await (update(shortcuts)..where((t) => t.id.equals(shortcutId))).write(
ShortcutsCompanion(emoji: Value(emoji)),
);
}
Future<void> deleteShortcutMembers(int shortcutId) async {
await (delete(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).go();
}
Future<void> deleteShortcut(int shortcutId) async {
await (delete(shortcuts)..where((t) => t.id.equals(shortcutId))).go();
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shortcuts.dao.dart';
// ignore_for_file: type=lint
mixin _$ShortcutsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ShortcutsTable get shortcuts => attachedDatabase.shortcuts;
$GroupsTable get groups => attachedDatabase.groups;
$ShortcutMembersTable get shortcutMembers => attachedDatabase.shortcutMembers;
ShortcutsDaoManager get managers => ShortcutsDaoManager(this);
}
class ShortcutsDaoManager {
final _$ShortcutsDaoMixin _db;
ShortcutsDaoManager(this._db);
$$ShortcutsTableTableManager get shortcuts =>
$$ShortcutsTableTableManager(_db.attachedDatabase, _db.shortcuts);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$ShortcutMembersTableTableManager get shortcutMembers =>
$$ShortcutMembersTableTableManager(
_db.attachedDatabase,
_db.shortcutMembers,
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
@DataClassName('Shortcut')
class Shortcuts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get emoji => text().unique()();
IntColumn get usageCounter => integer().withDefault(const Constant(0))();
}
@DataClassName('ShortcutMember')
class ShortcutMembers extends Table {
IntColumn get shortcutId => integer().references(
Shortcuts,
#id,
onDelete: KeyAction.cascade,
)();
TextColumn get groupId => text().references(
Groups,
#groupId,
onDelete: KeyAction.cascade,
)();
@override
Set<Column> get primaryKey => {shortcutId, groupId};
}

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart'; import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
@ -17,6 +18,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
@ -52,6 +54,8 @@ part 'twonly.db.g.dart';
UserDiscoveryOtherPromotions, UserDiscoveryOtherPromotions,
UserDiscoveryOwnPromotions, UserDiscoveryOwnPromotions,
UserDiscoveryShares, UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
], ],
daos: [ daos: [
MessagesDao, MessagesDao,
@ -62,6 +66,7 @@ part 'twonly.db.g.dart';
MediaFilesDao, MediaFilesDao,
UserDiscoveryDao, UserDiscoveryDao,
KeyVerificationDao, KeyVerificationDao,
ShortcutsDao,
], ],
) )
class TwonlyDB extends _$TwonlyDB { class TwonlyDB extends _$TwonlyDB {
@ -74,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 12; int get schemaVersion => 13;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -186,6 +191,10 @@ class TwonlyDB extends _$TwonlyDB {
await m.addColumn(schema.contacts, column); await m.addColumn(schema.contacts, column);
} }
}, },
from12To13: (m, schema) async {
await m.createTable(schema.shortcuts);
await m.createTable(schema.shortcutMembers);
},
)(m, from, to); )(m, from, to);
}, },
); );

File diff suppressed because it is too large Load diff

View file

@ -6582,6 +6582,480 @@ i1.GeneratedColumn<i2.Uint8List> _column_235(String aliasedName) =>
type: i1.DriftSqlType.blob, type: i1.DriftSqlType.blob,
$customConstraints: 'NOT NULL', $customConstraints: 'NOT NULL',
); );
final class Schema13 extends i0.VersionedSchema {
Schema13({required super.database}) : super(version: 13);
@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 Shape36 mediaFiles = Shape36(
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_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
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 Shape47 extends i0.VersionedTable {
Shape47({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get emoji =>
columnsByName['emoji']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get usageCounter =>
columnsByName['usage_counter']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_236(String aliasedName) =>
i1.GeneratedColumn<String>(
'emoji',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL UNIQUE',
);
i1.GeneratedColumn<int> _column_237(String aliasedName) =>
i1.GeneratedColumn<int>(
'usage_counter',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
class Shape48 extends i0.VersionedTable {
Shape48({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get shortcutId =>
columnsByName['shortcut_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get groupId =>
columnsByName['group_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_238(String aliasedName) =>
i1.GeneratedColumn<int>(
'shortcut_id',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE',
);
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,
@ -6594,6 +7068,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
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,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -6652,6 +7127,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema); await from11To12(migrator, schema);
return 12; return 12;
case 12:
final schema = Schema13(database: database);
final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema);
return 13;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -6670,6 +7150,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
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,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -6683,5 +7164,6 @@ i1.OnUpgrade stepByStep({
from9To10: from9To10, from9To10: from9To10,
from10To11: from10To11, from10To11: from10To11,
from11To12: from11To12, from11To12: from11To12,
from12To13: from12To13,
), ),
); );

View file

@ -3115,6 +3115,48 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Registering new account'** /// **'Registering new account'**
String get registeringNewAccount; String get registeringNewAccount;
/// No description provided for @createShortcut.
///
/// In en, this message translates to:
/// **'Create shortcut'**
String get createShortcut;
/// No description provided for @editShortcut.
///
/// In en, this message translates to:
/// **'Edit shortcut'**
String get editShortcut;
/// No description provided for @deleteShortcut.
///
/// In en, this message translates to:
/// **'Delete shortcut'**
String get deleteShortcut;
/// No description provided for @deleteShortcutBody.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this shortcut?'**
String get deleteShortcutBody;
/// No description provided for @updateShortcut.
///
/// In en, this message translates to:
/// **'Update shortcut'**
String get updateShortcut;
/// No description provided for @selectEmoji.
///
/// In en, this message translates to:
/// **'Select Emoji'**
String get selectEmoji;
/// No description provided for @errorEmojiUsedOrInvalid.
///
/// In en, this message translates to:
/// **'Emoji already used or invalid'**
String get errorEmojiUsedOrInvalid;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1757,4 +1757,27 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get registeringNewAccount => 'Neues Konto wird registriert'; String get registeringNewAccount => 'Neues Konto wird registriert';
@override
String get createShortcut => 'Shortcut erstellen';
@override
String get editShortcut => 'Shortcut bearbeiten';
@override
String get deleteShortcut => 'Shortcut löschen';
@override
String get deleteShortcutBody =>
'Bist du sicher, dass du diesen Shortcut löschen möchtest?';
@override
String get updateShortcut => 'Shortcut aktualisieren';
@override
String get selectEmoji => 'Emoji auswählen';
@override
String get errorEmojiUsedOrInvalid =>
'Emoji wird bereits verwendet oder ist ungültig';
} }

View file

@ -1742,4 +1742,26 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get registeringNewAccount => 'Registering new account'; String get registeringNewAccount => 'Registering new account';
@override
String get createShortcut => 'Create shortcut';
@override
String get editShortcut => 'Edit shortcut';
@override
String get deleteShortcut => 'Delete shortcut';
@override
String get deleteShortcutBody =>
'Are you sure you want to delete this shortcut?';
@override
String get updateShortcut => 'Update shortcut';
@override
String get selectEmoji => 'Select Emoji';
@override
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
} }

@ -1 +1 @@
Subproject commit 75b97e912f2e72a8e2a5da65e8ad12f0d1091855 Subproject commit 9218abf0961c072edd2f8aa5035d06a331b853c6

View file

@ -417,6 +417,7 @@ class ApiService {
), ),
); );
} }
await twonlyDB.receiptsDao.deleteReceiptForUser(contactId);
} }
} }
return res; return res;

View file

@ -31,7 +31,7 @@ Future<void> createThumbnailsForVideo(
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
); );
} else { } else {
Log.error( Log.warn(
'Thumbnail creation failed for the video with exit code.', 'Thumbnail creation failed for the video with exit code.',
); );
} }

View file

@ -29,7 +29,10 @@ class Log {
static String filterLogMessage(String msg) { static String filterLogMessage(String msg) {
if (msg.contains('SqliteException')) { if (msg.contains('SqliteException')) {
// Do not log data which would be inserted into the DB. // Do not log data which would be inserted into the DB.
return msg.substring(0, msg.indexOf('parameters: ')); final paramIndex = msg.indexOf('parameters: ');
if (paramIndex != -1) {
return msg.substring(0, paramIndex);
}
} }
return msg; return msg;
} }

View file

@ -82,6 +82,7 @@ class EmojiAnimationComp extends StatelessWidget {
'😴': 'sleep.lottie', '😴': 'sleep.lottie',
'🤒': 'thermometer-face.lottie', '🤒': 'thermometer-face.lottie',
'🤕': 'bandage-face.lottie', '🤕': 'bandage-face.lottie',
'🫪': 'distorted_face.json',
'🤥': 'liar.lottie', '🤥': 'liar.lottie',
'😇': 'halo.lottie', '😇': 'halo.lottie',
'🤠': 'cowboy.lottie', '🤠': 'cowboy.lottie',

View file

@ -0,0 +1,292 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
class AddNewShortcutView extends StatefulWidget {
const AddNewShortcutView({this.shortcut, super.key});
final Shortcut? shortcut;
@override
State<AddNewShortcutView> createState() => _StartNewChatView();
}
class _StartNewChatView extends State<AddNewShortcutView> {
List<Group> _groups = [];
List<Group> _allGroups = [];
final TextEditingController _searchGroupName = TextEditingController();
late StreamSubscription<List<Group>> _groupSub;
final HashSet<String> _selectedGroups = HashSet();
String? shortcutEmoji;
@override
void initState() {
super.initState();
if (widget.shortcut != null) {
shortcutEmoji = widget.shortcut!.emoji;
twonlyDB.shortcutsDao.getShortcutMembers(widget.shortcut!.id).then((
members,
) {
if (mounted) {
setState(() {
for (final m in members) {
_selectedGroups.add(m.groupId);
}
});
}
});
}
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_groupSub = stream.listen((update) async {
update.sort(
(a, b) => a.groupName.compareTo(b.groupName),
);
setState(() {
_allGroups = update;
});
await filterUsers();
});
}
@override
void dispose() {
unawaited(_groupSub.cancel());
super.dispose();
}
Future<void> filterUsers() async {
if (_searchGroupName.value.text.isEmpty) {
setState(() {
_groups = _allGroups;
});
return;
}
final usersFiltered = _allGroups
.where(
(group) => group.groupName.toLowerCase().contains(
_searchGroupName.value.text.toLowerCase(),
),
)
.toList();
setState(() {
_groups = usersFiltered;
});
}
void toggleSelectedGroup(String groupId) {
if (!_selectedGroups.contains(groupId)) {
if (_selectedGroups.length > 256) {
showSnackbar(context, context.lang.groupSizeLimitError(256));
return;
}
_selectedGroups.add(groupId);
} else {
_selectedGroups.remove(groupId);
}
setState(() {});
}
Future<void> submitChanges() async {
try {
if (widget.shortcut != null) {
await twonlyDB.shortcutsDao.updateShortcut(
widget.shortcut!.id,
shortcutEmoji!,
);
await twonlyDB.shortcutsDao.deleteShortcutMembers(widget.shortcut!.id);
await twonlyDB.shortcutsDao.addShortcutMembers(
widget.shortcut!.id,
_selectedGroups.toList(),
);
} else {
await twonlyDB.shortcutsDao.createShortcut(
shortcutEmoji!,
);
final shortcutId = (await twonlyDB.shortcutsDao.getShortcutByEmoji(
shortcutEmoji!,
))!.id;
await twonlyDB.shortcutsDao.deleteShortcutMembers(shortcutId);
await twonlyDB.shortcutsDao.addShortcutMembers(
shortcutId,
_selectedGroups.toList(),
);
}
if (mounted) Navigator.pop(context);
} catch (e) {
Log.error(e);
if (mounted) {
showSnackbar(context, context.lang.errorEmojiUsedOrInvalid);
}
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: Text(
widget.shortcut == null
? context.lang.createShortcut
: context.lang.editShortcut,
),
actions: [
if (widget.shortcut != null)
IconButton(
icon: const FaIcon(
FontAwesomeIcons.trashCan,
size: 18,
color: Colors.red,
),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.lang.deleteShortcut),
content: Text(context.lang.deleteShortcutBody),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.lang.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.lang.delete),
),
],
),
);
if (confirm == true) {
await twonlyDB.shortcutsDao.deleteShortcut(
widget.shortcut!.id,
);
if (context.mounted) Navigator.pop(context);
}
},
),
TextButton(
onPressed: () async {
// ignore: inference_failure_on_function_invocation
final result = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (context) => const EmojiPickerBottom(),
);
if (result is EmojiLayerData) {
setState(() {
shortcutEmoji = result.text;
});
}
},
child: Text(
shortcutEmoji ?? context.lang.selectEmoji,
style: TextStyle(
fontSize: shortcutEmoji == null ? 14 : 22,
),
),
),
const SizedBox(width: 8),
],
),
floatingActionButton: FilledButton.icon(
onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null)
? null
: submitChanges,
label: Text(
widget.shortcut == null
? context.lang.createShortcut
: context.lang.updateShortcut,
),
icon: const FaIcon(FontAwesomeIcons.check),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(
bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
onChanged: (_) async {
await filterUsers();
},
controller: _searchGroupName,
decoration: getInputDecoration(
context,
context.lang.shareImageSearchAllContacts,
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
restorationId: 'new_message_users_list',
itemCount: _groups.length,
itemBuilder: (context, i) {
final group = _groups[i];
return ListTile(
key: ValueKey(group.groupId),
title: Row(
children: [
Text(substringBy(group.groupName, 12)),
FlameCounterWidget(
groupId: group.groupId,
prefix: true,
),
],
),
leading: AvatarIcon(
group: group,
fontSize: 15,
),
trailing: Checkbox(
value: _selectedGroups.contains(group.groupId),
side: WidgetStateBorderSide.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return const BorderSide(width: 0);
}
return BorderSide(
color: Theme.of(context).colorScheme.outline,
);
},
),
onChanged: (value) {
toggleSelectedGroup(group.groupId);
},
),
onTap: () {
toggleSelectedGroup(group.groupId);
},
);
},
),
),
],
),
),
),
),
);
}
}

View file

@ -98,9 +98,12 @@ class MainCameraController {
final cameraControllerTemp = cameraController; final cameraControllerTemp = cameraController;
cameraController = null; cameraController = null;
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
_pendingDisposal = Future.delayed(const Duration(milliseconds: 100), () async { _pendingDisposal = Future.delayed(
await cameraControllerTemp?.dispose(); const Duration(milliseconds: 100),
}); () async {
await cameraControllerTemp?.dispose();
},
);
initCameraStarted = false; initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
} }
@ -226,7 +229,7 @@ class MainCameraController {
(e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) { (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
Log.info('Focus point or mode not supported on this device'); Log.info('Focus point or mode not supported on this device');
} else { } else {
Log.error(e); Log.warn(e);
} }
} }

View file

@ -20,6 +20,7 @@ import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/elements/headline.element.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
@ -194,6 +195,11 @@ class _ShareImageView extends State<ShareImageView> {
), ),
), ),
), ),
const SizedBox(height: 10),
ShortcutRowComp(
selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds,
),
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
BestFriendsSelector( BestFriendsSelector(
groups: _pinnedContacts, groups: _pinnedContacts,

View file

@ -0,0 +1,82 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/camera/add_new_shortcut.view.dart';
class ShortcutRowComp extends StatefulWidget {
const ShortcutRowComp({
required this.selectedGroupIds,
required this.updateSelectedGroupIds,
super.key,
});
final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
@override
State<ShortcutRowComp> createState() => _ShortcutRowCompState();
}
class _ShortcutRowCompState extends State<ShortcutRowComp> {
Future<void> _openCreateDialog() async {
await context.navPush(const AddNewShortcutView());
}
Future<void> _applyShortcut(Shortcut shortcut) async {
await twonlyDB.shortcutsDao.incrementUsage(shortcut.id);
final members = await twonlyDB.shortcutsDao.getShortcutMembers(shortcut.id);
for (final m in members) {
widget.updateSelectedGroupIds(m.groupId, true);
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: StreamBuilder<List<Shortcut>>(
stream: twonlyDB.shortcutsDao.watchAllShortcuts(),
builder: (context, snapshot) {
final shortcuts = snapshot.data ?? [];
return ListView(
scrollDirection: Axis.horizontal,
children: [
Row(
children: [
ActionChip(
padding: EdgeInsets.zero,
onPressed: _openCreateDialog,
label: shortcuts.isEmpty
? Text(
context.lang.createShortcut,
style: const TextStyle(fontSize: 9),
)
: const Icon(Icons.add_reaction_outlined, size: 20),
shape: const StadiumBorder(),
),
for (final shortcut in shortcuts)
GestureDetector(
onLongPress: () {
context.navPush(AddNewShortcutView(shortcut: shortcut));
},
child: ActionChip(
padding: EdgeInsets.zero,
onPressed: () => _applyShortcut(shortcut),
label: Text(
shortcut.emoji,
style: const TextStyle(fontSize: 18),
),
shape: const StadiumBorder(),
),
),
],
),
],
);
},
),
);
}
}

View file

@ -443,8 +443,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: HEAD ref: "23a0595f7dde50728afce917c1c58f284ccbb495"
resolved-ref: c5bffd3414c1e640389b41165b831df7df1cf517 resolved-ref: "23a0595f7dde50728afce917c1c58f284ccbb495"
url: "https://github.com/otsmr/emoji_picker_flutter.git" url: "https://github.com/otsmr/emoji_picker_flutter.git"
source: git source: git
version: "4.4.0" version: "4.4.0"

View file

@ -167,6 +167,7 @@ dependency_overrides:
# Using override until this gets merged. # Using override until this gets merged.
git: git:
url: https://github.com/otsmr/emoji_picker_flutter.git url: https://github.com/otsmr/emoji_picker_flutter.git
ref: 23a0595f7dde50728afce917c1c58f284ccbb495
flutter_android_volume_keydown: flutter_android_volume_keydown:
git: git:
url: https://github.com/yenchieh/flutter_android_volume_keydown.git url: https://github.com/yenchieh/flutter_android_volume_keydown.git

View file

@ -16,6 +16,7 @@ import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10; 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;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -45,10 +46,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v11.DatabaseAtV11(db); return v11.DatabaseAtV11(db);
case 12: case 12:
return v12.DatabaseAtV12(db); return v12.DatabaseAtV12(db);
case 13:
return v13.DatabaseAtV13(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]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
} }

File diff suppressed because it is too large Load diff