Compare commits
	
		
			19 Commits
		
	
	
		
			2ea9f5e907
			...
			3.0.0+112
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 996462f1fd | |||
| 778f6bb79f | |||
| 8747f948b9 | |||
| 9546d6e4b8 | |||
| f8d1940af6 | |||
| b2b0891d24 | |||
| 274168d4bc | |||
| 2c98b348d5 | |||
| afc7887ddd | |||
| 99ff78a3d5 | |||
| 2ad85addf6 | |||
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | |||
| 46919dec31 | |||
| 9dd6cffe0c | 
@@ -42,6 +42,15 @@
 | 
				
			|||||||
                <category android:name="android.intent.category.LAUNCHER" />
 | 
					                <category android:name="android.intent.category.LAUNCHER" />
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Deeplinking -->
 | 
				
			||||||
 | 
					            <intent-filter android:autoVerify="true">
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.VIEW" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.BROWSABLE" />
 | 
				
			||||||
 | 
					                <data android:scheme="http" android:host="solian.app" />
 | 
				
			||||||
 | 
					                <data android:scheme="https" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- Share Intent Filters -->
 | 
					            <!-- Share Intent Filters -->
 | 
				
			||||||
            <intent-filter>
 | 
					            <intent-filter>
 | 
				
			||||||
                <action android:name="android.intent.action.SEND" />
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
@@ -588,6 +590,7 @@
 | 
				
			|||||||
  "yes": "Yes",
 | 
					  "yes": "Yes",
 | 
				
			||||||
  "navigateToChat": "Navigate to Chat",
 | 
					  "navigateToChat": "Navigate to Chat",
 | 
				
			||||||
  "wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
 | 
					  "wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
 | 
				
			||||||
 | 
					  "abuseReports": "Abuse Reports",
 | 
				
			||||||
  "abuseReport": "Report",
 | 
					  "abuseReport": "Report",
 | 
				
			||||||
  "abuseReportTitle": "Report Content",
 | 
					  "abuseReportTitle": "Report Content",
 | 
				
			||||||
  "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
 | 
					  "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
 | 
				
			||||||
@@ -686,8 +689,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": "文章"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -772,6 +772,7 @@
 | 
				
			|||||||
				ENABLE_BITCODE = NO;
 | 
									ENABLE_BITCODE = NO;
 | 
				
			||||||
				INFOPLIST_FILE = Runner/Info.plist;
 | 
									INFOPLIST_FILE = Runner/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
				
			||||||
 | 
									INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
@@ -1202,6 +1203,7 @@
 | 
				
			|||||||
				ENABLE_BITCODE = NO;
 | 
									ENABLE_BITCODE = NO;
 | 
				
			||||||
				INFOPLIST_FILE = Runner/Info.plist;
 | 
									INFOPLIST_FILE = Runner/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
				
			||||||
 | 
									INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
@@ -1229,6 +1231,7 @@
 | 
				
			|||||||
				ENABLE_BITCODE = NO;
 | 
									ENABLE_BITCODE = NO;
 | 
				
			||||||
				INFOPLIST_FILE = Runner/Info.plist;
 | 
									INFOPLIST_FILE = Runner/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = Solian;
 | 
				
			||||||
 | 
									INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,16 +2,10 @@
 | 
				
			|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
					<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
				
			||||||
<plist version="1.0">
 | 
					<plist version="1.0">
 | 
				
			||||||
<dict>
 | 
					<dict>
 | 
				
			||||||
	<key>CLIENT_ID</key>
 | 
						<key>AppGroupId</key>
 | 
				
			||||||
	<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
 | 
						<string>$(CUSTOM_GROUP_ID)</string>
 | 
				
			||||||
	<key>REVERSED_CLIENT_ID</key>
 | 
					 | 
				
			||||||
	<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
 | 
					 | 
				
			||||||
	<key>PLIST_VERSION</key>
 | 
					 | 
				
			||||||
	<string>1</string>
 | 
					 | 
				
			||||||
	<key>BUNDLE_ID</key>
 | 
						<key>BUNDLE_ID</key>
 | 
				
			||||||
	<string>dev.solsynth.solian</string>
 | 
						<string>dev.solsynth.solian</string>
 | 
				
			||||||
	<key>ITSAppUsesNonExemptEncryption</key>
 | 
					 | 
				
			||||||
	<false/>
 | 
					 | 
				
			||||||
	<key>CADisableMinimumFrameDurationOnPhone</key>
 | 
						<key>CADisableMinimumFrameDurationOnPhone</key>
 | 
				
			||||||
	<true/>
 | 
						<true/>
 | 
				
			||||||
	<key>CFBundleDevelopmentRegion</key>
 | 
						<key>CFBundleDevelopmentRegion</key>
 | 
				
			||||||
@@ -32,8 +26,6 @@
 | 
				
			|||||||
	<string>$(FLUTTER_BUILD_NAME)</string>
 | 
						<string>$(FLUTTER_BUILD_NAME)</string>
 | 
				
			||||||
	<key>CFBundleSignature</key>
 | 
						<key>CFBundleSignature</key>
 | 
				
			||||||
	<string>????</string>
 | 
						<string>????</string>
 | 
				
			||||||
	<key>CFBundleVersion</key>
 | 
					 | 
				
			||||||
	<string>$(FLUTTER_BUILD_NUMBER)</string>
 | 
					 | 
				
			||||||
	<key>CFBundleURLTypes</key>
 | 
						<key>CFBundleURLTypes</key>
 | 
				
			||||||
	<array>
 | 
						<array>
 | 
				
			||||||
		<dict>
 | 
							<dict>
 | 
				
			||||||
@@ -45,18 +37,35 @@
 | 
				
			|||||||
			</array>
 | 
								</array>
 | 
				
			||||||
		</dict>
 | 
							</dict>
 | 
				
			||||||
	</array>
 | 
						</array>
 | 
				
			||||||
 | 
						<key>CFBundleVersion</key>
 | 
				
			||||||
 | 
						<string>$(FLUTTER_BUILD_NUMBER)</string>
 | 
				
			||||||
 | 
						<key>CLIENT_ID</key>
 | 
				
			||||||
 | 
						<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
 | 
				
			||||||
 | 
						<key>ITSAppUsesNonExemptEncryption</key>
 | 
				
			||||||
 | 
						<false/>
 | 
				
			||||||
	<key>LSRequiresIPhoneOS</key>
 | 
						<key>LSRequiresIPhoneOS</key>
 | 
				
			||||||
	<true/>
 | 
						<true/>
 | 
				
			||||||
	<key>NSCalendarsUsageDescription</key>
 | 
						<key>NSCalendarsUsageDescription</key>
 | 
				
			||||||
	<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
 | 
						<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
 | 
				
			||||||
	<key>NSCameraUsageDescription</key>
 | 
						<key>NSCameraUsageDescription</key>
 | 
				
			||||||
	<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
 | 
						<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
 | 
				
			||||||
 | 
						<key>NSFaceIDUsageDescription</key>
 | 
				
			||||||
 | 
						<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
 | 
				
			||||||
	<key>NSMicrophoneUsageDescription</key>
 | 
						<key>NSMicrophoneUsageDescription</key>
 | 
				
			||||||
	<string>Grant access to Microphone will allow Solian record audio for your post.</string>
 | 
						<string>Grant access to Microphone will allow Solian record audio for your post.</string>
 | 
				
			||||||
	<key>NSPhotoLibraryAddUsageDescription</key>
 | 
						<key>NSPhotoLibraryAddUsageDescription</key>
 | 
				
			||||||
	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
 | 
						<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
 | 
				
			||||||
	<key>NSPhotoLibraryUsageDescription</key>
 | 
						<key>NSPhotoLibraryUsageDescription</key>
 | 
				
			||||||
	<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
 | 
						<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
 | 
				
			||||||
 | 
						<key>NSUserActivityTypes</key>
 | 
				
			||||||
 | 
						<array>
 | 
				
			||||||
 | 
							<string>INStartCallIntent</string>
 | 
				
			||||||
 | 
							<string>INSendMessageIntent</string>
 | 
				
			||||||
 | 
						</array>
 | 
				
			||||||
 | 
						<key>PLIST_VERSION</key>
 | 
				
			||||||
 | 
						<string>1</string>
 | 
				
			||||||
 | 
						<key>REVERSED_CLIENT_ID</key>
 | 
				
			||||||
 | 
						<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
 | 
				
			||||||
	<key>UIApplicationSupportsIndirectInputEvents</key>
 | 
						<key>UIApplicationSupportsIndirectInputEvents</key>
 | 
				
			||||||
	<true/>
 | 
						<true/>
 | 
				
			||||||
	<key>UIBackgroundModes</key>
 | 
						<key>UIBackgroundModes</key>
 | 
				
			||||||
@@ -75,24 +84,13 @@
 | 
				
			|||||||
	<key>UISupportedInterfaceOrientations</key>
 | 
						<key>UISupportedInterfaceOrientations</key>
 | 
				
			||||||
	<array>
 | 
						<array>
 | 
				
			||||||
		<string>UIInterfaceOrientationPortrait</string>
 | 
							<string>UIInterfaceOrientationPortrait</string>
 | 
				
			||||||
		<string>UIInterfaceOrientationLandscapeLeft</string>
 | 
					 | 
				
			||||||
		<string>UIInterfaceOrientationLandscapeRight</string>
 | 
					 | 
				
			||||||
	</array>
 | 
						</array>
 | 
				
			||||||
	<key>UISupportedInterfaceOrientations~ipad</key>
 | 
						<key>UISupportedInterfaceOrientations~ipad</key>
 | 
				
			||||||
	<array>
 | 
						<array>
 | 
				
			||||||
		<string>UIInterfaceOrientationPortrait</string>
 | 
					 | 
				
			||||||
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
 | 
					 | 
				
			||||||
		<string>UIInterfaceOrientationLandscapeLeft</string>
 | 
							<string>UIInterfaceOrientationLandscapeLeft</string>
 | 
				
			||||||
		<string>UIInterfaceOrientationLandscapeRight</string>
 | 
							<string>UIInterfaceOrientationLandscapeRight</string>
 | 
				
			||||||
	</array>
 | 
							<string>UIInterfaceOrientationPortrait</string>
 | 
				
			||||||
	<key>NSFaceIDUsageDescription</key>
 | 
							<string>UIInterfaceOrientationPortraitUpsideDown</string>
 | 
				
			||||||
	<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
 | 
					 | 
				
			||||||
	<key>AppGroupId</key>
 | 
					 | 
				
			||||||
	<string>$(CUSTOM_GROUP_ID)</string>
 | 
					 | 
				
			||||||
	<key>NSUserActivityTypes</key>
 | 
					 | 
				
			||||||
	<array>
 | 
					 | 
				
			||||||
		<string>INStartCallIntent</string>
 | 
					 | 
				
			||||||
		<string>INSendMessageIntent</string>
 | 
					 | 
				
			||||||
	</array>
 | 
						</array>
 | 
				
			||||||
</dict>
 | 
					</dict>
 | 
				
			||||||
</plist>
 | 
					</plist>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								lib/models/abuse_report.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/models/abuse_report.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'abuse_report.freezed.dart';
 | 
				
			||||||
 | 
					part 'abuse_report.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					sealed class SnAbuseReport with _$SnAbuseReport {
 | 
				
			||||||
 | 
					  const factory SnAbuseReport({
 | 
				
			||||||
 | 
					    required String id,
 | 
				
			||||||
 | 
					    required String resourceIdentifier,
 | 
				
			||||||
 | 
					    required int type,
 | 
				
			||||||
 | 
					    required String reason,
 | 
				
			||||||
 | 
					    required DateTime? resolvedAt,
 | 
				
			||||||
 | 
					    required String? resolution,
 | 
				
			||||||
 | 
					    required String accountId,
 | 
				
			||||||
 | 
					    required DateTime createdAt,
 | 
				
			||||||
 | 
					    required DateTime updatedAt,
 | 
				
			||||||
 | 
					    required DateTime? deletedAt,
 | 
				
			||||||
 | 
					  }) = _SnAbuseReport;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory SnAbuseReport.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$SnAbuseReportFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										175
									
								
								lib/models/abuse_report.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								lib/models/abuse_report.freezed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					// dart format width=80
 | 
				
			||||||
 | 
					// coverage:ignore-file
 | 
				
			||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					// ignore_for_file: type=lint
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'abuse_report.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// FreezedGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dart format off
 | 
				
			||||||
 | 
					T _$identity<T>(T value) => value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					mixin _$SnAbuseReport {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 String get id; String get resourceIdentifier; int get type; String get reason; DateTime? get resolvedAt; String? get resolution; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
				
			||||||
 | 
					/// Create a copy of SnAbuseReport
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnAbuseReportCopyWith<SnAbuseReport> get copyWith => _$SnAbuseReportCopyWithImpl<SnAbuseReport>(this as SnAbuseReport, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Serializes this SnAbuseReport to a JSON map.
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class $SnAbuseReportCopyWith<$Res>  {
 | 
				
			||||||
 | 
					  factory $SnAbuseReportCopyWith(SnAbuseReport value, $Res Function(SnAbuseReport) _then) = _$SnAbuseReportCopyWithImpl;
 | 
				
			||||||
 | 
					@useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class _$SnAbuseReportCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements $SnAbuseReportCopyWith<$Res> {
 | 
				
			||||||
 | 
					  _$SnAbuseReportCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final SnAbuseReport _self;
 | 
				
			||||||
 | 
					  final $Res Function(SnAbuseReport) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnAbuseReport
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
				
			||||||
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
 | 
					id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _SnAbuseReport implements SnAbuseReport {
 | 
				
			||||||
 | 
					  const _SnAbuseReport({required this.id, required this.resourceIdentifier, required this.type, required this.reason, required this.resolvedAt, required this.resolution, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
 | 
				
			||||||
 | 
					  factory _SnAbuseReport.fromJson(Map<String, dynamic> json) => _$SnAbuseReportFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  String id;
 | 
				
			||||||
 | 
					@override final  String resourceIdentifier;
 | 
				
			||||||
 | 
					@override final  int type;
 | 
				
			||||||
 | 
					@override final  String reason;
 | 
				
			||||||
 | 
					@override final  DateTime? resolvedAt;
 | 
				
			||||||
 | 
					@override final  String? resolution;
 | 
				
			||||||
 | 
					@override final  String accountId;
 | 
				
			||||||
 | 
					@override final  DateTime createdAt;
 | 
				
			||||||
 | 
					@override final  DateTime updatedAt;
 | 
				
			||||||
 | 
					@override final  DateTime? deletedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnAbuseReport
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					_$SnAbuseReportCopyWith<_SnAbuseReport> get copyWith => __$SnAbuseReportCopyWithImpl<_SnAbuseReport>(this, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					  return _$SnAbuseReportToJson(this, );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class _$SnAbuseReportCopyWith<$Res> implements $SnAbuseReportCopyWith<$Res> {
 | 
				
			||||||
 | 
					  factory _$SnAbuseReportCopyWith(_SnAbuseReport value, $Res Function(_SnAbuseReport) _then) = __$SnAbuseReportCopyWithImpl;
 | 
				
			||||||
 | 
					@override @useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class __$SnAbuseReportCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements _$SnAbuseReportCopyWith<$Res> {
 | 
				
			||||||
 | 
					  __$SnAbuseReportCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _SnAbuseReport _self;
 | 
				
			||||||
 | 
					  final $Res Function(_SnAbuseReport) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnAbuseReport
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
				
			||||||
 | 
					  return _then(_SnAbuseReport(
 | 
				
			||||||
 | 
					id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dart format on
 | 
				
			||||||
							
								
								
									
										41
									
								
								lib/models/abuse_report.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								lib/models/abuse_report.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'abuse_report.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// JsonSerializableGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					    _SnAbuseReport(
 | 
				
			||||||
 | 
					      id: json['id'] as String,
 | 
				
			||||||
 | 
					      resourceIdentifier: json['resource_identifier'] as String,
 | 
				
			||||||
 | 
					      type: (json['type'] as num).toInt(),
 | 
				
			||||||
 | 
					      reason: json['reason'] as String,
 | 
				
			||||||
 | 
					      resolvedAt:
 | 
				
			||||||
 | 
					          json['resolved_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['resolved_at'] as String),
 | 
				
			||||||
 | 
					      resolution: json['resolution'] as String?,
 | 
				
			||||||
 | 
					      accountId: json['account_id'] as String,
 | 
				
			||||||
 | 
					      createdAt: DateTime.parse(json['created_at'] as String),
 | 
				
			||||||
 | 
					      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
				
			||||||
 | 
					      deletedAt:
 | 
				
			||||||
 | 
					          json['deleted_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['deleted_at'] as String),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'id': instance.id,
 | 
				
			||||||
 | 
					      'resource_identifier': instance.resourceIdentifier,
 | 
				
			||||||
 | 
					      'type': instance.type,
 | 
				
			||||||
 | 
					      'reason': instance.reason,
 | 
				
			||||||
 | 
					      'resolved_at': instance.resolvedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'resolution': instance.resolution,
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					      'created_at': instance.createdAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'updated_at': instance.updatedAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
							
								
								
									
										38
									
								
								lib/models/abuse_report_type.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/models/abuse_report_type.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					enum AbuseReportType {
 | 
				
			||||||
 | 
					  copyright(0),
 | 
				
			||||||
 | 
					  harassment(1),
 | 
				
			||||||
 | 
					  impersonation(2),
 | 
				
			||||||
 | 
					  offensiveContent(3),
 | 
				
			||||||
 | 
					  spam(4),
 | 
				
			||||||
 | 
					  privacyViolation(5),
 | 
				
			||||||
 | 
					  illegalContent(6),
 | 
				
			||||||
 | 
					  other(7);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AbuseReportType(this.value);
 | 
				
			||||||
 | 
					  final int value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static AbuseReportType fromValue(int value) {
 | 
				
			||||||
 | 
					    return values.firstWhere((e) => e.value == value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get displayName {
 | 
				
			||||||
 | 
					    switch (this) {
 | 
				
			||||||
 | 
					      case AbuseReportType.copyright:
 | 
				
			||||||
 | 
					        return 'Copyright';
 | 
				
			||||||
 | 
					      case AbuseReportType.harassment:
 | 
				
			||||||
 | 
					        return 'Harassment';
 | 
				
			||||||
 | 
					      case AbuseReportType.impersonation:
 | 
				
			||||||
 | 
					        return 'Impersonation';
 | 
				
			||||||
 | 
					      case AbuseReportType.offensiveContent:
 | 
				
			||||||
 | 
					        return 'Offensive Content';
 | 
				
			||||||
 | 
					      case AbuseReportType.spam:
 | 
				
			||||||
 | 
					        return 'Spam';
 | 
				
			||||||
 | 
					      case AbuseReportType.privacyViolation:
 | 
				
			||||||
 | 
					        return 'Privacy Violation';
 | 
				
			||||||
 | 
					      case AbuseReportType.illegalContent:
 | 
				
			||||||
 | 
					        return 'Illegal Content';
 | 
				
			||||||
 | 
					      case AbuseReportType.other:
 | 
				
			||||||
 | 
					        return 'Other';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import 'package:island/screens/posts/post_search.dart';
 | 
				
			|||||||
import 'package:island/widgets/app_wrapper.dart';
 | 
					import 'package:island/widgets/app_wrapper.dart';
 | 
				
			||||||
import 'package:island/screens/tabs.dart';
 | 
					import 'package:island/screens/tabs.dart';
 | 
				
			||||||
import 'package:island/screens/explore.dart';
 | 
					import 'package:island/screens/explore.dart';
 | 
				
			||||||
import 'package:island/screens/article_detail_screen.dart';
 | 
					import 'package:island/screens/discovery/article_detail.dart';
 | 
				
			||||||
import 'package:island/screens/account.dart';
 | 
					import 'package:island/screens/account.dart';
 | 
				
			||||||
import 'package:island/screens/notification.dart';
 | 
					import 'package:island/screens/notification.dart';
 | 
				
			||||||
import 'package:island/screens/wallet.dart';
 | 
					import 'package:island/screens/wallet.dart';
 | 
				
			||||||
@@ -41,6 +41,8 @@ import 'package:island/screens/realm/realms.dart';
 | 
				
			|||||||
import 'package:island/screens/realm/realm_detail.dart';
 | 
					import 'package:island/screens/realm/realm_detail.dart';
 | 
				
			||||||
import 'package:island/screens/account/event_calendar.dart';
 | 
					import 'package:island/screens/account/event_calendar.dart';
 | 
				
			||||||
import 'package:island/screens/discovery/realms.dart';
 | 
					import 'package:island/screens/discovery/realms.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/reports/report_detail.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/reports/report_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Shell route keys for nested navigation
 | 
					// Shell route keys for nested navigation
 | 
				
			||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
 | 
					final rootNavigatorKey = GlobalKey<NavigatorState>();
 | 
				
			||||||
@@ -258,6 +260,19 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
            builder: (context, state) => const AboutScreen(),
 | 
					            builder: (context, state) => const AboutScreen(),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          GoRoute(
 | 
				
			||||||
 | 
					            path: '/safety/reports/me',
 | 
				
			||||||
 | 
					            builder: (context, state) => const AbuseReportListScreen(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          GoRoute(
 | 
				
			||||||
 | 
					            path: '/safety/reports/me/:id',
 | 
				
			||||||
 | 
					            builder: (context, state) {
 | 
				
			||||||
 | 
					              final id = state.pathParameters['id']!;
 | 
				
			||||||
 | 
					              return AbuseReportDetailScreen(reportId: id);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Main tabs with TabsScreen shell
 | 
					          // Main tabs with TabsScreen shell
 | 
				
			||||||
          ShellRoute(
 | 
					          ShellRoute(
 | 
				
			||||||
            navigatorKey: _tabsShellKey,
 | 
					            navigatorKey: _tabsShellKey,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,35 @@
 | 
				
			|||||||
 | 
					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:island/widgets/app_scaffold.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 +37,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 +61,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)) {
 | 
				
			||||||
@@ -60,7 +91,7 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final theme = Theme.of(context);
 | 
					    final theme = Theme.of(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(title: Text('about'.tr()), elevation: 0),
 | 
					      appBar: AppBar(title: Text('about'.tr()), elevation: 0),
 | 
				
			||||||
      body:
 | 
					      body:
 | 
				
			||||||
          _isLoading
 | 
					          _isLoading
 | 
				
			||||||
@@ -108,25 +139,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 +208,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 +217,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 +226,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 +249,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 +274,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(
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
                            'aboutScreenCopyright'.tr(
 | 
					                            'aboutScreenCopyright'.tr(
 | 
				
			||||||
                          args: [DateTime.now().year.toString(), "Solsynth"],
 | 
					                              args: [DateTime.now().year.toString()],
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            style: theme.textTheme.bodySmall,
 | 
					                            style: theme.textTheme.bodySmall,
 | 
				
			||||||
                            textAlign: TextAlign.center,
 | 
					                            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 +330,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 +347,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 +386,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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -222,9 +222,17 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
					              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
              title: Text('relationships').tr(),
 | 
					              title: Text('relationships').tr(),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                context.push('/account/relationship');
 | 
					                context.push('/account/relationships');
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            ListTile(
 | 
				
			||||||
 | 
					              minTileHeight: 48,
 | 
				
			||||||
 | 
					              title: Text('abuseReports').tr(),
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
				
			||||||
 | 
					              leading: const Icon(Symbols.gavel),
 | 
				
			||||||
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
 | 
					              onTap: () => context.push('/safety/reports/me'),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            const Divider(height: 1).padding(vertical: 8),
 | 
					            const Divider(height: 1).padding(vertical: 8),
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              minTileHeight: 48,
 | 
					              minTileHeight: 48,
 | 
				
			||||||
@@ -367,12 +375,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(8),
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Row(
 | 
				
			||||||
 | 
					                  mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    TextButton(
 | 
				
			||||||
 | 
					                      onPressed: () {
 | 
				
			||||||
 | 
					                        context.push('/about');
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: Text('about').tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    TextButton(
 | 
					                    TextButton(
 | 
				
			||||||
                      onPressed: () {
 | 
					                      onPressed: () {
 | 
				
			||||||
                        context.push('/settings');
 | 
					                        context.push('/settings');
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                      child: Text('appSettings').tr(),
 | 
					                      child: Text('appSettings').tr(),
 | 
				
			||||||
                ).center(),
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ).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),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ import 'package:island/widgets/account/status.dart';
 | 
				
			|||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/safety/abuse_report_helper.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:palette_generator/palette_generator.dart';
 | 
					import 'package:palette_generator/palette_generator.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
@@ -72,6 +73,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 +90,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 {
 | 
				
			||||||
@@ -139,6 +144,23 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Future<void> blockAction() async {
 | 
				
			||||||
 | 
					      showLoadingModal(context);
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        final client = ref.watch(apiClientProvider);
 | 
				
			||||||
 | 
					        if (accountRelationship.value == null) {
 | 
				
			||||||
 | 
					          await client.post('/relationships/${account.value!.id}/block');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          await client.delete('/relationships/${account.value!.id}/block');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ref.invalidate(accountRelationshipProvider(name));
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        showErrorAlert(err);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        if (context.mounted) hideLoadingModal(context);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Future<void> directMessageAction() async {
 | 
					    Future<void> directMessageAction() async {
 | 
				
			||||||
      if (!account.hasValue) return;
 | 
					      if (!account.hasValue) return;
 | 
				
			||||||
      if (accountChat.value != null) {
 | 
					      if (accountChat.value != null) {
 | 
				
			||||||
@@ -219,6 +241,8 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
      ];
 | 
					      ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return account.when(
 | 
					    return account.when(
 | 
				
			||||||
      data:
 | 
					      data:
 | 
				
			||||||
          (data) => AppScaffold(
 | 
					          (data) => AppScaffold(
 | 
				
			||||||
@@ -379,13 +403,19 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  ).padding(horizontal: 24),
 | 
					                  ).padding(horizontal: 24),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.value != null)
 | 
				
			||||||
                  SliverToBoxAdapter(
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                  child: const Divider(height: 1).padding(top: 24, bottom: 12),
 | 
					                    child: const Divider(
 | 
				
			||||||
 | 
					                      height: 1,
 | 
				
			||||||
 | 
					                    ).padding(top: 24, bottom: 12),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                if (user.value != null)
 | 
				
			||||||
                  SliverToBoxAdapter(
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
                      spacing: 8,
 | 
					                      spacing: 8,
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        if (accountRelationship.value == null ||
 | 
				
			||||||
 | 
					                            accountRelationship.value!.status > -100)
 | 
				
			||||||
                          Expanded(
 | 
					                          Expanded(
 | 
				
			||||||
                            child: FilledButton.icon(
 | 
					                            child: FilledButton.icon(
 | 
				
			||||||
                              style: ButtonStyle(
 | 
					                              style: ButtonStyle(
 | 
				
			||||||
@@ -397,7 +427,9 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                foregroundColor: WidgetStatePropertyAll(
 | 
					                                foregroundColor: WidgetStatePropertyAll(
 | 
				
			||||||
                                  accountRelationship.value == null
 | 
					                                  accountRelationship.value == null
 | 
				
			||||||
                                      ? null
 | 
					                                      ? null
 | 
				
			||||||
                                  : Theme.of(context).colorScheme.onSecondary,
 | 
					                                      : Theme.of(
 | 
				
			||||||
 | 
					                                        context,
 | 
				
			||||||
 | 
					                                      ).colorScheme.onSecondary,
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              onPressed: relationshipAction,
 | 
					                              onPressed: relationshipAction,
 | 
				
			||||||
@@ -413,6 +445,44 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                      : const Icon(Symbols.person_check),
 | 
					                                      : const Icon(Symbols.person_check),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
 | 
					                        if (accountRelationship.value == null ||
 | 
				
			||||||
 | 
					                            accountRelationship.value!.status <= -100)
 | 
				
			||||||
 | 
					                          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: blockAction,
 | 
				
			||||||
 | 
					                              label:
 | 
				
			||||||
 | 
					                                  Text(
 | 
				
			||||||
 | 
					                                    accountRelationship.value == null
 | 
				
			||||||
 | 
					                                        ? 'blockUser'
 | 
				
			||||||
 | 
					                                        : 'unblockUser',
 | 
				
			||||||
 | 
					                                  ).tr(),
 | 
				
			||||||
 | 
					                              icon:
 | 
				
			||||||
 | 
					                                  accountRelationship.value == null
 | 
				
			||||||
 | 
					                                      ? const Icon(Symbols.block)
 | 
				
			||||||
 | 
					                                      : const Icon(Symbols.person_cancel),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                SliverToBoxAdapter(
 | 
				
			||||||
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    spacing: 8,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
                      Expanded(
 | 
					                      Expanded(
 | 
				
			||||||
                        child: FilledButton.icon(
 | 
					                        child: FilledButton.icon(
 | 
				
			||||||
                          onPressed: directMessageAction,
 | 
					                          onPressed: directMessageAction,
 | 
				
			||||||
@@ -426,8 +496,25 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                              ).tr(),
 | 
					                              ).tr(),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					                      IconButton.filled(
 | 
				
			||||||
 | 
					                        onPressed: () {
 | 
				
			||||||
 | 
					                          showAbuseReportSheet(
 | 
				
			||||||
 | 
					                            context,
 | 
				
			||||||
 | 
					                            resourceIdentifier: 'account/${data.id}',
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        icon: Icon(
 | 
				
			||||||
 | 
					                          Symbols.flag,
 | 
				
			||||||
 | 
					                          color: Theme.of(context).colorScheme.onError,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        style: ButtonStyle(
 | 
				
			||||||
 | 
					                          backgroundColor: WidgetStatePropertyAll(
 | 
				
			||||||
 | 
					                            Theme.of(context).colorScheme.error,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ).padding(horizontal: 16),
 | 
					                  ).padding(horizontal: 16, top: 4),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					                SliverToBoxAdapter(
 | 
				
			||||||
                  child: const Divider(height: 1).padding(top: 12),
 | 
					                  child: const Divider(height: 1).padding(top: 12),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -395,7 +395,7 @@ class _AccountAppbarForcegroundColorProviderElement
 | 
				
			|||||||
  String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
 | 
					  String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$accountDirectChatHash() => r'60d0015fc2a3c8fc2190bb41d6818cf3027d9d0a';
 | 
					String _$accountDirectChatHash() => r'3d28c8ba8079159f724fe3cd47bbe00db55cedcc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [accountDirectChat].
 | 
					/// See also [accountDirectChat].
 | 
				
			||||||
@ProviderFor(accountDirectChat)
 | 
					@ProviderFor(accountDirectChat)
 | 
				
			||||||
@@ -517,7 +517,7 @@ class _AccountDirectChatProviderElement
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$accountRelationshipHash() =>
 | 
					String _$accountRelationshipHash() =>
 | 
				
			||||||
    r'cb7d0d3f8cd4f23ad9d2d529872c540dac483d4f';
 | 
					    r'0be2420e1f6a65b8dcead9617191471924aaf232';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [accountRelationship].
 | 
					/// See also [accountRelationship].
 | 
				
			||||||
@ProviderFor(accountRelationship)
 | 
					@ProviderFor(accountRelationship)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,105 +0,0 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/content/markdown.dart';
 | 
					 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					 | 
				
			||||||
import 'package:island/models/webfeed.dart';
 | 
					 | 
				
			||||||
import 'package:island/pods/article_detail.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/loading_indicator.dart';
 | 
					 | 
				
			||||||
import 'package:html2md/html2md.dart' as html2md;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ArticleDetailScreen extends ConsumerWidget {
 | 
					 | 
				
			||||||
  final String articleId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const ArticleDetailScreen({super.key, required this.articleId});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					 | 
				
			||||||
    final articleAsync = ref.watch(articleDetailProvider(articleId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return AppScaffold(
 | 
					 | 
				
			||||||
      body: articleAsync.when(
 | 
					 | 
				
			||||||
        data:
 | 
					 | 
				
			||||||
            (article) => AppScaffold(
 | 
					 | 
				
			||||||
              appBar: AppBar(
 | 
					 | 
				
			||||||
                leading: const BackButton(),
 | 
					 | 
				
			||||||
                title: Text(article.title),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              body: _ArticleDetailContent(article: article),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        loading: () => const Center(child: LoadingIndicator()),
 | 
					 | 
				
			||||||
        error:
 | 
					 | 
				
			||||||
            (error, stackTrace) =>
 | 
					 | 
				
			||||||
                Center(child: Text('Failed to load article: $error')),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _ArticleDetailContent extends HookConsumerWidget {
 | 
					 | 
				
			||||||
  final SnWebArticle article;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const _ArticleDetailContent({required this.article});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					 | 
				
			||||||
    final markdownContent = useMemoized(
 | 
					 | 
				
			||||||
      () => html2md.convert(article.content ?? ''),
 | 
					 | 
				
			||||||
      [article],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                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,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                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),
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:island/models/webfeed.dart';
 | 
					import 'package:island/models/webfeed.dart';
 | 
				
			||||||
import 'package:island/pods/webfeed.dart';
 | 
					import 'package:island/pods/webfeed.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -183,7 +184,7 @@ class WebFeedEditScreen extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }, [pubName, feedId, ref, context]);
 | 
					    }, [pubName, feedId, ref, context]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
 | 
					        title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										110
									
								
								lib/screens/discovery/article_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								lib/screens/discovery/article_detail.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/webfeed.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/article_detail.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:html2md/html2md.dart' as html2md;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ArticleDetailScreen extends ConsumerWidget {
 | 
				
			||||||
 | 
					  final String articleId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ArticleDetailScreen({super.key, required this.articleId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final articleAsync = ref.watch(articleDetailProvider(articleId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      body: articleAsync.when(
 | 
				
			||||||
 | 
					        data:
 | 
				
			||||||
 | 
					            (article) => AppScaffold(
 | 
				
			||||||
 | 
					              appBar: AppBar(
 | 
				
			||||||
 | 
					                leading: const BackButton(),
 | 
				
			||||||
 | 
					                title: Text(article.title),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              body: _ArticleDetailContent(article: article),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        loading: () => const Center(child: LoadingIndicator()),
 | 
				
			||||||
 | 
					        error:
 | 
				
			||||||
 | 
					            (error, stackTrace) =>
 | 
				
			||||||
 | 
					                Center(child: Text('Failed to load article: $error')),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ArticleDetailContent extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final SnWebArticle article;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _ArticleDetailContent({required this.article});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final markdownContent = useMemoized(
 | 
				
			||||||
 | 
					      () => html2md.convert(article.content ?? ''),
 | 
				
			||||||
 | 
					      [article],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return SingleChildScrollView(
 | 
				
			||||||
 | 
					      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,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              Padding(
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					                child: Column(
 | 
				
			||||||
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      article.title,
 | 
				
			||||||
 | 
					                      style: Theme.of(context).textTheme.headlineSmall,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    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,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    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),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/webfeed.dart';
 | 
					import 'package:island/models/webfeed.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/web_article_card.dart';
 | 
					import 'package:island/widgets/web_article_card.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
					import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
				
			||||||
@@ -124,9 +125,12 @@ class ArticlesScreen extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(title: Text(title ?? 'Articles')),
 | 
					      appBar: AppBar(title: Text(title ?? 'Articles')),
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: Center(
 | 
				
			||||||
 | 
					        child: ConstrainedBox(
 | 
				
			||||||
 | 
					          constraints: const BoxConstraints(maxWidth: 560),
 | 
				
			||||||
 | 
					          child: CustomScrollView(
 | 
				
			||||||
            slivers: [
 | 
					            slivers: [
 | 
				
			||||||
              SliverPadding(
 | 
					              SliverPadding(
 | 
				
			||||||
                padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
					                padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
				
			||||||
@@ -137,6 +141,8 @@ class ArticlesScreen extends ConsumerWidget {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
@@ -338,7 +338,7 @@ class _ActivityListView extends HookConsumerWidget {
 | 
				
			|||||||
                            bottom: 16,
 | 
					                            bottom: 16,
 | 
				
			||||||
                          )
 | 
					                          )
 | 
				
			||||||
                          : null,
 | 
					                          : null,
 | 
				
			||||||
                  onRefresh: (_) {
 | 
					                  onRefresh: () {
 | 
				
			||||||
                    activitiesNotifier.forceRefresh();
 | 
					                    activitiesNotifier.forceRefresh();
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  onUpdate: (post) {
 | 
					                  onUpdate: (post) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ 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/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_item.dart';
 | 
					import 'package:island/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
					import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -107,7 +108,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: TextField(
 | 
					        title: TextField(
 | 
				
			||||||
          controller: _searchController,
 | 
					          controller: _searchController,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -187,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        onTap: () {
 | 
					                        onTap: () {
 | 
				
			||||||
                          Navigator.pop(context, true);
 | 
					                          Navigator.pop(context, true);
 | 
				
			||||||
                          context.push('/account/${data.name}');
 | 
					                          context.push('/account/${data.account?.name}');
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      Expanded(
 | 
					                      Expanded(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,6 +77,7 @@ class RealmDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      noBackground: false,
 | 
				
			||||||
      body: realmState.when(
 | 
					      body: realmState.when(
 | 
				
			||||||
        loading: () => const Center(child: CircularProgressIndicator()),
 | 
					        loading: () => const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
        error: (error, _) => Center(child: Text('Error: $error')),
 | 
					        error: (error, _) => Center(child: Text('Error: $error')),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										105
									
								
								lib/screens/reports/report_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								lib/screens/reports/report_detail.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/abuse_report.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/abuse_report_type.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/abuse_report_service.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AbuseReportDetailScreen extends ConsumerStatefulWidget {
 | 
				
			||||||
 | 
					  final String reportId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AbuseReportDetailScreen({super.key, required this.reportId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  ConsumerState<AbuseReportDetailScreen> createState() =>
 | 
				
			||||||
 | 
					      _AbuseReportDetailScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _AbuseReportDetailScreenState
 | 
				
			||||||
 | 
					    extends ConsumerState<AbuseReportDetailScreen> {
 | 
				
			||||||
 | 
					  Future<SnAbuseReport>? _reportFuture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _reportFuture = ref
 | 
				
			||||||
 | 
					        .read(abuseReportServiceProvider)
 | 
				
			||||||
 | 
					        .getReport(widget.reportId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: const Text('Abuse Report Details')),
 | 
				
			||||||
 | 
					      body: FutureBuilder<SnAbuseReport>(
 | 
				
			||||||
 | 
					        future: _reportFuture,
 | 
				
			||||||
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
 | 
					          if (snapshot.connectionState == ConnectionState.waiting) {
 | 
				
			||||||
 | 
					            return const Center(child: CircularProgressIndicator());
 | 
				
			||||||
 | 
					          } else if (snapshot.hasError) {
 | 
				
			||||||
 | 
					            return Center(child: Text('Error: ${snapshot.error}'));
 | 
				
			||||||
 | 
					          } else if (snapshot.hasData) {
 | 
				
			||||||
 | 
					            final report = snapshot.data!;
 | 
				
			||||||
 | 
					            return Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					              child: Column(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  _buildDetailRow(context, 'Report ID', report.id),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Resource Identifier',
 | 
				
			||||||
 | 
					                    report.resourceIdentifier,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Type',
 | 
				
			||||||
 | 
					                    AbuseReportType.fromValue(report.type).displayName,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  _buildDetailRow(context, 'Reason', report.reason),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Resolved At',
 | 
				
			||||||
 | 
					                    report.resolvedAt?.toString() ?? 'N/A',
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Resolution',
 | 
				
			||||||
 | 
					                    report.resolution ?? 'N/A',
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  _buildDetailRow(context, 'Account ID', report.accountId),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Created At',
 | 
				
			||||||
 | 
					                    report.createdAt.toString(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  _buildDetailRow(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                    'Updated At',
 | 
				
			||||||
 | 
					                    report.updatedAt.toString(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return const Center(child: Text('No data'));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildDetailRow(BuildContext context, String label, String value) {
 | 
				
			||||||
 | 
					    return Padding(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(label, style: Theme.of(context).textTheme.titleMedium).bold(),
 | 
				
			||||||
 | 
					          Text(value, style: Theme.of(context).textTheme.bodyLarge),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										153
									
								
								lib/screens/reports/report_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								lib/screens/reports/report_list.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
				
			|||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/abuse_report.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/abuse_report_type.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/abuse_report_service.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/time.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/safety/abuse_report_helper.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AbuseReportListScreen extends ConsumerStatefulWidget {
 | 
				
			||||||
 | 
					  const AbuseReportListScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  ConsumerState<AbuseReportListScreen> createState() =>
 | 
				
			||||||
 | 
					      _AbuseReportListScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _AbuseReportListScreenState extends ConsumerState<AbuseReportListScreen> {
 | 
				
			||||||
 | 
					  Future<List<SnAbuseReport>>? _reportsFuture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _reportsFuture = ref.read(abuseReportServiceProvider).getReports();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: Text('abuseReports').tr()),
 | 
				
			||||||
 | 
					      floatingActionButton: FloatingActionButton(
 | 
				
			||||||
 | 
					        child: const Icon(Icons.add),
 | 
				
			||||||
 | 
					        onPressed: () {
 | 
				
			||||||
 | 
					          showAbuseReportSheet(context, resourceIdentifier: 'unidentified');
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: FutureBuilder<List<SnAbuseReport>>(
 | 
				
			||||||
 | 
					        future: _reportsFuture,
 | 
				
			||||||
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
 | 
					          if (snapshot.connectionState == ConnectionState.waiting) {
 | 
				
			||||||
 | 
					            return const Center(child: CircularProgressIndicator());
 | 
				
			||||||
 | 
					          } else if (snapshot.hasError) {
 | 
				
			||||||
 | 
					            return Center(child: Text('Error: ${snapshot.error}'));
 | 
				
			||||||
 | 
					          } else if (snapshot.hasData) {
 | 
				
			||||||
 | 
					            final reports = snapshot.data!;
 | 
				
			||||||
 | 
					            return ListView.builder(
 | 
				
			||||||
 | 
					              padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					              itemCount: reports.length,
 | 
				
			||||||
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                final report = reports[index];
 | 
				
			||||||
 | 
					                return Card(
 | 
				
			||||||
 | 
					                  elevation: 2,
 | 
				
			||||||
 | 
					                  margin: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                    horizontal: 16,
 | 
				
			||||||
 | 
					                    vertical: 8,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  child: InkWell(
 | 
				
			||||||
 | 
					                    onTap: () {
 | 
				
			||||||
 | 
					                      context.push('/safety/reports/me/${report.id}');
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    child: Padding(
 | 
				
			||||||
 | 
					                      padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					                      child: Column(
 | 
				
			||||||
 | 
					                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
 | 
					                            report.reason,
 | 
				
			||||||
 | 
					                            style: Theme.of(context).textTheme.titleMedium,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const SizedBox(height: 8),
 | 
				
			||||||
 | 
					                          Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                'ID',
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                report.id,
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const SizedBox(height: 4),
 | 
				
			||||||
 | 
					                          Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                'Type',
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                AbuseReportType.fromValue(
 | 
				
			||||||
 | 
					                                  report.type,
 | 
				
			||||||
 | 
					                                ).displayName,
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const SizedBox(height: 4),
 | 
				
			||||||
 | 
					                          Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                'Created at',
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                '${report.createdAt.formatRelative(context)} · ${report.createdAt.formatSystem()}',
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const SizedBox(height: 4),
 | 
				
			||||||
 | 
					                          Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                'Status',
 | 
				
			||||||
 | 
					                                style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                report.resolvedAt != null
 | 
				
			||||||
 | 
					                                    ? 'Resolved'
 | 
				
			||||||
 | 
					                                    : 'Unresolved',
 | 
				
			||||||
 | 
					                                style: Theme.of(
 | 
				
			||||||
 | 
					                                  context,
 | 
				
			||||||
 | 
					                                ).textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
 | 
					                                  color:
 | 
				
			||||||
 | 
					                                      report.resolvedAt != null
 | 
				
			||||||
 | 
					                                          ? Colors.green
 | 
				
			||||||
 | 
					                                          : Colors.orange,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return const Center(child: Text('No data'));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								lib/services/abuse_report_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/services/abuse_report_service.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/abuse_report.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final abuseReportServiceProvider = Provider<AbuseReportService>((ref) {
 | 
				
			||||||
 | 
					  return AbuseReportService(ref);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AbuseReportService {
 | 
				
			||||||
 | 
					  final Ref ref;
 | 
				
			||||||
 | 
					  AbuseReportService(this.ref);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<SnAbuseReport> getReport(String id) async {
 | 
				
			||||||
 | 
					    final response =
 | 
				
			||||||
 | 
					        await ref.read(apiClientProvider).get('/safety/reports/me/$id');
 | 
				
			||||||
 | 
					    return SnAbuseReport.fromJson(response.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<SnAbuseReport>> getReports() async {
 | 
				
			||||||
 | 
					    final response = await ref.read(apiClientProvider).get('/safety/reports/me');
 | 
				
			||||||
 | 
					    return (response.data as List)
 | 
				
			||||||
 | 
					        .map((json) => SnAbuseReport.fromJson(json))
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -63,9 +63,11 @@ 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,
 | 
					 | 
				
			||||||
    alert: true,
 | 
					    alert: true,
 | 
				
			||||||
    badge: true,
 | 
					    badge: true,
 | 
				
			||||||
    sound: true,
 | 
					    sound: true,
 | 
				
			||||||
@@ -97,6 +99,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.");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,6 @@ import 'dart:convert';
 | 
				
			|||||||
import 'package:flutter/widgets.dart';
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/widgets/tour/techincal_review_intro.dart';
 | 
					 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'tour.g.dart';
 | 
					part 'tour.g.dart';
 | 
				
			||||||
@@ -12,7 +11,7 @@ part 'tour.freezed.dart';
 | 
				
			|||||||
const kAppTourStatusKey = "app_tour_statuses";
 | 
					const kAppTourStatusKey = "app_tour_statuses";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const List<Tour> kAllTours = [
 | 
					const List<Tour> kAllTours = [
 | 
				
			||||||
  Tour(id: 'technical_review_intro', isStartup: true),
 | 
					  // Tour(id: 'technical_review_intro', isStartup: true),
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
@@ -22,7 +21,7 @@ sealed class Tour with _$Tour {
 | 
				
			|||||||
  const factory Tour({required String id, required bool isStartup}) = _Tour;
 | 
					  const factory Tour({required String id, required bool isStartup}) = _Tour;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget get widget => switch (id) {
 | 
					  Widget get widget => switch (id) {
 | 
				
			||||||
    'technical_review_intro' => const TechicalReviewIntroWidget(),
 | 
					    // 'technical_review_intro' => const TechicalReviewIntroWidget(),
 | 
				
			||||||
    _ => throw UnimplementedError(),
 | 
					    _ => throw UnimplementedError(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								lib/utils/abuse_report_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								lib/utils/abuse_report_utils.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					String getAbuseReportTypeString(int type) {
 | 
				
			||||||
 | 
					  switch (type) {
 | 
				
			||||||
 | 
					    case 0:
 | 
				
			||||||
 | 
					      return 'Copyright';
 | 
				
			||||||
 | 
					    case 1:
 | 
				
			||||||
 | 
					      return 'Harassment';
 | 
				
			||||||
 | 
					    case 2:
 | 
				
			||||||
 | 
					      return 'Impersonation';
 | 
				
			||||||
 | 
					    case 3:
 | 
				
			||||||
 | 
					      return 'Offensive Content';
 | 
				
			||||||
 | 
					    case 4:
 | 
				
			||||||
 | 
					      return 'Spam';
 | 
				
			||||||
 | 
					    case 5:
 | 
				
			||||||
 | 
					      return 'Privacy Violation';
 | 
				
			||||||
 | 
					    case 6:
 | 
				
			||||||
 | 
					      return 'Illegal Content';
 | 
				
			||||||
 | 
					    case 7:
 | 
				
			||||||
 | 
					      return 'Other';
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return 'Unknown';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
@@ -383,13 +384,18 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
                        // Show truncation hint if post is truncated
 | 
					                        // Show truncation hint if post is truncated
 | 
				
			||||||
                        if (item.isTruncated && !isFullPost && item.type != 1)
 | 
					                        if (item.isTruncated && !isFullPost && item.type != 1)
 | 
				
			||||||
                          _PostTruncateHint().padding(
 | 
					                          _PostTruncateHint().padding(
 | 
				
			||||||
                            bottom: item.attachments.isNotEmpty ? 8 : null,
 | 
					                            bottom:
 | 
				
			||||||
 | 
					                                (item.attachments.isNotEmpty ||
 | 
				
			||||||
 | 
					                                        item.repliedPost != null ||
 | 
				
			||||||
 | 
					                                        item.forwardedPost != null)
 | 
				
			||||||
 | 
					                                    ? 8
 | 
				
			||||||
 | 
					                                    : null,
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        if ((item.repliedPost != null ||
 | 
					                        if ((item.repliedPost != null ||
 | 
				
			||||||
                                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(
 | 
				
			||||||
@@ -570,7 +576,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
              callback: () {
 | 
					              callback: () {
 | 
				
			||||||
                showAbuseReportSheet(
 | 
					                showAbuseReportSheet(
 | 
				
			||||||
                  context,
 | 
					                  context,
 | 
				
			||||||
                  resourceIdentifier: 'posts:${item.id}',
 | 
					                  resourceIdentifier: 'post/${item.id}',
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -689,6 +695,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 +703,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 +1038,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,13 +24,16 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
				
			|||||||
          // Replies list
 | 
					          // Replies list
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: CustomScrollView(
 | 
					            child: CustomScrollView(
 | 
				
			||||||
              slivers: [PostRepliesList(
 | 
					              slivers: [
 | 
				
			||||||
 | 
					                PostRepliesList(
 | 
				
			||||||
                  postId: post.id.toString(),
 | 
					                  postId: post.id.toString(),
 | 
				
			||||||
                  backgroundColor: Colors.transparent,
 | 
					                  backgroundColor: Colors.transparent,
 | 
				
			||||||
              )],
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          // Quick reply section
 | 
					          // Quick reply section
 | 
				
			||||||
 | 
					          if (user.value != null)
 | 
				
			||||||
            Material(
 | 
					            Material(
 | 
				
			||||||
              elevation: 2,
 | 
					              elevation: 2,
 | 
				
			||||||
              child: PostQuickReply(
 | 
					              child: PostQuickReply(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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+112
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										25
									
								
								web/.well-known/apple-app-site-association
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/.well-known/apple-app-site-association
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "applinks": {
 | 
				
			||||||
 | 
					    "apps": [],
 | 
				
			||||||
 | 
					    "details": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "appIDs": [
 | 
				
			||||||
 | 
					          "W7HPZ53V6B.dev.solsynth.solian"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "paths": [
 | 
				
			||||||
 | 
					          "*"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "components": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "/": "/*"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "webcredentials": {
 | 
				
			||||||
 | 
					    "apps": [
 | 
				
			||||||
 | 
					      "W7HPZ53V6B.dev.solsynth.solian"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								web/.well-known/assetlinks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/.well-known/assetlinks.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					[
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "relation": ["delegate_permission/common.handle_all_urls"],
 | 
				
			||||||
 | 
					    "target": {
 | 
				
			||||||
 | 
					      "namespace": "android_app",
 | 
				
			||||||
 | 
					      "package_name": "dev.solsynth.solian",
 | 
				
			||||||
 | 
					      "sha256_cert_fingerprints": [
 | 
				
			||||||
 | 
					        "57:0C:A4:E6:1F:57:DF:56:70:42:05:4B:43:E2:DD:9E:00:E6:77:C3:D8:3C:5F:D5:A0:05:59:30:5A:85:F9:BC"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
		Reference in New Issue
	
	Block a user