Compare commits
	
		
			17 Commits
		
	
	
		
			46919dec31
			...
			3.0.0+112
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 996462f1fd | |||
| 778f6bb79f | |||
| 8747f948b9 | |||
| 9546d6e4b8 | |||
| f8d1940af6 | |||
| b2b0891d24 | |||
| 274168d4bc | |||
| 2c98b348d5 | |||
| afc7887ddd | |||
| 99ff78a3d5 | |||
| 2ad85addf6 | |||
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | 
| @@ -41,7 +41,16 @@ | |||||||
|                 <action android:name="android.intent.action.MAIN" /> |                 <action android:name="android.intent.action.MAIN" /> | ||||||
|                 <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.", | ||||||
|   | |||||||
| @@ -532,7 +532,7 @@ | |||||||
|   "aboutScreenContactUsTitle": "联系我们", |   "aboutScreenContactUsTitle": "联系我们", | ||||||
|   "aboutScreenLicenseTitle": "许可证", |   "aboutScreenLicenseTitle": "许可证", | ||||||
|   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", |   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", | ||||||
|   "aboutScreenCopyright": "版权所有 © Solsynth {}", |   "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}", | ||||||
|   "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", |   "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", | ||||||
|   "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}", |   "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}", | ||||||
|   "copiedToClipboard": "已复制到剪贴板", |   "copiedToClipboard": "已复制到剪贴板", | ||||||
|   | |||||||
| @@ -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,31 +26,46 @@ | |||||||
| 	<string>$(FLUTTER_BUILD_NAME)</string> | 	<string>$(FLUTTER_BUILD_NAME)</string> | ||||||
| 	<key>CFBundleSignature</key> | 	<key>CFBundleSignature</key> | ||||||
| 	<string>????</string> | 	<string>????</string> | ||||||
|  | 	<key>CFBundleURLTypes</key> | ||||||
|  | 	<array> | ||||||
|  | 		<dict> | ||||||
|  | 			<key>CFBundleTypeRole</key> | ||||||
|  | 			<string>Editor</string> | ||||||
|  | 			<key>CFBundleURLSchemes</key> | ||||||
|  | 			<array> | ||||||
|  | 				<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||||||
|  | 			</array> | ||||||
|  | 		</dict> | ||||||
|  | 	</array> | ||||||
| 	<key>CFBundleVersion</key> | 	<key>CFBundleVersion</key> | ||||||
| 	<string>$(FLUTTER_BUILD_NUMBER)</string> | 	<string>$(FLUTTER_BUILD_NUMBER)</string> | ||||||
|     <key>CFBundleURLTypes</key> | 	<key>CLIENT_ID</key> | ||||||
|     <array> | 	<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string> | ||||||
|         <dict> | 	<key>ITSAppUsesNonExemptEncryption</key> | ||||||
|             <key>CFBundleTypeRole</key> | 	<false/> | ||||||
|             <string>Editor</string> |  | ||||||
|             <key>CFBundleURLSchemes</key> |  | ||||||
|             <array> |  | ||||||
|                 <string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string> |  | ||||||
|             </array> |  | ||||||
|         </dict> |  | ||||||
|     </array> |  | ||||||
| 	<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, | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import 'package:island/pods/network.dart'; | |||||||
| import 'package:island/services/notify.dart'; | import 'package:island/services/notify.dart'; | ||||||
| import 'package:island/services/udid.native.dart'; | import 'package:island/services/udid.native.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: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'; | ||||||
| @@ -90,7 +91,7 @@ class _AboutScreenState extends ConsumerState<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 | ||||||
|   | |||||||
| @@ -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), | ||||||
|                 TextButton( |                 Row( | ||||||
|                   onPressed: () { |                   mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                     context.push('/settings'); |                   children: [ | ||||||
|                   }, |                     TextButton( | ||||||
|                   child: Text('appSettings').tr(), |                       onPressed: () { | ||||||
|                 ).center(), |                         context.push('/about'); | ||||||
|  |                       }, | ||||||
|  |                       child: Text('about').tr(), | ||||||
|  |                     ), | ||||||
|  |                     TextButton( | ||||||
|  |                       onPressed: () { | ||||||
|  |                         context.push('/settings'); | ||||||
|  |                       }, | ||||||
|  |                       child: Text('appSettings').tr(), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ).center(), |           ).center(), | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget { | |||||||
|                       ), |                       ), | ||||||
|  |  | ||||||
|                       // Show user profile if viewing someone else's calendar |                       // Show user profile if viewing someone else's calendar | ||||||
|                       if (name != 'me' && user.hasValue) |                       if (name != 'me' && user.value != null) | ||||||
|                         AccountNameplate(name: name), |                         AccountNameplate(name: name), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
| @@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget { | |||||||
|                     ).padding(horizontal: 8, vertical: 4), |                     ).padding(horizontal: 8, vertical: 4), | ||||||
|  |  | ||||||
|                     // Show user profile if viewing someone else's calendar |                     // Show user profile if viewing someone else's calendar | ||||||
|                     if (name != 'me' && user.hasValue) |                     if (name != 'me' && user.value != null) | ||||||
|                       AccountNameplate(name: name), |                       AccountNameplate(name: name), | ||||||
|                     Gap(MediaQuery.of(context).padding.bottom + 16), |                     Gap(MediaQuery.of(context).padding.bottom + 16), | ||||||
|                   ], |                   ], | ||||||
|   | |||||||
| @@ -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,40 +403,86 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                   ).padding(horizontal: 24), |                   ).padding(horizontal: 24), | ||||||
|                 ), |                 ), | ||||||
|  |  | ||||||
|                 SliverToBoxAdapter( |                 if (user.value != null) | ||||||
|                   child: const Divider(height: 1).padding(top: 24, bottom: 12), |                   SliverToBoxAdapter( | ||||||
|                 ), |                     child: const Divider( | ||||||
|  |                       height: 1, | ||||||
|  |                     ).padding(top: 24, bottom: 12), | ||||||
|  |                   ), | ||||||
|  |                 if (user.value != null) | ||||||
|  |                   SliverToBoxAdapter( | ||||||
|  |                     child: Row( | ||||||
|  |                       spacing: 8, | ||||||
|  |                       children: [ | ||||||
|  |                         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: relationshipAction, | ||||||
|  |                               label: | ||||||
|  |                                   Text( | ||||||
|  |                                     accountRelationship.value == null | ||||||
|  |                                         ? 'addFriendShort' | ||||||
|  |                                         : 'added', | ||||||
|  |                                   ).tr(), | ||||||
|  |                               icon: | ||||||
|  |                                   accountRelationship.value == null | ||||||
|  |                                       ? const Icon(Symbols.person_add) | ||||||
|  |                                       : 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( |                 SliverToBoxAdapter( | ||||||
|                   child: Row( |                   child: Row( | ||||||
|                     spacing: 8, |                     spacing: 8, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Expanded( |  | ||||||
|                         child: FilledButton.icon( |  | ||||||
|                           style: ButtonStyle( |  | ||||||
|                             backgroundColor: WidgetStatePropertyAll( |  | ||||||
|                               accountRelationship.value == null |  | ||||||
|                                   ? null |  | ||||||
|                                   : Theme.of(context).colorScheme.secondary, |  | ||||||
|                             ), |  | ||||||
|                             foregroundColor: WidgetStatePropertyAll( |  | ||||||
|                               accountRelationship.value == null |  | ||||||
|                                   ? null |  | ||||||
|                                   : Theme.of(context).colorScheme.onSecondary, |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                           onPressed: relationshipAction, |  | ||||||
|                           label: |  | ||||||
|                               Text( |  | ||||||
|                                 accountRelationship.value == null |  | ||||||
|                                     ? 'addFriendShort' |  | ||||||
|                                     : 'added', |  | ||||||
|                               ).tr(), |  | ||||||
|                           icon: |  | ||||||
|                               accountRelationship.value == null |  | ||||||
|                                   ? const Icon(Symbols.person_add) |  | ||||||
|                                   : const Icon(Symbols.person_check), |  | ||||||
|                         ), |  | ||||||
|                       ), |  | ||||||
|                       Expanded( |                       Expanded( | ||||||
|                         child: FilledButton.icon( |                         child: FilledButton.icon( | ||||||
|                           onPressed: directMessageAction, |                           onPressed: directMessageAction, | ||||||
| @@ -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,18 +125,23 @@ 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( | ||||||
|         slivers: [ |         child: ConstrainedBox( | ||||||
|           SliverPadding( |           constraints: const BoxConstraints(maxWidth: 560), | ||||||
|             padding: const EdgeInsets.only(top: 8, left: 8, right: 8), |           child: CustomScrollView( | ||||||
|             sliver: SliverArticlesList( |             slivers: [ | ||||||
|               feedId: feedId, |               SliverPadding( | ||||||
|               publisherId: publisherId, |                 padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||||
|             ), |                 sliver: SliverArticlesList( | ||||||
|  |                   feedId: feedId, | ||||||
|  |                   publisherId: publisherId, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|           ), |           ), | ||||||
|         ], |         ), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -307,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return CustomScrollView( |     return CustomScrollView( | ||||||
|       slivers: [ |       slivers: [ | ||||||
|         if (user.hasValue && !contentOnly) |         if (user.value != null && !contentOnly) | ||||||
|           SliverToBoxAdapter(child: CheckInWidget()), |           SliverToBoxAdapter(child: CheckInWidget()), | ||||||
|         SliverList.builder( |         SliverList.builder( | ||||||
|           itemCount: widgetCount, |           itemCount: widgetCount, | ||||||
| @@ -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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -68,7 +68,6 @@ Future<void> subscribePushNotification( | |||||||
|   bool detailedErrors = false, |   bool detailedErrors = false, | ||||||
| }) async { | }) async { | ||||||
|   await FirebaseMessaging.instance.requestPermission( |   await FirebaseMessaging.instance.requestPermission( | ||||||
|     provisional: true, |  | ||||||
|     alert: true, |     alert: true, | ||||||
|     badge: true, |     badge: true, | ||||||
|     sound: true, |     sound: true, | ||||||
|   | |||||||
| @@ -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,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget { | |||||||
|           // Replies list |           // Replies list | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: CustomScrollView( |             child: CustomScrollView( | ||||||
|               slivers: [PostRepliesList( |               slivers: [ | ||||||
|                 postId: post.id.toString(), |                 PostRepliesList( | ||||||
|                 backgroundColor: Colors.transparent, |                   postId: post.id.toString(), | ||||||
|               )], |                   backgroundColor: Colors.transparent, | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           // Quick reply section |           // Quick reply section | ||||||
|           Material( |           if (user.value != null) | ||||||
|             elevation: 2, |             Material( | ||||||
|             child: PostQuickReply( |               elevation: 2, | ||||||
|               parent: post, |               child: PostQuickReply( | ||||||
|               onPosted: () { |                 parent: post, | ||||||
|                 ref.invalidate(postRepliesNotifierProvider(post.id)); |                 onPosted: () { | ||||||
|               }, |                   ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||||
|             ).padding( |                 }, | ||||||
|               bottom: MediaQuery.of(context).padding.bottom + 16, |               ).padding( | ||||||
|               top: 16, |                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||||
|               horizontal: 16, |                 top: 16, | ||||||
|  |                 horizontal: 16, | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ), |  | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.0.0+110 | version: 3.0.0+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