Compare commits

..

No commits in common. "97ddc18b8ef759332d5104921deaeb6c25c9b9e1" and "ce6e9c185aeb7151930234828236a2bec2ca9fe0" have entirely different histories.

25 changed files with 193 additions and 5622 deletions

File diff suppressed because one or more lines are too long

View File

@ -287,26 +287,23 @@ class ChatMessageController extends ChangeNotifier {
};
// Mock the message locally
// Do not mock the editing message
if (editingMessage == null) {
final createdAt = DateTime.now();
final message = SnChatMessage(
id: 0,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
uuid: nonce,
body: body,
type: type,
channel: channel!,
channelId: channel!.id,
sender: profile!,
senderId: profile!.id,
quoteEventId: quoteId,
relatedEventId: relatedId,
);
_addUnconfirmedMessage(message);
}
final createdAt = DateTime.now();
final message = SnChatMessage(
id: 0,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
uuid: nonce,
body: body,
type: type,
channel: channel!,
channelId: channel!.id,
sender: profile!,
senderId: profile!.id,
quoteEventId: quoteId,
relatedEventId: relatedId,
);
_addUnconfirmedMessage(message);
// Send to server
try {

View File

@ -1,42 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/account.dart';
class SnAccountConverter extends TypeConverter<SnAccount, String>
with JsonTypeConverter2<SnAccount, String, Map<String, Object?>> {
const SnAccountConverter();
@override
SnAccount fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnAccount value) {
return jsonEncode(toJson(value));
}
@override
SnAccount fromJson(Map<String, Object?> json) {
return SnAccount.fromJson(json);
}
@override
Map<String, Object?> toJson(SnAccount value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_account_name', columns: {#name})
class SnLocalAccount extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get content => text().map(const SnAccountConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,47 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/attachment.dart';
class SnAttachmentConverter extends TypeConverter<SnAttachment, String>
with JsonTypeConverter2<SnAttachment, String, Map<String, Object?>> {
const SnAttachmentConverter();
@override
SnAttachment fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnAttachment value) {
return jsonEncode(toJson(value));
}
@override
SnAttachment fromJson(Map<String, Object?> json) {
return SnAttachment.fromJson(json);
}
@override
Map<String, Object?> toJson(SnAttachment value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_attachment_rid', columns: {#rid})
@TableIndex(name: 'idx_attachment_account', columns: {#accountId})
class SnLocalAttachment extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get rid => text().unique()();
TextColumn get uuid => text().unique()();
TextColumn get content => text().map(const SnAttachmentConverter())();
IntColumn get accountId => integer()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -28,7 +28,6 @@ class SnChannelConverter extends TypeConverter<SnChannel, String>
}
}
@TableIndex(name: 'idx_channel_alias', columns: {#alias})
class SnLocalChatChannel extends Table {
IntColumn get id => integer().autoIncrement()();
@ -64,54 +63,12 @@ class SnMessageConverter extends TypeConverter<SnChatMessage, String>
}
}
@TableIndex(name: 'idx_chat_channel', columns: {#channelId})
class SnLocalChatMessage extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
IntColumn get senderId => integer().nullable()();
TextColumn get content => text().map(const SnMessageConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnChannelMemberConverter extends TypeConverter<SnChannelMember, String>
with JsonTypeConverter2<SnChannelMember, String, Map<String, Object?>> {
const SnChannelMemberConverter();
@override
SnChannelMember fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChannelMember value) {
return jsonEncode(toJson(value));
}
@override
SnChannelMember fromJson(Map<String, Object?> json) {
return SnChannelMember.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChannelMember value) {
return value.toJson();
}
}
class SnLocalChannelMember extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
IntColumn get accountId => integer()();
TextColumn get content => text().map(SnChannelMemberConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,33 +1,19 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/account.dart';
import 'package:surface/database/attachment.dart';
import 'package:surface/database/chat.dart';
import 'package:surface/database/database.steps.dart';
import 'package:surface/database/keypair.dart';
import 'package:surface/database/sticker.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/account.dart';
part 'database.g.dart';
@DriftDatabase(tables: [
SnLocalChatChannel,
SnLocalChatMessage,
SnLocalChannelMember,
SnLocalKeyPair,
SnLocalAccount,
SnLocalAttachment,
SnLocalSticker,
SnLocalStickerPack,
])
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage, SnLocalKeyPair])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
@override
int get schemaVersion => 3;
int get schemaVersion => 2;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -47,8 +33,6 @@ class AppDatabase extends _$AppDatabase {
return MigrationStrategy(
onUpgrade: stepByStep(from1To2: (m, schema) async {
// Nothing else to do here
}, from2To3: (m, schema) async {
// Nothing else to do here, too
}),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -140,281 +140,8 @@ i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_active" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
final class Schema3 extends i0.VersionedSchema {
Schema3({required super.database}) : super(version: 3);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
snLocalChatChannel,
snLocalChatMessage,
snLocalChannelMember,
snLocalKeyPair,
snLocalAccount,
snLocalAttachment,
snLocalSticker,
snLocalStickerPack,
idxChannelAlias,
idxChatChannel,
idxAccountName,
idxAttachmentRid,
idxAttachmentAccount,
];
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 Shape3 snLocalChatMessage = Shape3(
source: i0.VersionedTable(
entityName: 'sn_local_chat_message',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_10,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 snLocalChannelMember = Shape4(
source: i0.VersionedTable(
entityName: 'sn_local_channel_member',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_6,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 snLocalKeyPair = Shape2(
source: i0.VersionedTable(
entityName: 'sn_local_key_pair',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 snLocalAccount = Shape5(
source: i0.VersionedTable(
entityName: 'sn_local_account',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_12,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 snLocalAttachment = Shape6(
source: i0.VersionedTable(
entityName: 'sn_local_attachment',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_13,
_column_14,
_column_2,
_column_6,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 snLocalSticker = Shape7(
source: i0.VersionedTable(
entityName: 'sn_local_sticker',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_15,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 snLocalStickerPack = Shape8(
source: i0.VersionedTable(
entityName: 'sn_local_sticker_pack',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
final i1.Index idxAccountName = i1.Index('idx_account_name',
'CREATE INDEX idx_account_name ON sn_local_account (name)');
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
}
class Shape3 extends i0.VersionedTable {
Shape3({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<int> get senderId =>
columnsByName['sender_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_10(String aliasedName) =>
i1.GeneratedColumn<int>('sender_id', aliasedName, true,
type: i1.DriftSqlType.int);
class Shape4 extends i0.VersionedTable {
Shape4({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<int> get accountId =>
columnsByName['account_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<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
i1.GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
type: i1.DriftSqlType.dateTime);
class Shape5 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get rid =>
columnsByName['rid']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uuid =>
columnsByName['uuid']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
i1.GeneratedColumn<String>('rid', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
i1.GeneratedColumn<String>('uuid', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
class Shape7 extends i0.VersionedTable {
Shape7({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 fullAlias =>
columnsByName['full_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<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>('full_alias', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape8 extends i0.VersionedTable {
Shape8({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['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>;
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -423,11 +150,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
case 2:
final schema = Schema3(database: database);
final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema);
return 3;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -436,10 +158,8 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
));

View File

@ -1,74 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/attachment.dart';
class SnStickerConverter extends TypeConverter<SnSticker, String>
with JsonTypeConverter2<SnSticker, String, Map<String, Object?>> {
const SnStickerConverter();
@override
SnSticker fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnSticker value) {
return jsonEncode(toJson(value));
}
@override
SnSticker fromJson(Map<String, Object?> json) {
return SnSticker.fromJson(json);
}
@override
Map<String, Object?> toJson(SnSticker value) {
return value.toJson();
}
}
class SnLocalSticker extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get alias => text()();
TextColumn get fullAlias => text()();
TextColumn get content => text().map(const SnStickerConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnStickerPackConverter extends TypeConverter<SnStickerPack, String>
with JsonTypeConverter2<SnStickerPack, String, Map<String, Object?>> {
const SnStickerPackConverter();
@override
SnStickerPack fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnStickerPack value) {
return jsonEncode(toJson(value));
}
@override
SnStickerPack fromJson(Map<String, Object?> json) {
return SnStickerPack.fromJson(json);
}
@override
Map<String, Object?> toJson(SnStickerPack value) {
return value.toJson();
}
}
class SnLocalStickerPack extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text().map(const SnStickerPackConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View File

@ -314,10 +314,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (!mounted) return;
final sticker = context.read<SnStickerProvider>();
await sticker.listSticker();
if (!mounted) return;
final ud = context.read<UserDirectoryProvider>();
final userCacheSize = await ud.loadAccountCache();
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
logging.info('[Bootstrap] Everything initialized!');
} catch (err) {
if (!mounted) return;

View File

@ -1,36 +1,19 @@
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/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
class UserDirectoryProvider {
late final SnNetworkProvider _sn;
late final DatabaseProvider _dt;
UserDirectoryProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_dt = context.read<DatabaseProvider>();
}
final Map<String, int> _idCache = {};
final Map<int, SnAccount> _cache = {};
Future<int> loadAccountCache({int max = 100}) async {
final out = await (_dt.db.snLocalAccount.select()..limit(max)).get();
for (final ele in out) {
_cache[ele.id] = ele.content;
_idCache[ele.name] = ele.id;
}
return out.length;
}
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
// In-memory cache
final out = List<SnAccount?>.generate(id.length, (e) => null);
final plannedQuery = <int>{};
for (var idx = 0; idx < out.length; idx++) {
@ -44,24 +27,6 @@ class UserDirectoryProvider {
plannedQuery.add(item);
}
}
// On-disk cache
if (plannedQuery.isEmpty) return out;
final dbResp = await (_dt.db.snLocalAccount.select()
..where((e) => e.id.isIn(plannedQuery))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))
..limit(plannedQuery.length))
.get();
for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue;
if (dbResp.length <= idx) {
break;
}
out[idx] = dbResp[idx].content;
_cache[dbResp[idx].id] = dbResp[idx].content;
_idCache[dbResp[idx].name] = dbResp[idx].id;
plannedQuery.remove(dbResp[idx].id);
}
// Remote server
if (plannedQuery.isEmpty) return out;
final resp = await _sn.client
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
@ -78,29 +43,17 @@ class UserDirectoryProvider {
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
sideIdx++;
}
if (respDecoded.isNotEmpty) _saveToLocal(respDecoded);
return out;
}
Future<SnAccount?> getAccount(dynamic id) async {
// In-memory cache
if (id is String && _idCache.containsKey(id)) {
id = _idCache[id];
}
if (_cache.containsKey(id)) {
return _cache[id];
}
// On-disk cache
final dbResp = await (_dt.db.snLocalAccount.select()
..where((e) => e.id.equals(id))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
.getSingleOrNull();
if (dbResp != null) {
_cache[dbResp.id] = dbResp.content;
_idCache[dbResp.name] = dbResp.id;
return dbResp.content;
}
// Remote server
try {
final resp = await _sn.client.get('/cgi/id/users/$id');
final account = SnAccount.fromJson(
@ -108,40 +61,16 @@ class UserDirectoryProvider {
);
_cache[account.id] = account;
if (id is String) _idCache[id] = account.id;
_saveToLocal([account]);
return account;
} catch (err) {
return null;
}
}
SnAccount? getFromCache(dynamic id) {
SnAccount? getAccountFromCache(dynamic id) {
if (id is String && _idCache.containsKey(id)) {
id = _idCache[id];
}
return _cache[id];
}
Future<void> _saveToLocal(Iterable<SnAccount> out) async {
// For better on conflict resolution
// And consider the method usually called with usually small amount of data
// Use for to insert each record instead of bulk insert
List<Future<int>> queries = out.map((ele) {
return _dt.db.snLocalAccount.insertOne(
SnLocalAccountCompanion.insert(
id: Value(ele.id),
name: ele.name,
content: ele,
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
),
onConflict: DoUpdate(
(_) => SnLocalAccountCompanion.custom(
name: Constant(ele.name),
content: Constant(jsonEncode(ele.toJson())),
),
),
);
}).toList();
await Future.wait(queries);
}
}

View File

@ -50,8 +50,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/badges/${badge.id}/active');
if (!mounted) return;
context.showSnackbar('badgeActivated'
.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
context.showSnackbar('badgeActivated'.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
await _fetchBadges();
} catch (err) {
if (!mounted) return;
@ -91,12 +90,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
title: Text(
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
).tr(),
contentPadding: const EdgeInsets.only(
left: 24,
right: 16,
top: 4,
bottom: 4,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@ -336,7 +336,7 @@ class _ChatChannelEntry extends StatelessWidget {
: null;
final title = otherMember != null
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
? ud.getAccountFromCache(otherMember.accountId)?.nick ?? channel.name
: channel.name;
return ListTile(
@ -354,9 +354,10 @@ class _ChatChannelEntry extends StatelessWidget {
? Row(
children: [
Badge(
label: Text(
ud.getFromCache(lastMessage!.sender.accountId)?.nick ??
'unknown'.tr()),
label: Text(ud
.getAccountFromCache(lastMessage!.sender.accountId)
?.nick ??
'unknown'.tr()),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
@ -399,7 +400,7 @@ class _ChatChannelEntry extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: otherMember != null
? ud.getFromCache(otherMember.accountId)?.avatar
? ud.getAccountFromCache(otherMember.accountId)?.avatar
: channel.realm?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),

View File

@ -289,14 +289,15 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
),
ListTile(
leading: AccountImage(
content: ud.getFromCache(_profile!.accountId)?.avatar,
content:
ud.getAccountFromCache(_profile!.accountId)?.avatar,
radius: 18,
),
trailing: const Icon(Symbols.chevron_right),
title: Text('channelEditProfile').tr(),
subtitle: Text(
(_profile?.nick?.isEmpty ?? true)
? ud.getFromCache(_profile!.accountId)!.nick
? ud.getAccountFromCache(_profile!.accountId)!.nick
: _profile!.nick!,
),
contentPadding: const EdgeInsets.only(left: 20, right: 20),
@ -574,10 +575,11 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
return ListTile(
contentPadding: const EdgeInsets.only(right: 24, left: 16),
leading: AccountImage(
content: ud.getFromCache(member.accountId)?.avatar,
content: ud.getAccountFromCache(member.accountId)?.avatar,
),
title: Text(
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.name ??
'unknown'.tr(),
),
subtitle: Text(member.nick ?? 'unknown'.tr()),
trailing: SizedBox(

View File

@ -277,7 +277,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
appBar: AppBar(
title: Text(
_channel?.type == 1
? ud.getFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ??
_channel!.name
: _channel?.name ?? 'loading'.tr(),
),
actions: [

View File

@ -51,8 +51,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
Future<void> _fetchPublishers() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
_publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);
@ -69,8 +68,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
Future<void> _fetchChannels() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/im/channels/${widget.alias}/public');
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
_channels = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
);
@ -100,32 +98,15 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(_realm?.name ?? 'loading'.tr()),
bottom: TabBar(
tabs: [
Tab(
icon: Icon(Symbols.home,
color: Theme.of(context)
.appBarTheme
.foregroundColor)),
Tab(
icon: Icon(Symbols.explore,
color: Theme.of(context)
.appBarTheme
.foregroundColor)),
Tab(
icon: Icon(Symbols.group,
color: Theme.of(context)
.appBarTheme
.foregroundColor)),
Tab(
icon: Icon(Symbols.settings,
color: Theme.of(context)
.appBarTheme
.foregroundColor)),
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
],
),
),
@ -134,8 +115,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
},
body: TabBarView(
children: [
_RealmDetailHomeWidget(
realm: _realm, publishers: _publishers, channels: _channels),
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
_RealmPostListWidget(realm: _realm),
_RealmMemberListWidget(realm: _realm),
_RealmSettingsWidget(
@ -157,8 +137,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
final List<SnPublisher>? publishers;
final List<SnChannel>? channels;
const _RealmDetailHomeWidget(
{required this.realm, this.publishers, this.channels});
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
@override
Widget build(BuildContext context) {
@ -189,8 +168,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublishersHint'.tr(),
style: Theme.of(context).textTheme.bodyMedium)
child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
),
@ -221,8 +199,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublicChannelsHint'.tr(),
style: Theme.of(context).textTheme.bodyMedium)
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
),
@ -346,12 +323,10 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
try {
final ud = context.read<UserDirectoryProvider>();
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/id/realms/${widget.realm!.alias}/members',
queryParameters: {
'take': 10,
'offset': _members.length,
});
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
'take': 10,
'offset': _members.length,
});
final out = List<SnRealmMember>.from(
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
@ -457,14 +432,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
return ListTile(
contentPadding: const EdgeInsets.only(right: 24, left: 16),
leading: AccountImage(
content: ud.getFromCache(member.accountId)?.avatar,
content: ud.getAccountFromCache(member.accountId)?.avatar,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
title: Text(
ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
),
subtitle: Text(
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
),
trailing: IconButton(
icon: const Icon(Symbols.person_remove),

View File

@ -51,10 +51,8 @@ class _AppSharingListenerState extends State<AppSharingListener> {
child: Column(
children: [
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
leading: Icon(Icons.post_add),
trailing: const Icon(Icons.chevron_right),
title: Text('shareIntentPostStory').tr(),
@ -66,20 +64,13 @@ class _AppSharingListenerState extends State<AppSharingListener> {
},
extra: PostEditorExtra(
text: value
.where((e) => [
SharedMediaType.text,
SharedMediaType.url
].contains(e.type))
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path)
.join('\n'),
attachments: value
.where((e) => [
SharedMediaType.video,
SharedMediaType.file,
SharedMediaType.image
].contains(e.type))
.map((e) =>
PostWriteMedia.fromFile(XFile(e.path)))
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
.contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
),
);
@ -87,18 +78,15 @@ class _AppSharingListenerState extends State<AppSharingListener> {
},
),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
leading: Icon(Icons.chat_outlined),
trailing: const Icon(Icons.chevron_right),
title: Text('shareIntentSendChannel').tr(),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) =>
_ShareIntentChannelSelect(value: value),
builder: (context) => _ShareIntentChannelSelect(value: value),
).then((val) {
if (!context.mounted) return;
if (val == true) Navigator.pop(context);
@ -122,8 +110,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
}
void _initialize() async {
_shareIntentSubscription =
ReceiveSharingIntent.instance.getMediaStream().listen((value) {
_shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {
if (value.isEmpty) return;
if (mounted) {
_gotoPost(value);
@ -170,8 +157,7 @@ class _ShareIntentChannelSelect extends StatefulWidget {
const _ShareIntentChannelSelect({required this.value});
@override
State<_ShareIntentChannelSelect> createState() =>
_ShareIntentChannelSelectState();
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
}
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
@ -192,11 +178,8 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) {
if (_lastMessages!.containsKey(a.id) &&
_lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!
.createdAt
.compareTo(_lastMessages![a.id]!.createdAt);
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
}
if (_lastMessages!.containsKey(a.id)) return -1;
if (_lastMessages!.containsKey(b.id)) return 1;
@ -249,9 +232,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
children: [
const Icon(Symbols.chat, size: 24),
const Gap(16),
Text('shareIntentSendChannel',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
LoadingIndicator(isActive: _isBusy),
@ -268,34 +249,29 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) {
final otherMember =
channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
return ListTile(
title: Text(
ud.getFromCache(otherMember?.accountId)?.nick ??
channel.name),
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getFromCache(otherMember?.accountId)?.name}',
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content:
ud.getFromCache(otherMember?.accountId)?.avatar,
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
@ -315,7 +291,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
@ -340,20 +316,13 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
},
extra: ChatRoomScreenExtra(
initialText: widget.value
.where((e) => [
SharedMediaType.text,
SharedMediaType.url
].contains(e.type))
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path)
.join('\n'),
initialAttachments: widget.value
.where((e) => [
SharedMediaType.video,
SharedMediaType.file,
SharedMediaType.image
].contains(e.type))
.map(
(e) => PostWriteMedia.fromFile(XFile(e.path)))
.where((e) =>
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
),
)

View File

@ -42,8 +42,7 @@ class AttachmentZoomView extends StatefulWidget {
}
class _AttachmentZoomViewState extends State<AttachmentZoomView> {
late final PageController _pageController =
PageController(initialPage: widget.initialIndex ?? 0);
late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0);
bool _showOverlay = true;
bool _dismissable = true;
@ -108,9 +107,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
if (!mounted) return;
context.showSnackbar(
(!kIsWeb && (Platform.isIOS || Platform.isAndroid))
? 'attachmentSaved'.tr()
: 'attachmentSavedDesktop'.tr(),
(!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? 'attachmentSaved'.tr() : 'attachmentSavedDesktop'.tr(),
action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid))
? SnackBarAction(
label: 'openInAlbum'.tr(),
@ -134,8 +131,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
super.dispose();
}
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@ -154,9 +150,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () {
Navigator.of(context).pop();
},
direction: _dismissable
? DismissiblePageDismissDirection.multi
: DismissiblePageDismissDirection.none,
direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent,
isFullScreen: true,
child: GestureDetector(
@ -171,13 +165,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
return Hero(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
child: PhotoView(
key: Key(
'attachment-detail-${widget.data.first.rid}-$heroTag'),
backgroundDecoration:
BoxDecoration(color: Colors.transparent),
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable =
scaleState == PhotoViewScaleState.initial);
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.first.rid),
@ -190,12 +181,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
pageController: _pageController,
enableRotation: true,
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable =
scaleState == PhotoViewScaleState.initial);
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
builder: (context, idx) {
final heroTag =
widget.heroTags?.elementAt(idx) ?? uuid.v4();
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
@ -211,15 +200,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null
? 0
: event.cumulativeBytesLoaded /
(event.expectedTotalBytes ?? 1),
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
),
),
backgroundDecoration:
BoxDecoration(color: Colors.transparent),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
);
}),
Positioned(
@ -238,8 +223,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onPressed: () {
Navigator.of(context).pop();
},
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.easeInOut),
)
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
),
),
Align(
@ -271,11 +257,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
child: Builder(builder: (context) {
final ud = context.read<UserDirectoryProvider>();
final item = widget.data.elementAt(
widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0,
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
);
final account = ud.getFromCache(item.accountId);
final account = ud.getAccountFromCache(item.accountId);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -293,20 +277,15 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context)
.textTheme
.bodySmall,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context)
.textTheme
.bodyMedium,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
@ -320,13 +299,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
).padding(right: 8),
),
InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(16)),
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0),
: () =>
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
@ -374,8 +351,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] !=
null &&
if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
@ -386,8 +362,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null &&
item.metadata['height'] != null)
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
@ -402,10 +377,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(
widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0),
data: widget.data
.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
@ -413,15 +386,15 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
},
child: Text(
'viewDetailedAttachment'.tr(),
style: metaTextStyle.copyWith(
decoration: TextDecoration.underline),
style: metaTextStyle.copyWith(decoration: TextDecoration.underline),
),
),
],
);
}),
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.easeInOut),
)
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
),
],
),
@ -436,9 +409,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0),
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
@ -458,7 +429,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final account = ud.getFromCache(data.accountId);
final account = ud.getAccountFromCache(data.accountId);
const tableGap = TableRow(
children: [
@ -476,9 +447,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
children: [
const Icon(Symbols.info, size: 24),
const Gap(16),
Text('attachmentDetailInfo')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
@ -492,8 +461,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
TableRow(
children: [
TableCell(
child:
Text('attachmentUploadBy').tr().padding(right: 16),
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
@ -504,13 +472,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
radius: 8,
),
const Gap(8),
Text(data.accountId > 0
? account?.nick ?? 'unknown'.tr()
: 'unknown'.tr()),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}',
style: GoogleFonts.robotoMono())
.opacity(0.75),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
),
),
@ -531,9 +495,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes',
style: GoogleFonts.robotoMono())
.opacity(0.75),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
@ -548,27 +510,19 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(
child: Text(data.hash,
style: GoogleFonts.robotoMono(fontSize: 11))
.opacity(0.9)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
),
tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [
TableCell(child: Text(k).padding(right: 16)),
TableCell(
child: Text(
data.metadata['exif'][k].toString())),
TableCell(child: Text(data.metadata['exif'][k].toString())),
],
)) ??
[]),
],
).padding(
horizontal: 20,
vertical: 8,
bottom: MediaQuery.of(context).padding.bottom),
).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom),
),
),
],

View File

@ -51,7 +51,7 @@ class ChatMessage extends StatelessWidget {
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final ud = context.read<UserDirectoryProvider>();
final user = ud.getFromCache(data.sender.accountId);
final user = ud.getAccountFromCache(data.sender.accountId);
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;

View File

@ -380,7 +380,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
hintText: widget.otherMember != null
? 'fieldChatMessageDirect'.tr(args: [
'@${ud.getFromCache(widget.otherMember?.accountId)?.name}',
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
])
: 'fieldChatMessage'.tr(args: [
widget.controller.channel?.name ?? 'loading'.tr()

View File

@ -33,13 +33,11 @@ class ChatTypingIndicator extends StatelessWidget {
const Icon(Symbols.more_horiz, weight: 600, size: 20),
const Gap(8),
Text(
'messageTyping'
.plural(controller.typingMembers.length, args: [
'messageTyping'.plural(controller.typingMembers.length, args: [
controller.typingMembers
.map((ele) => (ele.nick?.isNotEmpty ?? false)
? ele.nick!
: ud.getFromCache(ele.accountId)?.name ??
'unknown')
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
.join(', '),
]),
),

View File

@ -95,10 +95,9 @@ class OpenablePostItem extends StatelessWidget {
openColor: Colors.transparent,
openElevation: 0,
transitionType: ContainerTransitionType.fade,
closedColor:
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
@ -139,11 +138,9 @@ class PostItem extends StatelessWidget {
final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else {
Share.share(url,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
@ -161,8 +158,7 @@ class PostItem extends StatelessWidget {
child: MultiProvider(
providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(
create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
],
child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@ -190,8 +186,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
await FileSaver.instance.saveFile(
name: 'Solar Network Post #${data.id}.png', file: imageFile);
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
}
await imageFile.delete();
@ -205,9 +200,7 @@ class PostItem extends StatelessWidget {
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
// Video full view
if (showFullPost &&
data.type == 'video' &&
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -227,8 +220,7 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(bottom: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
_PostFeaturedComment(data: data),
_PostBottomAction(
@ -276,8 +268,7 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(horizontal: 12, top: 8, bottom: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
@ -320,13 +311,8 @@ class PostItem extends StatelessWidget {
],
),
),
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostBottomAction(
data: data,
showComments: showComments,
@ -341,8 +327,7 @@ class PostItem extends StatelessWidget {
}
final displayableAttachments = data.preload?.attachments
?.where((ele) =>
ele?.mediaType != SnMediaType.image || data.type != 'article')
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
final cfg = context.read<ConfigProvider>();
@ -367,13 +352,9 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) onDeleted!();
},
).padding(horizontal: 12, vertical: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.type == 'question')
_PostQuestionHint(data: data)
.padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null ||
data.body['description'] != null)
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null || data.body['description'] != null)
_PostHeadline(
data: data,
isEnlarge: data.type == 'article' && showFullPost,
@ -387,8 +368,7 @@ class PostItem extends StatelessWidget {
if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12,
bottom:
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
),
if (data.visibility > 0)
_PostVisibilityHint(data: data).padding(
@ -400,9 +380,7 @@ class PostItem extends StatelessWidget {
horizontal: 16,
vertical: 4,
),
if (data.tags.isNotEmpty)
_PostTagsList(data: data)
.padding(horizontal: 16, top: 4, bottom: 6),
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
],
),
),
@ -415,16 +393,12 @@ class PostItem extends StatelessWidget {
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12),
),
if (data.preload?.poll != null)
PostPoll(poll: data.preload!.poll!)
.padding(horizontal: 12, vertical: 4),
if (data.body['content'] != null &&
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column(
@ -486,8 +460,7 @@ class PostShareImageWidget extends StatelessWidget {
showMenu: false,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type == 'question')
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
@ -502,8 +475,7 @@ class PostShareImageWidget extends StatelessWidget {
child: data.repostTo!,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false))
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList(
data: data.preload!.attachments!,
columned: true,
@ -512,8 +484,7 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true)
_PostTruncatedHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
],
).padding(horizontal: 16),
_PostBottomAction(
@ -573,8 +544,7 @@ class PostShareImageWidget extends StatelessWidget {
version: QrVersions.auto,
size: 100,
gapless: true,
embeddedImage:
AssetImage('assets/icon/icon-light-radius.png'),
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(28, 28),
),
@ -605,11 +575,9 @@ class _PostQuestionHint extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle,
size: 20),
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
const Gap(4),
if (data.body['answer'] == null &&
data.body['reward']?.toDouble() != null)
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
Text('postQuestionUnansweredWithReward'.tr(args: [
'${data.body['reward']}',
])).opacity(0.75)
@ -645,9 +613,7 @@ class _PostBottomAction extends StatelessWidget {
);
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
? data.metric.reactionList.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key
: null;
return Row(
@ -661,8 +627,7 @@ class _PostBottomAction extends StatelessWidget {
InkWell(
child: Row(
children: [
if (mostTypicalReaction == null ||
kTemplateReactions[mostTypicalReaction] == null)
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
Icon(Symbols.add_reaction, size: 20, color: iconColor)
else
Text(
@ -674,8 +639,7 @@ class _PostBottomAction extends StatelessWidget {
),
),
const Gap(8),
if (data.totalUpvote > 0 &&
data.totalUpvote >= data.totalDownvote)
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
Text('postReactionUpvote').plural(
data.totalUpvote,
)
@ -694,12 +658,8 @@ class _PostBottomAction extends StatelessWidget {
data: data,
onChanged: (value, attr, delta) {
onChanged(data.copyWith(
totalUpvote: attr == 1
? data.totalUpvote + delta
: data.totalUpvote,
totalDownvote: attr == 2
? data.totalDownvote + delta
: data.totalDownvote,
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
metric: data.metric.copyWith(reactionList: value),
));
},
@ -806,9 +766,7 @@ class _PostHeadline extends StatelessWidget {
children: [
Text(
'articleWrittenAt'.tr(
args: [
DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())
],
args: [DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())],
),
style: TextStyle(fontSize: 13),
),
@ -816,9 +774,7 @@ class _PostHeadline extends StatelessWidget {
if (data.editedAt != null)
Text(
'articleEditedAt'.tr(
args: [
DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())
],
args: [DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())],
),
style: TextStyle(fontSize: 13),
),
@ -915,9 +871,7 @@ class _PostContentHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final user = data.publisher.type == 0
? ud.getFromCache(data.publisher.accountId)
: null;
final user = data.publisher.type == 0 ? ud.getAccountFromCache(data.publisher.accountId) : null;
return Row(
children: [
@ -928,8 +882,7 @@ class _PostContentHeader extends StatelessWidget {
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ??
Symbols.question_mark,
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
@ -973,10 +926,8 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context).format(
(data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format(
(data.publishedAt ?? data.createdAt).toLocal()),
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
).fontSize(13),
],
).opacity(0.8),
@ -994,10 +945,8 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context).format(
(data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format(
(data.publishedAt ?? data.createdAt).toLocal()),
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
).fontSize(13),
],
).opacity(0.8),
@ -1180,8 +1129,7 @@ class _PostContentBody extends StatelessWidget {
if (data.body['content'] == null) return const SizedBox.shrink();
final content = MarkdownTextContent(
isAutoWarp: data.type == 'story',
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
@ -1230,12 +1178,10 @@ class _PostQuoteContent extends StatelessWidget {
onDeleted: () {},
).padding(bottom: 4),
_PostContentBody(data: child),
if (child.visibility > 0)
_PostVisibilityHint(data: child).padding(top: 4),
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
],
).padding(horizontal: 16),
if (child.type != 'article' &&
(child.preload?.attachments?.isNotEmpty ?? false))
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
@ -1386,9 +1332,7 @@ class _PostTruncatedHint extends StatelessWidget {
const Gap(4),
Text('postReadEstimate').tr(args: [
'${Duration(
seconds: (data.body['content_length'] as num).toDouble() *
60 ~/
kHumanReadSpeed,
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
).inSeconds}s',
]),
],
@ -1427,8 +1371,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
// If this is a answered question, fetch the answer instead
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
_isAnswer = true;
setState(() => _featuredComment = SnPost.fromJson(resp.data));
return;
@ -1436,11 +1379,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/co/posts/${widget.data.id}/replies/featured',
queryParameters: {
'take': 1,
});
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
'take': 1,
});
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
} catch (err) {
if (!mounted) return;
@ -1469,9 +1410,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
width: double.infinity,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: _isAnswer
? Colors.green.withOpacity(0.5)
: Theme.of(context).colorScheme.surfaceContainerHigh,
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
@ -1491,17 +1430,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(2),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
size: 20),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
const Gap(10),
Text(
_isAnswer
? 'postQuestionAnswerTitle'
: 'postFeaturedComment',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 15),
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
).tr(),
],
),
@ -1639,8 +1572,7 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
}
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -1663,16 +1595,11 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
children: [
const Icon(Symbols.book_4_spark, size: 24),
const Gap(16),
Text('postGetInsightTitle',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
const Gap(4),
Text('postGetInsightDescription',
style: Theme.of(context).textTheme.bodySmall)
.tr()
.padding(horizontal: 20),
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
const Gap(4),
if (_response == null)
Expanded(
@ -1690,16 +1617,12 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
leading: const Icon(Symbols.info),
title: Text('aiThinkingProcess'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
minTileHeight: 32,
children: [
SelectableText(
_thinkingProcess!,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
).padding(horizontal: 20, vertical: 8),
],
).padding(vertical: 8),
@ -1736,8 +1659,7 @@ class _PostVideoPlayer extends StatelessWidget {
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem(
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
),
),
);

View File

@ -23,7 +23,7 @@ class PublisherPopoverCard extends StatelessWidget {
final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final user = data.type == 0 ? ud.getFromCache(data.accountId) : null;
final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -85,9 +85,7 @@ class PublisherPopoverCard extends StatelessWidget {
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ??
'unknown'.tr()),
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
@ -148,10 +146,7 @@ class PublisherPopoverCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('publisherTotalDownvote')
.tr()
.fontSize(13)
.opacity(0.75),
Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75),
Text(data.totalDownvote.toString()),
],
),

View File

@ -5,7 +5,6 @@ import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -15,12 +14,10 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
case 3:
return v3.DatabaseAtV3(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3];
static const versions = const [1, 2];
}

File diff suppressed because it is too large Load Diff