✨ User cache
This commit is contained in:
		| @@ -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()), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user