Compare commits
	
		
			6 Commits
		
	
	
		
			46919dec31
			...
			3.0.0+111
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | 
| @@ -375,7 +375,9 @@ | ||||
|   "postContent": "Content", | ||||
|   "postSettings": "Settings", | ||||
|   "postPublisherUnselected": "Publisher Unspecified", | ||||
|   "postVisibility": "Visibility", | ||||
|   "postType": "Post Type", | ||||
|   "articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.", | ||||
|   "postVisibility": "Post Visibility", | ||||
|   "postVisibilityPublic": "Public", | ||||
|   "postVisibilityFriends": "Friends Only", | ||||
|   "postVisibilityUnlisted": "Unlisted", | ||||
|   | ||||
| @@ -532,7 +532,7 @@ | ||||
|   "aboutScreenContactUsTitle": "联系我们", | ||||
|   "aboutScreenLicenseTitle": "许可证", | ||||
|   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", | ||||
|   "aboutScreenCopyright": "版权所有 © Solsynth {}", | ||||
|   "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}", | ||||
|   "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", | ||||
|   "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}", | ||||
|   "copiedToClipboard": "已复制到剪贴板", | ||||
|   | ||||
| @@ -221,7 +221,7 @@ class IslandApp extends HookConsumerWidget { | ||||
|       Future(() { | ||||
|         userNotifier.fetchUser().then((_) { | ||||
|           final user = ref.watch(userInfoProvider); | ||||
|           if (user.hasValue) { | ||||
|           if (user.value != null) { | ||||
|             final apiClient = ref.read(apiClientProvider); | ||||
|             subscribePushNotification(apiClient); | ||||
|             final wsNotifier = ref.read(websocketStateProvider.notifier); | ||||
|   | ||||
| @@ -18,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|       final user = SnAccount.fromJson(response.data); | ||||
|       state = AsyncValue.data(user); | ||||
|     } catch (error, stackTrace) { | ||||
|       log("[UserInfo] Failed to fetch user info: $error"); | ||||
|       state = AsyncValue.error(error, stackTrace); | ||||
|       log( | ||||
|         "[UserInfo] Failed to fetch user info...", | ||||
|         name: 'UserInfoNotifier', | ||||
|         error: error, | ||||
|         stackTrace: stackTrace, | ||||
|       ); | ||||
|       state = AsyncValue.data(null); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|       notificationUnreadCountNotifierProvider, | ||||
|     ); | ||||
|  | ||||
|     if (!user.hasValue || user.value == null) { | ||||
|     if (user.value == null || user.value == null) { | ||||
|       return _UnauthorizedAccountScreen(); | ||||
|     } | ||||
|  | ||||
| @@ -367,12 +367,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(8), | ||||
|                 TextButton( | ||||
|                   onPressed: () { | ||||
|                     context.push('/settings'); | ||||
|                   }, | ||||
|                   child: Text('appSettings').tr(), | ||||
|                 ).center(), | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     TextButton( | ||||
|                       onPressed: () { | ||||
|                         context.push('/about'); | ||||
|                       }, | ||||
|                       child: Text('about').tr(), | ||||
|                     ), | ||||
|                     TextButton( | ||||
|                       onPressed: () { | ||||
|                         context.push('/settings'); | ||||
|                       }, | ||||
|                       child: Text('appSettings').tr(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ).center(), | ||||
|   | ||||
| @@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget { | ||||
|                       ), | ||||
|  | ||||
|                       // Show user profile if viewing someone else's calendar | ||||
|                       if (name != 'me' && user.hasValue) | ||||
|                       if (name != 'me' && user.value != null) | ||||
|                         AccountNameplate(name: name), | ||||
|                     ], | ||||
|                   ), | ||||
| @@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget { | ||||
|                     ).padding(horizontal: 8, vertical: 4), | ||||
|  | ||||
|                     // Show user profile if viewing someone else's calendar | ||||
|                     if (name != 'me' && user.hasValue) | ||||
|                     if (name != 'me' && user.value != null) | ||||
|                       AccountNameplate(name: name), | ||||
|                     Gap(MediaQuery.of(context).padding.bottom + 16), | ||||
|                   ], | ||||
|   | ||||
| @@ -72,6 +72,8 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | ||||
|  | ||||
| @riverpod | ||||
| Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
| @@ -87,6 +89,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | ||||
|  | ||||
| @riverpod | ||||
| Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
| @@ -219,6 +223,8 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|  | ||||
|     return account.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
| @@ -379,56 +385,60 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                   ).padding(horizontal: 24), | ||||
|                 ), | ||||
|  | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(top: 24, bottom: 12), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       Expanded( | ||||
|                         child: FilledButton.icon( | ||||
|                           style: ButtonStyle( | ||||
|                             backgroundColor: WidgetStatePropertyAll( | ||||
|                               accountRelationship.value == null | ||||
|                                   ? null | ||||
|                                   : Theme.of(context).colorScheme.secondary, | ||||
|                             ), | ||||
|                             foregroundColor: WidgetStatePropertyAll( | ||||
|                               accountRelationship.value == null | ||||
|                                   ? null | ||||
|                                   : Theme.of(context).colorScheme.onSecondary, | ||||
|                             ), | ||||
|                           ), | ||||
|                           onPressed: relationshipAction, | ||||
|                           label: | ||||
|                               Text( | ||||
|                 if (user.value != null) | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: const Divider( | ||||
|                       height: 1, | ||||
|                     ).padding(top: 24, bottom: 12), | ||||
|                   ), | ||||
|                 if (user.value != null) | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: Row( | ||||
|                       spacing: 8, | ||||
|                       children: [ | ||||
|                         Expanded( | ||||
|                           child: FilledButton.icon( | ||||
|                             style: ButtonStyle( | ||||
|                               backgroundColor: WidgetStatePropertyAll( | ||||
|                                 accountRelationship.value == null | ||||
|                                     ? 'addFriendShort' | ||||
|                                     : 'added', | ||||
|                               ).tr(), | ||||
|                           icon: | ||||
|                               accountRelationship.value == null | ||||
|                                   ? const Icon(Symbols.person_add) | ||||
|                                   : const Icon(Symbols.person_check), | ||||
|                                     ? null | ||||
|                                     : Theme.of(context).colorScheme.secondary, | ||||
|                               ), | ||||
|                               foregroundColor: WidgetStatePropertyAll( | ||||
|                                 accountRelationship.value == null | ||||
|                                     ? null | ||||
|                                     : Theme.of(context).colorScheme.onSecondary, | ||||
|                               ), | ||||
|                             ), | ||||
|                             onPressed: relationshipAction, | ||||
|                             label: | ||||
|                                 Text( | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? 'addFriendShort' | ||||
|                                       : 'added', | ||||
|                                 ).tr(), | ||||
|                             icon: | ||||
|                                 accountRelationship.value == null | ||||
|                                     ? const Icon(Symbols.person_add) | ||||
|                                     : const Icon(Symbols.person_check), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                       Expanded( | ||||
|                         child: FilledButton.icon( | ||||
|                           onPressed: directMessageAction, | ||||
|                           icon: const Icon(Symbols.message), | ||||
|                           label: | ||||
|                               Text( | ||||
|                                 accountChat.value == null | ||||
|                                     ? 'createDirectMessage' | ||||
|                                     : 'gotoDirectMessage', | ||||
|                                 maxLines: 1, | ||||
|                               ).tr(), | ||||
|                         Expanded( | ||||
|                           child: FilledButton.icon( | ||||
|                             onPressed: directMessageAction, | ||||
|                             icon: const Icon(Symbols.message), | ||||
|                             label: | ||||
|                                 Text( | ||||
|                                   accountChat.value == null | ||||
|                                       ? 'createDirectMessage' | ||||
|                                       : 'gotoDirectMessage', | ||||
|                                   maxLines: 1, | ||||
|                                 ).tr(), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 16), | ||||
|                 ), | ||||
|                       ], | ||||
|                     ).padding(horizontal: 16), | ||||
|                   ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(top: 12), | ||||
|                 ), | ||||
|   | ||||
| @@ -51,54 +51,59 @@ class _ArticleDetailContent extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     return SingleChildScrollView( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           if (article.preview?.imageUrl != null) | ||||
|             Image.network( | ||||
|               article.preview!.imageUrl!, | ||||
|               width: double.infinity, | ||||
|               height: 200, | ||||
|               fit: BoxFit.cover, | ||||
|             ), | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(16.0), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text( | ||||
|                   article.title, | ||||
|                   style: Theme.of(context).textTheme.headlineSmall, | ||||
|       child: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               if (article.preview?.imageUrl != null) | ||||
|                 Image.network( | ||||
|                   article.preview!.imageUrl!, | ||||
|                   width: double.infinity, | ||||
|                   height: 200, | ||||
|                   fit: BoxFit.cover, | ||||
|                 ), | ||||
|                 const SizedBox(height: 8), | ||||
|                 if (article.feed?.title != null) | ||||
|                   Text( | ||||
|                     article.feed!.title, | ||||
|                     style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                       color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.all(16.0), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     Text( | ||||
|                       article.title, | ||||
|                       style: Theme.of(context).textTheme.headlineSmall, | ||||
|                     ), | ||||
|                   ), | ||||
|                 const Divider(height: 32), | ||||
|                 if (article.content != null) | ||||
|                   ...MarkdownTextContent.buildGenerator( | ||||
|                     isDark: Theme.of(context).brightness == Brightness.dark, | ||||
|                   ).buildWidgets(markdownContent) | ||||
|                 else if (article.preview?.description != null) | ||||
|                   Text(article.preview!.description!), | ||||
|                 const Gap(24), | ||||
|                 FilledButton( | ||||
|                   onPressed: | ||||
|                       () => launchUrlString( | ||||
|                         article.url, | ||||
|                         mode: LaunchMode.externalApplication, | ||||
|                     const SizedBox(height: 8), | ||||
|                     if (article.feed?.title != null) | ||||
|                       Text( | ||||
|                         article.feed!.title, | ||||
|                         style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                         ), | ||||
|                       ), | ||||
|                   child: const Text('Read Full Article'), | ||||
|                     const Divider(height: 32), | ||||
|                     if (article.content != null) | ||||
|                       ...MarkdownTextContent.buildGenerator( | ||||
|                         isDark: Theme.of(context).brightness == Brightness.dark, | ||||
|                       ).buildWidgets(markdownContent) | ||||
|                     else if (article.preview?.description != null) | ||||
|                       Text(article.preview!.description!), | ||||
|                     const Gap(24), | ||||
|                     FilledButton( | ||||
|                       onPressed: | ||||
|                           () => launchUrlString( | ||||
|                             article.url, | ||||
|                             mode: LaunchMode.externalApplication, | ||||
|                           ), | ||||
|                       child: const Text('Read Full Article'), | ||||
|                     ), | ||||
|                     Gap(MediaQuery.of(context).padding.bottom), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Gap(MediaQuery.of(context).padding.bottom), | ||||
|               ], | ||||
|             ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -126,16 +126,21 @@ class ArticlesScreen extends ConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||
|       body: CustomScrollView( | ||||
|         slivers: [ | ||||
|           SliverPadding( | ||||
|             padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|             sliver: SliverArticlesList( | ||||
|               feedId: feedId, | ||||
|               publisherId: publisherId, | ||||
|             ), | ||||
|       body: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverPadding( | ||||
|                 padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|                 sliver: SliverArticlesList( | ||||
|                   feedId: feedId, | ||||
|                   publisherId: publisherId, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -307,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|  | ||||
|     return CustomScrollView( | ||||
|       slivers: [ | ||||
|         if (user.hasValue && !contentOnly) | ||||
|         if (user.value != null && !contentOnly) | ||||
|           SliverToBoxAdapter(child: CheckInWidget()), | ||||
|         SliverList.builder( | ||||
|           itemCount: widgetCount, | ||||
|   | ||||
| @@ -321,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|             builder: (context, attachments, _) { | ||||
|               if (attachments.isEmpty) return const SizedBox.shrink(); | ||||
|               return Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   const Gap(16), | ||||
|                   Text( | ||||
|                     'articleAttachmentHint'.tr(), | ||||
|                     style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||
|                       color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                     ), | ||||
|                   ).padding(bottom: 8), | ||||
|                   ValueListenableBuilder<Map<int, double>>( | ||||
|                     valueListenable: state.attachmentProgress, | ||||
|                     builder: (context, progressMap, _) { | ||||
| @@ -332,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                         children: [ | ||||
|                           for (var idx = 0; idx < attachments.length; idx++) | ||||
|                             SizedBox( | ||||
|                               width: 120, | ||||
|                               height: 120, | ||||
|                               width: 280, | ||||
|                               height: 280, | ||||
|                               child: AttachmentPreview( | ||||
|                                 item: attachments[idx], | ||||
|                                 progress: progressMap[idx], | ||||
| @@ -358,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                                     delta, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                                 onInsert: | ||||
|                                     () => ComposeLogic.insertAttachment( | ||||
|                                       ref, | ||||
|                                       state, | ||||
|                                       idx, | ||||
|                                     ), | ||||
|                               ), | ||||
|                             ), | ||||
|                         ], | ||||
|   | ||||
| @@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget { | ||||
|           }, | ||||
|           options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (user.hasValue) { | ||||
|         if (user.value != null) { | ||||
|           ref.invalidate(accountStatusProvider(user.value!.name)); | ||||
|         } | ||||
|         if (!context.mounted) return; | ||||
|   | ||||
| @@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget { | ||||
|     return AnimatedPositioned( | ||||
|       duration: Duration(milliseconds: 1850), | ||||
|       top: | ||||
|           !user.hasValue || | ||||
|           user.value == null || | ||||
|                   user.value == null || | ||||
|                   websocketState == WebSocketState.connected() | ||||
|               ? -indicatorHeight | ||||
| @@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget { | ||||
|       child: IgnorePointer( | ||||
|         child: Material( | ||||
|           elevation: | ||||
|               !user.hasValue || websocketState == WebSocketState.connected() | ||||
|               user.value == null || websocketState == WebSocketState.connected() | ||||
|                   ? 0 | ||||
|                   : 4, | ||||
|           child: AnimatedContainer( | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget { | ||||
|   final double? progress; | ||||
|   final Function(int)? onMove; | ||||
|   final Function? onDelete; | ||||
|   final Function? onInsert; | ||||
|   final Function? onRequestUpload; | ||||
|   const AttachmentPreview({ | ||||
|     super.key, | ||||
| @@ -23,6 +24,7 @@ class AttachmentPreview extends StatelessWidget { | ||||
|     this.onRequestUpload, | ||||
|     this.onMove, | ||||
|     this.onDelete, | ||||
|     this.onInsert, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -104,7 +106,11 @@ class AttachmentPreview extends StatelessWidget { | ||||
|                           style: TextStyle(color: Colors.white), | ||||
|                         ), | ||||
|                       Gap(6), | ||||
|                       Center(child: LinearProgressIndicator(value: progress)), | ||||
|                       Center( | ||||
|                         child: LinearProgressIndicator( | ||||
|                           value: progress != null ? progress! / 100.0 : null, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
| @@ -166,6 +172,18 @@ class AttachmentPreview extends StatelessWidget { | ||||
|                               onMove?.call(1); | ||||
|                             }, | ||||
|                           ), | ||||
|                         if (onInsert != null) | ||||
|                           InkWell( | ||||
|                             borderRadius: BorderRadius.circular(8), | ||||
|                             child: const Icon( | ||||
|                               Symbols.add, | ||||
|                               size: 14, | ||||
|                               color: Colors.white, | ||||
|                             ).padding(horizontal: 8, vertical: 6), | ||||
|                             onTap: () { | ||||
|                               onInsert?.call(); | ||||
|                             }, | ||||
|                           ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| @@ -6,11 +7,14 @@ import 'package:flutter_highlight/themes/a11y-dark.dart'; | ||||
| import 'package:flutter_highlight/themes/a11y-light.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/markdown_latex.dart'; | ||||
| import 'package:markdown/markdown.dart' as markdown; | ||||
| import 'package:markdown_widget/markdown_widget.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| import 'image.dart'; | ||||
| @@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|   final TextStyle? linkStyle; | ||||
|   final EdgeInsets? linesMargin; | ||||
|   final bool isSelectable; | ||||
|   final List<SnCloudFile>? attachments; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
|     super.key, | ||||
| @@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|     this.linkStyle, | ||||
|     this.isSelectable = false, | ||||
|     this.linesMargin, | ||||
|     this.attachments, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -109,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|               final uri = Uri.parse(url); | ||||
|               if (uri.scheme == 'solian') { | ||||
|                 switch (uri.host) { | ||||
|                   case 'files': | ||||
|                     final file = attachments?.firstWhereOrNull( | ||||
|                       (file) => file.id == uri.pathSegments[0], | ||||
|                     ); | ||||
|                     if (file == null) { | ||||
|                       return const SizedBox.shrink(); | ||||
|                     } | ||||
|  | ||||
|                     return ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: Container( | ||||
|                         decoration: BoxDecoration( | ||||
|                           color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                           borderRadius: const BorderRadius.all( | ||||
|                             Radius.circular(8), | ||||
|                           ), | ||||
|                         ), | ||||
|                         child: CloudFileWidget( | ||||
|                           item: file, | ||||
|                           fit: BoxFit.cover, | ||||
|                         ).clipRRect(all: 8), | ||||
|                       ), | ||||
|                     ); | ||||
|                   case 'stickers': | ||||
|                     final size = doesEnlargeSticker ? 96.0 : 24.0; | ||||
|                     return ClipRRect( | ||||
| @@ -132,9 +161,9 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|                     ); | ||||
|                 } | ||||
|               } | ||||
|               final content = UniversalImage( | ||||
|                 uri: uri.toString(), | ||||
|                 fit: BoxFit.cover, | ||||
|               final content = ConstrainedBox( | ||||
|                 constraints: BoxConstraints(maxHeight: 360), | ||||
|                 child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain), | ||||
|               ); | ||||
|               return content; | ||||
|             }, | ||||
|   | ||||
| @@ -474,6 +474,23 @@ class ComposeLogic { | ||||
|     state.attachments.value = clone; | ||||
|   } | ||||
|  | ||||
|   static void insertAttachment(WidgetRef ref, ComposeState state, int index) { | ||||
|     final attachment = state.attachments.value[index]; | ||||
|     if (!attachment.isOnCloud) { | ||||
|       return; | ||||
|     } | ||||
|     final cloudFile = attachment.data as SnCloudFile; | ||||
|     final markdown = ''; | ||||
|     final controller = state.contentController; | ||||
|     final text = controller.text; | ||||
|     final selection = controller.selection; | ||||
|     final newText = text.replaceRange(selection.start, selection.end, markdown); | ||||
|     controller.text = newText; | ||||
|     controller.selection = TextSelection.fromPosition( | ||||
|       TextPosition(offset: selection.start + markdown.length), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static Future<void> performAction( | ||||
|     WidgetRef ref, | ||||
|     ComposeState state, | ||||
|   | ||||
| @@ -56,7 +56,7 @@ class PostItem extends HookConsumerWidget { | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final isAuthor = useMemoized( | ||||
|       () => user.hasValue && user.value?.id == item.publisher.accountId, | ||||
|       () => user.value != null && user.value?.id == item.publisher.accountId, | ||||
|       [user], | ||||
|     ); | ||||
|  | ||||
| @@ -163,7 +163,7 @@ class PostItem extends HookConsumerWidget { | ||||
|             if ((item.repliedPost != null || item.forwardedPost != null) && | ||||
|                 showReferencePost) | ||||
|               _buildReferencePost(context, item), | ||||
|             if (item.attachments.isNotEmpty) | ||||
|             if (item.attachments.isNotEmpty && item.type != 1) | ||||
|               CloudFileList( | ||||
|                 files: item.attachments, | ||||
|                 maxWidth: math.min( | ||||
| @@ -331,6 +331,7 @@ class PostItem extends HookConsumerWidget { | ||||
|                                   item.type == 0 | ||||
|                                       ? EdgeInsets.only(bottom: 8) | ||||
|                                       : null, | ||||
|                               attachments: item.attachments, | ||||
|                             ), | ||||
|                         ], | ||||
|                         // Render tags and categories if they exist | ||||
| @@ -389,7 +390,7 @@ class PostItem extends HookConsumerWidget { | ||||
|                                 item.forwardedPost != null) && | ||||
|                             showReferencePost) | ||||
|                           _buildReferencePost(context, item), | ||||
|                         if (item.attachments.isNotEmpty) | ||||
|                         if (item.attachments.isNotEmpty && item.type != 1) | ||||
|                           CloudFileList( | ||||
|                             files: item.attachments, | ||||
|                             maxWidth: math.min( | ||||
| @@ -689,6 +690,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | ||||
|                           referencePost.type == 0 | ||||
|                               ? EdgeInsets.only(bottom: 4) | ||||
|                               : null, | ||||
|                       attachments: item.attachments, | ||||
|                     ).padding(bottom: 4), | ||||
|                   // Truncation hint for referenced post | ||||
|                   if (referencePost.isTruncated) | ||||
| @@ -696,7 +698,8 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | ||||
|                       isCompact: true, | ||||
|                       margin: const EdgeInsets.only(top: 4, bottom: 8), | ||||
|                     ), | ||||
|                   if (referencePost.attachments.isNotEmpty) | ||||
|                   if (referencePost.attachments.isNotEmpty && | ||||
|                       referencePost.type != 1) | ||||
|                     Row( | ||||
|                       mainAxisSize: MainAxisSize.min, | ||||
|                       children: [ | ||||
| @@ -1030,6 +1033,7 @@ class _ArticlePostDisplay extends StatelessWidget { | ||||
|             MarkdownTextContent( | ||||
|               content: item.content!, | ||||
|               textStyle: Theme.of(context).textTheme.bodyLarge, | ||||
|               attachments: item.attachments, | ||||
|             ), | ||||
|         ], | ||||
|       ); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/post/post_replies.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| @@ -14,6 +15,8 @@ class PostRepliesSheet extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|  | ||||
|     return SheetScaffold( | ||||
|       titleText: 'repliesCount'.plural(post.repliesCount), | ||||
|       child: Column( | ||||
| @@ -21,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget { | ||||
|           // Replies list | ||||
|           Expanded( | ||||
|             child: CustomScrollView( | ||||
|               slivers: [PostRepliesList( | ||||
|                 postId: post.id.toString(), | ||||
|                 backgroundColor: Colors.transparent, | ||||
|               )], | ||||
|               slivers: [ | ||||
|                 PostRepliesList( | ||||
|                   postId: post.id.toString(), | ||||
|                   backgroundColor: Colors.transparent, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           // Quick reply section | ||||
|           Material( | ||||
|             elevation: 2, | ||||
|             child: PostQuickReply( | ||||
|               parent: post, | ||||
|               onPosted: () { | ||||
|                 ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||
|               }, | ||||
|             ).padding( | ||||
|               bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|               top: 16, | ||||
|               horizontal: 16, | ||||
|           if (user.value != null) | ||||
|             Material( | ||||
|               elevation: 2, | ||||
|               child: PostQuickReply( | ||||
|                 parent: post, | ||||
|                 onPosted: () { | ||||
|                   ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||
|                 }, | ||||
|               ).padding( | ||||
|                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                 top: 16, | ||||
|                 horizontal: 16, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | ||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 3.0.0+110 | ||||
| version: 3.0.0+111 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.7.2 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user