Compare commits
	
		
			8 Commits
		
	
	
		
			2ea9f5e907
			...
			3.0.0+111
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | |||
| 46919dec31 | |||
| 9dd6cffe0c | 
| @@ -375,7 +375,9 @@ | |||||||
|   "postContent": "Content", |   "postContent": "Content", | ||||||
|   "postSettings": "Settings", |   "postSettings": "Settings", | ||||||
|   "postPublisherUnselected": "Publisher Unspecified", |   "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", |   "postVisibilityPublic": "Public", | ||||||
|   "postVisibilityFriends": "Friends Only", |   "postVisibilityFriends": "Friends Only", | ||||||
|   "postVisibilityUnlisted": "Unlisted", |   "postVisibilityUnlisted": "Unlisted", | ||||||
| @@ -686,8 +688,9 @@ | |||||||
|   "aboutScreenDeveloperSectionTitle": "Developer", |   "aboutScreenDeveloperSectionTitle": "Developer", | ||||||
|   "aboutScreenContactUsTitle": "Contact Us", |   "aboutScreenContactUsTitle": "Contact Us", | ||||||
|   "aboutScreenLicenseTitle": "License", |   "aboutScreenLicenseTitle": "License", | ||||||
|   "aboutScreenLicenseContent": "All copyright reserved © {} Solsynth\nOpen-sourced under license GNU AGPL v3.0", |   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", | ||||||
|   "aboutScreenCopyright": "© {} {}. All rights reserved.", |   "aboutScreenCopyright": "All rights reserved © Solsynth {}", | ||||||
|  |   "aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team", | ||||||
|   "aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}", |   "aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}", | ||||||
|   "copiedToClipboard": "Copied to clipboard", |   "copiedToClipboard": "Copied to clipboard", | ||||||
|   "copyToClipboardTooltip": "Copy to clipboard", |   "copyToClipboardTooltip": "Copy to clipboard", | ||||||
|   | |||||||
| @@ -512,5 +512,33 @@ | |||||||
|   "orderId": "订单 ID", |   "orderId": "订单 ID", | ||||||
|   "enterOrderId": "输入您的订单 ID", |   "enterOrderId": "输入您的订单 ID", | ||||||
|   "restore": "恢复", |   "restore": "恢复", | ||||||
|   "keyboardShortcuts": "键盘快捷键" |   "keyboardShortcuts": "键盘快捷键", | ||||||
|  |   "about": "关于", | ||||||
|  |   "membershipCancel": "取消会员订阅", | ||||||
|  |   "membershipCancelConfirm": "您确定要取消您的会员订阅?", | ||||||
|  |   "membershipCancelHint": "您确定要取消您的会员订阅吗?您将不会再被收费。您的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。", | ||||||
|  |   "membershipCancelSuccess": "您的会员订阅已成功取消。", | ||||||
|  |   "aboutScreenTitle": "关于", | ||||||
|  |   "aboutScreenVersionInfo": "版本 {} ({})", | ||||||
|  |   "aboutScreenAppInfoSectionTitle": "应用信息", | ||||||
|  |   "aboutScreenPackageNameLabel": "包名", | ||||||
|  |   "aboutScreenVersionLabel": "版本", | ||||||
|  |   "aboutScreenBuildNumberLabel": "构建编号", | ||||||
|  |   "aboutScreenLinksSectionTitle": "链接", | ||||||
|  |   "aboutScreenPrivacyPolicyTitle": "隐私政策", | ||||||
|  |   "aboutScreenTermsOfServiceTitle": "服务条款", | ||||||
|  |   "aboutScreenOpenSourceLicensesTitle": "开源许可证", | ||||||
|  |   "aboutScreenDeveloperSectionTitle": "开发者", | ||||||
|  |   "aboutScreenContactUsTitle": "联系我们", | ||||||
|  |   "aboutScreenLicenseTitle": "许可证", | ||||||
|  |   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", | ||||||
|  |   "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}", | ||||||
|  |   "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", | ||||||
|  |   "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}", | ||||||
|  |   "copiedToClipboard": "已复制到剪贴板", | ||||||
|  |   "copyToClipboardTooltip": "复制到剪贴板", | ||||||
|  |   "postForwardingTo": "转发给", | ||||||
|  |   "postReplyingTo": "回复给", | ||||||
|  |   "postEditing": "您正在编辑现有帖子", | ||||||
|  |   "postArticle": "文章" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -221,7 +221,7 @@ class IslandApp extends HookConsumerWidget { | |||||||
|       Future(() { |       Future(() { | ||||||
|         userNotifier.fetchUser().then((_) { |         userNotifier.fetchUser().then((_) { | ||||||
|           final user = ref.watch(userInfoProvider); |           final user = ref.watch(userInfoProvider); | ||||||
|           if (user.hasValue) { |           if (user.value != null) { | ||||||
|             final apiClient = ref.read(apiClientProvider); |             final apiClient = ref.read(apiClientProvider); | ||||||
|             subscribePushNotification(apiClient); |             subscribePushNotification(apiClient); | ||||||
|             final wsNotifier = ref.read(websocketStateProvider.notifier); |             final wsNotifier = ref.read(websocketStateProvider.notifier); | ||||||
|   | |||||||
| @@ -18,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | |||||||
|       final user = SnAccount.fromJson(response.data); |       final user = SnAccount.fromJson(response.data); | ||||||
|       state = AsyncValue.data(user); |       state = AsyncValue.data(user); | ||||||
|     } catch (error, stackTrace) { |     } catch (error, stackTrace) { | ||||||
|       log("[UserInfo] Failed to fetch user info: $error"); |       log( | ||||||
|       state = AsyncValue.error(error, stackTrace); |         "[UserInfo] Failed to fetch user info...", | ||||||
|  |         name: 'UserInfoNotifier', | ||||||
|  |         error: error, | ||||||
|  |         stackTrace: stackTrace, | ||||||
|  |       ); | ||||||
|  |       state = AsyncValue.data(null); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,24 +1,34 @@ | |||||||
|  | import 'package:device_info_plus/device_info_plus.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/services/notify.dart'; | ||||||
|  | import 'package:island/services/udid.native.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  |  | ||||||
| class AboutScreen extends StatefulWidget { | class AboutScreen extends ConsumerStatefulWidget { | ||||||
|   const AboutScreen({super.key}); |   const AboutScreen({super.key}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<AboutScreen> createState() => _AboutScreenState(); |   ConsumerState<AboutScreen> createState() => _AboutScreenState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _AboutScreenState extends State<AboutScreen> { | class _AboutScreenState extends ConsumerState<AboutScreen> { | ||||||
|   PackageInfo _packageInfo = PackageInfo( |   PackageInfo _packageInfo = PackageInfo( | ||||||
|     appName: 'Solian', |     appName: 'Solian', | ||||||
|     packageName: 'dev.solsynth.solian', |     packageName: 'dev.solsynth.solian', | ||||||
|     version: '1.0.0', |     version: '1.0.0', | ||||||
|     buildNumber: '1', |     buildNumber: '1', | ||||||
|   ); |   ); | ||||||
|  |   BaseDeviceInfo? _deviceInfo; | ||||||
|  |   String? _deviceUdid; | ||||||
|   bool _isLoading = true; |   bool _isLoading = true; | ||||||
|   String? _errorMessage; |   String? _errorMessage; | ||||||
|  |  | ||||||
| @@ -26,6 +36,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _initPackageInfo(); |     _initPackageInfo(); | ||||||
|  |     _initDeviceInfo(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> _initPackageInfo() async { |   Future<void> _initPackageInfo() async { | ||||||
| @@ -49,6 +60,25 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _initDeviceInfo() async { | ||||||
|  |     try { | ||||||
|  |       final deviceInfoPlugin = DeviceInfoPlugin(); | ||||||
|  |       _deviceInfo = await deviceInfoPlugin.deviceInfo; | ||||||
|  |       _deviceUdid = await getUdid(); | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() {}); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() { | ||||||
|  |           _errorMessage = 'aboutScreenFailedToLoadDeviceInfo'.tr( | ||||||
|  |             args: [e.toString()], | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<void> _launchURL(String url) async { |   Future<void> _launchURL(String url) async { | ||||||
|     final uri = Uri.parse(url); |     final uri = Uri.parse(url); | ||||||
|     if (await canLaunchUrl(uri)) { |     if (await canLaunchUrl(uri)) { | ||||||
| @@ -108,25 +138,66 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                       children: [ |                       children: [ | ||||||
|                         _buildInfoItem( |                         _buildInfoItem( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.info_outline, |                           icon: Symbols.info, | ||||||
|                           label: 'aboutScreenPackageNameLabel'.tr(), |                           label: 'aboutScreenPackageNameLabel'.tr(), | ||||||
|                           value: _packageInfo.packageName, |                           value: _packageInfo.packageName, | ||||||
|                         ), |                         ), | ||||||
|                         _buildInfoItem( |                         _buildInfoItem( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.update, |                           icon: Symbols.update, | ||||||
|                           label: 'aboutScreenVersionLabel'.tr(), |                           label: 'aboutScreenVersionLabel'.tr(), | ||||||
|                           value: _packageInfo.version, |                           value: _packageInfo.version, | ||||||
|                         ), |                         ), | ||||||
|                         _buildInfoItem( |                         _buildInfoItem( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.build, |                           icon: Symbols.build, | ||||||
|                           label: 'aboutScreenBuildNumberLabel'.tr(), |                           label: 'aboutScreenBuildNumberLabel'.tr(), | ||||||
|                           value: _packageInfo.buildNumber, |                           value: _packageInfo.buildNumber, | ||||||
|                         ), |                         ), | ||||||
|                       ], |                       ], | ||||||
|                     ), |                     ), | ||||||
|  |  | ||||||
|  |                     if (_deviceInfo != null) const SizedBox(height: 16), | ||||||
|  |  | ||||||
|  |                     if (_deviceInfo != null) | ||||||
|  |                       _buildSection( | ||||||
|  |                         context, | ||||||
|  |                         title: 'Device Information', | ||||||
|  |                         children: [ | ||||||
|  |                           _buildInfoItem( | ||||||
|  |                             context, | ||||||
|  |                             icon: Symbols.label, | ||||||
|  |                             label: 'Device Name', | ||||||
|  |                             value: _deviceInfo?.data['name'], | ||||||
|  |                           ), | ||||||
|  |                           _buildInfoItem( | ||||||
|  |                             context, | ||||||
|  |                             icon: Symbols.fingerprint, | ||||||
|  |                             label: 'Device Identifier', | ||||||
|  |                             value: _deviceUdid ?? 'N/A', | ||||||
|  |                             copyable: true, | ||||||
|  |                           ), | ||||||
|  |                           const Divider(height: 1), | ||||||
|  |                           _buildListTile( | ||||||
|  |                             context, | ||||||
|  |                             icon: Symbols.notifications_active, | ||||||
|  |                             title: 'Reactivate Push Notifications', | ||||||
|  |                             onTap: () async { | ||||||
|  |                               showLoadingModal(context); | ||||||
|  |                               try { | ||||||
|  |                                 await subscribePushNotification( | ||||||
|  |                                   ref.watch(apiClientProvider), | ||||||
|  |                                 ); | ||||||
|  |                               } catch (err) { | ||||||
|  |                                 showErrorAlert(err); | ||||||
|  |                               } finally { | ||||||
|  |                                 if (context.mounted) hideLoadingModal(context); | ||||||
|  |                               } | ||||||
|  |                             }, | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|  |  | ||||||
|                     const SizedBox(height: 16), |                     const SizedBox(height: 16), | ||||||
|  |  | ||||||
|                     // Links Card |                     // Links Card | ||||||
| @@ -136,7 +207,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                       children: [ |                       children: [ | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.privacy_tip_outlined, |                           icon: Symbols.privacy_tip, | ||||||
|                           title: 'aboutScreenPrivacyPolicyTitle'.tr(), |                           title: 'aboutScreenPrivacyPolicyTitle'.tr(), | ||||||
|                           onTap: |                           onTap: | ||||||
|                               () => _launchURL( |                               () => _launchURL( | ||||||
| @@ -145,7 +216,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                         ), |                         ), | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.description_outlined, |                           icon: Symbols.description, | ||||||
|                           title: 'aboutScreenTermsOfServiceTitle'.tr(), |                           title: 'aboutScreenTermsOfServiceTitle'.tr(), | ||||||
|                           onTap: |                           onTap: | ||||||
|                               () => _launchURL( |                               () => _launchURL( | ||||||
| @@ -154,7 +225,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                         ), |                         ), | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.code, |                           icon: Symbols.code, | ||||||
|                           title: 'aboutScreenOpenSourceLicensesTitle'.tr(), |                           title: 'aboutScreenOpenSourceLicensesTitle'.tr(), | ||||||
|                           onTap: () { |                           onTap: () { | ||||||
|                             showLicensePage( |                             showLicensePage( | ||||||
| @@ -177,14 +248,14 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                       children: [ |                       children: [ | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.email_outlined, |                           icon: Symbols.email, | ||||||
|                           title: 'aboutScreenContactUsTitle'.tr(), |                           title: 'aboutScreenContactUsTitle'.tr(), | ||||||
|                           subtitle: 'lily@solsynth.dev', |                           subtitle: 'lily@solsynth.dev', | ||||||
|                           onTap: () => _launchURL('mailto:lily@solsynth.dev'), |                           onTap: () => _launchURL('mailto:lily@solsynth.dev'), | ||||||
|                         ), |                         ), | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|                           context, |                           context, | ||||||
|                           icon: Icons.copyright, |                           icon: Symbols.copyright, | ||||||
|                           title: 'aboutScreenLicenseTitle'.tr(), |                           title: 'aboutScreenLicenseTitle'.tr(), | ||||||
|                           subtitle: 'aboutScreenLicenseContent'.tr( |                           subtitle: 'aboutScreenLicenseContent'.tr( | ||||||
|                             args: [DateTime.now().year.toString()], |                             args: [DateTime.now().year.toString()], | ||||||
| @@ -202,14 +273,25 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                     // Copyright |                     // Copyright | ||||||
|                     Padding( |                     Padding( | ||||||
|                       padding: const EdgeInsets.all(16.0), |                       padding: const EdgeInsets.all(16.0), | ||||||
|                       child: Text( |                       child: Column( | ||||||
|                         'aboutScreenCopyright'.tr( |                         children: [ | ||||||
|                           args: [DateTime.now().year.toString(), "Solsynth"], |                           Text( | ||||||
|                         ), |                             'aboutScreenCopyright'.tr( | ||||||
|                         style: theme.textTheme.bodySmall, |                               args: [DateTime.now().year.toString()], | ||||||
|                         textAlign: TextAlign.center, |                             ), | ||||||
|  |                             style: theme.textTheme.bodySmall, | ||||||
|  |                             textAlign: TextAlign.center, | ||||||
|  |                           ), | ||||||
|  |                           const Gap(1), | ||||||
|  |                           Text( | ||||||
|  |                             'aboutScreenMadeWith'.tr(), | ||||||
|  |                             textAlign: TextAlign.center, | ||||||
|  |                           ).fontSize(10).opacity(0.8), | ||||||
|  |                         ], | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|  |  | ||||||
|  |                     Gap(MediaQuery.of(context).padding.bottom + 16), | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
| @@ -247,6 +329,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|     required IconData icon, |     required IconData icon, | ||||||
|     required String label, |     required String label, | ||||||
|     required String value, |     required String value, | ||||||
|  |     bool copyable = false, | ||||||
|   }) { |   }) { | ||||||
|     return Padding( |     return Padding( | ||||||
|       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||||
| @@ -263,13 +346,14 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|                 SelectableText( |                 SelectableText( | ||||||
|                   value, |                   value, | ||||||
|                   style: Theme.of(context).textTheme.bodyMedium, |                   style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |                   maxLines: copyable ? 1 : null, | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           if (value.startsWith('http') || value.contains('@')) |           if (value.startsWith('http') || value.contains('@') || copyable) | ||||||
|             IconButton( |             IconButton( | ||||||
|               icon: const Icon(Icons.copy, size: 16), |               icon: const Icon(Symbols.content_copy, size: 16), | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 Clipboard.setData(ClipboardData(text: value)); |                 Clipboard.setData(ClipboardData(text: value)); | ||||||
|                 ScaffoldMessenger.of(context).showSnackBar( |                 ScaffoldMessenger.of(context).showSnackBar( | ||||||
| @@ -301,7 +385,7 @@ class _AboutScreenState extends State<AboutScreen> { | |||||||
|           subtitle: subtitle != null ? Text(subtitle) : null, |           subtitle: subtitle != null ? Text(subtitle) : null, | ||||||
|           isThreeLine: multipleLines, |           isThreeLine: multipleLines, | ||||||
|           trailing: const Icon( |           trailing: const Icon( | ||||||
|             Icons.chevron_right, |             Symbols.chevron_right, | ||||||
|           ).padding(top: multipleLines ? 8 : 0), |           ).padding(top: multipleLines ? 8 : 0), | ||||||
|           shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), |           shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||||||
|           onTap: onTap, |           onTap: onTap, | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|       notificationUnreadCountNotifierProvider, |       notificationUnreadCountNotifierProvider, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (!user.hasValue || user.value == null) { |     if (user.value == null || user.value == null) { | ||||||
|       return _UnauthorizedAccountScreen(); |       return _UnauthorizedAccountScreen(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -367,12 +367,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 const Gap(8), |                 const Gap(8), | ||||||
|                 TextButton( |                 Row( | ||||||
|                   onPressed: () { |                   mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                     context.push('/settings'); |                   children: [ | ||||||
|                   }, |                     TextButton( | ||||||
|                   child: Text('appSettings').tr(), |                       onPressed: () { | ||||||
|                 ).center(), |                         context.push('/about'); | ||||||
|  |                       }, | ||||||
|  |                       child: Text('about').tr(), | ||||||
|  |                     ), | ||||||
|  |                     TextButton( | ||||||
|  |                       onPressed: () { | ||||||
|  |                         context.push('/settings'); | ||||||
|  |                       }, | ||||||
|  |                       child: Text('appSettings').tr(), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ).center(), |           ).center(), | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget { | |||||||
|                       ), |                       ), | ||||||
|  |  | ||||||
|                       // Show user profile if viewing someone else's calendar |                       // Show user profile if viewing someone else's calendar | ||||||
|                       if (name != 'me' && user.hasValue) |                       if (name != 'me' && user.value != null) | ||||||
|                         AccountNameplate(name: name), |                         AccountNameplate(name: name), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
| @@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget { | |||||||
|                     ).padding(horizontal: 8, vertical: 4), |                     ).padding(horizontal: 8, vertical: 4), | ||||||
|  |  | ||||||
|                     // Show user profile if viewing someone else's calendar |                     // Show user profile if viewing someone else's calendar | ||||||
|                     if (name != 'me' && user.hasValue) |                     if (name != 'me' && user.value != null) | ||||||
|                       AccountNameplate(name: name), |                       AccountNameplate(name: name), | ||||||
|                     Gap(MediaQuery.of(context).padding.bottom + 16), |                     Gap(MediaQuery.of(context).padding.bottom + 16), | ||||||
|                   ], |                   ], | ||||||
|   | |||||||
| @@ -72,6 +72,8 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | |||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | 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 account = await ref.watch(accountProvider(uname).future); | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final apiClient = ref.watch(apiClientProvider); | ||||||
|   try { |   try { | ||||||
| @@ -87,6 +89,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | |||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | 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 account = await ref.watch(accountProvider(uname).future); | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final apiClient = ref.watch(apiClientProvider); | ||||||
|   try { |   try { | ||||||
| @@ -219,6 +223,8 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|       ]; |       ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     final user = ref.watch(userInfoProvider); | ||||||
|  |  | ||||||
|     return account.when( |     return account.when( | ||||||
|       data: |       data: | ||||||
|           (data) => AppScaffold( |           (data) => AppScaffold( | ||||||
| @@ -379,56 +385,60 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                   ).padding(horizontal: 24), |                   ).padding(horizontal: 24), | ||||||
|                 ), |                 ), | ||||||
|  |  | ||||||
|                 SliverToBoxAdapter( |                 if (user.value != null) | ||||||
|                   child: const Divider(height: 1).padding(top: 24, bottom: 12), |                   SliverToBoxAdapter( | ||||||
|                 ), |                     child: const Divider( | ||||||
|                 SliverToBoxAdapter( |                       height: 1, | ||||||
|                   child: Row( |                     ).padding(top: 24, bottom: 12), | ||||||
|                     spacing: 8, |                   ), | ||||||
|                     children: [ |                 if (user.value != null) | ||||||
|                       Expanded( |                   SliverToBoxAdapter( | ||||||
|                         child: FilledButton.icon( |                     child: Row( | ||||||
|                           style: ButtonStyle( |                       spacing: 8, | ||||||
|                             backgroundColor: WidgetStatePropertyAll( |                       children: [ | ||||||
|                               accountRelationship.value == null |                         Expanded( | ||||||
|                                   ? null |                           child: FilledButton.icon( | ||||||
|                                   : Theme.of(context).colorScheme.secondary, |                             style: ButtonStyle( | ||||||
|                             ), |                               backgroundColor: WidgetStatePropertyAll( | ||||||
|                             foregroundColor: WidgetStatePropertyAll( |  | ||||||
|                               accountRelationship.value == null |  | ||||||
|                                   ? null |  | ||||||
|                                   : Theme.of(context).colorScheme.onSecondary, |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                           onPressed: relationshipAction, |  | ||||||
|                           label: |  | ||||||
|                               Text( |  | ||||||
|                                 accountRelationship.value == null |                                 accountRelationship.value == null | ||||||
|                                     ? 'addFriendShort' |                                     ? null | ||||||
|                                     : 'added', |                                     : Theme.of(context).colorScheme.secondary, | ||||||
|                               ).tr(), |                               ), | ||||||
|                           icon: |                               foregroundColor: WidgetStatePropertyAll( | ||||||
|                               accountRelationship.value == null |                                 accountRelationship.value == null | ||||||
|                                   ? const Icon(Symbols.person_add) |                                     ? null | ||||||
|                                   : const Icon(Symbols.person_check), |                                     : 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( | ||||||
|                       Expanded( |                           child: FilledButton.icon( | ||||||
|                         child: FilledButton.icon( |                             onPressed: directMessageAction, | ||||||
|                           onPressed: directMessageAction, |                             icon: const Icon(Symbols.message), | ||||||
|                           icon: const Icon(Symbols.message), |                             label: | ||||||
|                           label: |                                 Text( | ||||||
|                               Text( |                                   accountChat.value == null | ||||||
|                                 accountChat.value == null |                                       ? 'createDirectMessage' | ||||||
|                                     ? 'createDirectMessage' |                                       : 'gotoDirectMessage', | ||||||
|                                     : 'gotoDirectMessage', |                                   maxLines: 1, | ||||||
|                                 maxLines: 1, |                                 ).tr(), | ||||||
|                               ).tr(), |                           ), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ], | ||||||
|                     ], |                     ).padding(horizontal: 16), | ||||||
|                   ).padding(horizontal: 16), |                   ), | ||||||
|                 ), |  | ||||||
|                 SliverToBoxAdapter( |                 SliverToBoxAdapter( | ||||||
|                   child: const Divider(height: 1).padding(top: 12), |                   child: const Divider(height: 1).padding(top: 12), | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -51,54 +51,59 @@ class _ArticleDetailContent extends HookConsumerWidget { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return SingleChildScrollView( |     return SingleChildScrollView( | ||||||
|       child: Column( |       child: Center( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |         child: ConstrainedBox( | ||||||
|         children: [ |           constraints: const BoxConstraints(maxWidth: 560), | ||||||
|           if (article.preview?.imageUrl != null) |           child: Column( | ||||||
|             Image.network( |             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|               article.preview!.imageUrl!, |             children: [ | ||||||
|               width: double.infinity, |               if (article.preview?.imageUrl != null) | ||||||
|               height: 200, |                 Image.network( | ||||||
|               fit: BoxFit.cover, |                   article.preview!.imageUrl!, | ||||||
|             ), |                   width: double.infinity, | ||||||
|           Padding( |                   height: 200, | ||||||
|             padding: const EdgeInsets.all(16.0), |                   fit: BoxFit.cover, | ||||||
|             child: Column( |  | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|               children: [ |  | ||||||
|                 Text( |  | ||||||
|                   article.title, |  | ||||||
|                   style: Theme.of(context).textTheme.headlineSmall, |  | ||||||
|                 ), |                 ), | ||||||
|                 const SizedBox(height: 8), |               Padding( | ||||||
|                 if (article.feed?.title != null) |                 padding: const EdgeInsets.all(16.0), | ||||||
|                   Text( |                 child: Column( | ||||||
|                     article.feed!.title, |                   crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                     style: Theme.of(context).textTheme.bodyMedium?.copyWith( |                   children: [ | ||||||
|                       color: Theme.of(context).colorScheme.onSurfaceVariant, |                     Text( | ||||||
|  |                       article.title, | ||||||
|  |                       style: Theme.of(context).textTheme.headlineSmall, | ||||||
|                     ), |                     ), | ||||||
|                   ), |                     const SizedBox(height: 8), | ||||||
|                 const Divider(height: 32), |                     if (article.feed?.title != null) | ||||||
|                 if (article.content != null) |                       Text( | ||||||
|                   ...MarkdownTextContent.buildGenerator( |                         article.feed!.title, | ||||||
|                     isDark: Theme.of(context).brightness == Brightness.dark, |                         style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|                   ).buildWidgets(markdownContent) |                           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|                 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'), |                     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) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), |       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||||
|       body: CustomScrollView( |       body: Center( | ||||||
|         slivers: [ |         child: ConstrainedBox( | ||||||
|           SliverPadding( |           constraints: const BoxConstraints(maxWidth: 560), | ||||||
|             padding: const EdgeInsets.only(top: 8, left: 8, right: 8), |           child: CustomScrollView( | ||||||
|             sliver: SliverArticlesList( |             slivers: [ | ||||||
|               feedId: feedId, |               SliverPadding( | ||||||
|               publisherId: publisherId, |                 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( |     return CustomScrollView( | ||||||
|       slivers: [ |       slivers: [ | ||||||
|         if (user.hasValue && !contentOnly) |         if (user.value != null && !contentOnly) | ||||||
|           SliverToBoxAdapter(child: CheckInWidget()), |           SliverToBoxAdapter(child: CheckInWidget()), | ||||||
|         SliverList.builder( |         SliverList.builder( | ||||||
|           itemCount: widgetCount, |           itemCount: widgetCount, | ||||||
|   | |||||||
| @@ -321,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|             builder: (context, attachments, _) { |             builder: (context, attachments, _) { | ||||||
|               if (attachments.isEmpty) return const SizedBox.shrink(); |               if (attachments.isEmpty) return const SizedBox.shrink(); | ||||||
|               return Column( |               return Column( | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                 children: [ |                 children: [ | ||||||
|                   const Gap(16), |                   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>>( |                   ValueListenableBuilder<Map<int, double>>( | ||||||
|                     valueListenable: state.attachmentProgress, |                     valueListenable: state.attachmentProgress, | ||||||
|                     builder: (context, progressMap, _) { |                     builder: (context, progressMap, _) { | ||||||
| @@ -332,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                         children: [ |                         children: [ | ||||||
|                           for (var idx = 0; idx < attachments.length; idx++) |                           for (var idx = 0; idx < attachments.length; idx++) | ||||||
|                             SizedBox( |                             SizedBox( | ||||||
|                               width: 120, |                               width: 280, | ||||||
|                               height: 120, |                               height: 280, | ||||||
|                               child: AttachmentPreview( |                               child: AttachmentPreview( | ||||||
|                                 item: attachments[idx], |                                 item: attachments[idx], | ||||||
|                                 progress: progressMap[idx], |                                 progress: progressMap[idx], | ||||||
| @@ -358,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                                     delta, |                                     delta, | ||||||
|                                   ); |                                   ); | ||||||
|                                 }, |                                 }, | ||||||
|  |                                 onInsert: | ||||||
|  |                                     () => ComposeLogic.insertAttachment( | ||||||
|  |                                       ref, | ||||||
|  |                                       state, | ||||||
|  |                                       idx, | ||||||
|  |                                     ), | ||||||
|                               ), |                               ), | ||||||
|                             ), |                             ), | ||||||
|                         ], |                         ], | ||||||
|   | |||||||
| @@ -63,7 +63,10 @@ StreamSubscription<WebSocketPacket> setupNotificationListener( | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| Future<void> subscribePushNotification(Dio apiClient) async { | Future<void> subscribePushNotification( | ||||||
|  |   Dio apiClient, { | ||||||
|  |   bool detailedErrors = false, | ||||||
|  | }) async { | ||||||
|   await FirebaseMessaging.instance.requestPermission( |   await FirebaseMessaging.instance.requestPermission( | ||||||
|     provisional: true, |     provisional: true, | ||||||
|     alert: true, |     alert: true, | ||||||
| @@ -97,6 +100,8 @@ Future<void> subscribePushNotification(Dio apiClient) async { | |||||||
|       deviceToken, |       deviceToken, | ||||||
|       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, |       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||||
|     ); |     ); | ||||||
|  |   } else if (detailedErrors) { | ||||||
|  |     throw Exception("Failed to get device token for push notifications."); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget { | |||||||
|           }, |           }, | ||||||
|           options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), |           options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), | ||||||
|         ); |         ); | ||||||
|         if (user.hasValue) { |         if (user.value != null) { | ||||||
|           ref.invalidate(accountStatusProvider(user.value!.name)); |           ref.invalidate(accountStatusProvider(user.value!.name)); | ||||||
|         } |         } | ||||||
|         if (!context.mounted) return; |         if (!context.mounted) return; | ||||||
|   | |||||||
| @@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget { | |||||||
|     return AnimatedPositioned( |     return AnimatedPositioned( | ||||||
|       duration: Duration(milliseconds: 1850), |       duration: Duration(milliseconds: 1850), | ||||||
|       top: |       top: | ||||||
|           !user.hasValue || |           user.value == null || | ||||||
|                   user.value == null || |                   user.value == null || | ||||||
|                   websocketState == WebSocketState.connected() |                   websocketState == WebSocketState.connected() | ||||||
|               ? -indicatorHeight |               ? -indicatorHeight | ||||||
| @@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget { | |||||||
|       child: IgnorePointer( |       child: IgnorePointer( | ||||||
|         child: Material( |         child: Material( | ||||||
|           elevation: |           elevation: | ||||||
|               !user.hasValue || websocketState == WebSocketState.connected() |               user.value == null || websocketState == WebSocketState.connected() | ||||||
|                   ? 0 |                   ? 0 | ||||||
|                   : 4, |                   : 4, | ||||||
|           child: AnimatedContainer( |           child: AnimatedContainer( | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget { | |||||||
|   final double? progress; |   final double? progress; | ||||||
|   final Function(int)? onMove; |   final Function(int)? onMove; | ||||||
|   final Function? onDelete; |   final Function? onDelete; | ||||||
|  |   final Function? onInsert; | ||||||
|   final Function? onRequestUpload; |   final Function? onRequestUpload; | ||||||
|   const AttachmentPreview({ |   const AttachmentPreview({ | ||||||
|     super.key, |     super.key, | ||||||
| @@ -23,6 +24,7 @@ class AttachmentPreview extends StatelessWidget { | |||||||
|     this.onRequestUpload, |     this.onRequestUpload, | ||||||
|     this.onMove, |     this.onMove, | ||||||
|     this.onDelete, |     this.onDelete, | ||||||
|  |     this.onInsert, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -104,7 +106,11 @@ class AttachmentPreview extends StatelessWidget { | |||||||
|                           style: TextStyle(color: Colors.white), |                           style: TextStyle(color: Colors.white), | ||||||
|                         ), |                         ), | ||||||
|                       Gap(6), |                       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); |                               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:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:go_router/go_router.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_highlight/themes/a11y-light.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/widgets/alert.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:island/widgets/content/markdown_latex.dart'; | ||||||
| import 'package:markdown/markdown.dart' as markdown; | import 'package:markdown/markdown.dart' as markdown; | ||||||
| import 'package:markdown_widget/markdown_widget.dart'; | import 'package:markdown_widget/markdown_widget.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart'; | ||||||
|  |  | ||||||
| import 'image.dart'; | import 'image.dart'; | ||||||
| @@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|   final TextStyle? linkStyle; |   final TextStyle? linkStyle; | ||||||
|   final EdgeInsets? linesMargin; |   final EdgeInsets? linesMargin; | ||||||
|   final bool isSelectable; |   final bool isSelectable; | ||||||
|  |   final List<SnCloudFile>? attachments; | ||||||
|  |  | ||||||
|   const MarkdownTextContent({ |   const MarkdownTextContent({ | ||||||
|     super.key, |     super.key, | ||||||
| @@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|     this.linkStyle, |     this.linkStyle, | ||||||
|     this.isSelectable = false, |     this.isSelectable = false, | ||||||
|     this.linesMargin, |     this.linesMargin, | ||||||
|  |     this.attachments, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -109,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|               final uri = Uri.parse(url); |               final uri = Uri.parse(url); | ||||||
|               if (uri.scheme == 'solian') { |               if (uri.scheme == 'solian') { | ||||||
|                 switch (uri.host) { |                 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': |                   case 'stickers': | ||||||
|                     final size = doesEnlargeSticker ? 96.0 : 24.0; |                     final size = doesEnlargeSticker ? 96.0 : 24.0; | ||||||
|                     return ClipRRect( |                     return ClipRRect( | ||||||
| @@ -132,9 +161,9 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|               final content = UniversalImage( |               final content = ConstrainedBox( | ||||||
|                 uri: uri.toString(), |                 constraints: BoxConstraints(maxHeight: 360), | ||||||
|                 fit: BoxFit.cover, |                 child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain), | ||||||
|               ); |               ); | ||||||
|               return content; |               return content; | ||||||
|             }, |             }, | ||||||
|   | |||||||
| @@ -474,6 +474,23 @@ class ComposeLogic { | |||||||
|     state.attachments.value = clone; |     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( |   static Future<void> performAction( | ||||||
|     WidgetRef ref, |     WidgetRef ref, | ||||||
|     ComposeState state, |     ComposeState state, | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ class PostItem extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
|     final isAuthor = useMemoized( |     final isAuthor = useMemoized( | ||||||
|       () => user.hasValue && user.value?.id == item.publisher.accountId, |       () => user.value != null && user.value?.id == item.publisher.accountId, | ||||||
|       [user], |       [user], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -163,7 +163,7 @@ class PostItem extends HookConsumerWidget { | |||||||
|             if ((item.repliedPost != null || item.forwardedPost != null) && |             if ((item.repliedPost != null || item.forwardedPost != null) && | ||||||
|                 showReferencePost) |                 showReferencePost) | ||||||
|               _buildReferencePost(context, item), |               _buildReferencePost(context, item), | ||||||
|             if (item.attachments.isNotEmpty) |             if (item.attachments.isNotEmpty && item.type != 1) | ||||||
|               CloudFileList( |               CloudFileList( | ||||||
|                 files: item.attachments, |                 files: item.attachments, | ||||||
|                 maxWidth: math.min( |                 maxWidth: math.min( | ||||||
| @@ -331,6 +331,7 @@ class PostItem extends HookConsumerWidget { | |||||||
|                                   item.type == 0 |                                   item.type == 0 | ||||||
|                                       ? EdgeInsets.only(bottom: 8) |                                       ? EdgeInsets.only(bottom: 8) | ||||||
|                                       : null, |                                       : null, | ||||||
|  |                               attachments: item.attachments, | ||||||
|                             ), |                             ), | ||||||
|                         ], |                         ], | ||||||
|                         // Render tags and categories if they exist |                         // Render tags and categories if they exist | ||||||
| @@ -389,7 +390,7 @@ class PostItem extends HookConsumerWidget { | |||||||
|                                 item.forwardedPost != null) && |                                 item.forwardedPost != null) && | ||||||
|                             showReferencePost) |                             showReferencePost) | ||||||
|                           _buildReferencePost(context, item), |                           _buildReferencePost(context, item), | ||||||
|                         if (item.attachments.isNotEmpty) |                         if (item.attachments.isNotEmpty && item.type != 1) | ||||||
|                           CloudFileList( |                           CloudFileList( | ||||||
|                             files: item.attachments, |                             files: item.attachments, | ||||||
|                             maxWidth: math.min( |                             maxWidth: math.min( | ||||||
| @@ -689,6 +690,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | |||||||
|                           referencePost.type == 0 |                           referencePost.type == 0 | ||||||
|                               ? EdgeInsets.only(bottom: 4) |                               ? EdgeInsets.only(bottom: 4) | ||||||
|                               : null, |                               : null, | ||||||
|  |                       attachments: item.attachments, | ||||||
|                     ).padding(bottom: 4), |                     ).padding(bottom: 4), | ||||||
|                   // Truncation hint for referenced post |                   // Truncation hint for referenced post | ||||||
|                   if (referencePost.isTruncated) |                   if (referencePost.isTruncated) | ||||||
| @@ -696,7 +698,8 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | |||||||
|                       isCompact: true, |                       isCompact: true, | ||||||
|                       margin: const EdgeInsets.only(top: 4, bottom: 8), |                       margin: const EdgeInsets.only(top: 4, bottom: 8), | ||||||
|                     ), |                     ), | ||||||
|                   if (referencePost.attachments.isNotEmpty) |                   if (referencePost.attachments.isNotEmpty && | ||||||
|  |                       referencePost.type != 1) | ||||||
|                     Row( |                     Row( | ||||||
|                       mainAxisSize: MainAxisSize.min, |                       mainAxisSize: MainAxisSize.min, | ||||||
|                       children: [ |                       children: [ | ||||||
| @@ -1030,6 +1033,7 @@ class _ArticlePostDisplay extends StatelessWidget { | |||||||
|             MarkdownTextContent( |             MarkdownTextContent( | ||||||
|               content: item.content!, |               content: item.content!, | ||||||
|               textStyle: Theme.of(context).textTheme.bodyLarge, |               textStyle: Theme.of(context).textTheme.bodyLarge, | ||||||
|  |               attachments: item.attachments, | ||||||
|             ), |             ), | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/post/post_replies.dart'; | import 'package:island/widgets/post/post_replies.dart'; | ||||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | import 'package:island/widgets/post/post_quick_reply.dart'; | ||||||
| @@ -14,6 +15,8 @@ class PostRepliesSheet extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final user = ref.watch(userInfoProvider); | ||||||
|  |  | ||||||
|     return SheetScaffold( |     return SheetScaffold( | ||||||
|       titleText: 'repliesCount'.plural(post.repliesCount), |       titleText: 'repliesCount'.plural(post.repliesCount), | ||||||
|       child: Column( |       child: Column( | ||||||
| @@ -21,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget { | |||||||
|           // Replies list |           // Replies list | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: CustomScrollView( |             child: CustomScrollView( | ||||||
|               slivers: [PostRepliesList( |               slivers: [ | ||||||
|                 postId: post.id.toString(), |                 PostRepliesList( | ||||||
|                 backgroundColor: Colors.transparent, |                   postId: post.id.toString(), | ||||||
|               )], |                   backgroundColor: Colors.transparent, | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           // Quick reply section |           // Quick reply section | ||||||
|           Material( |           if (user.value != null) | ||||||
|             elevation: 2, |             Material( | ||||||
|             child: PostQuickReply( |               elevation: 2, | ||||||
|               parent: post, |               child: PostQuickReply( | ||||||
|               onPosted: () { |                 parent: post, | ||||||
|                 ref.invalidate(postRepliesNotifierProvider(post.id)); |                 onPosted: () { | ||||||
|               }, |                   ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||||
|             ).padding( |                 }, | ||||||
|               bottom: MediaQuery.of(context).padding.bottom + 16, |               ).padding( | ||||||
|               top: 16, |                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||||
|               horizontal: 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 | # 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 | # 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. | # 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: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user