✨ User cache
This commit is contained in:
parent
1478933cf1
commit
288c0399f9
@ -314,6 +314,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listSticker();
|
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!');
|
logging.info('[Bootstrap] Everything initialized!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
@ -1,19 +1,36 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.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/providers/sn_network.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
class UserDirectoryProvider {
|
class UserDirectoryProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
UserDirectoryProvider(BuildContext context) {
|
UserDirectoryProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, int> _idCache = {};
|
final Map<String, int> _idCache = {};
|
||||||
final Map<int, SnAccount> _cache = {};
|
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 {
|
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
||||||
|
// In-memory cache
|
||||||
final out = List<SnAccount?>.generate(id.length, (e) => null);
|
final out = List<SnAccount?>.generate(id.length, (e) => null);
|
||||||
final plannedQuery = <int>{};
|
final plannedQuery = <int>{};
|
||||||
for (var idx = 0; idx < out.length; idx++) {
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
@ -27,6 +44,23 @@ class UserDirectoryProvider {
|
|||||||
plannedQuery.add(item);
|
plannedQuery.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
if (plannedQuery.isEmpty) return out;
|
||||||
|
final dbResp = await (_dt.db.snLocalAccount.select()
|
||||||
|
..where((e) => e.id.isIn(plannedQuery))
|
||||||
|
..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;
|
if (plannedQuery.isEmpty) return out;
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client
|
||||||
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
||||||
@ -43,17 +77,28 @@ class UserDirectoryProvider {
|
|||||||
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
|
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
|
||||||
sideIdx++;
|
sideIdx++;
|
||||||
}
|
}
|
||||||
|
if (respDecoded.isNotEmpty) _saveToLocal(respDecoded);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAccount?> getAccount(dynamic id) async {
|
Future<SnAccount?> getAccount(dynamic id) async {
|
||||||
|
// In-memory cache
|
||||||
if (id is String && _idCache.containsKey(id)) {
|
if (id is String && _idCache.containsKey(id)) {
|
||||||
id = _idCache[id];
|
id = _idCache[id];
|
||||||
}
|
}
|
||||||
if (_cache.containsKey(id)) {
|
if (_cache.containsKey(id)) {
|
||||||
return _cache[id];
|
return _cache[id];
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
final dbResp = await (_dt.db.snLocalAccount.select()
|
||||||
|
..where((e) => e.id.equals(id)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (dbResp != null) {
|
||||||
|
_cache[dbResp.id] = dbResp.content;
|
||||||
|
_idCache[dbResp.name] = dbResp.id;
|
||||||
|
return dbResp.content;
|
||||||
|
}
|
||||||
|
// Remote server
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/id/users/$id');
|
final resp = await _sn.client.get('/cgi/id/users/$id');
|
||||||
final account = SnAccount.fromJson(
|
final account = SnAccount.fromJson(
|
||||||
@ -61,16 +106,39 @@ class UserDirectoryProvider {
|
|||||||
);
|
);
|
||||||
_cache[account.id] = account;
|
_cache[account.id] = account;
|
||||||
if (id is String) _idCache[id] = account.id;
|
if (id is String) _idCache[id] = account.id;
|
||||||
|
_saveToLocal([account]);
|
||||||
return account;
|
return account;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SnAccount? getAccountFromCache(dynamic id) {
|
SnAccount? getFromCache(dynamic id) {
|
||||||
if (id is String && _idCache.containsKey(id)) {
|
if (id is String && _idCache.containsKey(id)) {
|
||||||
id = _idCache[id];
|
id = _idCache[id];
|
||||||
}
|
}
|
||||||
return _cache[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,
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalAccountCompanion.custom(
|
||||||
|
name: Constant(ele.name),
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
await Future.wait(queries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,7 +336,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
final title = otherMember != null
|
final title = otherMember != null
|
||||||
? ud.getAccountFromCache(otherMember.accountId)?.nick ?? channel.name
|
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
||||||
: channel.name;
|
: channel.name;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@ -354,10 +354,9 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
Badge(
|
Badge(
|
||||||
label: Text(ud
|
label: Text(
|
||||||
.getAccountFromCache(lastMessage!.sender.accountId)
|
ud.getFromCache(lastMessage!.sender.accountId)?.nick ??
|
||||||
?.nick ??
|
'unknown'.tr()),
|
||||||
'unknown'.tr()),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
@ -400,7 +399,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: otherMember != null
|
content: otherMember != null
|
||||||
? ud.getAccountFromCache(otherMember.accountId)?.avatar
|
? ud.getFromCache(otherMember.accountId)?.avatar
|
||||||
: channel.realm?.avatar,
|
: channel.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
|
@ -289,15 +289,14 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content:
|
content: ud.getFromCache(_profile!.accountId)?.avatar,
|
||||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
|
||||||
radius: 18,
|
radius: 18,
|
||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('channelEditProfile').tr(),
|
title: Text('channelEditProfile').tr(),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
(_profile?.nick?.isEmpty ?? true)
|
(_profile?.nick?.isEmpty ?? true)
|
||||||
? ud.getAccountFromCache(_profile!.accountId)!.nick
|
? ud.getFromCache(_profile!.accountId)!.nick
|
||||||
: _profile!.nick!,
|
: _profile!.nick!,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
@ -575,11 +574,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getFromCache(member.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ??
|
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
'unknown'.tr(),
|
|
||||||
),
|
),
|
||||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
|
@ -277,8 +277,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ??
|
? ud.getFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
|
||||||
_channel!.name
|
|
||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -51,7 +51,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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(
|
_publishers = List<SnPublisher>.from(
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@ -68,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Future<void> _fetchChannels() async {
|
Future<void> _fetchChannels() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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(
|
_channels = List<SnChannel>.from(
|
||||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||||
);
|
);
|
||||||
@ -98,15 +100,32 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle:
|
||||||
|
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(
|
||||||
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
|
icon: Icon(Symbols.home,
|
||||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
color: Theme.of(context)
|
||||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
.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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -115,7 +134,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
},
|
},
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
|
_RealmDetailHomeWidget(
|
||||||
|
realm: _realm, publishers: _publishers, channels: _channels),
|
||||||
_RealmPostListWidget(realm: _realm),
|
_RealmPostListWidget(realm: _realm),
|
||||||
_RealmMemberListWidget(realm: _realm),
|
_RealmMemberListWidget(realm: _realm),
|
||||||
_RealmSettingsWidget(
|
_RealmSettingsWidget(
|
||||||
@ -137,7 +157,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
final List<SnChannel>? channels;
|
final List<SnChannel>? channels;
|
||||||
|
|
||||||
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
|
const _RealmDetailHomeWidget(
|
||||||
|
{required this.realm, this.publishers, this.channels});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -168,7 +189,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
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),
|
.padding(horizontal: 24, vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -199,7 +221,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
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),
|
.padding(horizontal: 24, vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -323,10 +346,12 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
try {
|
try {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
'take': 10,
|
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||||
'offset': _members.length,
|
queryParameters: {
|
||||||
});
|
'take': 10,
|
||||||
|
'offset': _members.length,
|
||||||
|
});
|
||||||
|
|
||||||
final out = List<SnRealmMember>.from(
|
final out = List<SnRealmMember>.from(
|
||||||
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
|
||||||
@ -432,14 +457,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getFromCache(member.accountId)?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Symbols.person_remove),
|
icon: const Icon(Symbols.person_remove),
|
||||||
|
@ -51,8 +51,10 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
leading: Icon(Icons.post_add),
|
leading: Icon(Icons.post_add),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
title: Text('shareIntentPostStory').tr(),
|
title: Text('shareIntentPostStory').tr(),
|
||||||
@ -64,13 +66,20 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
},
|
},
|
||||||
extra: PostEditorExtra(
|
extra: PostEditorExtra(
|
||||||
text: value
|
text: value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [
|
||||||
|
SharedMediaType.text,
|
||||||
|
SharedMediaType.url
|
||||||
|
].contains(e.type))
|
||||||
.map((e) => e.path)
|
.map((e) => e.path)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
attachments: value
|
attachments: value
|
||||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
.where((e) => [
|
||||||
.contains(e.type))
|
SharedMediaType.video,
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
SharedMediaType.file,
|
||||||
|
SharedMediaType.image
|
||||||
|
].contains(e.type))
|
||||||
|
.map((e) =>
|
||||||
|
PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -78,15 +87,18 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
leading: Icon(Icons.chat_outlined),
|
leading: Icon(Icons.chat_outlined),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
title: Text('shareIntentSendChannel').tr(),
|
title: Text('shareIntentSendChannel').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
builder: (context) =>
|
||||||
|
_ShareIntentChannelSelect(value: value),
|
||||||
).then((val) {
|
).then((val) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (val == true) Navigator.pop(context);
|
if (val == true) Navigator.pop(context);
|
||||||
@ -110,7 +122,8 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initialize() async {
|
void _initialize() async {
|
||||||
_shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
_shareIntentSubscription =
|
||||||
|
ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
||||||
if (value.isEmpty) return;
|
if (value.isEmpty) return;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_gotoPost(value);
|
_gotoPost(value);
|
||||||
@ -157,7 +170,8 @@ class _ShareIntentChannelSelect extends StatefulWidget {
|
|||||||
const _ShareIntentChannelSelect({required this.value});
|
const _ShareIntentChannelSelect({required this.value});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
State<_ShareIntentChannelSelect> createState() =>
|
||||||
|
_ShareIntentChannelSelectState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||||
@ -178,8 +192,11 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
final lastMessages = await chan.getLastMessages(channels);
|
final lastMessages = await chan.getLastMessages(channels);
|
||||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||||
channels.sort((a, b) {
|
channels.sort((a, b) {
|
||||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
if (_lastMessages!.containsKey(a.id) &&
|
||||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
_lastMessages!.containsKey(b.id)) {
|
||||||
|
return _lastMessages![b.id]!
|
||||||
|
.createdAt
|
||||||
|
.compareTo(_lastMessages![a.id]!.createdAt);
|
||||||
}
|
}
|
||||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||||
@ -232,7 +249,9 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.chat, size: 24),
|
const Icon(Symbols.chat, size: 24),
|
||||||
const Gap(16),
|
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),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
@ -249,29 +268,34 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
final lastMessage = _lastMessages?[channel.id];
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
final otherMember =
|
||||||
(ele) => ele?.accountId != ua.user?.id,
|
channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
orElse: () => null,
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
);
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
title: Text(
|
||||||
|
ud.getFromCache(otherMember?.accountId)?.nick ??
|
||||||
|
channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
'channelDirectMessageDescription'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
'@${ud.getFromCache(otherMember?.accountId)?.name}',
|
||||||
]),
|
]),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
content:
|
||||||
|
ud.getFromCache(otherMember?.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
@ -291,7 +315,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
title: Text(channel.name),
|
title: Text(channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
@ -316,13 +340,20 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
},
|
},
|
||||||
extra: ChatRoomScreenExtra(
|
extra: ChatRoomScreenExtra(
|
||||||
initialText: widget.value
|
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)
|
.map((e) => e.path)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
initialAttachments: widget.value
|
initialAttachments: widget.value
|
||||||
.where((e) =>
|
.where((e) => [
|
||||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
SharedMediaType.video,
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
SharedMediaType.file,
|
||||||
|
SharedMediaType.image
|
||||||
|
].contains(e.type))
|
||||||
|
.map(
|
||||||
|
(e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -42,7 +42,8 @@ class AttachmentZoomView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
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 _showOverlay = true;
|
||||||
bool _dismissable = true;
|
bool _dismissable = true;
|
||||||
@ -107,7 +108,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
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))
|
action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid))
|
||||||
? SnackBarAction(
|
? SnackBarAction(
|
||||||
label: 'openInAlbum'.tr(),
|
label: 'openInAlbum'.tr(),
|
||||||
@ -131,7 +134,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
super.dispose();
|
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;
|
bool _showDetail = false;
|
||||||
|
|
||||||
@ -150,7 +154,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none,
|
direction: _dismissable
|
||||||
|
? DismissiblePageDismissDirection.multi
|
||||||
|
: DismissiblePageDismissDirection.none,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -165,10 +171,13 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
key: Key(
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
'attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||||
|
backgroundDecoration:
|
||||||
|
BoxDecoration(color: Colors.transparent),
|
||||||
scaleStateChangedCallback: (scaleState) {
|
scaleStateChangedCallback: (scaleState) {
|
||||||
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
|
setState(() => _dismissable =
|
||||||
|
scaleState == PhotoViewScaleState.initial);
|
||||||
},
|
},
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
sn.getAttachmentUrl(widget.data.first.rid),
|
||||||
@ -181,10 +190,12 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
enableRotation: true,
|
enableRotation: true,
|
||||||
scaleStateChangedCallback: (scaleState) {
|
scaleStateChangedCallback: (scaleState) {
|
||||||
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
|
setState(() => _dismissable =
|
||||||
|
scaleState == PhotoViewScaleState.initial);
|
||||||
},
|
},
|
||||||
builder: (context, idx) {
|
builder: (context, idx) {
|
||||||
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
final heroTag =
|
||||||
|
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||||
@ -200,11 +211,15 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
width: 20.0,
|
width: 20.0,
|
||||||
height: 20.0,
|
height: 20.0,
|
||||||
child: CircularProgressIndicator(
|
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(
|
Positioned(
|
||||||
@ -223,9 +238,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
)
|
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
|
||||||
.opacity(_showOverlay ? 1 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
@ -257,9 +271,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final item = widget.data.elementAt(
|
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.getAccountFromCache(item.accountId);
|
final account = ud.getFromCache(item.accountId);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -277,15 +293,20 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'attachmentUploadBy'.tr(),
|
'attachmentUploadBy'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
account?.nick ?? 'unknown'.tr(),
|
account?.nick ?? 'unknown'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -299,11 +320,13 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
).padding(right: 8),
|
).padding(right: 8),
|
||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
onTap: _isDownloading
|
onTap: _isDownloading
|
||||||
? null
|
? null
|
||||||
: () =>
|
: () => _saveToAlbum(widget.data.length > 1
|
||||||
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
? _pageController.page?.round() ?? 0
|
||||||
|
: 0),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: !_isDownloading
|
child: !_isDownloading
|
||||||
@ -351,7 +374,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
]),
|
]),
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
).padding(right: 2),
|
).padding(right: 2),
|
||||||
if (item.metadata['exif']?['Megapixels'] != null &&
|
if (item.metadata['exif']?['Megapixels'] !=
|
||||||
|
null &&
|
||||||
item.metadata['exif']?['Model'] != null)
|
item.metadata['exif']?['Model'] != null)
|
||||||
Text(
|
Text(
|
||||||
'${item.metadata['exif']?['Megapixels']}MP',
|
'${item.metadata['exif']?['Megapixels']}MP',
|
||||||
@ -362,7 +386,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
item.size.formatBytes(),
|
item.size.formatBytes(),
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
),
|
),
|
||||||
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
if (item.metadata['width'] != null &&
|
||||||
|
item.metadata['height'] != null)
|
||||||
Text(
|
Text(
|
||||||
'${item.metadata['width']}x${item.metadata['height']}',
|
'${item.metadata['width']}x${item.metadata['height']}',
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
@ -377,8 +402,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
builder: (context) => _AttachmentZoomDetailPopup(
|
||||||
data: widget.data
|
data: widget.data.elementAt(
|
||||||
.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
widget.data.length > 1
|
||||||
|
? _pageController.page?.round() ?? 0
|
||||||
|
: 0),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_showDetail = false;
|
_showDetail = false;
|
||||||
@ -386,15 +413,15 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'viewDetailedAttachment'.tr(),
|
'viewDetailedAttachment'.tr(),
|
||||||
style: metaTextStyle.copyWith(decoration: TextDecoration.underline),
|
style: metaTextStyle.copyWith(
|
||||||
|
decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
|
||||||
.opacity(_showOverlay ? 1 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -409,7 +436,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
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((_) {
|
).then((_) {
|
||||||
_showDetail = false;
|
_showDetail = false;
|
||||||
@ -429,7 +458,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final account = ud.getAccountFromCache(data.accountId);
|
final account = ud.getFromCache(data.accountId);
|
||||||
|
|
||||||
const tableGap = TableRow(
|
const tableGap = TableRow(
|
||||||
children: [
|
children: [
|
||||||
@ -447,7 +476,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.info, size: 24),
|
const Icon(Symbols.info, size: 24),
|
||||||
const Gap(16),
|
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),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -461,7 +492,8 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
TableRow(
|
TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(
|
TableCell(
|
||||||
child: Text('attachmentUploadBy').tr().padding(right: 16),
|
child:
|
||||||
|
Text('attachmentUploadBy').tr().padding(right: 16),
|
||||||
),
|
),
|
||||||
TableCell(
|
TableCell(
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -472,9 +504,13 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
radius: 8,
|
radius: 8,
|
||||||
),
|
),
|
||||||
const Gap(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),
|
const Gap(8),
|
||||||
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
|
Text('#${data.accountId}',
|
||||||
|
style: GoogleFonts.robotoMono())
|
||||||
|
.opacity(0.75),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -495,7 +531,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(data.size.formatBytes()),
|
Text(data.size.formatBytes()),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
|
Text('${data.size} Bytes',
|
||||||
|
style: GoogleFonts.robotoMono())
|
||||||
|
.opacity(0.75),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
@ -510,19 +548,27 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
TableRow(
|
TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(child: Text('Hash').padding(right: 16)),
|
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,
|
tableGap,
|
||||||
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(child: Text(k).padding(right: 16)),
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -51,7 +51,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final user = ud.getAccountFromCache(data.sender.accountId);
|
final user = ud.getFromCache(data.sender.accountId);
|
||||||
|
|
||||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||||
|
|
||||||
|
@ -380,7 +380,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
||||||
hintText: widget.otherMember != null
|
hintText: widget.otherMember != null
|
||||||
? 'fieldChatMessageDirect'.tr(args: [
|
? 'fieldChatMessageDirect'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
'@${ud.getFromCache(widget.otherMember?.accountId)?.name}',
|
||||||
])
|
])
|
||||||
: 'fieldChatMessage'.tr(args: [
|
: 'fieldChatMessage'.tr(args: [
|
||||||
widget.controller.channel?.name ?? 'loading'.tr()
|
widget.controller.channel?.name ?? 'loading'.tr()
|
||||||
|
@ -33,11 +33,13 @@ class ChatTypingIndicator extends StatelessWidget {
|
|||||||
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
'messageTyping'.plural(controller.typingMembers.length, args: [
|
'messageTyping'
|
||||||
|
.plural(controller.typingMembers.length, args: [
|
||||||
controller.typingMembers
|
controller.typingMembers
|
||||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
? ele.nick!
|
? ele.nick!
|
||||||
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
: ud.getFromCache(ele.accountId)?.name ??
|
||||||
|
'unknown')
|
||||||
.join(', '),
|
.join(', '),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -95,9 +95,10 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
openColor: Colors.transparent,
|
openColor: Colors.transparent,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
transitionType: ContainerTransitionType.fade,
|
transitionType: ContainerTransitionType.fade,
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
closedColor:
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||||
),
|
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||||
|
),
|
||||||
closedShape: const RoundedRectangleBorder(
|
closedShape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@ -138,9 +139,11 @@ class PostItem extends StatelessWidget {
|
|||||||
final box = context.findRenderObject() as RenderBox?;
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
final url = 'https://solsynth.dev/posts/${data.id}';
|
final url = 'https://solsynth.dev/posts/${data.id}';
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
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 {
|
} else {
|
||||||
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
Share.share(url,
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +161,8 @@ class PostItem extends StatelessWidget {
|
|||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
ChangeNotifierProvider<ConfigProvider>(
|
||||||
|
create: (_) => context.read()),
|
||||||
],
|
],
|
||||||
child: ResponsiveBreakpoints.builder(
|
child: ResponsiveBreakpoints.builder(
|
||||||
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
||||||
@ -186,7 +190,8 @@ class PostItem extends StatelessWidget {
|
|||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
);
|
);
|
||||||
} else {
|
} 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();
|
await imageFile.delete();
|
||||||
@ -200,7 +205,9 @@ class PostItem extends StatelessWidget {
|
|||||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||||
|
|
||||||
// Video full view
|
// 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(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -220,7 +227,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) {}
|
if (onDeleted != null) {}
|
||||||
},
|
},
|
||||||
).padding(bottom: 8),
|
).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),
|
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||||
_PostFeaturedComment(data: data),
|
_PostFeaturedComment(data: data),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@ -268,7 +276,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) {}
|
if (onDeleted != null) {}
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
).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(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||||
@ -311,8 +320,13 @@ class PostItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
Text('postArticle')
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75)
|
||||||
|
.padding(horizontal: 24, bottom: 8),
|
||||||
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
showComments: showComments,
|
||||||
@ -327,7 +341,8 @@ class PostItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final displayableAttachments = data.preload?.attachments
|
final displayableAttachments = data.preload?.attachments
|
||||||
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
|
?.where((ele) =>
|
||||||
|
ele?.mediaType != SnMediaType.image || data.type != 'article')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@ -352,9 +367,13 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) onDeleted!();
|
if (onDeleted != null) onDeleted!();
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, vertical: 8),
|
).padding(horizontal: 12, vertical: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
if (data.preload?.video != null)
|
||||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
if (data.body['title'] != null || data.body['description'] != null)
|
if (data.type == 'question')
|
||||||
|
_PostQuestionHint(data: data)
|
||||||
|
.padding(horizontal: 16, bottom: 8),
|
||||||
|
if (data.body['title'] != null ||
|
||||||
|
data.body['description'] != null)
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
@ -368,7 +387,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(child: data.repostTo!).padding(
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
bottom:
|
||||||
|
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
||||||
),
|
),
|
||||||
if (data.visibility > 0)
|
if (data.visibility > 0)
|
||||||
_PostVisibilityHint(data: data).padding(
|
_PostVisibilityHint(data: data).padding(
|
||||||
@ -380,7 +400,9 @@ class PostItem extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -393,12 +415,16 @@ class PostItem extends StatelessWidget {
|
|||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
if (data.preload?.poll != null)
|
||||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
PostPoll(poll: data.preload!.poll!)
|
||||||
|
.padding(horizontal: 12, vertical: 4),
|
||||||
|
if (data.body['content'] != null &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -460,7 +486,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).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(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article',
|
isEnlarge: data.type == 'article',
|
||||||
@ -475,7 +502,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).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(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
columned: true,
|
columned: true,
|
||||||
@ -484,7 +512,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
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),
|
).padding(horizontal: 16),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@ -544,7 +573,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
version: QrVersions.auto,
|
version: QrVersions.auto,
|
||||||
size: 100,
|
size: 100,
|
||||||
gapless: true,
|
gapless: true,
|
||||||
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
|
embeddedImage:
|
||||||
|
AssetImage('assets/icon/icon-light-radius.png'),
|
||||||
embeddedImageStyle: QrEmbeddedImageStyle(
|
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||||
size: Size(28, 28),
|
size: Size(28, 28),
|
||||||
),
|
),
|
||||||
@ -575,9 +605,11 @@ class _PostQuestionHint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
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),
|
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: [
|
Text('postQuestionUnansweredWithReward'.tr(args: [
|
||||||
'${data.body['reward']}',
|
'${data.body['reward']}',
|
||||||
])).opacity(0.75)
|
])).opacity(0.75)
|
||||||
@ -613,7 +645,9 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
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;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@ -627,7 +661,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
InkWell(
|
InkWell(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
|
if (mostTypicalReaction == null ||
|
||||||
|
kTemplateReactions[mostTypicalReaction] == null)
|
||||||
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
@ -639,7 +674,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
|
if (data.totalUpvote > 0 &&
|
||||||
|
data.totalUpvote >= data.totalDownvote)
|
||||||
Text('postReactionUpvote').plural(
|
Text('postReactionUpvote').plural(
|
||||||
data.totalUpvote,
|
data.totalUpvote,
|
||||||
)
|
)
|
||||||
@ -658,8 +694,12 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
data: data,
|
data: data,
|
||||||
onChanged: (value, attr, delta) {
|
onChanged: (value, attr, delta) {
|
||||||
onChanged(data.copyWith(
|
onChanged(data.copyWith(
|
||||||
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
|
totalUpvote: attr == 1
|
||||||
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
|
? data.totalUpvote + delta
|
||||||
|
: data.totalUpvote,
|
||||||
|
totalDownvote: attr == 2
|
||||||
|
? data.totalDownvote + delta
|
||||||
|
: data.totalDownvote,
|
||||||
metric: data.metric.copyWith(reactionList: value),
|
metric: data.metric.copyWith(reactionList: value),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -766,7 +806,9 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'articleWrittenAt'.tr(
|
'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),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
@ -774,7 +816,9 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
if (data.editedAt != null)
|
if (data.editedAt != null)
|
||||||
Text(
|
Text(
|
||||||
'articleEditedAt'.tr(
|
'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),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
@ -871,7 +915,9 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final user = data.publisher.type == 0 ? ud.getAccountFromCache(data.publisher.accountId) : null;
|
final user = data.publisher.type == 0
|
||||||
|
? ud.getFromCache(data.publisher.accountId)
|
||||||
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@ -882,7 +928,8 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||||
badge: (user?.badges.isNotEmpty ?? false)
|
badge: (user?.badges.isNotEmpty ?? false)
|
||||||
? Icon(
|
? 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,
|
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
size: 18,
|
size: 18,
|
||||||
@ -926,8 +973,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
|
? RelativeTime(context).format(
|
||||||
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
|
(data.publishedAt ?? data.createdAt).toLocal())
|
||||||
|
: DateFormat('y/M/d HH:mm').format(
|
||||||
|
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@ -945,8 +994,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
|
? RelativeTime(context).format(
|
||||||
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
|
(data.publishedAt ?? data.createdAt).toLocal())
|
||||||
|
: DateFormat('y/M/d HH:mm').format(
|
||||||
|
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@ -1129,7 +1180,8 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||||
final content = MarkdownTextContent(
|
final content = MarkdownTextContent(
|
||||||
isAutoWarp: data.type == 'story',
|
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,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: data.body['content'],
|
content: data.body['content'],
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.preload?.attachments,
|
||||||
@ -1178,10 +1230,12 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
onDeleted: () {},
|
onDeleted: () {},
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
_PostContentBody(data: child),
|
_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),
|
).padding(horizontal: 16),
|
||||||
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
|
if (child.type != 'article' &&
|
||||||
|
(child.preload?.attachments?.isNotEmpty ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(8),
|
bottomLeft: Radius.circular(8),
|
||||||
@ -1332,7 +1386,9 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postReadEstimate').tr(args: [
|
Text('postReadEstimate').tr(args: [
|
||||||
'${Duration(
|
'${Duration(
|
||||||
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
|
seconds: (data.body['content_length'] as num).toDouble() *
|
||||||
|
60 ~/
|
||||||
|
kHumanReadSpeed,
|
||||||
).inSeconds}s',
|
).inSeconds}s',
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
@ -1371,7 +1427,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
// If this is a answered question, fetch the answer instead
|
// If this is a answered question, fetch the answer instead
|
||||||
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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;
|
_isAnswer = true;
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
||||||
return;
|
return;
|
||||||
@ -1379,9 +1436,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
'take': 1,
|
'/cgi/co/posts/${widget.data.id}/replies/featured',
|
||||||
});
|
queryParameters: {
|
||||||
|
'take': 1,
|
||||||
|
});
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -1410,7 +1469,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
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(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -1430,11 +1491,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Gap(2),
|
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),
|
const Gap(10),
|
||||||
Text(
|
Text(
|
||||||
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
|
_isAnswer
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
|
? 'postQuestionAnswerTitle'
|
||||||
|
: 'postFeaturedComment',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontSize: 15),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1572,7 +1639,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
||||||
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
setState(
|
||||||
|
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -1595,11 +1663,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.book_4_spark, size: 24),
|
const Icon(Symbols.book_4_spark, size: 24),
|
||||||
const Gap(16),
|
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),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
const Gap(4),
|
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),
|
const Gap(4),
|
||||||
if (_response == null)
|
if (_response == null)
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -1617,12 +1690,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
leading: const Icon(Symbols.info),
|
leading: const Icon(Symbols.info),
|
||||||
title: Text('aiThinkingProcess'.tr()),
|
title: Text('aiThinkingProcess'.tr()),
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
collapsedBackgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
minTileHeight: 32,
|
minTileHeight: 32,
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
SelectableText(
|
||||||
_thinkingProcess!,
|
_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(horizontal: 20, vertical: 8),
|
||||||
],
|
],
|
||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
@ -1659,7 +1736,8 @@ class _PostVideoPlayer extends StatelessWidget {
|
|||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
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}'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,7 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null;
|
final user = data.type == 0 ? ud.getFromCache(data.accountId) : null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -85,7 +85,9 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
(ele) => Tooltip(
|
(ele) => Tooltip(
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
TextSpan(
|
||||||
|
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
||||||
|
'unknown'.tr()),
|
||||||
if (ele.metadata['title'] != null)
|
if (ele.metadata['title'] != null)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '\n${ele.metadata['title']}',
|
text: '\n${ele.metadata['title']}',
|
||||||
@ -146,7 +148,10 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75),
|
Text('publisherTotalDownvote')
|
||||||
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75),
|
||||||
Text(data.totalDownvote.toString()),
|
Text(data.totalDownvote.toString()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user