mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:24:07 +00:00
add contact labels
This commit is contained in:
parent
0291a4c3a2
commit
0e06a21948
26 changed files with 18350 additions and 131 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## 0.4.3
|
||||
|
||||
- New: Contact labels
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- New: Encrypted Cloud Backup of Memories
|
||||
|
|
|
|||
91
lib/src/database/daos/labels.dao.dart
Normal file
91
lib/src/database/daos/labels.dao.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/labels.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
part 'labels.dao.g.dart';
|
||||
|
||||
@DriftAccessor(
|
||||
tables: [
|
||||
Labels,
|
||||
ContactLabels,
|
||||
],
|
||||
)
|
||||
class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
|
||||
LabelsDao(super.db);
|
||||
|
||||
Stream<List<Label>> watchAllLabels() {
|
||||
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).watch();
|
||||
}
|
||||
|
||||
Future<List<Label>> getAllLabels() {
|
||||
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).get();
|
||||
}
|
||||
|
||||
Stream<List<Label>> watchContactLabels(int contactId) {
|
||||
final query = select(contactLabels).join([
|
||||
innerJoin(labels, labels.id.equalsExp(contactLabels.labelId)),
|
||||
])..where(contactLabels.contactId.equals(contactId));
|
||||
|
||||
return query.watch().map(
|
||||
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Label>> getContactLabels(int contactId) {
|
||||
final query = select(contactLabels).join([
|
||||
innerJoin(labels, labels.id.equalsExp(contactLabels.labelId)),
|
||||
])..where(contactLabels.contactId.equals(contactId));
|
||||
|
||||
return query.get().then(
|
||||
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setContactLabels(int contactId, List<int> labelIds) async {
|
||||
final sanitizedLabelIds = labelIds.take(3).toList();
|
||||
await transaction(() async {
|
||||
await (delete(contactLabels)..where((t) => t.contactId.equals(contactId))).go();
|
||||
if (sanitizedLabelIds.isNotEmpty) {
|
||||
await batch((b) {
|
||||
b.insertAll(
|
||||
contactLabels,
|
||||
sanitizedLabelIds.map(
|
||||
(lId) => ContactLabelsCompanion.insert(
|
||||
contactId: contactId,
|
||||
labelId: lId,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> createLabel(String name, int textColor, int backgroundColor) {
|
||||
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
|
||||
return into(labels).insert(
|
||||
LabelsCompanion.insert(
|
||||
name: sanitizedName,
|
||||
textColor: textColor,
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> updateLabel(int id, String name, int textColor, int backgroundColor) {
|
||||
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
|
||||
return (update(labels)..where((t) => t.id.equals(id)))
|
||||
.write(
|
||||
LabelsCompanion(
|
||||
name: Value(sanitizedName),
|
||||
textColor: Value(textColor),
|
||||
backgroundColor: Value(backgroundColor),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows > 0);
|
||||
}
|
||||
|
||||
Future<int> deleteLabel(int id) {
|
||||
return (delete(labels)..where((t) => t.id.equals(id))).go();
|
||||
}
|
||||
}
|
||||
22
lib/src/database/daos/labels.dao.g.dart
Normal file
22
lib/src/database/daos/labels.dao.g.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'labels.dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$LabelsDaoMixin on DatabaseAccessor<TwonlyDB> {
|
||||
$LabelsTable get labels => attachedDatabase.labels;
|
||||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$ContactLabelsTable get contactLabels => attachedDatabase.contactLabels;
|
||||
LabelsDaoManager get managers => LabelsDaoManager(this);
|
||||
}
|
||||
|
||||
class LabelsDaoManager {
|
||||
final _$LabelsDaoMixin _db;
|
||||
LabelsDaoManager(this._db);
|
||||
$$LabelsTableTableManager get labels =>
|
||||
$$LabelsTableTableManager(_db.attachedDatabase, _db.labels);
|
||||
$$ContactsTableTableManager get contacts =>
|
||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$ContactLabelsTableTableManager get contactLabels =>
|
||||
$$ContactLabelsTableTableManager(_db.attachedDatabase, _db.contactLabels);
|
||||
}
|
||||
3322
lib/src/database/schemas/twonly_db/drift_schema_v24.json
Normal file
3322
lib/src/database/schemas/twonly_db/drift_schema_v24.json
Normal file
File diff suppressed because it is too large
Load diff
28
lib/src/database/tables/labels.table.dart
Normal file
28
lib/src/database/tables/labels.table.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
|
||||
@DataClassName('Label')
|
||||
class Labels extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
IntColumn get textColor => integer()();
|
||||
IntColumn get backgroundColor => integer()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
@DataClassName('ContactLabel')
|
||||
class ContactLabels extends Table {
|
||||
IntColumn get contactId => integer().references(
|
||||
Contacts,
|
||||
#userId,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
IntColumn get labelId => integer().references(
|
||||
Labels,
|
||||
#id,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {contactId, labelId};
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/locator.dart';
|
|||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||
import 'package:twonly/src/database/daos/labels.dao.dart';
|
||||
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
|
||||
import 'package:twonly/src/database/daos/messages.dao.dart';
|
||||
import 'package:twonly/src/database/daos/reactions.dao.dart';
|
||||
|
|
@ -15,6 +16,7 @@ import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
|||
import 'package:twonly/src/database/drift_logging_interceptor.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/labels.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/tables/reactions.table.dart';
|
||||
|
|
@ -59,6 +61,8 @@ part 'twonly.db.g.dart';
|
|||
UserDiscoveryShares,
|
||||
Shortcuts,
|
||||
ShortcutMembers,
|
||||
Labels,
|
||||
ContactLabels,
|
||||
],
|
||||
daos: [
|
||||
MessagesDao,
|
||||
|
|
@ -70,6 +74,7 @@ part 'twonly.db.g.dart';
|
|||
UserDiscoveryDao,
|
||||
KeyVerificationDao,
|
||||
ShortcutsDao,
|
||||
LabelsDao,
|
||||
],
|
||||
)
|
||||
class TwonlyDB extends _$TwonlyDB {
|
||||
|
|
@ -82,7 +87,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 23;
|
||||
int get schemaVersion => 24;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
final connection = driftDatabase(
|
||||
|
|
@ -285,6 +290,10 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
schema.mediaFiles.blurhash,
|
||||
);
|
||||
},
|
||||
from23To24: (m, schema) async {
|
||||
await m.createTable(schema.labels);
|
||||
await m.createTable(schema.contactLabels);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12133,6 +12133,546 @@ i1.GeneratedColumn<String> _column_256(String aliasedName) =>
|
|||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
|
||||
final class Schema24 extends i0.VersionedSchema {
|
||||
Schema24({required super.database}) : super(version: 24);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
contacts,
|
||||
groups,
|
||||
mediaFiles,
|
||||
messages,
|
||||
messageHistories,
|
||||
reactions,
|
||||
groupMembers,
|
||||
receipts,
|
||||
receivedReceipts,
|
||||
signalIdentityKeyStores,
|
||||
signalPreKeyStores,
|
||||
signalSenderKeyStores,
|
||||
signalSessionStores,
|
||||
signalSignedPreKeyStores,
|
||||
messageActions,
|
||||
groupHistories,
|
||||
keyVerifications,
|
||||
verificationTokens,
|
||||
userDiscoveryAnnouncedUsers,
|
||||
userDiscoveryUserRelations,
|
||||
userDiscoveryOtherPromotions,
|
||||
userDiscoveryOwnPromotions,
|
||||
userDiscoveryShares,
|
||||
shortcuts,
|
||||
shortcutMembers,
|
||||
labels,
|
||||
contactLabels,
|
||||
];
|
||||
late final Shape57 contacts = Shape57(
|
||||
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_249,
|
||||
_column_250,
|
||||
_column_251,
|
||||
_column_252,
|
||||
_column_253,
|
||||
_column_254,
|
||||
_column_247,
|
||||
_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 Shape58 mediaFiles = Shape58(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'media_files',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(media_id)'],
|
||||
columns: [
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
_column_255,
|
||||
_column_256,
|
||||
_column_146,
|
||||
_column_147,
|
||||
_column_148,
|
||||
_column_149,
|
||||
_column_239,
|
||||
_column_240,
|
||||
_column_207,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
_column_153,
|
||||
_column_154,
|
||||
_column_155,
|
||||
_column_156,
|
||||
_column_157,
|
||||
_column_244,
|
||||
_column_245,
|
||||
_column_118,
|
||||
_column_241,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape25 messages = Shape25(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'messages',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id)'],
|
||||
columns: [
|
||||
_column_158,
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_144,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_165,
|
||||
_column_153,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_169,
|
||||
_column_118,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape26 messageHistories = Shape26(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_161,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape27 reactions = Shape27(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'reactions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
|
||||
columns: [_column_174, _column_176, _column_177, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 groupMembers = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_members',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
|
||||
columns: [
|
||||
_column_158,
|
||||
_column_178,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_209,
|
||||
_column_210,
|
||||
_column_181,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape37 receipts = Shape37(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||
columns: [
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
_column_208,
|
||||
_column_187,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape30 receivedReceipts = Shape30(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'received_receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||
columns: [_column_182, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape31 signalIdentityKeyStores = Shape31(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_identity_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||
columns: [_column_192, _column_193, _column_194, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 signalPreKeyStores = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_pre_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
|
||||
columns: [_column_195, _column_196, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 signalSenderKeyStores = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_sender_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
|
||||
columns: [_column_197, _column_198],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape33 signalSessionStores = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_session_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||
columns: [_column_192, _column_193, _column_199, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape50 signalSignedPreKeyStores = Shape50(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_signed_pre_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'],
|
||||
columns: [_column_242, _column_243, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape34 messageActions = Shape34(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_actions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
|
||||
columns: [_column_174, _column_183, _column_144, _column_200],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 groupHistories = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(group_history_id)'],
|
||||
columns: [
|
||||
_column_201,
|
||||
_column_158,
|
||||
_column_202,
|
||||
_column_203,
|
||||
_column_204,
|
||||
_column_205,
|
||||
_column_206,
|
||||
_column_144,
|
||||
_column_200,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape54 keyVerifications = Shape54(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'key_verifications',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_216,
|
||||
_column_183,
|
||||
_column_144,
|
||||
_column_248,
|
||||
_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 Shape52 userDiscoveryAnnouncedUsers = Shape52(
|
||||
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,
|
||||
_column_246,
|
||||
],
|
||||
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,
|
||||
);
|
||||
late final Shape59 labels = Shape59(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'labels',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_173,
|
||||
_column_193,
|
||||
_column_257,
|
||||
_column_258,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape60 contactLabels = Shape60(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'contact_labels',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(contact_id, label_id)'],
|
||||
columns: [_column_183, _column_259],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
}
|
||||
|
||||
class Shape59 extends i0.VersionedTable {
|
||||
Shape59({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get textColor =>
|
||||
columnsByName['text_color']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get backgroundColor =>
|
||||
columnsByName['background_color']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_257(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'text_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_258(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'background_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
class Shape60 extends i0.VersionedTable {
|
||||
Shape60({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get contactId =>
|
||||
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get labelId =>
|
||||
columnsByName['label_id']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_259(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'label_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL REFERENCES labels(id)ON DELETE CASCADE',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
|
|
@ -12156,6 +12696,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
|
||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -12269,6 +12810,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from22To23(migrator, schema);
|
||||
return 23;
|
||||
case 23:
|
||||
final schema = Schema24(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from23To24(migrator, schema);
|
||||
return 24;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -12298,6 +12844,7 @@ i1.OnUpgrade stepByStep({
|
|||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
|
||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
|
|
@ -12322,5 +12869,6 @@ i1.OnUpgrade stepByStep({
|
|||
from20To21: from20To21,
|
||||
from21To22: from21To22,
|
||||
from22To23: from22To23,
|
||||
from23To24: from23To24,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3917,7 +3917,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecoveryTrustedFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Trusted Friends'**
|
||||
/// **'Account recovery'**
|
||||
String get passwordlessRecoveryTrustedFriends;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryDoneBtn.
|
||||
|
|
@ -4285,6 +4285,90 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Show flame restore warning'**
|
||||
String get settingsShowRestoreFlameTitle;
|
||||
|
||||
/// No description provided for @contactLabelsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Labels'**
|
||||
String get contactLabelsTitle;
|
||||
|
||||
/// No description provided for @contactLabelsSubtitleEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No labels selected'**
|
||||
String get contactLabelsSubtitleEmpty;
|
||||
|
||||
/// No description provided for @contactLabelsMaxLimit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Maximum 3 labels per contact'**
|
||||
String get contactLabelsMaxLimit;
|
||||
|
||||
/// No description provided for @createLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create new label'**
|
||||
String get createLabel;
|
||||
|
||||
/// No description provided for @editLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit label'**
|
||||
String get editLabel;
|
||||
|
||||
/// No description provided for @deleteLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete label'**
|
||||
String get deleteLabel;
|
||||
|
||||
/// No description provided for @deleteLabelConfirmation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to delete this label? It will be removed from all contacts.'**
|
||||
String get deleteLabelConfirmation;
|
||||
|
||||
/// No description provided for @labelNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Label name'**
|
||||
String get labelNameHint;
|
||||
|
||||
/// No description provided for @labelTextColor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Text color'**
|
||||
String get labelTextColor;
|
||||
|
||||
/// No description provided for @labelBackgroundColor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Background color'**
|
||||
String get labelBackgroundColor;
|
||||
|
||||
/// No description provided for @customColor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Custom Color'**
|
||||
String get customColor;
|
||||
|
||||
/// No description provided for @hue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hue'**
|
||||
String get hue;
|
||||
|
||||
/// No description provided for @saturation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saturation'**
|
||||
String get saturation;
|
||||
|
||||
/// No description provided for @brightness.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Brightness'**
|
||||
String get brightness;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -2261,7 +2261,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTrustedFriends => 'Vertrauenswürdige Freunde';
|
||||
String get passwordlessRecoveryTrustedFriends => 'Kontowiederherstellung';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryDoneBtn(num count) {
|
||||
|
|
@ -2484,4 +2484,47 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsShowRestoreFlameTitle =>
|
||||
'Hinweis zur Flammen-Wiederherstellung anzeigen';
|
||||
|
||||
@override
|
||||
String get contactLabelsTitle => 'Kontaktlabels';
|
||||
|
||||
@override
|
||||
String get contactLabelsSubtitleEmpty => 'Keine Labels ausgewählt';
|
||||
|
||||
@override
|
||||
String get contactLabelsMaxLimit => 'Maximal 3 Labels pro Kontakt';
|
||||
|
||||
@override
|
||||
String get createLabel => 'Neues Label erstellen';
|
||||
|
||||
@override
|
||||
String get editLabel => 'Label bearbeiten';
|
||||
|
||||
@override
|
||||
String get deleteLabel => 'Label löschen';
|
||||
|
||||
@override
|
||||
String get deleteLabelConfirmation =>
|
||||
'Möchtest du dieses Label wirklich löschen? Es wird von allen Kontakten entfernt.';
|
||||
|
||||
@override
|
||||
String get labelNameHint => 'Label-Name';
|
||||
|
||||
@override
|
||||
String get labelTextColor => 'Textfarbe';
|
||||
|
||||
@override
|
||||
String get labelBackgroundColor => 'Hintergrundfarbe';
|
||||
|
||||
@override
|
||||
String get customColor => 'Eigene Farbe';
|
||||
|
||||
@override
|
||||
String get hue => 'Farbton';
|
||||
|
||||
@override
|
||||
String get saturation => 'Sättigung';
|
||||
|
||||
@override
|
||||
String get brightness => 'Helligkeit';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2245,7 +2245,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get passwordlessRecoverySelectFriends => 'Select trusted friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTrustedFriends => 'Trusted Friends';
|
||||
String get passwordlessRecoveryTrustedFriends => 'Account recovery';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryDoneBtn(num count) {
|
||||
|
|
@ -2461,4 +2461,47 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsShowRestoreFlameTitle => 'Show flame restore warning';
|
||||
|
||||
@override
|
||||
String get contactLabelsTitle => 'Contact Labels';
|
||||
|
||||
@override
|
||||
String get contactLabelsSubtitleEmpty => 'No labels selected';
|
||||
|
||||
@override
|
||||
String get contactLabelsMaxLimit => 'Maximum 3 labels per contact';
|
||||
|
||||
@override
|
||||
String get createLabel => 'Create new label';
|
||||
|
||||
@override
|
||||
String get editLabel => 'Edit label';
|
||||
|
||||
@override
|
||||
String get deleteLabel => 'Delete label';
|
||||
|
||||
@override
|
||||
String get deleteLabelConfirmation =>
|
||||
'Are you sure you want to delete this label? It will be removed from all contacts.';
|
||||
|
||||
@override
|
||||
String get labelNameHint => 'Label name';
|
||||
|
||||
@override
|
||||
String get labelTextColor => 'Text color';
|
||||
|
||||
@override
|
||||
String get labelBackgroundColor => 'Background color';
|
||||
|
||||
@override
|
||||
String get customColor => 'Custom Color';
|
||||
|
||||
@override
|
||||
String get hue => 'Hue';
|
||||
|
||||
@override
|
||||
String get saturation => 'Saturation';
|
||||
|
||||
@override
|
||||
String get brightness => 'Brightness';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c5f554b36deab290c091ae46b3af1255f74c6b98
|
||||
Subproject commit d08a6d4261135b80fba6a02f1e7e70fefcf17b8d
|
||||
144
lib/src/visual/components/contact_labels.comp.dart
Normal file
144
lib/src/visual/components/contact_labels.comp.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import 'dart:async';
|
||||
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';
|
||||
|
||||
class ContactLabels extends StatefulWidget {
|
||||
const ContactLabels({
|
||||
required this.contactId,
|
||||
this.fontSize = 8,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
this.emptyText,
|
||||
this.showEmptyText = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int contactId;
|
||||
final double fontSize;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final String? emptyText;
|
||||
final bool showEmptyText;
|
||||
|
||||
@override
|
||||
State<ContactLabels> createState() => _ContactLabelsState();
|
||||
}
|
||||
|
||||
class _ContactLabelsState extends State<ContactLabels> {
|
||||
List<Label> _labels = [];
|
||||
late StreamSubscription<List<Label>> _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((
|
||||
labels,
|
||||
) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_labels = labels;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_labels.isEmpty) {
|
||||
if (!widget.showEmptyText && widget.emptyText == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
widget.emptyText ?? context.lang.contactLabelsSubtitleEmpty,
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 2,
|
||||
children: _labels.map((label) {
|
||||
final bgColor = Color(label.backgroundColor);
|
||||
final textColor = Color(label.textColor);
|
||||
|
||||
return Container(
|
||||
padding: widget.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label.name,
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize,
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget? buildContactLabelsSubtitle({
|
||||
required int contactId,
|
||||
required List<Label> labels,
|
||||
Widget? additionalSubtitle,
|
||||
}) {
|
||||
if (additionalSubtitle != null) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
additionalSubtitle,
|
||||
if (labels.isNotEmpty)
|
||||
ContactLabels(contactId: contactId),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (labels.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ContactLabels(contactId: contactId);
|
||||
}
|
||||
|
||||
class ContactLabelsSubtitleBuilder extends StatelessWidget {
|
||||
const ContactLabelsSubtitleBuilder({
|
||||
required this.contactId,
|
||||
required this.builder,
|
||||
this.additionalSubtitle,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int contactId;
|
||||
final Widget? additionalSubtitle;
|
||||
final Widget Function(BuildContext context, Widget? subtitleWidget) builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<Label>>(
|
||||
stream: twonlyDB.labelsDao.watchContactLabels(contactId),
|
||||
builder: (context, snapshot) {
|
||||
final labels = snapshot.data ?? [];
|
||||
final subtitle = buildContactLabelsSubtitle(
|
||||
contactId: contactId,
|
||||
labels: labels,
|
||||
additionalSubtitle: additionalSubtitle,
|
||||
);
|
||||
return builder(context, subtitle);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/src/visual/components/custom_color_picker_dialog.comp.dart
Normal file
114
lib/src/visual/components/custom_color_picker_dialog.comp.dart
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class CustomColorPickerDialog extends StatefulWidget {
|
||||
const CustomColorPickerDialog({
|
||||
required this.initialColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Color initialColor;
|
||||
|
||||
@override
|
||||
State<CustomColorPickerDialog> createState() => _CustomColorPickerDialogState();
|
||||
}
|
||||
|
||||
class _CustomColorPickerDialogState extends State<CustomColorPickerDialog> {
|
||||
late HSVColor _hsvColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hsvColor = HSVColor.fromColor(widget.initialColor);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentColor = _hsvColor.toColor();
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(context.lang.customColor),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Color preview box
|
||||
Center(
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Hue Slider
|
||||
Text(context.lang.hue, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Slider(
|
||||
value: _hsvColor.hue,
|
||||
max: 360,
|
||||
activeColor: HSVColor.fromAHSV(1, _hsvColor.hue, 1, 1).toColor(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_hsvColor = _hsvColor.withHue(val);
|
||||
});
|
||||
},
|
||||
),
|
||||
// Saturation Slider
|
||||
Text(context.lang.saturation, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Slider(
|
||||
value: _hsvColor.saturation,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_hsvColor = _hsvColor.withSaturation(val);
|
||||
});
|
||||
},
|
||||
),
|
||||
// Brightness / Value Slider
|
||||
Text(context.lang.brightness, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Slider(
|
||||
value: _hsvColor.value,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_hsvColor = _hsvColor.withValue(val);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.text,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () => Navigator.of(context).pop(currentColor.toARGB32()),
|
||||
child: Text(context.lang.ok),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
328
lib/src/visual/components/label_editor_bottom_sheet.comp.dart
Normal file
328
lib/src/visual/components/label_editor_bottom_sheet.comp.dart
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/custom_color_picker_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class LabelEditorBottomSheet extends StatefulWidget {
|
||||
const LabelEditorBottomSheet({
|
||||
this.label,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Label? label;
|
||||
|
||||
@override
|
||||
State<LabelEditorBottomSheet> createState() => _LabelEditorBottomSheetState();
|
||||
}
|
||||
|
||||
class _LabelEditorBottomSheetState extends State<LabelEditorBottomSheet> {
|
||||
late TextEditingController _nameController;
|
||||
late int _selectedBgColor;
|
||||
late int _selectedTextColor;
|
||||
|
||||
static const List<int> defaultBgColors = [
|
||||
0xFFE57373, // Red
|
||||
0xFFF06292, // Pink
|
||||
0xFFBA68C8, // Purple
|
||||
0xFF7986CB, // Indigo
|
||||
0xFF64B5F6, // Blue
|
||||
0xFF4DD0E1, // Cyan
|
||||
0xFF4DB6AC, // Teal
|
||||
0xFF81C784, // Green
|
||||
0xFFFFB74D, // Amber
|
||||
0xFFFF8A65, // Deep Orange
|
||||
0xFF90A4AE, // Blue Grey
|
||||
0xFF424242, // Dark Grey
|
||||
];
|
||||
|
||||
static const List<int> defaultTextColors = [
|
||||
0xFFFFFFFF, // White
|
||||
0xFF121212, // Dark/Black
|
||||
0xFF1B263B, // Deep Navy
|
||||
0xFF8B0000, // Dark Red
|
||||
0xFF004D40, // Dark Teal
|
||||
0xFF4A148C, // Dark Purple
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.label?.name ?? '');
|
||||
_selectedBgColor = widget.label?.backgroundColor ?? defaultBgColors[4];
|
||||
_selectedTextColor = widget.label?.textColor ?? defaultTextColors[0];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickCustomColor({required bool isBgColor}) async {
|
||||
final initial = isBgColor ? Color(_selectedBgColor) : Color(_selectedTextColor);
|
||||
final pickedColorInt = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => CustomColorPickerDialog(initialColor: initial),
|
||||
);
|
||||
|
||||
if (pickedColorInt != null && mounted) {
|
||||
setState(() {
|
||||
if (isBgColor) {
|
||||
_selectedBgColor = pickedColorInt;
|
||||
} else {
|
||||
_selectedTextColor = pickedColorInt;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditing = widget.label != null;
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: bottomInset),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag indicator handle
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade400,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isEditing ? context.lang.editLabel : context.lang.createLabel,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Interactive Inline Label Badge
|
||||
Center(
|
||||
child: IntrinsicWidth(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100, maxWidth: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(_selectedBgColor),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||
child: TextField(
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
maxLength: 8,
|
||||
textAlign: TextAlign.center,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
style: TextStyle(
|
||||
color: Color(_selectedTextColor),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.labelNameHint,
|
||||
hintStyle: TextStyle(
|
||||
color: Color(_selectedTextColor).withValues(alpha: 0.6),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
counterText: '',
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Background Color Section
|
||||
Text(
|
||||
context.lang.labelBackgroundColor,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
...defaultBgColors.map((colorValue) {
|
||||
final selected = _selectedBgColor == colorValue;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedBgColor = colorValue;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(colorValue),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: selected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: Color(colorValue).computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
// Custom Color Picker Button
|
||||
GestureDetector(
|
||||
onTap: () => _pickCustomColor(isBgColor: true),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
colors: [
|
||||
Colors.red,
|
||||
Colors.yellow,
|
||||
Colors.green,
|
||||
Colors.cyan,
|
||||
Colors.blue,
|
||||
Colors.purple,
|
||||
Colors.red,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.colorize,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Text Color Section
|
||||
Text(
|
||||
context.lang.labelTextColor,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
...defaultTextColors.map((colorValue) {
|
||||
final selected = _selectedTextColor == colorValue;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedTextColor = colorValue;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(colorValue),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: selected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: Color(colorValue).computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
// Custom Color Picker Button for Text
|
||||
GestureDetector(
|
||||
onTap: () => _pickCustomColor(isBgColor: false),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
colors: [
|
||||
Colors.red,
|
||||
Colors.yellow,
|
||||
Colors.green,
|
||||
Colors.cyan,
|
||||
Colors.blue,
|
||||
Colors.purple,
|
||||
Colors.red,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.colorize,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.text,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isNotEmpty) {
|
||||
Navigator.of(context).pop({
|
||||
'name': name,
|
||||
'backgroundColor': _selectedBgColor,
|
||||
'textColor': _selectedTextColor,
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(context.lang.ok),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@ import 'package:go_router/go_router.dart';
|
|||
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/context_menu/group.context_menu.dart';
|
||||
|
|
@ -42,6 +42,8 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
StreamSubscription<Message?>? _lastMessageStream;
|
||||
StreamSubscription<Reaction?>? _lastReactionStream;
|
||||
StreamSubscription<List<MediaFile>>? _lastMediaFilesStream;
|
||||
Contact? _directContact;
|
||||
StreamSubscription<List<Contact>>? _directContactStream;
|
||||
|
||||
List<Message> _previewMessages = [];
|
||||
final List<MediaFile> _previewMediaFiles = [];
|
||||
|
|
@ -60,6 +62,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
_lastReactionStream?.cancel();
|
||||
_lastMessageStream?.cancel();
|
||||
_lastMediaFilesStream?.cancel();
|
||||
_directContactStream?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +105,19 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
setState(() {});
|
||||
});
|
||||
|
||||
if (widget.group.isDirectChat) {
|
||||
_directContactStream = twonlyDB.groupsDao
|
||||
.watchGroupContact(widget.group.groupId)
|
||||
.listen((contacts) {
|
||||
if (!mounted) return;
|
||||
if (contacts.isNotEmpty) {
|
||||
setState(() {
|
||||
_directContact = contacts.first;
|
||||
_receiverDeletedAccount = _directContact!.accountDeleted;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||
widget.group.groupId,
|
||||
);
|
||||
|
|
@ -110,6 +126,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateState(
|
||||
Message? newLastMessage,
|
||||
|
|
@ -239,8 +256,12 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
substringBy(widget.group.groupName, 30),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.group.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
VerificationBadgeComp(
|
||||
|
|
@ -249,6 +270,18 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
clickable: false,
|
||||
size: 12,
|
||||
),
|
||||
if (widget.group.isDirectChat && _directContact != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: ContactLabels(
|
||||
contactId: _directContact!.userId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: _receiverDeletedAccount
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
|
|||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/themes/colors.dart';
|
||||
|
|
@ -303,7 +304,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
Expanded(
|
||||
child: ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
substringBy(group.groupName, 20),
|
||||
|
|
@ -317,6 +322,17 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
FlameCounterWidget(groupId: group.groupId),
|
||||
],
|
||||
),
|
||||
if (group.isDirectChat)
|
||||
StreamBuilder<List<Contact>>(
|
||||
stream: twonlyDB.groupsDao.watchGroupContact(group.groupId),
|
||||
builder: (context, snapshot) {
|
||||
final contacts = snapshot.data ?? [];
|
||||
if (contacts.isEmpty) return const SizedBox.shrink();
|
||||
return ContactLabels(contactId: contacts.first.userId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/context_menu/group.context_menu.dart';
|
||||
|
|
@ -216,32 +217,39 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
}
|
||||
|
||||
if (i < filteredContacts.length) {
|
||||
final contact = filteredContacts[i];
|
||||
return UserContextMenu(
|
||||
key: ValueKey(filteredContacts[i].userId),
|
||||
contact: filteredContacts[i],
|
||||
child: ListTile(
|
||||
key: ValueKey(contact.userId),
|
||||
contact: contact,
|
||||
child: ContactLabelsSubtitleBuilder(
|
||||
contactId: contact.userId,
|
||||
builder: (context, subtitleWidget) {
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(filteredContacts[i])),
|
||||
Text(getContactDisplayName(contact)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 8,
|
||||
left: 1,
|
||||
),
|
||||
child: VerificationBadgeComp(
|
||||
contact: filteredContacts[i],
|
||||
contact: contact,
|
||||
),
|
||||
),
|
||||
FlameCounterWidget(
|
||||
contactId: filteredContacts[i].userId,
|
||||
contactId: contact.userId,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: subtitleWidget,
|
||||
leading: AvatarIcon(
|
||||
contactId: filteredContacts[i].userId,
|
||||
contactId: contact.userId,
|
||||
fontSize: 13,
|
||||
),
|
||||
onTap: () => _onTapUser(filteredContacts[i]),
|
||||
onTap: () => _onTapUser(contact),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
|
|
@ -20,6 +21,7 @@ import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups
|
|||
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/select_contact_labels.view.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
|
||||
class ContactView extends StatefulWidget {
|
||||
|
|
@ -225,6 +227,21 @@ class _ContactViewState extends State<ContactView> {
|
|||
userService.currentUser.userId,
|
||||
),
|
||||
),
|
||||
ContactLabelsSubtitleBuilder(
|
||||
contactId: contact.userId,
|
||||
builder: (context, subtitleWidget) {
|
||||
return BetterListTile(
|
||||
icon: FontAwesomeIcons.tag,
|
||||
text: context.lang.contactLabelsTitle,
|
||||
subtitle: subtitleWidget,
|
||||
onTap: () {
|
||||
context.navPush(
|
||||
SelectContactLabelsView(contactId: contact.userId),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
RestoreFlameComp(
|
||||
contactId: widget.userId,
|
||||
|
|
|
|||
313
lib/src/visual/views/contact/select_contact_labels.view.dart
Normal file
313
lib/src/visual/views/contact/select_contact_labels.view.dart
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import 'dart:async';
|
||||
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/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/label_editor_bottom_sheet.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class SelectContactLabelsView extends StatefulWidget {
|
||||
const SelectContactLabelsView({
|
||||
required this.contactId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int contactId;
|
||||
|
||||
@override
|
||||
State<SelectContactLabelsView> createState() => _SelectContactLabelsViewState();
|
||||
}
|
||||
|
||||
class _SelectContactLabelsViewState extends State<SelectContactLabelsView> {
|
||||
Contact? _contact;
|
||||
List<Label> _allLabels = [];
|
||||
Set<int> _selectedLabelIds = {};
|
||||
|
||||
late StreamSubscription<Contact?> _contactSub;
|
||||
late StreamSubscription<List<Label>> _allLabelsSub;
|
||||
late StreamSubscription<List<Label>> _contactLabelsSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_contactSub = twonlyDB.contactsDao.watchContact(widget.contactId).listen((contact) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_contact = contact;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_allLabelsSub = twonlyDB.labelsDao.watchAllLabels().listen((labels) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_allLabels = labels;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_contactLabelsSub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((labels) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedLabelIds = labels.map((l) => l.id).toSet();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contactSub.cancel();
|
||||
_allLabelsSub.cancel();
|
||||
_contactLabelsSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _toggleLabel(int labelId) async {
|
||||
final newSet = Set<int>.from(_selectedLabelIds);
|
||||
if (newSet.contains(labelId)) {
|
||||
newSet.remove(labelId);
|
||||
} else {
|
||||
if (newSet.length >= 3) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.contactLabelsMaxLimit,
|
||||
level: SnackbarLevel.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
newSet.add(labelId);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_selectedLabelIds = newSet;
|
||||
});
|
||||
|
||||
await twonlyDB.labelsDao.setContactLabels(widget.contactId, _selectedLabelIds.toList());
|
||||
}
|
||||
|
||||
Future<void> _createLabel() async {
|
||||
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const LabelEditorBottomSheet(),
|
||||
);
|
||||
|
||||
if (result != null && mounted) {
|
||||
final name = result['name'] as String;
|
||||
final bgColor = result['backgroundColor'] as int;
|
||||
final textColor = result['textColor'] as int;
|
||||
|
||||
await twonlyDB.labelsDao.createLabel(name, textColor, bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editLabel(Label label) async {
|
||||
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => LabelEditorBottomSheet(label: label),
|
||||
);
|
||||
|
||||
if (result != null && mounted) {
|
||||
final name = result['name'] as String;
|
||||
final bgColor = result['backgroundColor'] as int;
|
||||
final textColor = result['textColor'] as int;
|
||||
|
||||
await twonlyDB.labelsDao.updateLabel(label.id, name, textColor, bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteLabel(Label label) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.lang.deleteLabel),
|
||||
content: Text(context.lang.deleteLabelConfirmation),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.text,
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.errorMiddle,
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(context.lang.deleteLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if ((confirm ?? false) && mounted) {
|
||||
await twonlyDB.labelsDao.deleteLabel(label.id);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContactHeader() {
|
||||
if (_contact == null) return const SizedBox.shrink();
|
||||
final contact = _contact!;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AvatarIcon(contactId: contact.userId, fontSize: 24),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
getContactDisplayName(contact, maxLength: 25),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (getContactDisplayName(contact) != contact.username)
|
||||
Text(
|
||||
'@${contact.username}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ContactLabels(contactId: contact.userId),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.contactLabelsTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: context.lang.createLabel,
|
||||
onPressed: _createLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildContactHeader(),
|
||||
Expanded(
|
||||
child: _allLabels.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.contactLabelsSubtitleEmpty,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: _createLabel,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.createLabel),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _allLabels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final label = _allLabels[index];
|
||||
final isSelected = _selectedLabelIds.contains(label.id);
|
||||
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
onChanged: (_) => _toggleLabel(label.id),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(label.backgroundColor),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
label.name,
|
||||
style: TextStyle(
|
||||
color: Color(label.textColor),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
secondary: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: context.lang.editLabel,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => _editLabel(label),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: FaIcon(FontAwesomeIcons.penToSquare, size: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Tooltip(
|
||||
message: context.lang.deleteLabel,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => _deleteLabel(label),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: FaIcon(FontAwesomeIcons.trashCan, size: 16, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/services/group.service.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
|
|
@ -248,7 +249,10 @@ class _GroupViewState extends State<GroupView> {
|
|||
group: _group!,
|
||||
contact: member.$1,
|
||||
member: member.$2,
|
||||
child: BetterListTile(
|
||||
child: ContactLabelsSubtitleBuilder(
|
||||
contactId: member.$1.userId,
|
||||
builder: (context, subtitleWidget) {
|
||||
return BetterListTile(
|
||||
padding: const EdgeInsets.only(left: 13),
|
||||
leading: AvatarIcon(
|
||||
contactId: member.$1.userId,
|
||||
|
|
@ -266,6 +270,7 @@ class _GroupViewState extends State<GroupView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
subtitle: subtitleWidget,
|
||||
trailing: (member.$2.memberState == MemberState.admin)
|
||||
? Text(context.lang.admin)
|
||||
: null,
|
||||
|
|
@ -277,6 +282,8 @@ class _GroupViewState extends State<GroupView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/services/group.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class GroupCreateSelectGroupNameView extends StatefulWidget {
|
||||
const GroupCreateSelectGroupNameView({
|
||||
|
|
@ -59,20 +62,28 @@ class _GroupCreateSelectGroupNameViewState
|
|||
title: Text(context.lang.selectGroupName),
|
||||
),
|
||||
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||
floatingActionButton: FilledButton.icon(
|
||||
floatingActionButton: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: (textFieldGroupName.text.isEmpty || _isLoading)
|
||||
? null
|
||||
: _createNewGroup,
|
||||
label: Text(context.lang.createGroup),
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isLoading)
|
||||
const SizedBox(
|
||||
width: 15,
|
||||
height: 15,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: const FaIcon(FontAwesomeIcons.penToSquare),
|
||||
else
|
||||
const FaIcon(FontAwesomeIcons.penToSquare, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.createGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
|
|
@ -111,20 +122,35 @@ class _GroupCreateSelectGroupNameViewState
|
|||
return UserContextMenu(
|
||||
key: ValueKey(user.userId),
|
||||
contact: user,
|
||||
child: ListTile(
|
||||
child: ContactLabelsSubtitleBuilder(
|
||||
contactId: user.userId,
|
||||
builder: (context, subtitleWidget) {
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 8,
|
||||
left: 1,
|
||||
),
|
||||
child: VerificationBadgeComp(
|
||||
contact: user,
|
||||
),
|
||||
),
|
||||
FlameCounterWidget(
|
||||
contactId: user.userId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: subtitleWidget,
|
||||
leading: AvatarIcon(
|
||||
contactId: user.userId,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||
import 'package:twonly/src/visual/elements/contact_chip.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
|
||||
|
||||
class GroupCreateSelectMembersView extends StatefulWidget {
|
||||
|
|
@ -131,14 +134,21 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
),
|
||||
),
|
||||
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||
floatingActionButton: FilledButton.icon(
|
||||
floatingActionButton: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: selectedUsers.isEmpty ? null : submitChanges,
|
||||
label: Text(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.penToSquare, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.groupId == null
|
||||
? context.lang.next
|
||||
: context.lang.updateGroup,
|
||||
),
|
||||
icon: const FaIcon(FontAwesomeIcons.penToSquare),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
|
|
@ -209,19 +219,32 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
return UserContextMenu(
|
||||
key: ValueKey(user.userId),
|
||||
contact: user,
|
||||
child: ListTile(
|
||||
child: ContactLabelsSubtitleBuilder(
|
||||
contactId: user.userId,
|
||||
additionalSubtitle: alreadyInGroup.contains(user.userId)
|
||||
? Text(context.lang.alreadyInGroup)
|
||||
: null,
|
||||
builder: (context, subtitleWidget) {
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 8,
|
||||
left: 1,
|
||||
),
|
||||
child: VerificationBadgeComp(
|
||||
contact: user,
|
||||
),
|
||||
),
|
||||
FlameCounterWidget(
|
||||
contactId: user.userId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: (alreadyInGroup.contains(user.userId))
|
||||
? Text(context.lang.alreadyInGroup)
|
||||
: null,
|
||||
subtitle: subtitleWidget,
|
||||
leading: AvatarIcon(
|
||||
contactId: user.userId,
|
||||
fontSize: 13,
|
||||
|
|
@ -247,6 +270,8 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
onTap: () {
|
||||
toggleSelectedUser(user.userId);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.4.2+161
|
||||
version: 0.4.3+162
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import 'schema_v20.dart' as v20;
|
|||
import 'schema_v21.dart' as v21;
|
||||
import 'schema_v22.dart' as v22;
|
||||
import 'schema_v23.dart' as v23;
|
||||
import 'schema_v24.dart' as v24;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -78,6 +79,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v22.DatabaseAtV22(db);
|
||||
case 23:
|
||||
return v23.DatabaseAtV23(db);
|
||||
case 24:
|
||||
return v24.DatabaseAtV24(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
|
|
@ -107,5 +110,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
11647
test/drift/twonly_db/generated/schema_v24.dart
Normal file
11647
test/drift/twonly_db/generated/schema_v24.dart
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue