Compare commits

..

5 Commits

Author SHA1 Message Date
cb2de52bee Key pairs 2025-03-03 23:04:02 +08:00
64e2644745 Keypair Infra 2025-03-03 22:25:59 +08:00
56711889ab 🗃️ Local keypair db 2025-03-03 21:31:41 +08:00
4f47cd2c0c 💄 Optimize chat style 2025-03-03 21:13:26 +08:00
2b61c372f5 Allow profile picture (avatar & banner) upload gif 2025-03-03 20:53:42 +08:00
38 changed files with 3004 additions and 441 deletions

View File

@ -753,5 +753,11 @@
"accountBadges": "Badges",
"accountBadgesDescription": "View and manage your badges.",
"badgeActivated": "Activated badge {}.",
"viewDetailedAttachment": "Details"
"viewDetailedAttachment": "Details",
"screenKeyPairs": "Key Pairs",
"accountKeyPairs": "Key Pairs",
"accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.",
"enrollNewKeyPair": "Enroll New One",
"enrollNewKeyPairDescription": "Generate a new key pair.",
"keyPairHasPrivateKey": "With private key"
}

View File

@ -751,5 +751,11 @@
"accountBadges": "徽章",
"accountBadgesDescription": "查看并管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件详情"
"viewDetailedAttachment": "查看附件详情",
"screenKeyPairs": "密钥对",
"accountKeyPairs": "密钥对",
"accountKeyPairsDescription": "管理用于加密信息的密钥对。",
"enrollNewKeyPair": "新建密钥对",
"enrollNewKeyPairDescription": "生成一对新密钥对。",
"keyPairHasPrivateKey": "有私钥"
}

View File

@ -751,5 +751,11 @@
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
"viewDetailedAttachment": "查看附件詳情",
"screenKeyPairs": "密鑰對",
"accountKeyPairs": "密鑰對",
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
"enrollNewKeyPair": "新建密鑰對",
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
"keyPairHasPrivateKey": "有私鑰"
}

View File

@ -751,5 +751,11 @@
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
"viewDetailedAttachment": "查看附件詳情",
"screenKeyPairs": "密鑰對",
"accountKeyPairs": "密鑰對",
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
"enrollNewKeyPair": "新建密鑰對",
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
"keyPairHasPrivateKey": "有私鑰"
}

View File

@ -4,4 +4,8 @@ targets:
json_serializable:
options:
explicit_to_json: true
field_rename: snake
field_rename: snake
drift_dev:
options:
databases:
my_database: lib/database/database.dart

View File

@ -0,0 +1 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]}

View File

@ -0,0 +1 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]}

View File

@ -37,6 +37,8 @@ PODS:
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- fast_rsa (0.6.0):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
@ -262,6 +264,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- fast_rsa (from `.symlinks/plugins/fast_rsa/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
@ -331,6 +334,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/croppy/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
fast_rsa:
:path: ".symlinks/plugins/fast_rsa/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_saver:
@ -411,6 +416,7 @@ SPEC CHECKSUMS:
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf

View File

@ -2,16 +2,18 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/chat.dart';
import 'package:surface/database/database.steps.dart';
import 'package:surface/database/keypair.dart';
import 'package:surface/types/chat.dart';
part 'database.g.dart';
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage])
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage, SnLocalKeyPair])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -25,4 +27,13 @@ class AppDatabase extends _$AppDatabase {
),
);
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: stepByStep(from1To2: (m, schema) async {
// Nothing else to do here
}),
);
}
}

View File

@ -538,6 +538,322 @@ class SnLocalChatMessageCompanion
}
}
class $SnLocalKeyPairTable extends SnLocalKeyPair
with TableInfo<$SnLocalKeyPairTable, SnLocalKeyPairData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$SnLocalKeyPairTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _accountIdMeta =
const VerificationMeta('accountId');
@override
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
'account_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _publicKeyMeta =
const VerificationMeta('publicKey');
@override
late final GeneratedColumn<String> publicKey = GeneratedColumn<String>(
'public_key', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _privateKeyMeta =
const VerificationMeta('privateKey');
@override
late final GeneratedColumn<String> privateKey = GeneratedColumn<String>(
'private_key', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _isActiveMeta =
const VerificationMeta('isActive');
@override
late final GeneratedColumn<bool> isActive = GeneratedColumn<bool>(
'is_active', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("is_active" IN (0, 1))'),
defaultValue: Constant(false));
@override
List<GeneratedColumn> get $columns =>
[id, accountId, publicKey, privateKey, isActive];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_key_pair';
@override
VerificationContext validateIntegrity(Insertable<SnLocalKeyPairData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('account_id')) {
context.handle(_accountIdMeta,
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
} else if (isInserting) {
context.missing(_accountIdMeta);
}
if (data.containsKey('public_key')) {
context.handle(_publicKeyMeta,
publicKey.isAcceptableOrUnknown(data['public_key']!, _publicKeyMeta));
} else if (isInserting) {
context.missing(_publicKeyMeta);
}
if (data.containsKey('private_key')) {
context.handle(
_privateKeyMeta,
privateKey.isAcceptableOrUnknown(
data['private_key']!, _privateKeyMeta));
}
if (data.containsKey('is_active')) {
context.handle(_isActiveMeta,
isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalKeyPairData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
accountId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
publicKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}public_key'])!,
privateKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}private_key']),
isActive: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}is_active'])!,
);
}
@override
$SnLocalKeyPairTable createAlias(String alias) {
return $SnLocalKeyPairTable(attachedDatabase, alias);
}
}
class SnLocalKeyPairData extends DataClass
implements Insertable<SnLocalKeyPairData> {
final String id;
final int accountId;
final String publicKey;
final String? privateKey;
final bool isActive;
const SnLocalKeyPairData(
{required this.id,
required this.accountId,
required this.publicKey,
this.privateKey,
required this.isActive});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['account_id'] = Variable<int>(accountId);
map['public_key'] = Variable<String>(publicKey);
if (!nullToAbsent || privateKey != null) {
map['private_key'] = Variable<String>(privateKey);
}
map['is_active'] = Variable<bool>(isActive);
return map;
}
SnLocalKeyPairCompanion toCompanion(bool nullToAbsent) {
return SnLocalKeyPairCompanion(
id: Value(id),
accountId: Value(accountId),
publicKey: Value(publicKey),
privateKey: privateKey == null && nullToAbsent
? const Value.absent()
: Value(privateKey),
isActive: Value(isActive),
);
}
factory SnLocalKeyPairData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalKeyPairData(
id: serializer.fromJson<String>(json['id']),
accountId: serializer.fromJson<int>(json['accountId']),
publicKey: serializer.fromJson<String>(json['publicKey']),
privateKey: serializer.fromJson<String?>(json['privateKey']),
isActive: serializer.fromJson<bool>(json['isActive']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'accountId': serializer.toJson<int>(accountId),
'publicKey': serializer.toJson<String>(publicKey),
'privateKey': serializer.toJson<String?>(privateKey),
'isActive': serializer.toJson<bool>(isActive),
};
}
SnLocalKeyPairData copyWith(
{String? id,
int? accountId,
String? publicKey,
Value<String?> privateKey = const Value.absent(),
bool? isActive}) =>
SnLocalKeyPairData(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
publicKey: publicKey ?? this.publicKey,
privateKey: privateKey.present ? privateKey.value : this.privateKey,
isActive: isActive ?? this.isActive,
);
SnLocalKeyPairData copyWithCompanion(SnLocalKeyPairCompanion data) {
return SnLocalKeyPairData(
id: data.id.present ? data.id.value : this.id,
accountId: data.accountId.present ? data.accountId.value : this.accountId,
publicKey: data.publicKey.present ? data.publicKey.value : this.publicKey,
privateKey:
data.privateKey.present ? data.privateKey.value : this.privateKey,
isActive: data.isActive.present ? data.isActive.value : this.isActive,
);
}
@override
String toString() {
return (StringBuffer('SnLocalKeyPairData(')
..write('id: $id, ')
..write('accountId: $accountId, ')
..write('publicKey: $publicKey, ')
..write('privateKey: $privateKey, ')
..write('isActive: $isActive')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, accountId, publicKey, privateKey, isActive);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalKeyPairData &&
other.id == this.id &&
other.accountId == this.accountId &&
other.publicKey == this.publicKey &&
other.privateKey == this.privateKey &&
other.isActive == this.isActive);
}
class SnLocalKeyPairCompanion extends UpdateCompanion<SnLocalKeyPairData> {
final Value<String> id;
final Value<int> accountId;
final Value<String> publicKey;
final Value<String?> privateKey;
final Value<bool> isActive;
final Value<int> rowid;
const SnLocalKeyPairCompanion({
this.id = const Value.absent(),
this.accountId = const Value.absent(),
this.publicKey = const Value.absent(),
this.privateKey = const Value.absent(),
this.isActive = const Value.absent(),
this.rowid = const Value.absent(),
});
SnLocalKeyPairCompanion.insert({
required String id,
required int accountId,
required String publicKey,
this.privateKey = const Value.absent(),
this.isActive = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
accountId = Value(accountId),
publicKey = Value(publicKey);
static Insertable<SnLocalKeyPairData> custom({
Expression<String>? id,
Expression<int>? accountId,
Expression<String>? publicKey,
Expression<String>? privateKey,
Expression<bool>? isActive,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (accountId != null) 'account_id': accountId,
if (publicKey != null) 'public_key': publicKey,
if (privateKey != null) 'private_key': privateKey,
if (isActive != null) 'is_active': isActive,
if (rowid != null) 'rowid': rowid,
});
}
SnLocalKeyPairCompanion copyWith(
{Value<String>? id,
Value<int>? accountId,
Value<String>? publicKey,
Value<String?>? privateKey,
Value<bool>? isActive,
Value<int>? rowid}) {
return SnLocalKeyPairCompanion(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
publicKey: publicKey ?? this.publicKey,
privateKey: privateKey ?? this.privateKey,
isActive: isActive ?? this.isActive,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (accountId.present) {
map['account_id'] = Variable<int>(accountId.value);
}
if (publicKey.present) {
map['public_key'] = Variable<String>(publicKey.value);
}
if (privateKey.present) {
map['private_key'] = Variable<String>(privateKey.value);
}
if (isActive.present) {
map['is_active'] = Variable<bool>(isActive.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalKeyPairCompanion(')
..write('id: $id, ')
..write('accountId: $accountId, ')
..write('publicKey: $publicKey, ')
..write('privateKey: $privateKey, ')
..write('isActive: $isActive, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
@ -545,12 +861,13 @@ abstract class _$AppDatabase extends GeneratedDatabase {
$SnLocalChatChannelTable(this);
late final $SnLocalChatMessageTable snLocalChatMessage =
$SnLocalChatMessageTable(this);
late final $SnLocalKeyPairTable snLocalKeyPair = $SnLocalKeyPairTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities =>
[snLocalChatChannel, snLocalChatMessage];
[snLocalChatChannel, snLocalChatMessage, snLocalKeyPair];
}
typedef $$SnLocalChatChannelTableCreateCompanionBuilder
@ -869,6 +1186,180 @@ typedef $$SnLocalChatMessageTableProcessedTableManager = ProcessedTableManager<
),
SnLocalChatMessageData,
PrefetchHooks Function()>;
typedef $$SnLocalKeyPairTableCreateCompanionBuilder = SnLocalKeyPairCompanion
Function({
required String id,
required int accountId,
required String publicKey,
Value<String?> privateKey,
Value<bool> isActive,
Value<int> rowid,
});
typedef $$SnLocalKeyPairTableUpdateCompanionBuilder = SnLocalKeyPairCompanion
Function({
Value<String> id,
Value<int> accountId,
Value<String> publicKey,
Value<String?> privateKey,
Value<bool> isActive,
Value<int> rowid,
});
class $$SnLocalKeyPairTableFilterComposer
extends Composer<_$AppDatabase, $SnLocalKeyPairTable> {
$$SnLocalKeyPairTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get accountId => $composableBuilder(
column: $table.accountId, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get publicKey => $composableBuilder(
column: $table.publicKey, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get privateKey => $composableBuilder(
column: $table.privateKey, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get isActive => $composableBuilder(
column: $table.isActive, builder: (column) => ColumnFilters(column));
}
class $$SnLocalKeyPairTableOrderingComposer
extends Composer<_$AppDatabase, $SnLocalKeyPairTable> {
$$SnLocalKeyPairTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get accountId => $composableBuilder(
column: $table.accountId, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get publicKey => $composableBuilder(
column: $table.publicKey, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get privateKey => $composableBuilder(
column: $table.privateKey, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get isActive => $composableBuilder(
column: $table.isActive, builder: (column) => ColumnOrderings(column));
}
class $$SnLocalKeyPairTableAnnotationComposer
extends Composer<_$AppDatabase, $SnLocalKeyPairTable> {
$$SnLocalKeyPairTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<int> get accountId =>
$composableBuilder(column: $table.accountId, builder: (column) => column);
GeneratedColumn<String> get publicKey =>
$composableBuilder(column: $table.publicKey, builder: (column) => column);
GeneratedColumn<String> get privateKey => $composableBuilder(
column: $table.privateKey, builder: (column) => column);
GeneratedColumn<bool> get isActive =>
$composableBuilder(column: $table.isActive, builder: (column) => column);
}
class $$SnLocalKeyPairTableTableManager extends RootTableManager<
_$AppDatabase,
$SnLocalKeyPairTable,
SnLocalKeyPairData,
$$SnLocalKeyPairTableFilterComposer,
$$SnLocalKeyPairTableOrderingComposer,
$$SnLocalKeyPairTableAnnotationComposer,
$$SnLocalKeyPairTableCreateCompanionBuilder,
$$SnLocalKeyPairTableUpdateCompanionBuilder,
(
SnLocalKeyPairData,
BaseReferences<_$AppDatabase, $SnLocalKeyPairTable, SnLocalKeyPairData>
),
SnLocalKeyPairData,
PrefetchHooks Function()> {
$$SnLocalKeyPairTableTableManager(
_$AppDatabase db, $SnLocalKeyPairTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$SnLocalKeyPairTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$SnLocalKeyPairTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$SnLocalKeyPairTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> id = const Value.absent(),
Value<int> accountId = const Value.absent(),
Value<String> publicKey = const Value.absent(),
Value<String?> privateKey = const Value.absent(),
Value<bool> isActive = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
SnLocalKeyPairCompanion(
id: id,
accountId: accountId,
publicKey: publicKey,
privateKey: privateKey,
isActive: isActive,
rowid: rowid,
),
createCompanionCallback: ({
required String id,
required int accountId,
required String publicKey,
Value<String?> privateKey = const Value.absent(),
Value<bool> isActive = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
SnLocalKeyPairCompanion.insert(
id: id,
accountId: accountId,
publicKey: publicKey,
privateKey: privateKey,
isActive: isActive,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$SnLocalKeyPairTableProcessedTableManager = ProcessedTableManager<
_$AppDatabase,
$SnLocalKeyPairTable,
SnLocalKeyPairData,
$$SnLocalKeyPairTableFilterComposer,
$$SnLocalKeyPairTableOrderingComposer,
$$SnLocalKeyPairTableAnnotationComposer,
$$SnLocalKeyPairTableCreateCompanionBuilder,
$$SnLocalKeyPairTableUpdateCompanionBuilder,
(
SnLocalKeyPairData,
BaseReferences<_$AppDatabase, $SnLocalKeyPairTable, SnLocalKeyPairData>
),
SnLocalKeyPairData,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
@ -877,4 +1368,6 @@ class $AppDatabaseManager {
$$SnLocalChatChannelTableTableManager(_db, _db.snLocalChatChannel);
$$SnLocalChatMessageTableTableManager get snLocalChatMessage =>
$$SnLocalChatMessageTableTableManager(_db, _db.snLocalChatMessage);
$$SnLocalKeyPairTableTableManager get snLocalKeyPair =>
$$SnLocalKeyPairTableTableManager(_db, _db.snLocalKeyPair);
}

View File

@ -0,0 +1,163 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema {
Schema2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
snLocalChatChannel,
snLocalChatMessage,
snLocalKeyPair,
];
late final Shape0 snLocalChatChannel = Shape0(
source: i0.VersionedTable(
entityName: 'sn_local_chat_channel',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 snLocalChatMessage = Shape1(
source: i0.VersionedTable(
entityName: 'sn_local_chat_message',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 snLocalKeyPair = Shape2(
source: i0.VersionedTable(
entityName: 'sn_local_key_pair',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
],
attachedDatabase: database,
),
alias: null);
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get alias =>
columnsByName['alias']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('alias', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>('content', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_3(String aliasedName) =>
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i1.DriftSqlType.dateTime,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get channelId =>
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_4(String aliasedName) =>
i1.GeneratedColumn<int>('channel_id', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get publicKey =>
columnsByName['public_key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get privateKey =>
columnsByName['private_key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isActive =>
columnsByName['is_active']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_5(String aliasedName) =>
i1.GeneratedColumn<String>('id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('account_id', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>('public_key', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
i1.GeneratedColumn<String>('private_key', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
i1.GeneratedColumn<bool>('is_active', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_active" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 1:
final schema = Schema2(database: database);
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
));

13
lib/database/keypair.dart Normal file
View File

@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
class SnLocalKeyPair extends Table {
TextColumn get id => text()();
IntColumn get accountId => integer()();
TextColumn get publicKey => text()();
TextColumn get privateKey => text().nullable()();
BoolColumn get isActive => boolean().withDefault(Constant(false))();
}

View File

@ -25,6 +25,7 @@ import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/keypair.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/notification.dart';
@ -108,7 +109,8 @@ void main() async {
}
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
final ImagePickerPlatform imagePickerImplementation =
ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
@ -160,6 +162,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnStickerProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
Provider(create: (ctx) => KeyPairProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
@ -227,7 +230,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
if (time != null &&
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
@ -258,12 +262,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
final remoteBuildNumber =
int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0;
logging.info(
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) &&
mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog');
config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog');
logging.info("[Update] Update available: $remoteVersionString");
}
} catch (e) {
@ -298,6 +308,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
notify.listen();
await notify.registerPushNotifications();
if (!mounted) return;
final kp = context.read<KeyPairProvider>();
await kp.reloadActive();
kp.listen();
if (!mounted) return;
final sticker = context.read<SnStickerProvider>();
await sticker.listSticker();
logging.info('[Bootstrap] Everything initialized!');
@ -355,7 +369,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
final icon = Platform.isWindows
? 'assets/icon/tray-icon.ico'
: 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);

237
lib/providers/keypair.dart Normal file
View File

@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/keypair.dart';
import 'package:fast_rsa/fast_rsa.dart';
import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart';
// Currently the keypair only provide RSA encryption
// Supported by the `fast_rsa` package
class KeyPairProvider {
late final DatabaseProvider _dt;
late final UserProvider _ua;
late final WebSocketProvider _ws;
SnKeyPair? activeKp;
KeyPairProvider(BuildContext context) {
_dt = context.read<DatabaseProvider>();
_ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
}
void listen() {
_ws.pk.stream.listen((event) {
switch (event.method) {
case 'kex.ack':
ackKeyExchange(event);
break;
case 'key.ask':
replyAskKeyExchange(event);
break;
}
});
}
Future<String> decryptText(String text, String kpId) async {
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId)))
.getSingleOrNull();
if (kp == null) throw Exception('Key pair not found');
return await RSA.decryptPKCS1v15(text, kp.privateKey!);
}
Future<String> encryptText(String text) async {
if (activeKp == null) throw Exception('No active key pair');
return await RSA.encryptPKCS1v15(text, activeKp!.publicKey);
}
final Map<String, Completer<SnKeyPair>> _requests = {};
Future<SnKeyPair> askKeyExchange(int kpOwner, String kpId) async {
if (_requests.containsKey(kpId)) return await _requests[kpId]!.future;
final completer = Completer<SnKeyPair>();
_requests[kpId] = completer;
_ws.conn?.sink.add(
jsonEncode(WebSocketPackage(
method: 'key.ask',
endpoint: 'id',
payload: {
'keypair_id': kpId,
'user_id': kpOwner,
},
)),
);
return Future.any([
_requests[kpId]!.future,
Future.delayed(const Duration(seconds: 60), () {
_requests.remove(kpId);
throw TimeoutException("Key exchange timed out");
}),
]);
}
Future<void> ackKeyExchange(WebSocketPackage pkt) async {
if (pkt.payload == null) return;
final kpMeta = SnKeyPair(
id: pkt.payload!['keypair_id'] as String,
accountId: pkt.payload!['user_id'] as int,
publicKey: pkt.payload!['public_key'] as String,
privateKey: pkt.payload?['private_key'] as String?,
);
if (_requests.containsKey(kpMeta.id)) {
_requests[kpMeta.id]!.complete(kpMeta);
_requests.remove(kpMeta.id);
}
// Save the keypair to the local database
await _dt.db.snLocalKeyPair.insertOne(
SnLocalKeyPairCompanion.insert(
id: kpMeta.id,
accountId: kpMeta.accountId,
publicKey: kpMeta.publicKey,
privateKey: Value(kpMeta.privateKey),
),
onConflict: DoUpdate(
(_) => SnLocalKeyPairCompanion.custom(
publicKey: Constant(kpMeta.publicKey),
privateKey: Constant(kpMeta.privateKey),
),
),
);
}
Future<void> replyAskKeyExchange(WebSocketPackage pkt) async {
final kpId = pkt.payload!['keypair_id'] as String;
final userId = pkt.payload!['user_id'] as int;
final clientId = pkt.payload!['client_id'] as String;
final localKp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId))
..limit(1))
.getSingleOrNull();
if (localKp == null) return;
logging.info(
'[Kex] Reply to key exchange request of $kpId from user $userId',
);
// We do not give the private key to the client
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'kex.ack',
endpoint: 'id',
payload: {
'keypair_id': localKp.id,
'user_id': localKp.accountId,
'public_key': localKp.publicKey,
'client_id': clientId,
},
).toJson(),
));
}
Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async {
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.accountId.equals(_ua.user!.id))
..where((e) => e.privateKey.isNotNull())
..where((e) => e.isActive.equals(true))
..limit(1))
.getSingleOrNull();
if (kp != null) {
activeKp = SnKeyPair(
id: kp.id,
accountId: kp.accountId,
publicKey: kp.publicKey,
privateKey: kp.privateKey,
);
}
if (kp == null && autoEnroll) {
return await enrollNew();
}
return activeKp;
}
Future<List<SnKeyPair>> listKeyPair() async {
final kps = await (_dt.db.snLocalKeyPair.select()).get();
return kps
.map((e) => SnKeyPair(
id: e.id,
accountId: e.accountId,
publicKey: e.publicKey,
privateKey: e.privateKey,
isActive: e.isActive,
))
.toList();
}
Future<void> activeKeyPair(String kpId) async {
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId))
..where((e) => e.privateKey.isNotNull())
..limit(1))
.getSingleOrNull();
if (kp == null) return;
await _dt.db.transaction(() async {
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.isActive.equals(true)))
.write(SnLocalKeyPairCompanion(isActive: Value(false)));
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.id.equals(kp.id)))
.write(SnLocalKeyPairCompanion(isActive: Value(true)));
});
}
Future<SnKeyPair> enrollNew() async {
if (!_ua.isAuthorized) throw Exception('Unauthorized');
final id = const Uuid().v4();
final kp = await RSA.generate(2048);
final kpMeta = SnKeyPair(
id: id,
accountId: _ua.user!.id,
publicKey: kp.publicKey,
privateKey: kp.privateKey,
);
// Save the keypair to the local database
// If there is already one with private key, it will be overwritten
await _dt.db.transaction(() async {
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.isActive.equals(true)))
.write(SnLocalKeyPairCompanion(isActive: Value(false)));
await _dt.db.snLocalKeyPair.insertOne(
SnLocalKeyPairCompanion.insert(
id: kpMeta.id,
accountId: kpMeta.accountId,
publicKey: kpMeta.publicKey,
privateKey: Value(kpMeta.privateKey),
isActive: Value(true),
),
);
});
await reloadActive(autoEnroll: false);
return kpMeta;
}
}

View File

@ -117,7 +117,8 @@ class WebSocketProvider extends ChangeNotifier {
(event) {
final packet = WebSocketPackage.fromJson(jsonDecode(event));
logging.debug(
'[Websocket] Incoming message: ${packet.method} ${packet.message}');
'[Websocket] Incoming message: ${packet.method} ${packet.message}',
);
pk.sink.add(packet);
},
onDone: () {

View File

@ -6,6 +6,7 @@ import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/account_settings.dart';
import 'package:surface/screens/account/badges.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/keypairs.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart';
@ -43,8 +44,8 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
@ -86,13 +87,15 @@ final _appRoutes = [
name: 'postSearch',
builder: (context, state) => PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
initialCategories:
state.uri.queryParameters['categories']?.split(','),
),
),
GoRoute(
path: '/publishers/:name',
name: 'postPublisher',
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!),
),
GoRoute(
path: '/:slug',
@ -119,6 +122,11 @@ final _appRoutes = [
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/keypairs',
name: 'accountKeyPairs',
builder: (context, state) => const KeyPairScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
@ -222,7 +230,8 @@ final _appRoutes = [
GoRoute(
path: '/:alias',
name: 'realmDetail',
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
builder: (context, state) =>
RealmDetailScreen(alias: state.pathParameters['alias']!),
),
],
),

View File

@ -45,7 +45,8 @@ class AccountScreen extends StatelessWidget {
? Stack(
fit: StackFit.expand,
children: [
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner),
fit: BoxFit.cover),
Positioned(
top: 0,
left: 0,
@ -79,7 +80,9 @@ class AccountScreen extends StatelessWidget {
],
),
body: SingleChildScrollView(
child: ua.isAuthorized ? _AuthorizedAccountScreen() : _UnauthorizedAccountScreen(),
child: ua.isAuthorized
? _AuthorizedAccountScreen()
: _UnauthorizedAccountScreen(),
),
);
}
@ -115,9 +118,11 @@ class _AuthorizedAccountScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(ua.user!.nick).textStyle(Theme.of(context).textTheme.titleLarge!),
Text(ua.user!.nick)
.textStyle(Theme.of(context).textTheme.titleLarge!),
const Gap(4),
Text('@${ua.user!.name}').textStyle(Theme.of(context).textTheme.bodySmall!),
Text('@${ua.user!.name}')
.textStyle(Theme.of(context).textTheme.bodySmall!),
],
),
Text(
@ -183,6 +188,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountBadges');
},
),
ListTile(
title: Text('accountKeyPairs').tr(),
subtitle: Text('accountKeyPairsDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.key),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountKeyPairs');
},
),
ListTile(
title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(),
@ -236,7 +251,9 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Icon(Symbols.waving_hand, size: 28),
),
const Gap(8),
Text('accountIntroTitle').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroTitle')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroSubtitle').tr(),
],
).padding(all: 20),

View File

@ -0,0 +1,106 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/keypair.dart';
import 'package:surface/types/keypair.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class KeyPairScreen extends StatefulWidget {
const KeyPairScreen({super.key});
@override
State<KeyPairScreen> createState() => _KeyPairScreenState();
}
class _KeyPairScreenState extends State<KeyPairScreen> {
bool _isBusy = false;
List<SnKeyPair>? _keyPairs;
Future<void> _loadKeyPairs() async {
setState(() => _isBusy = true);
final kps = await context.read<KeyPairProvider>().listKeyPair();
setState(() {
_keyPairs = kps;
_isBusy = false;
});
}
@override
void initState() {
super.initState();
_loadKeyPairs();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text('screenKeyPairs').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
ListTile(
leading: const Icon(Symbols.add),
title: Text('enrollNewKeyPair').tr(),
subtitle: Text('enrollNewKeyPairDescription').tr(),
onTap: () async {
await context.read<KeyPairProvider>().enrollNew();
_loadKeyPairs();
},
),
const Divider(height: 1),
if (_keyPairs != null)
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _loadKeyPairs,
child: ListView.builder(
itemCount: _keyPairs!.length,
itemBuilder: (context, index) {
final kp = _keyPairs![index];
return ListTile(
title: Text(kp.id.toUpperCase()),
subtitle: Row(
spacing: 8,
children: [
if (kp.privateKey != null)
Text(
'keyPairHasPrivateKey'.tr(),
),
if (kp.privateKey != null) Text('·'),
Flexible(
flex: 1,
child: Text(
'UID #${kp.accountId.toString().padLeft(8, '0')}',
style: GoogleFonts.robotoMono(),
),
),
],
),
trailing: IconButton(
icon: const Icon(Symbols.check),
onPressed: kp.isActive == true
? null
: () async {
final k = context.read<KeyPairProvider>();
await k.activeKeyPair(kp.id);
_loadKeyPairs();
},
),
);
},
),
),
),
),
],
),
);
}
}

View File

@ -68,38 +68,35 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
_banner = prof.banner;
_links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
_birthday = prof.profile!.birthday?.toLocal();
if(_birthday != null) {
_birthdayController.text = DateFormat(_kDateFormat).format(
prof.profile!.birthday!.toLocal(),
);
if (_birthday != null) {
_birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
}
}
void _selectBirthday() async {
await showCupertinoModalPopup<DateTime?>(
context: context,
builder: (BuildContext context) => Container(
height: 216,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
top: false,
child: CupertinoDatePicker(
initialDateTime: _birthday?.toLocal(),
mode: CupertinoDatePickerMode.date,
use24hFormat: true,
onDateTimeChanged: (DateTime newDate) {
setState(() {
_birthday = newDate;
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
});
},
builder:
(BuildContext context) => Container(
height: 216,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
top: false,
child: CupertinoDatePicker(
initialDateTime: _birthday?.toLocal(),
mode: CupertinoDatePickerMode.date,
use24hFormat: true,
onDateTimeChanged: (DateTime newDate) {
setState(() {
_birthday = newDate;
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
});
},
),
),
),
),
),
);
}
@ -108,32 +105,42 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (image == null) return;
if (!mounted) return;
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
final aspectRatios =
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
);
final skipCrop = image.path.endsWith('.gif');
if (result == null) return;
Uint8List? rawBytes;
if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
final aspectRatios =
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
final result =
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
);
if (result == null) return;
if (!mounted) return;
setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
} else {
if (!mounted) return;
setState(() => _isBusy = true);
rawBytes = await image.readAsBytes();
}
if (!mounted) return;
final attach = context.read<SnAttachmentProvider>();
setState(() => _isBusy = true);
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
try {
final attachment = await attach.directUploadOne(
rawBytes,
@ -145,10 +152,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
await sn.client.put(
'/cgi/id/users/me/$place',
data: {'attachment': attachment.rid},
);
await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
if (!mounted) return;
final ua = context.read<UserProvider>();
@ -184,7 +188,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
'location': _locationController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(),
'links': {
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2,
},
},
);
@ -231,10 +235,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>();
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr(),
),
appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -253,12 +254,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child:
_banner != null
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
: const SizedBox.shrink(),
),
),
),
@ -299,10 +298,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
@ -364,10 +360,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
),
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
@ -384,42 +377,40 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
),
),
const Gap(4),
StyledWidget(IconButton(
icon: const Icon(Symbols.calendar_month),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
_timezoneController.text = await FlutterTimezone.getLocalTimezone();
},
)).padding(top: 6),
StyledWidget(
IconButton(
icon: const Icon(Symbols.calendar_month),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
_timezoneController.text = await FlutterTimezone.getLocalTimezone();
},
),
).padding(top: 6),
const Gap(4),
StyledWidget(IconButton(
icon: const Icon(Symbols.clear),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_timezoneController.clear();
},
)).padding(top: 6),
StyledWidget(
IconButton(
icon: const Icon(Symbols.clear),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_timezoneController.clear();
},
),
).padding(top: 6),
],
),
TextField(
controller: _locationController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldLocation'.tr(),
),
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextField(
controller: _birthdayController,
readOnly: true,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldBirthday'.tr(),
),
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()),
onTap: () => _selectBirthday(),
),
if (_links != null)

View File

@ -68,16 +68,19 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
setState(() => _isBusy = true);
try {
await sn.client.put('/cgi/co/publishers/${widget.name}', data: {
'avatar': _avatar,
'banner': _banner,
'nick': _nickController.text,
'name': _nameController.text,
'description': _descriptionController.text,
});
await sn.client.put(
'/cgi/co/publishers/${widget.name}',
data: {
'avatar': _avatar,
'banner': _banner,
'nick': _nickController.text,
'name': _nameController.text,
'description': _descriptionController.text,
},
);
if (mounted) Navigator.pop(context, true);
} catch (err) {
if(mounted) context.showErrorDialog(err);
if (mounted) context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
@ -108,32 +111,42 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
if (image == null) return;
if (!mounted) return;
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
final aspectRatios =
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
);
final skipCrop = image.path.endsWith('.gif');
if (result == null) return;
Uint8List? rawBytes;
if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
final aspectRatios =
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
final result =
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
);
if (result == null) return;
if (!mounted) return;
setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
} else {
if (!mounted) return;
setState(() => _isBusy = true);
rawBytes = await image.readAsBytes();
}
if (!mounted) return;
final attach = context.read<SnAttachmentProvider>();
setState(() => _isBusy = true);
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
try {
final attachment = await attach.directUploadOne(
rawBytes,
@ -178,10 +191,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
final sn = context.read<SnNetworkProvider>();
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr(),
),
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()),
body: SingleChildScrollView(
child: Column(
children: [
@ -199,12 +209,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child:
_banner != null
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
: const SizedBox.shrink(),
),
),
),
@ -242,9 +250,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
const Gap(4),
TextField(
controller: _nickController,
decoration: InputDecoration(
labelText: 'fieldNickname'.tr(),
),
decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
@ -252,9 +258,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
controller: _descriptionController,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
labelText: 'fieldDescription'.tr(),
),
decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
@ -275,7 +279,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
icon: const Icon(Symbols.save),
),
],
)
),
],
).padding(horizontal: 24, vertical: 12),
),

View File

@ -58,18 +58,12 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp =
await sn.client.get('/cgi/id/notifications', queryParameters: {
'take': 10,
'offset': _notifications.length,
});
_totalCount = resp.data['count'];
_notifications.addAll(
resp.data['data']
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[],
final resp = await sn.client.get(
'/cgi/id/notifications',
queryParameters: {'take': 10, 'offset': _notifications.length},
);
_totalCount = resp.data['count'];
_notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []);
nty.updateTray();
} catch (err) {
if (!mounted) return;
@ -104,9 +98,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
nty.clear();
if (!mounted) return;
context.showSnackbar(
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
);
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -130,9 +122,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications();
if (!mounted) return;
context.showSnackbar(
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']),
);
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -153,13 +143,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!ua.isAuthorized) {
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(),
),
body: Center(
child: UnauthorizedHint(),
),
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
body: Center(child: UnauthorizedHint()),
);
}
@ -168,10 +153,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.checklist),
onPressed: _isSubmitting ? null : _markAllAsRead,
),
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
const Gap(8),
],
),
@ -185,17 +167,13 @@ class _NotificationScreenState extends State<NotificationScreen> {
return _fetchNotifications();
},
child: InfiniteList(
padding: EdgeInsets.only(
top: 16,
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
),
padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)),
itemCount: _notifications.length,
onFetchData: () {
_fetchNotifications();
},
isLoading: _isBusy,
hasReachedMax: _totalCount != null &&
_notifications.length >= _totalCount!,
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
itemBuilder: (context, idx) {
final nty = _notifications[idx];
return Row(
@ -208,45 +186,26 @@ class _NotificationScreenState extends State<NotificationScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (nty.readAt == null)
StyledWidget(Badge(
label: Text('notificationUnread').tr(),
)).padding(bottom: 4),
Text(
nty.title,
style: Theme.of(context).textTheme.titleMedium,
),
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
if (nty.subtitle != null)
Text(
nty.subtitle!,
style: Theme.of(context).textTheme.titleSmall,
),
Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall),
if (nty.subtitle != null) const Gap(4),
SelectionArea(
child: MarkdownTextContent(
content: nty.body,
isAutoWarp: true,
),
),
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
if ([
'interactive.reply',
'interactive.feedback',
'interactive.subscription'
'interactive.subscription',
].contains(nty.topic) &&
nty.metadata['related_post'] != null)
GestureDetector(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
),
child: PostItem(
data: SnPost.fromJson(
nty.metadata['related_post']!,
),
data: SnPost.fromJson(nty.metadata['related_post']!),
showComments: false,
showReactions: false,
showMenu: false,
@ -255,29 +214,18 @@ class _NotificationScreenState extends State<NotificationScreen> {
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {
'slug': nty
.metadata['related_post']!['id']
.toString(),
},
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
);
},
).padding(top: 8),
const Gap(8),
Row(
children: [
Text(
DateFormat('yy/MM/dd').format(nty.createdAt),
).fontSize(12),
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
const Gap(4),
Text(
'·',
style: TextStyle(fontSize: 12),
),
Text('·', style: TextStyle(fontSize: 12)),
const Gap(4),
Text(
RelativeTime(context).format(nty.createdAt),
).fontSize(12),
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
],
).opacity(0.75),
],
@ -287,10 +235,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton(
icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
),
],
).padding(horizontal: 16);

18
lib/types/keypair.dart Normal file
View File

@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'keypair.freezed.dart';
part 'keypair.g.dart';
@freezed
abstract class SnKeyPair with _$SnKeyPair {
const factory SnKeyPair({
required String id,
required int accountId,
required String publicKey,
bool? isActive,
String? privateKey,
}) = _SnKeyPair;
factory SnKeyPair.fromJson(Map<String, Object?> json) =>
_$SnKeyPairFromJson(json);
}

View File

@ -0,0 +1,241 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'keypair.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnKeyPair {
String get id;
int get accountId;
String get publicKey;
bool? get isActive;
String? get privateKey;
/// Create a copy of SnKeyPair
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnKeyPairCopyWith<SnKeyPair> get copyWith =>
_$SnKeyPairCopyWithImpl<SnKeyPair>(this as SnKeyPair, _$identity);
/// Serializes this SnKeyPair to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnKeyPair &&
(identical(other.id, id) || other.id == id) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.publicKey, publicKey) ||
other.publicKey == publicKey) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
(identical(other.privateKey, privateKey) ||
other.privateKey == privateKey));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey);
@override
String toString() {
return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)';
}
}
/// @nodoc
abstract mixin class $SnKeyPairCopyWith<$Res> {
factory $SnKeyPairCopyWith(SnKeyPair value, $Res Function(SnKeyPair) _then) =
_$SnKeyPairCopyWithImpl;
@useResult
$Res call(
{String id,
int accountId,
String publicKey,
bool? isActive,
String? privateKey});
}
/// @nodoc
class _$SnKeyPairCopyWithImpl<$Res> implements $SnKeyPairCopyWith<$Res> {
_$SnKeyPairCopyWithImpl(this._self, this._then);
final SnKeyPair _self;
final $Res Function(SnKeyPair) _then;
/// Create a copy of SnKeyPair
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? accountId = null,
Object? publicKey = null,
Object? isActive = freezed,
Object? privateKey = freezed,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as String,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
publicKey: null == publicKey
? _self.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as String,
isActive: freezed == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
privateKey: freezed == privateKey
? _self.privateKey
: privateKey // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnKeyPair implements SnKeyPair {
const _SnKeyPair(
{required this.id,
required this.accountId,
required this.publicKey,
this.isActive,
this.privateKey});
factory _SnKeyPair.fromJson(Map<String, dynamic> json) =>
_$SnKeyPairFromJson(json);
@override
final String id;
@override
final int accountId;
@override
final String publicKey;
@override
final bool? isActive;
@override
final String? privateKey;
/// Create a copy of SnKeyPair
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnKeyPairCopyWith<_SnKeyPair> get copyWith =>
__$SnKeyPairCopyWithImpl<_SnKeyPair>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnKeyPairToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnKeyPair &&
(identical(other.id, id) || other.id == id) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.publicKey, publicKey) ||
other.publicKey == publicKey) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
(identical(other.privateKey, privateKey) ||
other.privateKey == privateKey));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey);
@override
String toString() {
return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)';
}
}
/// @nodoc
abstract mixin class _$SnKeyPairCopyWith<$Res>
implements $SnKeyPairCopyWith<$Res> {
factory _$SnKeyPairCopyWith(
_SnKeyPair value, $Res Function(_SnKeyPair) _then) =
__$SnKeyPairCopyWithImpl;
@override
@useResult
$Res call(
{String id,
int accountId,
String publicKey,
bool? isActive,
String? privateKey});
}
/// @nodoc
class __$SnKeyPairCopyWithImpl<$Res> implements _$SnKeyPairCopyWith<$Res> {
__$SnKeyPairCopyWithImpl(this._self, this._then);
final _SnKeyPair _self;
final $Res Function(_SnKeyPair) _then;
/// Create a copy of SnKeyPair
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? accountId = null,
Object? publicKey = null,
Object? isActive = freezed,
Object? privateKey = freezed,
}) {
return _then(_SnKeyPair(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as String,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
publicKey: null == publicKey
? _self.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as String,
isActive: freezed == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
privateKey: freezed == privateKey
? _self.privateKey
: privateKey // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

24
lib/types/keypair.g.dart Normal file
View File

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'keypair.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnKeyPair _$SnKeyPairFromJson(Map<String, dynamic> json) => _SnKeyPair(
id: json['id'] as String,
accountId: (json['account_id'] as num).toInt(),
publicKey: json['public_key'] as String,
isActive: json['is_active'] as bool?,
privateKey: json['private_key'] as String?,
);
Map<String, dynamic> _$SnKeyPairToJson(_SnKeyPair instance) =>
<String, dynamic>{
'id': instance.id,
'account_id': instance.accountId,
'public_key': instance.publicKey,
'is_active': instance.isActive,
'private_key': instance.privateKey,
};

View File

@ -16,6 +16,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
import 'package:surface/widgets/dialog.dart';
class AttachmentItem extends StatelessWidget {
final SnAttachment? data;
@ -289,6 +290,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
shadows: labelShadows,
color: Colors.white,
),
maxLines: 1,
),
],
),
@ -480,12 +482,13 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
),
),
Text(
widget.data.size.toString(),
widget.data.size.formatBytes(),
style: GoogleFonts.robotoMono(
fontSize: 12,
shadows: labelShadows,
color: Colors.white,
),
maxLines: 1,
),
],
),

View File

@ -42,10 +42,7 @@ class AttachmentList extends StatefulWidget {
}
class _AttachmentListState extends State<AttachmentList> {
late final List<String> heroTags = List.generate(
widget.data.length,
(_) => const Uuid().v4(),
);
late final List<String> heroTags = List.generate(widget.data.length, (_) => const Uuid().v4());
@override
Widget build(BuildContext context) {
@ -61,13 +58,13 @@ class _AttachmentListState extends State<AttachmentList> {
if (widget.data.isEmpty) return const SizedBox.shrink();
if (widget.data.length == 1) {
final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
final singleAspectRatio =
widget.data[0]?.data['ratio']?.toDouble() ??
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9,
'video' => 16 / 9,
_ => 1,
}
.toDouble();
}.toDouble();
return Container(
padding: widget.padding ?? EdgeInsets.zero,
@ -83,11 +80,7 @@ class _AttachmentListState extends State<AttachmentList> {
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags[0],
fit: widget.fit,
),
child: AttachmentItem(data: widget.data[0], heroTag: heroTags[0], fit: widget.fit),
),
),
),
@ -115,10 +108,7 @@ class _AttachmentListState extends State<AttachmentList> {
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
border: Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
@ -127,32 +117,29 @@ class _AttachmentListState extends State<AttachmentList> {
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
children:
widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
),
),
);
@ -163,43 +150,37 @@ class _AttachmentListState extends State<AttachmentList> {
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
border: Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
children:
widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
),
),
),
),
),
)
.expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
)
.expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
),
),
);
}
return AspectRatio(
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
return Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: AspectRatio(
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
@ -216,7 +197,8 @@ class _AttachmentListState extends State<AttachmentList> {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
data:
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
initialIndex: idx,
heroTags: heroTags,
),
@ -230,26 +212,18 @@ class _AttachmentListState extends State<AttachmentList> {
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
border: Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
child: AttachmentItem(data: widget.data[idx], heroTag: heroTags[idx]),
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${widget.data.length}'),
),
child: Chip(label: Text('${idx + 1}/${widget.data.length}')),
),
],
),
@ -271,8 +245,5 @@ class _AttachmentListState extends State<AttachmentList> {
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};
}

View File

@ -106,37 +106,30 @@ class ChatMessage extends StatelessWidget {
GestureDetector(
child: AccountImage(
content: user?.avatar,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(200, 0, 0, 0),
),
],
)
: null,
badge:
(user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(offset: Offset(1, 1), blurRadius: 5.0, color: Color.fromARGB(200, 0, 0, 0)),
],
)
: null,
),
onTap: () {
if (user == null) return;
showPopover(
backgroundColor:
Theme.of(context).colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox(
width: math.min(
400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(
data: user,
),
),
bodyBuilder:
(context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(data: user),
),
direction: PopoverDirection.bottom,
arrowHeight: 5,
arrowWidth: 15,
@ -157,64 +150,46 @@ class ChatMessage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 8),
if (isCompact) AccountImage(content: user?.avatar, radius: 12).padding(right: 8),
Text(
(data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user?.nick ?? 'unknown',
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(8),
Text(
dateFormatter
.format(data.createdAt.toLocal()),
).fontSize(13),
Text(dateFormatter.format(data.createdAt.toLocal())).fontSize(13),
],
).height(21),
if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
constraints: BoxConstraints(
maxWidth: 360,
),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
StyledWidget(
Container(
constraints: BoxConstraints(maxWidth: 360),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
),
padding: const EdgeInsets.only(left: 4, right: 4, top: 8, bottom: 6),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
),
)
),
],
).opacity(isPending ? 0.5 : 1),
),
@ -222,22 +197,16 @@ class ChatMessage extends StatelessWidget {
data.type == 'messages.new' &&
(data.body['text']?.isNotEmpty ?? false) &&
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
LinkPreviewWidget(text: data.body['text']!),
LinkPreviewWidget(text: data.body['text']!).padding(left: 48),
if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
bordered: true,
maxHeight: 360,
maxWidth: 480 - 48 - padding.left,
padding: padding.copyWith(
top: 8,
left: isCompact ? padding.left : 48 + padding.left,
),
padding: padding.copyWith(top: 8, left: isCompact ? padding.left : 48 + padding.left),
),
if (!hasMerged && !isCompact)
const Gap(12)
else if (!isCompact)
const Gap(8),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
],
),
),
@ -251,8 +220,7 @@ class _ChatMessageText extends StatelessWidget {
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const _ChatMessageText(
{required this.data, this.onReply, this.onEdit, this.onDelete});
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
@override
Widget build(BuildContext context) {
@ -266,8 +234,7 @@ class _ChatMessageText extends StatelessWidget {
children: [
SelectionArea(
contextMenuBuilder: (context, editableTextState) {
final List<ContextMenuButtonItem> items =
editableTextState.contextMenuButtonItems;
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
if (onReply != null) {
items.insert(
@ -314,14 +281,10 @@ class _ChatMessageText extends StatelessWidget {
child: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
),
),
if (data.updatedAt != data.createdAt)
Text(
'messageEditedHint'.tr(),
).fontSize(13).opacity(0.75),
if (data.updatedAt != data.createdAt) Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
],
);
} else if (data.body['attachments']?.isNotEmpty) {
@ -329,11 +292,7 @@ class _ChatMessageText extends StatelessWidget {
children: [
const Icon(Symbols.file_present, size: 20),
const Gap(4),
Text(
'messageFileHint'.plural(
data.body['attachments']!.length,
),
),
Text('messageFileHint'.plural(data.body['attachments']!.length)),
],
).opacity(0.75);
}
@ -363,9 +322,7 @@ class _ChatMessageSystemNotify extends StatelessWidget {
children: [
const Icon(Symbols.edit, size: 20),
const Gap(4),
Text(
'messageEdited'.tr(args: ['#${data.relatedEventId}']),
),
Text('messageEdited'.tr(args: ['#${data.relatedEventId}'])),
],
).opacity(0.75);
case 'messages.delete':
@ -373,31 +330,19 @@ class _ChatMessageSystemNotify extends StatelessWidget {
children: [
const Icon(Symbols.delete, size: 20),
const Gap(4),
Text(
'messageDeleted'.tr(args: ['#${data.relatedEventId}']),
),
Text('messageDeleted'.tr(args: ['#${data.relatedEventId}'])),
],
).opacity(0.75);
case 'calls.start':
return Row(
children: [
const Icon(Symbols.call, size: 20),
const Gap(4),
Text(
'callMessageStarted'.tr(),
),
],
children: [const Icon(Symbols.call, size: 20), const Gap(4), Text('callMessageStarted'.tr())],
).opacity(0.75);
case 'calls.end':
return Row(
children: [
const Icon(Symbols.call_end, size: 20),
const Gap(4),
Text(
'callMessageEnded'.tr(args: [
_formatDuration(Duration(seconds: data.body['last'])),
]),
),
Text('callMessageEnded'.tr(args: [_formatDuration(Duration(seconds: data.body['last']))])),
],
).opacity(0.75);
default:

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <fast_rsa/fast_rsa_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin.h>
@ -25,6 +26,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) fast_rsa_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FastRsaPlugin");
fast_rsa_plugin_register_with_registrar(fast_rsa_registrar);
g_autoptr(FlPluginRegistrar) file_saver_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
file_saver_plugin_register_with_registrar(file_saver_registrar);

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
fast_rsa
file_saver
file_selector_linux
flutter_timezone

View File

@ -8,6 +8,7 @@ import Foundation
import bitsdojo_window_macos
import connectivity_plus
import device_info_plus
import fast_rsa
import file_picker
import file_saver
import file_selector_macos
@ -43,6 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FastRsaPlugin.register(with: registry.registrar(forPlugin: "FastRsaPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))

View File

@ -513,6 +513,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
fast_rsa:
dependency: "direct main"
description:
name: fast_rsa
sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270"
url: "https://pub.dev"
source: hosted
version: "3.8.0"
ffi:
dependency: transitive
description:
@ -665,6 +673,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.70.2"
flat_buffers:
dependency: transitive
description:
name: flat_buffers
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3"
url: "https://pub.dev"
source: hosted
version: "23.5.26"
flutter:
dependency: "direct main"
description: flutter

View File

@ -137,6 +137,7 @@ dependencies:
flutter_timezone: ^4.1.0
flutter_map: ^8.1.0
geolocator: ^13.0.2
fast_rsa: ^3.8.0
dev_dependencies:
flutter_test:

View File

@ -0,0 +1,23 @@
// dart format width=80
// GENERATED CODE, DO NOT EDIT BY HAND.
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2];
}

View File

@ -0,0 +1,462 @@
// dart format width=80
// GENERATED CODE, DO NOT EDIT BY HAND.
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
class SnLocalChatChannel extends Table
with TableInfo<SnLocalChatChannel, SnLocalChatChannelData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
SnLocalChatChannel(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
'alias', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
@override
List<GeneratedColumn> get $columns => [id, alias, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_channel';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatChannelData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatChannelData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
alias: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!,
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
SnLocalChatChannel createAlias(String alias) {
return SnLocalChatChannel(attachedDatabase, alias);
}
}
class SnLocalChatChannelData extends DataClass
implements Insertable<SnLocalChatChannelData> {
final int id;
final String alias;
final String content;
final DateTime createdAt;
const SnLocalChatChannelData(
{required this.id,
required this.alias,
required this.content,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['alias'] = Variable<String>(alias);
map['content'] = Variable<String>(content);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatChannelCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatChannelCompanion(
id: Value(id),
alias: Value(alias),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatChannelData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatChannelData(
id: serializer.fromJson<int>(json['id']),
alias: serializer.fromJson<String>(json['alias']),
content: serializer.fromJson<String>(json['content']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'alias': serializer.toJson<String>(alias),
'content': serializer.toJson<String>(content),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatChannelData copyWith(
{int? id, String? alias, String? content, DateTime? createdAt}) =>
SnLocalChatChannelData(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatChannelData copyWithCompanion(SnLocalChatChannelCompanion data) {
return SnLocalChatChannelData(
id: data.id.present ? data.id.value : this.id,
alias: data.alias.present ? data.alias.value : this.alias,
content: data.content.present ? data.content.value : this.content,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelData(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, alias, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatChannelData &&
other.id == this.id &&
other.alias == this.alias &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatChannelCompanion
extends UpdateCompanion<SnLocalChatChannelData> {
final Value<int> id;
final Value<String> alias;
final Value<String> content;
final Value<DateTime> createdAt;
const SnLocalChatChannelCompanion({
this.id = const Value.absent(),
this.alias = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatChannelCompanion.insert({
this.id = const Value.absent(),
required String alias,
required String content,
this.createdAt = const Value.absent(),
}) : alias = Value(alias),
content = Value(content);
static Insertable<SnLocalChatChannelData> custom({
Expression<int>? id,
Expression<String>? alias,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (alias != null) 'alias': alias,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatChannelCompanion copyWith(
{Value<int>? id,
Value<String>? alias,
Value<String>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatChannelCompanion(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (alias.present) {
map['alias'] = Variable<String>(alias.value);
}
if (content.present) {
map['content'] = Variable<String>(content.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelCompanion(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
class SnLocalChatMessage extends Table
with TableInfo<SnLocalChatMessage, SnLocalChatMessageData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
SnLocalChatMessage(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
@override
List<GeneratedColumn> get $columns => [id, channelId, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_message';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatMessageData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatMessageData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!,
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
SnLocalChatMessage createAlias(String alias) {
return SnLocalChatMessage(attachedDatabase, alias);
}
}
class SnLocalChatMessageData extends DataClass
implements Insertable<SnLocalChatMessageData> {
final int id;
final int channelId;
final String content;
final DateTime createdAt;
const SnLocalChatMessageData(
{required this.id,
required this.channelId,
required this.content,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
map['content'] = Variable<String>(content);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatMessageCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatMessageCompanion(
id: Value(id),
channelId: Value(channelId),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatMessageData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatMessageData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
content: serializer.fromJson<String>(json['content']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'content': serializer.toJson<String>(content),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatMessageData copyWith(
{int? id, int? channelId, String? content, DateTime? createdAt}) =>
SnLocalChatMessageData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatMessageData copyWithCompanion(SnLocalChatMessageCompanion data) {
return SnLocalChatMessageData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
content: data.content.present ? data.content.value : this.content,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatMessageData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatMessageCompanion
extends UpdateCompanion<SnLocalChatMessageData> {
final Value<int> id;
final Value<int> channelId;
final Value<String> content;
final Value<DateTime> createdAt;
const SnLocalChatMessageCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatMessageCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required String content,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
content = Value(content);
static Insertable<SnLocalChatMessageData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatMessageCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<String>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatMessageCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (content.present) {
map['content'] = Variable<String>(content.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
class DatabaseAtV1 extends GeneratedDatabase {
DatabaseAtV1(QueryExecutor e) : super(e);
late final SnLocalChatChannel snLocalChatChannel = SnLocalChatChannel(this);
late final SnLocalChatMessage snLocalChatMessage = SnLocalChatMessage(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities =>
[snLocalChatChannel, snLocalChatMessage];
@override
int get schemaVersion => 1;
}

View File

@ -0,0 +1,730 @@
// dart format width=80
// GENERATED CODE, DO NOT EDIT BY HAND.
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
class SnLocalChatChannel extends Table
with TableInfo<SnLocalChatChannel, SnLocalChatChannelData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
SnLocalChatChannel(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
'alias', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
@override
List<GeneratedColumn> get $columns => [id, alias, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_channel';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatChannelData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatChannelData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
alias: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!,
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
SnLocalChatChannel createAlias(String alias) {
return SnLocalChatChannel(attachedDatabase, alias);
}
}
class SnLocalChatChannelData extends DataClass
implements Insertable<SnLocalChatChannelData> {
final int id;
final String alias;
final String content;
final DateTime createdAt;
const SnLocalChatChannelData(
{required this.id,
required this.alias,
required this.content,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['alias'] = Variable<String>(alias);
map['content'] = Variable<String>(content);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatChannelCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatChannelCompanion(
id: Value(id),
alias: Value(alias),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatChannelData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatChannelData(
id: serializer.fromJson<int>(json['id']),
alias: serializer.fromJson<String>(json['alias']),
content: serializer.fromJson<String>(json['content']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'alias': serializer.toJson<String>(alias),
'content': serializer.toJson<String>(content),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatChannelData copyWith(
{int? id, String? alias, String? content, DateTime? createdAt}) =>
SnLocalChatChannelData(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatChannelData copyWithCompanion(SnLocalChatChannelCompanion data) {
return SnLocalChatChannelData(
id: data.id.present ? data.id.value : this.id,
alias: data.alias.present ? data.alias.value : this.alias,
content: data.content.present ? data.content.value : this.content,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelData(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, alias, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatChannelData &&
other.id == this.id &&
other.alias == this.alias &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatChannelCompanion
extends UpdateCompanion<SnLocalChatChannelData> {
final Value<int> id;
final Value<String> alias;
final Value<String> content;
final Value<DateTime> createdAt;
const SnLocalChatChannelCompanion({
this.id = const Value.absent(),
this.alias = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatChannelCompanion.insert({
this.id = const Value.absent(),
required String alias,
required String content,
this.createdAt = const Value.absent(),
}) : alias = Value(alias),
content = Value(content);
static Insertable<SnLocalChatChannelData> custom({
Expression<int>? id,
Expression<String>? alias,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (alias != null) 'alias': alias,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatChannelCompanion copyWith(
{Value<int>? id,
Value<String>? alias,
Value<String>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatChannelCompanion(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (alias.present) {
map['alias'] = Variable<String>(alias.value);
}
if (content.present) {
map['content'] = Variable<String>(content.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelCompanion(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
class SnLocalChatMessage extends Table
with TableInfo<SnLocalChatMessage, SnLocalChatMessageData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
SnLocalChatMessage(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
@override
List<GeneratedColumn> get $columns => [id, channelId, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_message';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatMessageData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatMessageData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!,
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
SnLocalChatMessage createAlias(String alias) {
return SnLocalChatMessage(attachedDatabase, alias);
}
}
class SnLocalChatMessageData extends DataClass
implements Insertable<SnLocalChatMessageData> {
final int id;
final int channelId;
final String content;
final DateTime createdAt;
const SnLocalChatMessageData(
{required this.id,
required this.channelId,
required this.content,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
map['content'] = Variable<String>(content);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatMessageCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatMessageCompanion(
id: Value(id),
channelId: Value(channelId),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatMessageData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatMessageData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
content: serializer.fromJson<String>(json['content']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'content': serializer.toJson<String>(content),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatMessageData copyWith(
{int? id, int? channelId, String? content, DateTime? createdAt}) =>
SnLocalChatMessageData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatMessageData copyWithCompanion(SnLocalChatMessageCompanion data) {
return SnLocalChatMessageData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
content: data.content.present ? data.content.value : this.content,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatMessageData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatMessageCompanion
extends UpdateCompanion<SnLocalChatMessageData> {
final Value<int> id;
final Value<int> channelId;
final Value<String> content;
final Value<DateTime> createdAt;
const SnLocalChatMessageCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatMessageCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required String content,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
content = Value(content);
static Insertable<SnLocalChatMessageData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatMessageCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<String>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatMessageCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (content.present) {
map['content'] = Variable<String>(content.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
class SnLocalKeyPair extends Table
with TableInfo<SnLocalKeyPair, SnLocalKeyPairData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
SnLocalKeyPair(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
'account_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
late final GeneratedColumn<String> publicKey = GeneratedColumn<String>(
'public_key', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<String> privateKey = GeneratedColumn<String>(
'private_key', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
late final GeneratedColumn<bool> isActive = GeneratedColumn<bool>(
'is_active', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("is_active" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
@override
List<GeneratedColumn> get $columns =>
[id, accountId, publicKey, privateKey, isActive];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_key_pair';
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalKeyPairData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
accountId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
publicKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}public_key'])!,
privateKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}private_key']),
isActive: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}is_active'])!,
);
}
@override
SnLocalKeyPair createAlias(String alias) {
return SnLocalKeyPair(attachedDatabase, alias);
}
}
class SnLocalKeyPairData extends DataClass
implements Insertable<SnLocalKeyPairData> {
final String id;
final int accountId;
final String publicKey;
final String? privateKey;
final bool isActive;
const SnLocalKeyPairData(
{required this.id,
required this.accountId,
required this.publicKey,
this.privateKey,
required this.isActive});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['account_id'] = Variable<int>(accountId);
map['public_key'] = Variable<String>(publicKey);
if (!nullToAbsent || privateKey != null) {
map['private_key'] = Variable<String>(privateKey);
}
map['is_active'] = Variable<bool>(isActive);
return map;
}
SnLocalKeyPairCompanion toCompanion(bool nullToAbsent) {
return SnLocalKeyPairCompanion(
id: Value(id),
accountId: Value(accountId),
publicKey: Value(publicKey),
privateKey: privateKey == null && nullToAbsent
? const Value.absent()
: Value(privateKey),
isActive: Value(isActive),
);
}
factory SnLocalKeyPairData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalKeyPairData(
id: serializer.fromJson<String>(json['id']),
accountId: serializer.fromJson<int>(json['accountId']),
publicKey: serializer.fromJson<String>(json['publicKey']),
privateKey: serializer.fromJson<String?>(json['privateKey']),
isActive: serializer.fromJson<bool>(json['isActive']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'accountId': serializer.toJson<int>(accountId),
'publicKey': serializer.toJson<String>(publicKey),
'privateKey': serializer.toJson<String?>(privateKey),
'isActive': serializer.toJson<bool>(isActive),
};
}
SnLocalKeyPairData copyWith(
{String? id,
int? accountId,
String? publicKey,
Value<String?> privateKey = const Value.absent(),
bool? isActive}) =>
SnLocalKeyPairData(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
publicKey: publicKey ?? this.publicKey,
privateKey: privateKey.present ? privateKey.value : this.privateKey,
isActive: isActive ?? this.isActive,
);
SnLocalKeyPairData copyWithCompanion(SnLocalKeyPairCompanion data) {
return SnLocalKeyPairData(
id: data.id.present ? data.id.value : this.id,
accountId: data.accountId.present ? data.accountId.value : this.accountId,
publicKey: data.publicKey.present ? data.publicKey.value : this.publicKey,
privateKey:
data.privateKey.present ? data.privateKey.value : this.privateKey,
isActive: data.isActive.present ? data.isActive.value : this.isActive,
);
}
@override
String toString() {
return (StringBuffer('SnLocalKeyPairData(')
..write('id: $id, ')
..write('accountId: $accountId, ')
..write('publicKey: $publicKey, ')
..write('privateKey: $privateKey, ')
..write('isActive: $isActive')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, accountId, publicKey, privateKey, isActive);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalKeyPairData &&
other.id == this.id &&
other.accountId == this.accountId &&
other.publicKey == this.publicKey &&
other.privateKey == this.privateKey &&
other.isActive == this.isActive);
}
class SnLocalKeyPairCompanion extends UpdateCompanion<SnLocalKeyPairData> {
final Value<String> id;
final Value<int> accountId;
final Value<String> publicKey;
final Value<String?> privateKey;
final Value<bool> isActive;
final Value<int> rowid;
const SnLocalKeyPairCompanion({
this.id = const Value.absent(),
this.accountId = const Value.absent(),
this.publicKey = const Value.absent(),
this.privateKey = const Value.absent(),
this.isActive = const Value.absent(),
this.rowid = const Value.absent(),
});
SnLocalKeyPairCompanion.insert({
required String id,
required int accountId,
required String publicKey,
this.privateKey = const Value.absent(),
this.isActive = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
accountId = Value(accountId),
publicKey = Value(publicKey);
static Insertable<SnLocalKeyPairData> custom({
Expression<String>? id,
Expression<int>? accountId,
Expression<String>? publicKey,
Expression<String>? privateKey,
Expression<bool>? isActive,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (accountId != null) 'account_id': accountId,
if (publicKey != null) 'public_key': publicKey,
if (privateKey != null) 'private_key': privateKey,
if (isActive != null) 'is_active': isActive,
if (rowid != null) 'rowid': rowid,
});
}
SnLocalKeyPairCompanion copyWith(
{Value<String>? id,
Value<int>? accountId,
Value<String>? publicKey,
Value<String?>? privateKey,
Value<bool>? isActive,
Value<int>? rowid}) {
return SnLocalKeyPairCompanion(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
publicKey: publicKey ?? this.publicKey,
privateKey: privateKey ?? this.privateKey,
isActive: isActive ?? this.isActive,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (accountId.present) {
map['account_id'] = Variable<int>(accountId.value);
}
if (publicKey.present) {
map['public_key'] = Variable<String>(publicKey.value);
}
if (privateKey.present) {
map['private_key'] = Variable<String>(privateKey.value);
}
if (isActive.present) {
map['is_active'] = Variable<bool>(isActive.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalKeyPairCompanion(')
..write('id: $id, ')
..write('accountId: $accountId, ')
..write('publicKey: $publicKey, ')
..write('privateKey: $privateKey, ')
..write('isActive: $isActive, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class DatabaseAtV2 extends GeneratedDatabase {
DatabaseAtV2(QueryExecutor e) : super(e);
late final SnLocalChatChannel snLocalChatChannel = SnLocalChatChannel(this);
late final SnLocalChatMessage snLocalChatMessage = SnLocalChatMessage(this);
late final SnLocalKeyPair snLocalKeyPair = SnLocalKeyPair(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities =>
[snLocalChatChannel, snLocalChatMessage, snLocalKeyPair];
@override
int get schemaVersion => 2;
}

View File

@ -0,0 +1,75 @@
// dart format width=80
// ignore_for_file: unused_local_variable, unused_import
import 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart';
import 'package:surface/database/database.dart';
import 'package:flutter_test/flutter_test.dart';
import 'generated/schema.dart';
import 'generated/schema_v1.dart' as v1;
import 'generated/schema_v2.dart' as v2;
void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
late SchemaVerifier verifier;
setUpAll(() {
verifier = SchemaVerifier(GeneratedHelper());
});
group('simple database migrations', () {
// These simple tests verify all possible schema updates with a simple (no
// data) migration. This is a quick way to ensure that written database
// migrations properly alter the schema.
const versions = GeneratedHelper.versions;
for (final (i, fromVersion) in versions.indexed) {
group('from $fromVersion', () {
for (final toVersion in versions.skip(i + 1)) {
test('to $toVersion', () async {
final schema = await verifier.schemaAt(fromVersion);
final db = AppDatabase(schema.newConnection());
await verifier.migrateAndValidate(db, toVersion);
await db.close();
});
}
});
}
});
// The following template shows how to write tests ensuring your migrations
// preserve existing data.
// Testing this can be useful for migrations that change existing columns
// (e.g. by alterating their type or constraints). Migrations that only add
// tables or columns typically don't need these advanced tests. For more
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
// TODO: This generated template shows how these tests could be written. Adopt
// it to your own needs when testing migrations with data integrity.
test('migration from v1 to v2 does not corrupt data', () async {
// Add data to insert into the old database, and the expected rows after the
// migration.
// TODO: Fill these lists
final oldSnLocalChatChannelData = <v1.SnLocalChatChannelData>[];
final expectedNewSnLocalChatChannelData = <v2.SnLocalChatChannelData>[];
final oldSnLocalChatMessageData = <v1.SnLocalChatMessageData>[];
final expectedNewSnLocalChatMessageData = <v2.SnLocalChatMessageData>[];
await verifier.testWithDataIntegrity(
oldVersion: 1,
newVersion: 2,
createOld: v1.DatabaseAtV1.new,
createNew: v2.DatabaseAtV2.new,
openTestedDatabase: AppDatabase.new,
createItems: (batch, oldDb) {
batch.insertAll(oldDb.snLocalChatChannel, oldSnLocalChatChannelData);
batch.insertAll(oldDb.snLocalChatMessage, oldSnLocalChatMessageData);
},
validateItems: (newDb) async {
expect(expectedNewSnLocalChatChannelData,
await newDb.select(newDb.snLocalChatChannel).get());
expect(expectedNewSnLocalChatMessageData,
await newDb.select(newDb.snLocalChatMessage).get());
},
);
});
}

View File

@ -8,6 +8,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <fast_rsa/fast_rsa_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
@ -35,6 +36,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FastRsaPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FastRsaPlugin"));
FileSaverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSaverPlugin"));
FileSelectorWindowsRegisterWithRegistrar(

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
connectivity_plus
fast_rsa
file_saver
file_selector_windows
firebase_core