Compare commits
	
		
			13 Commits
		
	
	
		
			356b7bf01a
			...
			3.0.0+110
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 007acedf29 | |||
| 8e903ec6c1 | |||
| b55e56c3c4 | |||
| 6f9de431b1 | |||
| a8efd26262 | |||
| e367fc3f5c | |||
| 8a1af120ea | |||
| f03f0181f8 | |||
| 6c7d42c31a | |||
| d6c829c26a | |||
| 666a2dfbf5 | |||
| fd979c3a35 | |||
| 847fc6e864 | 
| @@ -643,6 +643,8 @@ | ||||
|   "noCustomApps": "No custom apps yet.", | ||||
|   "createCustomApp": "Create Custom App", | ||||
|   "editCustomApp": "Edit Custom App", | ||||
|   "deleteCustomApp": "Delete Custom App", | ||||
|   "deleteCustomAppHint": "Are you sure you want to delete this custom app? This action cannot be undone.", | ||||
|   "publicRealm": "Public Realm", | ||||
|   "publicRealmDescription": "Anyone can preview the content of this realm.", | ||||
|   "communityRealm": "Community Realm", | ||||
| @@ -650,5 +652,31 @@ | ||||
|   "publicChat": "Public Chat", | ||||
|   "publicChatDescription": "Anyone can preview the content of this chat. Including unjoined bots.", | ||||
|   "communityChat": "Community Chat", | ||||
|   "communityChatDescription": "Anyone can join this chat and participate in discussions." | ||||
|   "communityChatDescription": "Anyone can join this chat and participate in discussions.", | ||||
|   "appLinks": "App Links", | ||||
|   "homePageUrl": "Home Page URL", | ||||
|   "privacyPolicyUrl": "Privacy Policy URL", | ||||
|   "termsOfServiceUrl": "Terms of Service URL", | ||||
|   "oauthConfig": "OAuth Configuration", | ||||
|   "clientUri": "Client URI", | ||||
|   "redirectUris": "Redirect URIs", | ||||
|   "addRedirectUri": "Add Redirect URI", | ||||
|   "allowedScopes": "Allowed Scopes", | ||||
|   "requirePkce": "Require PKCE", | ||||
|   "allowOfflineAccess": "Allow Offline Access", | ||||
|   "redirectUri": "Redirect URI", | ||||
|   "redirectUriHint": "The redirect URI is used for OAuth authentication. When the app goes to production, we will validate the redirect URI is match your configuration to reject invalid requests.", | ||||
|   "uriRequired": "The URI is required.", | ||||
|   "uriInvalid": "The URI is invalid.", | ||||
|   "add": "Add", | ||||
|   "addScope": "Add Scope", | ||||
|   "scope": "Scope", | ||||
|   "publisherFeatures": "Features", | ||||
|   "publisherFeatureDevelop": "Developer Program", | ||||
|   "publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.", | ||||
|   "publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.", | ||||
|   "learnMore": "Learn More", | ||||
|   "discoverWebArticles": "Articles from external sites", | ||||
|   "webArticlesStand": "Article Stand", | ||||
|   "about": "About" | ||||
| } | ||||
|   | ||||
| @@ -40,31 +40,31 @@ PODS: | ||||
|   - file_picker (0.0.1): | ||||
|     - DKImagePickerController/PhotoGallery | ||||
|     - Flutter | ||||
|   - Firebase/CoreOnly (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - Firebase/Messaging (11.13.0): | ||||
|   - Firebase/CoreOnly (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - Firebase/Messaging (11.15.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.13.0) | ||||
|   - firebase_core (3.14.0): | ||||
|     - Firebase/CoreOnly (= 11.13.0) | ||||
|     - FirebaseMessaging (~> 11.15.0) | ||||
|   - firebase_core (3.15.0): | ||||
|     - Firebase/CoreOnly (= 11.15.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.2.7): | ||||
|     - Firebase/Messaging (= 11.13.0) | ||||
|   - firebase_messaging (15.2.8): | ||||
|     - Firebase/Messaging (= 11.15.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseCore (11.13.0): | ||||
|     - FirebaseCoreInternal (~> 11.13.0) | ||||
|   - FirebaseCore (11.15.0): | ||||
|     - FirebaseCoreInternal (~> 11.15.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreInternal (11.13.0): | ||||
|   - FirebaseCoreInternal (11.15.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseInstallations (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - FirebaseInstallations (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - FirebaseMessaging (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
| @@ -80,6 +80,8 @@ PODS: | ||||
|   - flutter_inappwebview_ios/Core (0.0.1): | ||||
|     - Flutter | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_keyboard_visibility (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_native_splash (2.4.3): | ||||
|     - Flutter | ||||
|   - flutter_platform_alert (0.0.1): | ||||
| @@ -128,8 +130,8 @@ PODS: | ||||
|     - Flutter | ||||
|   - irondash_engine_context (0.0.1): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.3.2) | ||||
|   - livekit_client (2.4.8): | ||||
|   - Kingfisher (8.3.3) | ||||
|   - livekit_client (2.4.9): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
| @@ -155,6 +157,8 @@ PODS: | ||||
|   - path_provider_foundation (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - pointer_interceptor_ios (0.0.1): | ||||
|     - Flutter | ||||
|   - PromisesObjC (2.4.0) | ||||
|   - receive_sharing_intent (1.8.1): | ||||
|     - Flutter | ||||
| @@ -217,6 +221,7 @@ DEPENDENCIES: | ||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) | ||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||
| @@ -235,6 +240,7 @@ DEPENDENCIES: | ||||
|   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) | ||||
|   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) | ||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) | ||||
|   - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) | ||||
|   - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) | ||||
|   - record_ios (from `.symlinks/plugins/record_ios/ios`) | ||||
|   - share_plus (from `.symlinks/plugins/share_plus/ios`) | ||||
| @@ -286,6 +292,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter | ||||
|   flutter_inappwebview_ios: | ||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||
|   flutter_keyboard_visibility: | ||||
|     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" | ||||
|   flutter_native_splash: | ||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||
|   flutter_platform_alert: | ||||
| @@ -320,6 +328,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/pasteboard/ios" | ||||
|   path_provider_foundation: | ||||
|     :path: ".symlinks/plugins/path_provider_foundation/darwin" | ||||
|   pointer_interceptor_ios: | ||||
|     :path: ".symlinks/plugins/pointer_interceptor_ios/ios" | ||||
|   receive_sharing_intent: | ||||
|     :path: ".symlinks/plugins/receive_sharing_intent/ios" | ||||
|   record_ios: | ||||
| @@ -351,15 +361,16 @@ SPEC CHECKSUMS: | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||
|   Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327 | ||||
|   firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450 | ||||
|   firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954 | ||||
|   FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0 | ||||
|   FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c | ||||
|   FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02 | ||||
|   FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4 | ||||
|   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||
|   firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492 | ||||
|   firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c | ||||
|   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||
|   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||
|   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||
|   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||
|   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 | ||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||
| @@ -371,8 +382,8 @@ SPEC CHECKSUMS: | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||
|   Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5 | ||||
|   livekit_client: 9e901890552514206e5ff828903ed271531da264 | ||||
|   Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be | ||||
|   livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e | ||||
|   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 | ||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||
| @@ -382,6 +393,7 @@ SPEC CHECKSUMS: | ||||
|   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||
|   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c | ||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||
|   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed | ||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||
|   record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b | ||||
|   | ||||
							
								
								
									
										34
									
								
								lib/models/auto_completion.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/models/auto_completion.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'auto_completion.freezed.dart'; | ||||
| part 'auto_completion.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class AutoCompletionResponse with _$AutoCompletionResponse { | ||||
|   const factory AutoCompletionResponse.account({ | ||||
|     required String type, | ||||
|     required List<AutoCompletionItem> items, | ||||
|   }) = AutoCompletionAccountResponse; | ||||
|  | ||||
|   const factory AutoCompletionResponse.sticker({ | ||||
|     required String type, | ||||
|     required List<AutoCompletionItem> items, | ||||
|   }) = AutoCompletionStickerResponse; | ||||
|  | ||||
|   factory AutoCompletionResponse.fromJson(Map<String, dynamic> json) => | ||||
|       _$AutoCompletionResponseFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class AutoCompletionItem with _$AutoCompletionItem { | ||||
|   const factory AutoCompletionItem({ | ||||
|     required String id, | ||||
|     required String displayName, | ||||
|     required String? secondaryText, | ||||
|     required String type, | ||||
|     required dynamic data, | ||||
|   }) = _AutoCompletionItem; | ||||
|  | ||||
|   factory AutoCompletionItem.fromJson(Map<String, dynamic> json) => | ||||
|       _$AutoCompletionItemFromJson(json); | ||||
| } | ||||
							
								
								
									
										410
									
								
								lib/models/auto_completion.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								lib/models/auto_completion.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,410 @@ | ||||
| // 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 'auto_completion.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
| AutoCompletionResponse _$AutoCompletionResponseFromJson( | ||||
|   Map<String, dynamic> json | ||||
| ) { | ||||
|         switch (json['runtimeType']) { | ||||
|                   case 'account': | ||||
|           return AutoCompletionAccountResponse.fromJson( | ||||
|             json | ||||
|           ); | ||||
|                 case 'sticker': | ||||
|           return AutoCompletionStickerResponse.fromJson( | ||||
|             json | ||||
|           ); | ||||
|          | ||||
|           default: | ||||
|             throw CheckedFromJsonException( | ||||
|   json, | ||||
|   'runtimeType', | ||||
|   'AutoCompletionResponse', | ||||
|   'Invalid union type "${json['runtimeType']}"!' | ||||
| ); | ||||
|         } | ||||
|        | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$AutoCompletionResponse { | ||||
|  | ||||
|  String get type; List<AutoCompletionItem> get items; | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $AutoCompletionResponseCopyWith<AutoCompletionResponse> get copyWith => _$AutoCompletionResponseCopyWithImpl<AutoCompletionResponse>(this as AutoCompletionResponse, _$identity); | ||||
|  | ||||
|   /// Serializes this AutoCompletionResponse to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.items, items)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(items)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AutoCompletionResponse(type: $type, items: $items)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $AutoCompletionResponseCopyWith<$Res>  { | ||||
|   factory $AutoCompletionResponseCopyWith(AutoCompletionResponse value, $Res Function(AutoCompletionResponse) _then) = _$AutoCompletionResponseCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String type, List<AutoCompletionItem> items | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$AutoCompletionResponseCopyWithImpl<$Res> | ||||
|     implements $AutoCompletionResponseCopyWith<$Res> { | ||||
|   _$AutoCompletionResponseCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final AutoCompletionResponse _self; | ||||
|   final $Res Function(AutoCompletionResponse) _then; | ||||
|  | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? items = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable | ||||
| as List<AutoCompletionItem>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class AutoCompletionAccountResponse implements AutoCompletionResponse { | ||||
|   const AutoCompletionAccountResponse({required this.type, required final  List<AutoCompletionItem> items, final  String? $type}): _items = items,$type = $type ?? 'account'; | ||||
|   factory AutoCompletionAccountResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionAccountResponseFromJson(json); | ||||
|  | ||||
| @override final  String type; | ||||
|  final  List<AutoCompletionItem> _items; | ||||
| @override List<AutoCompletionItem> get items { | ||||
|   if (_items is EqualUnmodifiableListView) return _items; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_items); | ||||
| } | ||||
|  | ||||
|  | ||||
| @JsonKey(name: 'runtimeType') | ||||
| final String $type; | ||||
|  | ||||
|  | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $AutoCompletionAccountResponseCopyWith<AutoCompletionAccountResponse> get copyWith => _$AutoCompletionAccountResponseCopyWithImpl<AutoCompletionAccountResponse>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$AutoCompletionAccountResponseToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionAccountResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AutoCompletionResponse.account(type: $type, items: $items)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $AutoCompletionAccountResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> { | ||||
|   factory $AutoCompletionAccountResponseCopyWith(AutoCompletionAccountResponse value, $Res Function(AutoCompletionAccountResponse) _then) = _$AutoCompletionAccountResponseCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String type, List<AutoCompletionItem> items | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$AutoCompletionAccountResponseCopyWithImpl<$Res> | ||||
|     implements $AutoCompletionAccountResponseCopyWith<$Res> { | ||||
|   _$AutoCompletionAccountResponseCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final AutoCompletionAccountResponse _self; | ||||
|   final $Res Function(AutoCompletionAccountResponse) _then; | ||||
|  | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) { | ||||
|   return _then(AutoCompletionAccountResponse( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable | ||||
| as List<AutoCompletionItem>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class AutoCompletionStickerResponse implements AutoCompletionResponse { | ||||
|   const AutoCompletionStickerResponse({required this.type, required final  List<AutoCompletionItem> items, final  String? $type}): _items = items,$type = $type ?? 'sticker'; | ||||
|   factory AutoCompletionStickerResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionStickerResponseFromJson(json); | ||||
|  | ||||
| @override final  String type; | ||||
|  final  List<AutoCompletionItem> _items; | ||||
| @override List<AutoCompletionItem> get items { | ||||
|   if (_items is EqualUnmodifiableListView) return _items; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_items); | ||||
| } | ||||
|  | ||||
|  | ||||
| @JsonKey(name: 'runtimeType') | ||||
| final String $type; | ||||
|  | ||||
|  | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $AutoCompletionStickerResponseCopyWith<AutoCompletionStickerResponse> get copyWith => _$AutoCompletionStickerResponseCopyWithImpl<AutoCompletionStickerResponse>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$AutoCompletionStickerResponseToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionStickerResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AutoCompletionResponse.sticker(type: $type, items: $items)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $AutoCompletionStickerResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> { | ||||
|   factory $AutoCompletionStickerResponseCopyWith(AutoCompletionStickerResponse value, $Res Function(AutoCompletionStickerResponse) _then) = _$AutoCompletionStickerResponseCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String type, List<AutoCompletionItem> items | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$AutoCompletionStickerResponseCopyWithImpl<$Res> | ||||
|     implements $AutoCompletionStickerResponseCopyWith<$Res> { | ||||
|   _$AutoCompletionStickerResponseCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final AutoCompletionStickerResponse _self; | ||||
|   final $Res Function(AutoCompletionStickerResponse) _then; | ||||
|  | ||||
| /// Create a copy of AutoCompletionResponse | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) { | ||||
|   return _then(AutoCompletionStickerResponse( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable | ||||
| as List<AutoCompletionItem>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$AutoCompletionItem { | ||||
|  | ||||
|  String get id; String get displayName; String? get secondaryText; String get type; dynamic get data; | ||||
| /// Create a copy of AutoCompletionItem | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $AutoCompletionItemCopyWith<AutoCompletionItem> get copyWith => _$AutoCompletionItemCopyWithImpl<AutoCompletionItem>(this as AutoCompletionItem, _$identity); | ||||
|  | ||||
|   /// Serializes this AutoCompletionItem to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $AutoCompletionItemCopyWith<$Res>  { | ||||
|   factory $AutoCompletionItemCopyWith(AutoCompletionItem value, $Res Function(AutoCompletionItem) _then) = _$AutoCompletionItemCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String displayName, String? secondaryText, String type, dynamic data | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$AutoCompletionItemCopyWithImpl<$Res> | ||||
|     implements $AutoCompletionItemCopyWith<$Res> { | ||||
|   _$AutoCompletionItemCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final AutoCompletionItem _self; | ||||
|   final $Res Function(AutoCompletionItem) _then; | ||||
|  | ||||
| /// Create a copy of AutoCompletionItem | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable | ||||
| as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable | ||||
| as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable | ||||
| as dynamic, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _AutoCompletionItem implements AutoCompletionItem { | ||||
|   const _AutoCompletionItem({required this.id, required this.displayName, required this.secondaryText, required this.type, required this.data}); | ||||
|   factory _AutoCompletionItem.fromJson(Map<String, dynamic> json) => _$AutoCompletionItemFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String displayName; | ||||
| @override final  String? secondaryText; | ||||
| @override final  String type; | ||||
| @override final  dynamic data; | ||||
|  | ||||
| /// Create a copy of AutoCompletionItem | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$AutoCompletionItemCopyWith<_AutoCompletionItem> get copyWith => __$AutoCompletionItemCopyWithImpl<_AutoCompletionItem>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$AutoCompletionItemToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$AutoCompletionItemCopyWith<$Res> implements $AutoCompletionItemCopyWith<$Res> { | ||||
|   factory _$AutoCompletionItemCopyWith(_AutoCompletionItem value, $Res Function(_AutoCompletionItem) _then) = __$AutoCompletionItemCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String displayName, String? secondaryText, String type, dynamic data | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$AutoCompletionItemCopyWithImpl<$Res> | ||||
|     implements _$AutoCompletionItemCopyWith<$Res> { | ||||
|   __$AutoCompletionItemCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _AutoCompletionItem _self; | ||||
|   final $Res Function(_AutoCompletionItem) _then; | ||||
|  | ||||
| /// Create a copy of AutoCompletionItem | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) { | ||||
|   return _then(_AutoCompletionItem( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable | ||||
| as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable | ||||
| as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable | ||||
| as dynamic, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										63
									
								
								lib/models/auto_completion.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/models/auto_completion.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'auto_completion.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson( | ||||
|   Map<String, dynamic> json, | ||||
| ) => AutoCompletionAccountResponse( | ||||
|   type: json['type'] as String, | ||||
|   items: | ||||
|       (json['items'] as List<dynamic>) | ||||
|           .map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
|   $type: json['runtimeType'] as String?, | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$AutoCompletionAccountResponseToJson( | ||||
|   AutoCompletionAccountResponse instance, | ||||
| ) => <String, dynamic>{ | ||||
|   'type': instance.type, | ||||
|   'items': instance.items.map((e) => e.toJson()).toList(), | ||||
|   'runtimeType': instance.$type, | ||||
| }; | ||||
|  | ||||
| AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson( | ||||
|   Map<String, dynamic> json, | ||||
| ) => AutoCompletionStickerResponse( | ||||
|   type: json['type'] as String, | ||||
|   items: | ||||
|       (json['items'] as List<dynamic>) | ||||
|           .map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
|   $type: json['runtimeType'] as String?, | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$AutoCompletionStickerResponseToJson( | ||||
|   AutoCompletionStickerResponse instance, | ||||
| ) => <String, dynamic>{ | ||||
|   'type': instance.type, | ||||
|   'items': instance.items.map((e) => e.toJson()).toList(), | ||||
|   'runtimeType': instance.$type, | ||||
| }; | ||||
|  | ||||
| _AutoCompletionItem _$AutoCompletionItemFromJson(Map<String, dynamic> json) => | ||||
|     _AutoCompletionItem( | ||||
|       id: json['id'] as String, | ||||
|       displayName: json['display_name'] as String, | ||||
|       secondaryText: json['secondary_text'] as String?, | ||||
|       type: json['type'] as String, | ||||
|       data: json['data'], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$AutoCompletionItemToJson(_AutoCompletionItem instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'display_name': instance.displayName, | ||||
|       'secondary_text': instance.secondaryText, | ||||
|       'type': instance.type, | ||||
|       'data': instance.data, | ||||
|     }; | ||||
							
								
								
									
										64
									
								
								lib/models/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								lib/models/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/embed.dart'; | ||||
|  | ||||
| part 'webfeed.freezed.dart'; | ||||
| part 'webfeed.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class SnWebFeedConfig with _$SnWebFeedConfig { | ||||
|   const factory SnWebFeedConfig({@Default(false) bool scrapPage}) = | ||||
|       _SnWebFeedConfig; | ||||
|  | ||||
|   factory SnWebFeedConfig.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnWebFeedConfigFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnWebFeed with _$SnWebFeed { | ||||
|   const factory SnWebFeed({ | ||||
|     required String id, | ||||
|     required String url, | ||||
|     required String title, | ||||
|     String? description, | ||||
|     SnScrappedLink? preview, | ||||
|     @Default(SnWebFeedConfig()) SnWebFeedConfig config, | ||||
|     required String publisherId, | ||||
|     @Default([]) List<SnWebArticle> articles, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     DateTime? deletedAt, | ||||
|   }) = _SnWebFeed; | ||||
|  | ||||
|   factory SnWebFeed.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnWebFeedFromJson(json); | ||||
|  | ||||
|   factory SnWebFeed.fromJsonString(String jsonString) => | ||||
|       SnWebFeed.fromJson(jsonDecode(jsonString) as Map<String, dynamic>); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnWebArticle with _$SnWebArticle { | ||||
|   const factory SnWebArticle({ | ||||
|     required String id, | ||||
|     required String title, | ||||
|     required String url, | ||||
|     String? author, | ||||
|     Map<String, dynamic>? meta, | ||||
|     SnScrappedLink? preview, | ||||
|     SnWebFeed? feed, | ||||
|     String? content, | ||||
|     DateTime? publishedAt, | ||||
|     required String feedId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     DateTime? deletedAt, | ||||
|   }) = _SnWebArticle; | ||||
|  | ||||
|   factory SnWebArticle.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnWebArticleFromJson(json); | ||||
|  | ||||
|   factory SnWebArticle.fromJsonString(String jsonString) => | ||||
|       SnWebArticle.fromJson(jsonDecode(jsonString) as Map<String, dynamic>); | ||||
| } | ||||
							
								
								
									
										584
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										584
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,584 @@ | ||||
| // 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 'webfeed.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnWebFeedConfig { | ||||
|  | ||||
|  bool get scrapPage; | ||||
| /// Create a copy of SnWebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedConfigCopyWith<SnWebFeedConfig> get copyWith => _$SnWebFeedConfigCopyWithImpl<SnWebFeedConfig>(this as SnWebFeedConfig, _$identity); | ||||
|  | ||||
|   /// Serializes this SnWebFeedConfig to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,scrapPage); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebFeedConfig(scrapPage: $scrapPage)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnWebFeedConfigCopyWith<$Res>  { | ||||
|   factory $SnWebFeedConfigCopyWith(SnWebFeedConfig value, $Res Function(SnWebFeedConfig) _then) = _$SnWebFeedConfigCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  bool scrapPage | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnWebFeedConfigCopyWithImpl<$Res> | ||||
|     implements $SnWebFeedConfigCopyWith<$Res> { | ||||
|   _$SnWebFeedConfigCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnWebFeedConfig _self; | ||||
|   final $Res Function(SnWebFeedConfig) _then; | ||||
|  | ||||
| /// Create a copy of SnWebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? scrapPage = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable | ||||
| as bool, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnWebFeedConfig implements SnWebFeedConfig { | ||||
|   const _SnWebFeedConfig({this.scrapPage = false}); | ||||
|   factory _SnWebFeedConfig.fromJson(Map<String, dynamic> json) => _$SnWebFeedConfigFromJson(json); | ||||
|  | ||||
| @override@JsonKey() final  bool scrapPage; | ||||
|  | ||||
| /// Create a copy of SnWebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnWebFeedConfigCopyWith<_SnWebFeedConfig> get copyWith => __$SnWebFeedConfigCopyWithImpl<_SnWebFeedConfig>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnWebFeedConfigToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,scrapPage); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebFeedConfig(scrapPage: $scrapPage)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnWebFeedConfigCopyWith<$Res> implements $SnWebFeedConfigCopyWith<$Res> { | ||||
|   factory _$SnWebFeedConfigCopyWith(_SnWebFeedConfig value, $Res Function(_SnWebFeedConfig) _then) = __$SnWebFeedConfigCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  bool scrapPage | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnWebFeedConfigCopyWithImpl<$Res> | ||||
|     implements _$SnWebFeedConfigCopyWith<$Res> { | ||||
|   __$SnWebFeedConfigCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnWebFeedConfig _self; | ||||
|   final $Res Function(_SnWebFeedConfig) _then; | ||||
|  | ||||
| /// Create a copy of SnWebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? scrapPage = null,}) { | ||||
|   return _then(_SnWebFeedConfig( | ||||
| scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable | ||||
| as bool, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnWebFeed { | ||||
|  | ||||
|  String get id; String get url; String get title; String? get description; SnScrappedLink? get preview; SnWebFeedConfig get config; String get publisherId; List<SnWebArticle> get articles; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedCopyWith<SnWebFeed> get copyWith => _$SnWebFeedCopyWithImpl<SnWebFeed>(this as SnWebFeed, _$identity); | ||||
|  | ||||
|   /// Serializes this SnWebFeed to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other.articles, articles)&&(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,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(articles),createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnWebFeedCopyWith<$Res>  { | ||||
|   factory $SnWebFeedCopyWith(SnWebFeed value, $Res Function(SnWebFeed) _then) = _$SnWebFeedCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedConfigCopyWith<$Res> get config; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnWebFeedCopyWithImpl<$Res> | ||||
|     implements $SnWebFeedCopyWith<$Res> { | ||||
|   _$SnWebFeedCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnWebFeed _self; | ||||
|   final $Res Function(SnWebFeed) _then; | ||||
|  | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = 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,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable | ||||
| as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable | ||||
| as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,articles: null == articles ? _self.articles : articles // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnWebArticle>,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?, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview { | ||||
|     if (_self.preview == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) { | ||||
|     return _then(_self.copyWith(preview: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedConfigCopyWith<$Res> get config { | ||||
|    | ||||
|   return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnWebFeed implements SnWebFeed { | ||||
|   const _SnWebFeed({required this.id, required this.url, required this.title, this.description, this.preview, this.config = const SnWebFeedConfig(), required this.publisherId, final  List<SnWebArticle> articles = const [], required this.createdAt, required this.updatedAt, this.deletedAt}): _articles = articles; | ||||
|   factory _SnWebFeed.fromJson(Map<String, dynamic> json) => _$SnWebFeedFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String url; | ||||
| @override final  String title; | ||||
| @override final  String? description; | ||||
| @override final  SnScrappedLink? preview; | ||||
| @override@JsonKey() final  SnWebFeedConfig config; | ||||
| @override final  String publisherId; | ||||
|  final  List<SnWebArticle> _articles; | ||||
| @override@JsonKey() List<SnWebArticle> get articles { | ||||
|   if (_articles is EqualUnmodifiableListView) return _articles; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_articles); | ||||
| } | ||||
|  | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
|  | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnWebFeedCopyWith<_SnWebFeed> get copyWith => __$SnWebFeedCopyWithImpl<_SnWebFeed>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnWebFeedToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other._articles, _articles)&&(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,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(_articles),createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnWebFeedCopyWith<$Res> implements $SnWebFeedCopyWith<$Res> { | ||||
|   factory _$SnWebFeedCopyWith(_SnWebFeed value, $Res Function(_SnWebFeed) _then) = __$SnWebFeedCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedConfigCopyWith<$Res> get config; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnWebFeedCopyWithImpl<$Res> | ||||
|     implements _$SnWebFeedCopyWith<$Res> { | ||||
|   __$SnWebFeedCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnWebFeed _self; | ||||
|   final $Res Function(_SnWebFeed) _then; | ||||
|  | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_SnWebFeed( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable | ||||
| as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable | ||||
| as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,articles: null == articles ? _self._articles : articles // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnWebArticle>,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?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| /// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview { | ||||
|     if (_self.preview == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) { | ||||
|     return _then(_self.copyWith(preview: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnWebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedConfigCopyWith<$Res> get config { | ||||
|    | ||||
|   return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnWebArticle { | ||||
|  | ||||
|  String get id; String get title; String get url; String? get author; Map<String, dynamic>? get meta; SnScrappedLink? get preview; SnWebFeed? get feed; String? get content; DateTime? get publishedAt; String get feedId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebArticleCopyWith<SnWebArticle> get copyWith => _$SnWebArticleCopyWithImpl<SnWebArticle>(this as SnWebArticle, _$identity); | ||||
|  | ||||
|   /// Serializes this SnWebArticle to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(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,title,url,author,const DeepCollectionEquality().hash(meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnWebArticleCopyWith<$Res>  { | ||||
|   factory $SnWebArticleCopyWith(SnWebArticle value, $Res Function(SnWebArticle) _then) = _$SnWebArticleCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedCopyWith<$Res>? get feed; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnWebArticleCopyWithImpl<$Res> | ||||
|     implements $SnWebArticleCopyWith<$Res> { | ||||
|   _$SnWebArticleCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnWebArticle _self; | ||||
|   final $Res Function(SnWebArticle) _then; | ||||
|  | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = 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,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||
| as String?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable | ||||
| as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable | ||||
| as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||
| as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,feedId: null == feedId ? _self.feedId : feedId // 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?, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview { | ||||
|     if (_self.preview == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) { | ||||
|     return _then(_self.copyWith(preview: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedCopyWith<$Res>? get feed { | ||||
|     if (_self.feed == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) { | ||||
|     return _then(_self.copyWith(feed: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnWebArticle implements SnWebArticle { | ||||
|   const _SnWebArticle({required this.id, required this.title, required this.url, this.author, final  Map<String, dynamic>? meta, this.preview, this.feed, this.content, this.publishedAt, required this.feedId, required this.createdAt, required this.updatedAt, this.deletedAt}): _meta = meta; | ||||
|   factory _SnWebArticle.fromJson(Map<String, dynamic> json) => _$SnWebArticleFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String title; | ||||
| @override final  String url; | ||||
| @override final  String? author; | ||||
|  final  Map<String, dynamic>? _meta; | ||||
| @override Map<String, dynamic>? get meta { | ||||
|   final value = _meta; | ||||
|   if (value == null) return null; | ||||
|   if (_meta is EqualUnmodifiableMapView) return _meta; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableMapView(value); | ||||
| } | ||||
|  | ||||
| @override final  SnScrappedLink? preview; | ||||
| @override final  SnWebFeed? feed; | ||||
| @override final  String? content; | ||||
| @override final  DateTime? publishedAt; | ||||
| @override final  String feedId; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
|  | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnWebArticleCopyWith<_SnWebArticle> get copyWith => __$SnWebArticleCopyWithImpl<_SnWebArticle>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnWebArticleToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(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,title,url,author,const DeepCollectionEquality().hash(_meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnWebArticleCopyWith<$Res> implements $SnWebArticleCopyWith<$Res> { | ||||
|   factory _$SnWebArticleCopyWith(_SnWebArticle value, $Res Function(_SnWebArticle) _then) = __$SnWebArticleCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedCopyWith<$Res>? get feed; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnWebArticleCopyWithImpl<$Res> | ||||
|     implements _$SnWebArticleCopyWith<$Res> { | ||||
|   __$SnWebArticleCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnWebArticle _self; | ||||
|   final $Res Function(_SnWebArticle) _then; | ||||
|  | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_SnWebArticle( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||
| as String?,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable | ||||
| as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable | ||||
| as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||
| as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,feedId: null == feedId ? _self.feedId : feedId // 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?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| /// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview { | ||||
|     if (_self.preview == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) { | ||||
|     return _then(_self.copyWith(preview: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnWebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnWebFeedCopyWith<$Res>? get feed { | ||||
|     if (_self.feed == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) { | ||||
|     return _then(_self.copyWith(feed: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										103
									
								
								lib/models/webfeed.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								lib/models/webfeed.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'webfeed.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnWebFeedConfig _$SnWebFeedConfigFromJson(Map<String, dynamic> json) => | ||||
|     _SnWebFeedConfig(scrapPage: json['scrap_page'] as bool? ?? false); | ||||
|  | ||||
| Map<String, dynamic> _$SnWebFeedConfigToJson(_SnWebFeedConfig instance) => | ||||
|     <String, dynamic>{'scrap_page': instance.scrapPage}; | ||||
|  | ||||
| _SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed( | ||||
|   id: json['id'] as String, | ||||
|   url: json['url'] as String, | ||||
|   title: json['title'] as String, | ||||
|   description: json['description'] as String?, | ||||
|   preview: | ||||
|       json['preview'] == null | ||||
|           ? null | ||||
|           : SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>), | ||||
|   config: | ||||
|       json['config'] == null | ||||
|           ? const SnWebFeedConfig() | ||||
|           : SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>), | ||||
|   publisherId: json['publisher_id'] as String, | ||||
|   articles: | ||||
|       (json['articles'] as List<dynamic>?) | ||||
|           ?.map((e) => SnWebArticle.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
|   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> _$SnWebFeedToJson(_SnWebFeed instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'url': instance.url, | ||||
|       'title': instance.title, | ||||
|       'description': instance.description, | ||||
|       'preview': instance.preview?.toJson(), | ||||
|       'config': instance.config.toJson(), | ||||
|       'publisher_id': instance.publisherId, | ||||
|       'articles': instance.articles.map((e) => e.toJson()).toList(), | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|     }; | ||||
|  | ||||
| _SnWebArticle _$SnWebArticleFromJson(Map<String, dynamic> json) => | ||||
|     _SnWebArticle( | ||||
|       id: json['id'] as String, | ||||
|       title: json['title'] as String, | ||||
|       url: json['url'] as String, | ||||
|       author: json['author'] as String?, | ||||
|       meta: json['meta'] as Map<String, dynamic>?, | ||||
|       preview: | ||||
|           json['preview'] == null | ||||
|               ? null | ||||
|               : SnScrappedLink.fromJson( | ||||
|                 json['preview'] as Map<String, dynamic>, | ||||
|               ), | ||||
|       feed: | ||||
|           json['feed'] == null | ||||
|               ? null | ||||
|               : SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>), | ||||
|       content: json['content'] as String?, | ||||
|       publishedAt: | ||||
|           json['published_at'] == null | ||||
|               ? null | ||||
|               : DateTime.parse(json['published_at'] as String), | ||||
|       feedId: json['feed_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> _$SnWebArticleToJson(_SnWebArticle instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'title': instance.title, | ||||
|       'url': instance.url, | ||||
|       'author': instance.author, | ||||
|       'meta': instance.meta, | ||||
|       'preview': instance.preview?.toJson(), | ||||
|       'feed': instance.feed?.toJson(), | ||||
|       'content': instance.content, | ||||
|       'published_at': instance.publishedAt?.toIso8601String(), | ||||
|       'feed_id': instance.feedId, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|     }; | ||||
							
								
								
									
										31
									
								
								lib/pods/article_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lib/pods/article_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
|  | ||||
| /// Provider that fetches a single article by its ID | ||||
| final articleDetailProvider = FutureProvider.autoDispose.family<SnWebArticle, String>( | ||||
|   (ref, articleId) async { | ||||
|     final dio = ref.watch(apiClientProvider); | ||||
|      | ||||
|     try { | ||||
|       final response = await dio.get<Map<String, dynamic>>( | ||||
|         '/feeds/articles/$articleId', | ||||
|       ); | ||||
|        | ||||
|       if (response.statusCode == 200 && response.data != null) { | ||||
|         return SnWebArticle.fromJson(response.data!); | ||||
|       } else { | ||||
|         throw Exception('Failed to load article'); | ||||
|       } | ||||
|     } on DioException catch (e) { | ||||
|       if (e.response?.statusCode == 404) { | ||||
|         throw Exception('Article not found'); | ||||
|       } else { | ||||
|         throw Exception('Failed to load article: ${e.message}'); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       throw Exception('Failed to load article: $e'); | ||||
|     } | ||||
|   }, | ||||
| ); | ||||
							
								
								
									
										1
									
								
								lib/pods/article_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/pods/article_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
|  | ||||
							
								
								
									
										123
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
|  | ||||
| final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>(( | ||||
|   ref, | ||||
|   pubName, | ||||
| ) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final response = await client.get('/publishers/$pubName/feeds'); | ||||
|   return (response.data as List) | ||||
|       .map((json) => SnWebFeed.fromJson(json)) | ||||
|       .toList(); | ||||
| }); | ||||
|  | ||||
| class WebFeedNotifier | ||||
|     extends | ||||
|         AutoDisposeFamilyAsyncNotifier< | ||||
|           SnWebFeed, | ||||
|           ({String pubName, String? feedId}) | ||||
|         > { | ||||
|   @override | ||||
|   FutureOr<SnWebFeed> build(({String pubName, String? feedId}) arg) async { | ||||
|     if (arg.feedId == null || arg.feedId!.isEmpty) { | ||||
|       return SnWebFeed( | ||||
|         id: '', | ||||
|         url: '', | ||||
|         title: '', | ||||
|         publisherId: arg.pubName, | ||||
|         createdAt: DateTime.now(), | ||||
|         updatedAt: DateTime.now(), | ||||
|         deletedAt: null, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final response = await client.get( | ||||
|         '/publishers/${arg.pubName}/feeds/${arg.feedId}', | ||||
|       ); | ||||
|       return SnWebFeed.fromJson(response.data); | ||||
|     } catch (e) { | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> saveFeed(SnWebFeed feed) async { | ||||
|     state = const AsyncValue.loading(); | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final url = '/publishers/${feed.publisherId}/feeds'; | ||||
|  | ||||
|       final response = | ||||
|           feed.id.isEmpty | ||||
|               ? await client.post(url, data: feed.toJson()) | ||||
|               : await client.patch('$url/${feed.id}', data: feed.toJson()); | ||||
|  | ||||
|       state = AsyncValue.data(SnWebFeed.fromJson(response.data)); | ||||
|     } catch (error, stackTrace) { | ||||
|       state = AsyncValue.error(error, stackTrace); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> deleteFeed() async { | ||||
|     final feedId = arg.feedId; | ||||
|     if (feedId == null || feedId.isEmpty) return; | ||||
|  | ||||
|     state = const AsyncValue.loading(); | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       await client.delete('/publishers/${arg.pubName}/feeds/$feedId'); | ||||
|       state = AsyncValue.data( | ||||
|         SnWebFeed( | ||||
|           id: '', | ||||
|           url: '', | ||||
|           title: '', | ||||
|           publisherId: arg.pubName, | ||||
|           createdAt: DateTime.now(), | ||||
|           updatedAt: DateTime.now(), | ||||
|           deletedAt: null, | ||||
|         ), | ||||
|       ); | ||||
|     } catch (error, stackTrace) { | ||||
|       state = AsyncValue.error(error, stackTrace); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> scrapFeed() async { | ||||
|     final feedId = arg.feedId; | ||||
|     if (feedId == null || feedId.isEmpty) return; | ||||
|  | ||||
|     state = const AsyncValue.loading(); | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       await client.post( | ||||
|         '/publishers/${arg.pubName}/feeds/$feedId/scrap', | ||||
|         options: Options( | ||||
|           sendTimeout: const Duration(seconds: 60), | ||||
|           receiveTimeout: const Duration(seconds: 180), | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       // Reload the feed | ||||
|       final response = await client.get( | ||||
|         '/publishers/${arg.pubName}/feeds/$feedId', | ||||
|       ); | ||||
|       state = AsyncValue.data(SnWebFeed.fromJson(response.data)); | ||||
|     } catch (error, stackTrace) { | ||||
|       state = AsyncValue.error(error, stackTrace); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| final webFeedNotifierProvider = AsyncNotifierProvider.autoDispose | ||||
|     .family<WebFeedNotifier, SnWebFeed, ({String pubName, String? feedId})>( | ||||
|       WebFeedNotifier.new, | ||||
|     ); | ||||
| @@ -1,14 +1,17 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/screens/about.dart'; | ||||
| import 'package:island/screens/developers/apps.dart'; | ||||
| import 'package:island/screens/developers/edit_app.dart'; | ||||
| import 'package:island/screens/developers/new_app.dart'; | ||||
| import 'package:island/screens/developers/hub.dart'; | ||||
| import 'package:island/screens/discovery/articles.dart'; | ||||
| import 'package:island/screens/posts/post_search.dart'; | ||||
| import 'package:island/widgets/app_wrapper.dart'; | ||||
| import 'package:island/screens/tabs.dart'; | ||||
|  | ||||
| import 'package:island/screens/explore.dart'; | ||||
| import 'package:island/screens/article_detail_screen.dart'; | ||||
| import 'package:island/screens/account.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/screens/wallet.dart'; | ||||
| @@ -22,18 +25,20 @@ import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/screens/chat/room_detail.dart'; | ||||
| import 'package:island/screens/chat/call.dart'; | ||||
| import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/list.dart'; | ||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/screens/posts/pub_profile.dart'; | ||||
| import 'package:island/screens/auth/login.dart'; | ||||
| import 'package:island/screens/auth/create_account.dart'; | ||||
| import 'package:island/screens/settings.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/screens/realm/detail.dart'; | ||||
| import 'package:island/screens/realm/realm_detail.dart'; | ||||
| import 'package:island/screens/account/event_calendar.dart'; | ||||
| import 'package:island/screens/discovery/realms.dart'; | ||||
|  | ||||
| @@ -91,6 +96,33 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                 path: '/creators', | ||||
|                 builder: (context, state) => const CreatorHubScreen(), | ||||
|               ), | ||||
|               // Web Feed Routes | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/feeds', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return WebFeedListScreen(pubName: name); | ||||
|                 }, | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'new', | ||||
|                     builder: (context, state) { | ||||
|                       return WebFeedNewScreen( | ||||
|                         pubName: state.pathParameters['name']!, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':feedId', | ||||
|                     builder: (context, state) { | ||||
|                       return WebFeedEditScreen( | ||||
|                         pubName: state.pathParameters['name']!, | ||||
|                         feedId: state.pathParameters['feedId'], | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/posts', | ||||
|                 builder: (context, state) { | ||||
| @@ -167,26 +199,42 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps', | ||||
|                 builder: (context, state) => CustomAppsScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => CustomAppsScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                     ), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps/new', | ||||
|                 builder: (context, state) => NewCustomAppScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => NewCustomAppScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                     ), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps/:id', | ||||
|                 builder: (context, state) => EditAppScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                   id: state.pathParameters['id']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => EditAppScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                       id: state.pathParameters['id']!, | ||||
|                     ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|  | ||||
|           // Web articles | ||||
|           GoRoute( | ||||
|             path: '/feeds/articles', | ||||
|             builder: (context, state) => const ArticlesScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/feeds/articles/:id', | ||||
|             builder: (context, state) { | ||||
|               final id = state.pathParameters['id']!; | ||||
|               return ArticleDetailScreen(articleId: id); | ||||
|             }, | ||||
|           ), | ||||
|  | ||||
|           // Auth routes | ||||
|           GoRoute( | ||||
|             path: '/auth/login', | ||||
| @@ -202,6 +250,10 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|             path: '/settings', | ||||
|             builder: (context, state) => const SettingsScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/about', | ||||
|             builder: (context, state) => const AboutScreen(), | ||||
|           ), | ||||
|  | ||||
|           // Main tabs with TabsScreen shell | ||||
|           ShellRoute( | ||||
| @@ -219,6 +271,10 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                     path: '/', | ||||
|                     builder: (context, state) => const ExploreScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/posts/search', | ||||
|                     builder: (context, state) => const PostSearchScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: '/posts/:id', | ||||
|                     builder: (context, state) { | ||||
|   | ||||
							
								
								
									
										300
									
								
								lib/screens/about.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								lib/screens/about.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| class AboutScreen extends StatefulWidget { | ||||
|   const AboutScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<AboutScreen> createState() => _AboutScreenState(); | ||||
| } | ||||
|  | ||||
| class _AboutScreenState extends State<AboutScreen> { | ||||
|   PackageInfo _packageInfo = PackageInfo( | ||||
|     appName: 'Island', | ||||
|     packageName: 'com.example.island', | ||||
|     version: '1.0.0', | ||||
|     buildNumber: '1', | ||||
|   ); | ||||
|   bool _isLoading = true; | ||||
|   String? _errorMessage; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _initPackageInfo(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _initPackageInfo() async { | ||||
|     try { | ||||
|       final info = await PackageInfo.fromPlatform(); | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           _packageInfo = info; | ||||
|           _isLoading = false; | ||||
|         }); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           _errorMessage = 'Failed to load package info: $e'; | ||||
|           _isLoading = false; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _launchURL(String url) async { | ||||
|     final uri = Uri.parse(url); | ||||
|     if (await canLaunchUrl(uri)) { | ||||
|       await launchUrl(uri); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final theme = Theme.of(context); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: const Text('About'), elevation: 0), | ||||
|       body: | ||||
|           _isLoading | ||||
|               ? const Center(child: CircularProgressIndicator()) | ||||
|               : _errorMessage != null | ||||
|               ? Center(child: Text(_errorMessage!)) | ||||
|               : SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const SizedBox(height: 24), | ||||
|                     // App Icon and Name | ||||
|                     CircleAvatar( | ||||
|                       radius: 50, | ||||
|                       backgroundColor: theme.colorScheme.primary.withOpacity( | ||||
|                         0.1, | ||||
|                       ), | ||||
|                       child: Image.asset( | ||||
|                         'assets/icons/icon.png', | ||||
|                         width: 56, | ||||
|                         height: 56, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Text( | ||||
|                       _packageInfo.appName, | ||||
|                       style: theme.textTheme.headlineSmall?.copyWith( | ||||
|                         fontWeight: FontWeight.bold, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', | ||||
|                       style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                         color: theme.textTheme.bodySmall?.color, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // App Info Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'App Information', | ||||
|                       children: [ | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.info_outline, | ||||
|                           label: 'Package Name', | ||||
|                           value: _packageInfo.packageName, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.update, | ||||
|                           label: 'Version', | ||||
|                           value: _packageInfo.version, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Icons.build, | ||||
|                           label: 'Build Number', | ||||
|                           value: _packageInfo.buildNumber, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Links Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'Links', | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.privacy_tip_outlined, | ||||
|                           title: 'Privacy Policy', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://solsynth.dev/terms/privacy-policy', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.description_outlined, | ||||
|                           title: 'Terms of Service', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://example.com/terms/basic-law', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.code, | ||||
|                           title: 'Open Source Licenses', | ||||
|                           onTap: () { | ||||
|                             showLicensePage( | ||||
|                               context: context, | ||||
|                               applicationName: _packageInfo.appName, | ||||
|                               applicationVersion: | ||||
|                                   'Version ${_packageInfo.version}', | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Developer Info | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'Developer', | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.email_outlined, | ||||
|                           title: 'Contact Us', | ||||
|                           subtitle: 'lily@solsynth.dev', | ||||
|                           onTap: () => _launchURL('mailto:lily@solsynth.dev'), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Icons.copyright, | ||||
|                           title: 'License', | ||||
|                           subtitle: | ||||
|                               'Copyright reserved © ${DateTime.now().year} Solsynth\nGNU Affero General Public License v3.0', | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt', | ||||
|                               ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // Copyright | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.all(16.0), | ||||
|                       child: Text( | ||||
|                         '© ${DateTime.now().year} ${_packageInfo.appName}. All rights reserved.', | ||||
|                         style: theme.textTheme.bodySmall, | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSection( | ||||
|     BuildContext context, { | ||||
|     required String title, | ||||
|     required List<Widget> children, | ||||
|   }) { | ||||
|     return Card( | ||||
|       margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), | ||||
|             child: Text( | ||||
|               title, | ||||
|               style: Theme.of( | ||||
|                 context, | ||||
|               ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), | ||||
|             ), | ||||
|           ), | ||||
|           const Divider(height: 1), | ||||
|           ...children, | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildInfoItem( | ||||
|     BuildContext context, { | ||||
|     required IconData icon, | ||||
|     required String label, | ||||
|     required String value, | ||||
|   }) { | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Icon(icon, size: 20, color: Theme.of(context).hintColor), | ||||
|           const SizedBox(width: 16), | ||||
|           Expanded( | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text(label, style: Theme.of(context).textTheme.bodySmall), | ||||
|                 const SizedBox(height: 2), | ||||
|                 SelectableText( | ||||
|                   value, | ||||
|                   style: Theme.of(context).textTheme.bodyMedium, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           if (value.startsWith('http') || value.contains('@')) | ||||
|             IconButton( | ||||
|               icon: const Icon(Icons.copy, size: 16), | ||||
|               onPressed: () { | ||||
|                 Clipboard.setData(ClipboardData(text: value)); | ||||
|                 ScaffoldMessenger.of(context).showSnackBar( | ||||
|                   const SnackBar(content: Text('Copied to clipboard')), | ||||
|                 ); | ||||
|               }, | ||||
|               padding: EdgeInsets.zero, | ||||
|               constraints: const BoxConstraints(), | ||||
|               tooltip: 'Copy to clipboard', | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildListTile( | ||||
|     BuildContext context, { | ||||
|     required IconData icon, | ||||
|     required String title, | ||||
|     String? subtitle, | ||||
|     required VoidCallback onTap, | ||||
|   }) { | ||||
|     return Column( | ||||
|       children: [ | ||||
|         ListTile( | ||||
|           leading: Icon(icon), | ||||
|           title: Text(title), | ||||
|           subtitle: subtitle != null ? Text(subtitle) : null, | ||||
|           trailing: const Icon(Icons.chevron_right), | ||||
|           onTap: onTap, | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|           minLeadingWidth: 24, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -281,6 +281,16 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 }, | ||||
|               ), | ||||
|             const Divider(height: 1).padding(vertical: 8), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.info), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('about').tr(), | ||||
|               onTap: () { | ||||
|                 context.push('/about'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.logout), | ||||
|   | ||||
| @@ -341,7 +341,10 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                   ), | ||||
|  | ||||
|                   TextFormField( | ||||
|                     decoration: InputDecoration(labelText: 'bio'.tr()), | ||||
|                     decoration: InputDecoration( | ||||
|                       labelText: 'bio'.tr(), | ||||
|                       alignLabelWithHint: true, | ||||
|                     ), | ||||
|                     maxLines: null, | ||||
|                     minLines: 3, | ||||
|                     controller: bioController, | ||||
|   | ||||
							
								
								
									
										105
									
								
								lib/screens/article_detail_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								lib/screens/article_detail_screen.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| 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), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -669,7 +669,10 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|                 const SizedBox(height: 16), | ||||
|                 TextFormField( | ||||
|                   controller: descriptionController, | ||||
|                   decoration: const InputDecoration(labelText: 'Description'), | ||||
|                   decoration: const InputDecoration( | ||||
|                     labelText: 'Description', | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|                   onTapOutside: | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -46,6 +47,14 @@ Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<Map<String, bool>> publisherFeatures(Ref ref, String? uname) async { | ||||
|   if (uname == null) return {}; | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final response = await apiClient.get('/publishers/$uname/features'); | ||||
|   return Map<String, bool>.from(response.data); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnPublisherMember>> publisherInvites(Ref ref) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
| @@ -184,6 +193,10 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|       publisherStatsProvider(currentPublisher.value?.name), | ||||
|     ); | ||||
|  | ||||
|     final publisherFeatures = ref.watch( | ||||
|       publisherFeaturesProvider(currentPublisher.value?.name), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       appBar: AppBar( | ||||
| @@ -357,9 +370,9 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|                             title: Text('publisherMembers').tr(), | ||||
|                             trailing: Icon(Symbols.chevron_right), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             leading: const Icon(Symbols.group), | ||||
|                             contentPadding: EdgeInsets.symmetric( | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
| @@ -374,6 +387,66 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|                             title: const Text('Web Feeds').tr(), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             leading: const Icon(Symbols.rss_feed), | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               context.push( | ||||
|                                 '/creators/${currentPublisher.value!.name}/feeds', | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|                           ExpansionTile( | ||||
|                             title: Text('publisherFeatures').tr(), | ||||
|                             leading: const Icon(Symbols.flag), | ||||
|                             tilePadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                             minTileHeight: 48, | ||||
|                             children: [ | ||||
|                               ...publisherFeatures.when( | ||||
|                                 data: (data) { | ||||
|                                   return data.entries.map((entry) { | ||||
|                                     final keyPrefix = | ||||
|                                         'publisherFeature${entry.key.capitalizeEachWord()}'; | ||||
|                                     return ListTile( | ||||
|                                       minTileHeight: 48, | ||||
|                                       contentPadding: EdgeInsets.symmetric( | ||||
|                                         horizontal: 24, | ||||
|                                       ), | ||||
|                                       leading: Icon( | ||||
|                                         Symbols.circle, | ||||
|                                         color: | ||||
|                                             entry.value | ||||
|                                                 ? Colors.green | ||||
|                                                 : Colors.red, | ||||
|                                         fill: 1, | ||||
|                                         size: 16, | ||||
|                                       ).padding(left: 2, top: 4), | ||||
|                                       title: Text(keyPrefix).tr(), | ||||
|                                       subtitle: Column( | ||||
|                                         crossAxisAlignment: | ||||
|                                             CrossAxisAlignment.start, | ||||
|                                         children: [ | ||||
|                                           Text('${keyPrefix}Description').tr(), | ||||
|                                           if (!entry.value) | ||||
|                                             Text( | ||||
|                                               '${keyPrefix}Hint', | ||||
|                                             ).tr().bold(), | ||||
|                                         ], | ||||
|                                       ), | ||||
|                                       isThreeLine: true, | ||||
|                                     ); | ||||
|                                   }).toList(); | ||||
|                                 }, | ||||
|                                 error: (_, _) => [], | ||||
|                                 loading: () => [], | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           Divider(height: 1).padding(vertical: 8), | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|   | ||||
| @@ -271,6 +271,128 @@ class _PublisherIdentityProviderElement | ||||
|   String get uname => (origin as PublisherIdentityProvider).uname; | ||||
| } | ||||
|  | ||||
| String _$publisherFeaturesHash() => r'34db65d9a4b6b0c6961733ae79e67f25d5d111d3'; | ||||
|  | ||||
| /// See also [publisherFeatures]. | ||||
| @ProviderFor(publisherFeatures) | ||||
| const publisherFeaturesProvider = PublisherFeaturesFamily(); | ||||
|  | ||||
| /// See also [publisherFeatures]. | ||||
| class PublisherFeaturesFamily extends Family<AsyncValue<Map<String, bool>>> { | ||||
|   /// See also [publisherFeatures]. | ||||
|   const PublisherFeaturesFamily(); | ||||
|  | ||||
|   /// See also [publisherFeatures]. | ||||
|   PublisherFeaturesProvider call(String? uname) { | ||||
|     return PublisherFeaturesProvider(uname); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   PublisherFeaturesProvider getProviderOverride( | ||||
|     covariant PublisherFeaturesProvider provider, | ||||
|   ) { | ||||
|     return call(provider.uname); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'publisherFeaturesProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [publisherFeatures]. | ||||
| class PublisherFeaturesProvider | ||||
|     extends AutoDisposeFutureProvider<Map<String, bool>> { | ||||
|   /// See also [publisherFeatures]. | ||||
|   PublisherFeaturesProvider(String? uname) | ||||
|     : this._internal( | ||||
|         (ref) => publisherFeatures(ref as PublisherFeaturesRef, uname), | ||||
|         from: publisherFeaturesProvider, | ||||
|         name: r'publisherFeaturesProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$publisherFeaturesHash, | ||||
|         dependencies: PublisherFeaturesFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             PublisherFeaturesFamily._allTransitiveDependencies, | ||||
|         uname: uname, | ||||
|       ); | ||||
|  | ||||
|   PublisherFeaturesProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.uname, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? uname; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<Map<String, bool>> Function(PublisherFeaturesRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: PublisherFeaturesProvider._internal( | ||||
|         (ref) => create(ref as PublisherFeaturesRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         uname: uname, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<Map<String, bool>> createElement() { | ||||
|     return _PublisherFeaturesProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is PublisherFeaturesProvider && other.uname == uname; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, uname.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin PublisherFeaturesRef on AutoDisposeFutureProviderRef<Map<String, bool>> { | ||||
|   /// The parameter `uname` of this provider. | ||||
|   String? get uname; | ||||
| } | ||||
|  | ||||
| class _PublisherFeaturesProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<Map<String, bool>> | ||||
|     with PublisherFeaturesRef { | ||||
|   _PublisherFeaturesProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String? get uname => (origin as PublisherFeaturesProvider).uname; | ||||
| } | ||||
|  | ||||
| String _$publisherInvitesHash() => r'488cd443407895ce11f4edff07cb6ea58f2aa018'; | ||||
|  | ||||
| /// See also [publisherInvites]. | ||||
|   | ||||
| @@ -270,7 +270,10 @@ class EditPublisherScreen extends HookConsumerWidget { | ||||
|                     ), | ||||
|                     TextFormField( | ||||
|                       controller: bioController, | ||||
|                       decoration: InputDecoration(labelText: 'bio'.tr()), | ||||
|                       decoration: InputDecoration( | ||||
|                         labelText: 'bio'.tr(), | ||||
|                         alignLabelWithHint: true, | ||||
|                       ), | ||||
|                       minLines: 3, | ||||
|                       maxLines: null, | ||||
|                       onTapOutside: | ||||
|   | ||||
| @@ -71,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget { | ||||
|                 subtitle: Text(sticker.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   context.push( | ||||
|                     '/creators/$pubName/stickers/${sticker.id}', | ||||
|                   ); | ||||
|                   context.push('/creators/$pubName/stickers/${sticker.id}'); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
| @@ -230,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'description'.tr(), | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|   | ||||
							
								
								
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/webfeed.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class WebFeedNewScreen extends StatelessWidget { | ||||
|   final String pubName; | ||||
|   const WebFeedNewScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return WebFeedEditScreen(pubName: pubName, feedId: null); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class WebFeedEditScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   final String? feedId; | ||||
|  | ||||
|   const WebFeedEditScreen({super.key, required this.pubName, this.feedId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final formKey = useMemoized(() => GlobalKey<FormState>()); | ||||
|     final titleController = useTextEditingController(); | ||||
|     final urlController = useTextEditingController(); | ||||
|     final descriptionController = useTextEditingController(); | ||||
|     final isLoading = useState(false); | ||||
|     final isScrapEnabled = useState(false); | ||||
|  | ||||
|     final saveFeed = useCallback(() async { | ||||
|       if (!formKey.currentState!.validate()) return; | ||||
|  | ||||
|       isLoading.value = true; | ||||
|  | ||||
|       try { | ||||
|         final feed = SnWebFeed( | ||||
|           id: feedId ?? '', | ||||
|           title: titleController.text, | ||||
|           url: urlController.text, | ||||
|           description: descriptionController.text, | ||||
|           config: SnWebFeedConfig(scrapPage: isScrapEnabled.value), | ||||
|           publisherId: pubName, | ||||
|           createdAt: DateTime.now(), | ||||
|           updatedAt: DateTime.now(), | ||||
|           deletedAt: null, | ||||
|         ); | ||||
|  | ||||
|         await ref | ||||
|             .read( | ||||
|               webFeedNotifierProvider(( | ||||
|                 pubName: pubName, | ||||
|                 feedId: feedId, | ||||
|               )).notifier, | ||||
|             ) | ||||
|             .saveFeed(feed); | ||||
|  | ||||
|         // Refresh the feed list | ||||
|         ref.invalidate(webFeedListProvider(pubName)); | ||||
|  | ||||
|         if (context.mounted) { | ||||
|           showSnackBar('Web feed saved successfully'); | ||||
|           context.pop(); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         showErrorAlert(e); | ||||
|       } finally { | ||||
|         isLoading.value = false; | ||||
|       } | ||||
|     }, [pubName, feedId, isScrapEnabled.value, context]); | ||||
|  | ||||
|     final deleteFeed = useCallback(() async { | ||||
|       final confirmed = await showConfirmAlert( | ||||
|         'Are you sure you want to delete this web feed? This action cannot be undone.', | ||||
|         'Delete Web Feed', | ||||
|       ); | ||||
|       if (confirmed != true) return; | ||||
|  | ||||
|       isLoading.value = true; | ||||
|  | ||||
|       try { | ||||
|         await ref | ||||
|             .read( | ||||
|               webFeedNotifierProvider(( | ||||
|                 pubName: pubName, | ||||
|                 feedId: feedId!, | ||||
|               )).notifier, | ||||
|             ) | ||||
|             .deleteFeed(); | ||||
|  | ||||
|         ref.invalidate(webFeedListProvider(pubName)); | ||||
|  | ||||
|         if (context.mounted) { | ||||
|           showSnackBar('Web feed deleted successfully'); | ||||
|           context.pop(); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         showErrorAlert(e); | ||||
|       } finally { | ||||
|         isLoading.value = false; | ||||
|       } | ||||
|     }, [pubName, feedId, context, ref]); | ||||
|  | ||||
|     final feedAsync = ref.watch( | ||||
|       webFeedNotifierProvider((pubName: pubName, feedId: feedId)), | ||||
|     ); | ||||
|  | ||||
|     return feedAsync.when( | ||||
|       loading: | ||||
|           () => | ||||
|               const Scaffold(body: Center(child: CircularProgressIndicator())), | ||||
|       error: | ||||
|           (error, stack) => Scaffold( | ||||
|             appBar: AppBar(title: const Text('Error')), | ||||
|             body: Center(child: Text('Error: $error')), | ||||
|           ), | ||||
|       data: (feed) { | ||||
|         // Initialize form fields if they're empty and we have a feed | ||||
|         if (titleController.text.isEmpty) { | ||||
|           titleController.text = feed.title; | ||||
|           urlController.text = feed.url; | ||||
|           descriptionController.text = feed.description ?? ''; | ||||
|           isScrapEnabled.value = feed.config.scrapPage; | ||||
|         } | ||||
|  | ||||
|         return _buildForm( | ||||
|           context, | ||||
|           formKey: formKey, | ||||
|           titleController: titleController, | ||||
|           urlController: urlController, | ||||
|           descriptionController: descriptionController, | ||||
|           isScrapEnabled: isScrapEnabled.value, | ||||
|           onScrapEnabledChanged: (value) => isScrapEnabled.value = value, | ||||
|           onSave: saveFeed, | ||||
|           onDelete: deleteFeed, | ||||
|           isLoading: isLoading.value, | ||||
|           ref: ref, | ||||
|           hasFeedId: feedId != null, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildForm( | ||||
|     BuildContext context, { | ||||
|     required WidgetRef ref, | ||||
|     required GlobalKey<FormState> formKey, | ||||
|     required TextEditingController titleController, | ||||
|     required TextEditingController urlController, | ||||
|     required TextEditingController descriptionController, | ||||
|     required bool isScrapEnabled, | ||||
|     required ValueChanged<bool> onScrapEnabledChanged, | ||||
|     required VoidCallback onSave, | ||||
|     required VoidCallback onDelete, | ||||
|     required bool isLoading, | ||||
|     required bool hasFeedId, | ||||
|   }) { | ||||
|     final scrapNow = useCallback(() async { | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         await ref | ||||
|             .read( | ||||
|               webFeedNotifierProvider(( | ||||
|                 pubName: pubName, | ||||
|                 feedId: feedId!, | ||||
|               )).notifier, | ||||
|             ) | ||||
|             .scrapFeed(); | ||||
|  | ||||
|         if (context.mounted) { | ||||
|           showSnackBar('Feed scraping successfully.'); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         showErrorAlert(e); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     }, [pubName, feedId, ref, context]); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'), | ||||
|         actions: [ | ||||
|           if (hasFeedId) | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.delete_forever), | ||||
|               onPressed: isLoading ? null : onDelete, | ||||
|             ), | ||||
|           const SizedBox(width: 8), | ||||
|         ], | ||||
|       ), | ||||
|       body: Form( | ||||
|         key: formKey, | ||||
|         child: SingleChildScrollView( | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               TextFormField( | ||||
|                 controller: titleController, | ||||
|                 decoration: const InputDecoration(labelText: 'Title'), | ||||
|                 validator: (value) { | ||||
|                   if (value == null || value.isEmpty) { | ||||
|                     return 'Please enter a title'; | ||||
|                   } | ||||
|                   return null; | ||||
|                 }, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|               const SizedBox(height: 16), | ||||
|               TextFormField( | ||||
|                 controller: urlController, | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'URL', | ||||
|                   hintText: 'https://example.com/feed', | ||||
|                 ), | ||||
|                 keyboardType: TextInputType.url, | ||||
|                 validator: (value) { | ||||
|                   if (value == null || value.isEmpty) { | ||||
|                     return 'Please enter a URL'; | ||||
|                   } | ||||
|                   final uri = Uri.tryParse(value); | ||||
|                   if (uri == null || !uri.hasAbsolutePath) { | ||||
|                     return 'Please enter a valid URL'; | ||||
|                   } | ||||
|                   return null; | ||||
|                 }, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|               const SizedBox(height: 16), | ||||
|               TextFormField( | ||||
|                 controller: descriptionController, | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'Description', | ||||
|                   alignLabelWithHint: true, | ||||
|                 ), | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 maxLines: 3, | ||||
|               ), | ||||
|               const SizedBox(height: 24), | ||||
|               Card( | ||||
|                 margin: EdgeInsets.zero, | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     SwitchListTile( | ||||
|                       title: const Text('Scrape web page for content'), | ||||
|                       subtitle: const Text( | ||||
|                         'When enabled, the system will attempt to extract full content from the web page', | ||||
|                       ), | ||||
|                       shape: RoundedRectangleBorder( | ||||
|                         borderRadius: BorderRadius.circular(8), | ||||
|                       ), | ||||
|                       value: isScrapEnabled, | ||||
|                       onChanged: onScrapEnabledChanged, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|               const SizedBox(height: 20), | ||||
|               if (hasFeedId) ...[ | ||||
|                 FilledButton.tonalIcon( | ||||
|                   onPressed: isLoading ? null : scrapNow, | ||||
|                   icon: const Icon(Symbols.refresh), | ||||
|                   label: const Text('Scrape Now'), | ||||
|                 ).alignment(Alignment.centerRight), | ||||
|                 const SizedBox(height: 16), | ||||
|               ], | ||||
|               FilledButton.icon( | ||||
|                 onPressed: isLoading ? null : onSave, | ||||
|                 icon: const Icon(Symbols.save), | ||||
|                 label: Text('saveChanges').tr(), | ||||
|               ).alignment(Alignment.centerRight), | ||||
|             ], | ||||
|           ).padding(all: 20), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										78
									
								
								lib/screens/creators/webfeed/webfeed_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/screens/creators/webfeed/webfeed_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/pods/webfeed.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/empty_state.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| class WebFeedListScreen extends ConsumerWidget { | ||||
|   final String pubName; | ||||
|  | ||||
|   const WebFeedListScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final feedsAsync = ref.watch(webFeedListProvider(pubName)); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: const Text('Web Feeds')), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         child: const Icon(Symbols.add), | ||||
|         onPressed: () { | ||||
|           context.push('/creators/$pubName/feeds/new'); | ||||
|         }, | ||||
|       ), | ||||
|       body: feedsAsync.when( | ||||
|         data: (feeds) { | ||||
|           if (feeds.isEmpty) { | ||||
|             return EmptyState( | ||||
|               icon: Symbols.rss_feed, | ||||
|               title: 'No Web Feeds', | ||||
|               description: 'Add a new web feed to get started', | ||||
|             ); | ||||
|           } | ||||
|           return RefreshIndicator( | ||||
|             onRefresh: () => ref.refresh(webFeedListProvider(pubName).future), | ||||
|             child: ListView.builder( | ||||
|               padding: EdgeInsets.only(top: 8), | ||||
|               itemCount: feeds.length, | ||||
|               itemBuilder: (context, index) { | ||||
|                 final feed = feeds[index]; | ||||
|                 return Card( | ||||
|                   margin: const EdgeInsets.symmetric( | ||||
|                     horizontal: 12, | ||||
|                     vertical: 4, | ||||
|                   ), | ||||
|                   child: ListTile( | ||||
|                     leading: const Icon(Symbols.rss_feed, size: 32), | ||||
|                     shape: RoundedRectangleBorder( | ||||
|                       borderRadius: BorderRadius.circular(8), | ||||
|                     ), | ||||
|                     title: Text( | ||||
|                       feed.title, | ||||
|                       style: Theme.of(context).textTheme.titleMedium, | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                     subtitle: Text( | ||||
|                       feed.url, | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                     trailing: const Icon(Symbols.chevron_right), | ||||
|                     onTap: () { | ||||
|                       context.push('/creators/$pubName/feeds/${feed.id}'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|         error: (error, _) => Center(child: Text('Error: $error')), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,13 +1,17 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/custom_app.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'apps.g.dart'; | ||||
|  | ||||
| @@ -27,30 +31,123 @@ class CustomAppsScreen extends HookConsumerWidget { | ||||
|     final apps = ref.watch(customAppsProvider(publisherName)); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text('customApps').tr()), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         child: const Icon(Symbols.add), | ||||
|         onPressed: () { | ||||
|           context.push('/developers/$publisherName/apps/new'); | ||||
|         }, | ||||
|       appBar: AppBar( | ||||
|         title: Text('customApps').tr(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.add), | ||||
|             onPressed: () { | ||||
|               context.push('/developers/$publisherName/apps/new'); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       body: apps.when( | ||||
|         data: (data) { | ||||
|           if (data.isEmpty) { | ||||
|             return Center(child: Text('noCustomApps').tr()); | ||||
|           } | ||||
|           return ListView.builder( | ||||
|             itemCount: data.length, | ||||
|             itemBuilder: (context, index) { | ||||
|               final app = data[index]; | ||||
|               return ListTile( | ||||
|                 title: Text(app.name), | ||||
|                 subtitle: Text(app.slug), | ||||
|                 onTap: () { | ||||
|                   context.push('/developers/$publisherName/apps/${app.id}'); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           return RefreshIndicator( | ||||
|             onRefresh: | ||||
|                 () => ref.refresh(customAppsProvider(publisherName).future), | ||||
|             child: ListView.builder( | ||||
|               padding: EdgeInsets.only(top: 4), | ||||
|               itemCount: data.length, | ||||
|               itemBuilder: (context, index) { | ||||
|                 final app = data[index]; | ||||
|                 return Card( | ||||
|                   margin: const EdgeInsets.all(8.0), | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       SizedBox( | ||||
|                         height: 150, | ||||
|                         child: Stack( | ||||
|                           fit: StackFit.expand, | ||||
|                           children: [ | ||||
|                             if (app.background != null) | ||||
|                               CloudFileWidget( | ||||
|                                 item: app.background!, | ||||
|                                 fit: BoxFit.cover, | ||||
|                               ).clipRRect(topLeft: 8, topRight: 8), | ||||
|                             if (app.picture != null) | ||||
|                               Positioned( | ||||
|                                 left: 16, | ||||
|                                 bottom: 16, | ||||
|                                 child: ProfilePictureWidget( | ||||
|                                   fileId: app.picture!.id, | ||||
|                                   radius: 40, | ||||
|                                   fallbackIcon: Symbols.apps, | ||||
|                                 ), | ||||
|                               ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                       ListTile( | ||||
|                         title: Text(app.name), | ||||
|                         subtitle: Text( | ||||
|                           app.slug, | ||||
|                           style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                         ), | ||||
|                         contentPadding: EdgeInsets.only(left: 20, right: 12), | ||||
|                         trailing: PopupMenuButton( | ||||
|                           itemBuilder: | ||||
|                               (context) => [ | ||||
|                                 PopupMenuItem( | ||||
|                                   value: 'edit', | ||||
|                                   child: Row( | ||||
|                                     children: [ | ||||
|                                       const Icon(Symbols.edit), | ||||
|                                       const SizedBox(width: 12), | ||||
|                                       Text('edit').tr(), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 PopupMenuItem( | ||||
|                                   value: 'delete', | ||||
|                                   child: Row( | ||||
|                                     children: [ | ||||
|                                       const Icon( | ||||
|                                         Symbols.delete, | ||||
|                                         color: Colors.red, | ||||
|                                       ), | ||||
|                                       const SizedBox(width: 12), | ||||
|                                       Text( | ||||
|                                         'delete', | ||||
|                                         style: TextStyle(color: Colors.red), | ||||
|                                       ).tr(), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                           onSelected: (value) { | ||||
|                             if (value == 'edit') { | ||||
|                               context.push( | ||||
|                                 '/developers/$publisherName/apps/${app.id}', | ||||
|                               ); | ||||
|                             } else if (value == 'delete') { | ||||
|                               showConfirmAlert( | ||||
|                                 'deleteCustomAppHint'.tr(), | ||||
|                                 'deleteCustomApp'.tr(), | ||||
|                               ).then((confirm) { | ||||
|                                 if (confirm) { | ||||
|                                   final client = ref.read(apiClientProvider); | ||||
|                                   client.delete( | ||||
|                                     '/developers/$publisherName/apps/${app.id}', | ||||
|                                   ); | ||||
|                                   ref.invalidate( | ||||
|                                     customAppsProvider(publisherName), | ||||
|                                   ); | ||||
|                                 } | ||||
|                               }); | ||||
|                             } | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
|  | ||||
| part 'edit_app.g.dart'; | ||||
|  | ||||
| @@ -39,13 +40,32 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final formKey = useMemoized(() => GlobalKey<FormState>()); | ||||
|  | ||||
|     final submitting = useState(false); | ||||
|  | ||||
|     final nameController = useTextEditingController(); | ||||
|     final slugController = useTextEditingController(); | ||||
|     final descriptionController = useTextEditingController(); | ||||
|     final picture = useState<SnCloudFile?>(null); | ||||
|     final background = useState<SnCloudFile?>(null); | ||||
|  | ||||
|     final submitting = useState(false); | ||||
|     final enableLinks = useState(false); // Only for UI purposes | ||||
|     final homePageController = useTextEditingController(); | ||||
|     final privacyPolicyController = useTextEditingController(); | ||||
|     final termsController = useTextEditingController(); | ||||
|     final oauthEnabled = useState(false); | ||||
|     final redirectUris = useState<List<String>>([]); | ||||
|     final postLogoutUris = useState<List<String>>([]); | ||||
|     final allowedScopes = useState<List<String>>([ | ||||
|       'openid', | ||||
|       'profile', | ||||
|       'email', | ||||
|     ]); | ||||
|     final allowedGrantTypes = useState<List<String>>([ | ||||
|       'authorization_code', | ||||
|       'refresh_token', | ||||
|     ]); | ||||
|     final requirePkce = useState(true); | ||||
|     final allowOfflineAccess = useState(false); | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (app?.value != null) { | ||||
| @@ -54,6 +74,19 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|         descriptionController.text = app.value!.description ?? ''; | ||||
|         picture.value = app.value!.picture; | ||||
|         background.value = app.value!.background; | ||||
|         homePageController.text = app.value!.links?.homePage ?? ''; | ||||
|         privacyPolicyController.text = app.value!.links?.privacyPolicy ?? ''; | ||||
|         termsController.text = app.value!.links?.termsOfService ?? ''; | ||||
|         if (app.value!.oauthConfig != null) { | ||||
|           oauthEnabled.value = true; | ||||
|           redirectUris.value = app.value!.oauthConfig!.redirectUris; | ||||
|           postLogoutUris.value = | ||||
|               app.value!.oauthConfig!.postLogoutRedirectUris ?? []; | ||||
|           allowedScopes.value = app.value!.oauthConfig!.allowedScopes; | ||||
|           allowedGrantTypes.value = app.value!.oauthConfig!.allowedGrantTypes; | ||||
|           requirePkce.value = app.value!.oauthConfig!.requirePkce; | ||||
|           allowOfflineAccess.value = app.value!.oauthConfig!.allowOfflineAccess; | ||||
|         } | ||||
|       } | ||||
|       return null; | ||||
|     }, [app]); | ||||
| @@ -119,6 +152,100 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void showAddScopeDialog() { | ||||
|       final scopeController = TextEditingController(); | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'addScope'.tr(), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.all(20), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   children: [ | ||||
|                     TextFormField( | ||||
|                       controller: scopeController, | ||||
|                       decoration: InputDecoration(labelText: 'scopeName'.tr()), | ||||
|                     ), | ||||
|                     const SizedBox(height: 20), | ||||
|                     FilledButton.tonalIcon( | ||||
|                       onPressed: () { | ||||
|                         if (scopeController.text.isNotEmpty) { | ||||
|                           allowedScopes.value = [ | ||||
|                             ...allowedScopes.value, | ||||
|                             scopeController.text, | ||||
|                           ]; | ||||
|                           Navigator.pop(context); | ||||
|                         } | ||||
|                       }, | ||||
|                       icon: const Icon(Symbols.add), | ||||
|                       label: Text('add').tr(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void showAddRedirectUriDialog() { | ||||
|       final uriController = TextEditingController(); | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'addRedirectUri'.tr(), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.all(20), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   children: [ | ||||
|                     TextFormField( | ||||
|                       controller: uriController, | ||||
|                       decoration: InputDecoration( | ||||
|                         labelText: 'redirectUri'.tr(), | ||||
|                         hintText: 'https://example.com/auth/callback', | ||||
|                         helperText: 'redirectUriHint'.tr(), | ||||
|                         helperMaxLines: 3, | ||||
|                       ), | ||||
|                       keyboardType: TextInputType.url, | ||||
|                       validator: (value) { | ||||
|                         if (value == null || value.isEmpty) { | ||||
|                           return 'uriRequired'.tr(); | ||||
|                         } | ||||
|                         final uri = Uri.tryParse(value); | ||||
|                         if (uri == null || !uri.hasAbsolutePath) { | ||||
|                           return 'invalidUri'.tr(); | ||||
|                         } | ||||
|                         return null; | ||||
|                       }, | ||||
|                       onTapOutside: | ||||
|                           (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|                     const SizedBox(height: 20), | ||||
|                     FilledButton.tonalIcon( | ||||
|                       onPressed: () { | ||||
|                         if (uriController.text.isNotEmpty) { | ||||
|                           redirectUris.value = [ | ||||
|                             ...redirectUris.value, | ||||
|                             uriController.text, | ||||
|                           ]; | ||||
|                           Navigator.pop(context); | ||||
|                         } | ||||
|                       }, | ||||
|                       icon: const Icon(Symbols.add), | ||||
|                       label: Text('add').tr(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void performAction() async { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final data = { | ||||
| @@ -127,6 +254,32 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|         'description': descriptionController.text, | ||||
|         'picture_id': picture.value?.id, | ||||
|         'background_id': background.value?.id, | ||||
|         'links': { | ||||
|           'home_page': | ||||
|               homePageController.text.isNotEmpty | ||||
|                   ? homePageController.text | ||||
|                   : null, | ||||
|           'privacy_policy': | ||||
|               privacyPolicyController.text.isNotEmpty | ||||
|                   ? privacyPolicyController.text | ||||
|                   : null, | ||||
|           'terms_of_service': | ||||
|               termsController.text.isNotEmpty ? termsController.text : null, | ||||
|         }, | ||||
|         'oauth_config': | ||||
|             oauthEnabled.value | ||||
|                 ? { | ||||
|                   'redirect_uris': redirectUris.value, | ||||
|                   'post_logout_redirect_uris': | ||||
|                       postLogoutUris.value.isNotEmpty | ||||
|                           ? postLogoutUris.value | ||||
|                           : null, | ||||
|                   'allowed_scopes': allowedScopes.value, | ||||
|                   'allowed_grant_types': allowedGrantTypes.value, | ||||
|                   'require_pkce': requirePkce.value, | ||||
|                   'allow_offline_access': allowOfflineAccess.value, | ||||
|                 } | ||||
|                 : null, | ||||
|       }; | ||||
|       if (isNew) { | ||||
|         await client.post('/developers/$publisherName/apps', data: data); | ||||
| @@ -225,6 +378,7 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|                             controller: descriptionController, | ||||
|                             decoration: InputDecoration( | ||||
|                               labelText: 'description'.tr(), | ||||
|                               alignLabelWithHint: true, | ||||
|                             ), | ||||
|                             maxLines: 3, | ||||
|                             onTapOutside: | ||||
| @@ -233,6 +387,157 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|                                         ?.unfocus(), | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           ExpansionPanelList( | ||||
|                             expansionCallback: (index, isExpanded) { | ||||
|                               switch (index) { | ||||
|                                 case 0: | ||||
|                                   enableLinks.value = isExpanded; | ||||
|                                   break; | ||||
|                                 case 1: | ||||
|                                   oauthEnabled.value = isExpanded; | ||||
|                                   break; | ||||
|                               } | ||||
|                             }, | ||||
|                             children: [ | ||||
|                               ExpansionPanel( | ||||
|                                 headerBuilder: | ||||
|                                     (context, isExpanded) => | ||||
|                                         ListTile(title: Text('appLinks').tr()), | ||||
|                                 body: Column( | ||||
|                                   spacing: 16, | ||||
|                                   children: [ | ||||
|                                     TextFormField( | ||||
|                                       controller: homePageController, | ||||
|                                       decoration: InputDecoration( | ||||
|                                         labelText: 'homePageUrl'.tr(), | ||||
|                                         hintText: 'https://example.com', | ||||
|                                       ), | ||||
|                                       keyboardType: TextInputType.url, | ||||
|                                     ), | ||||
|                                     TextFormField( | ||||
|                                       controller: privacyPolicyController, | ||||
|                                       decoration: InputDecoration( | ||||
|                                         labelText: 'privacyPolicyUrl'.tr(), | ||||
|                                         hintText: 'https://example.com/privacy', | ||||
|                                       ), | ||||
|                                       keyboardType: TextInputType.url, | ||||
|                                     ), | ||||
|                                     TextFormField( | ||||
|                                       controller: termsController, | ||||
|                                       decoration: InputDecoration( | ||||
|                                         labelText: 'termsOfServiceUrl'.tr(), | ||||
|                                         hintText: 'https://example.com/terms', | ||||
|                                       ), | ||||
|                                       keyboardType: TextInputType.url, | ||||
|                                     ), | ||||
|                                   ], | ||||
|                                 ).padding(horizontal: 16, bottom: 24), | ||||
|                                 isExpanded: enableLinks.value, | ||||
|                               ), | ||||
|                               ExpansionPanel( | ||||
|                                 headerBuilder: | ||||
|                                     (context, isExpanded) => ListTile( | ||||
|                                       title: Text('oauthConfig').tr(), | ||||
|                                     ), | ||||
|                                 body: Column( | ||||
|                                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                   children: [ | ||||
|                                     Text('redirectUris'.tr()), | ||||
|                                     Card( | ||||
|                                       margin: const EdgeInsets.symmetric( | ||||
|                                         vertical: 8, | ||||
|                                       ), | ||||
|                                       child: Column( | ||||
|                                         children: [ | ||||
|                                           ...redirectUris.value.map( | ||||
|                                             (uri) => ListTile( | ||||
|                                               title: Text(uri), | ||||
|                                               trailing: IconButton( | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.delete, | ||||
|                                                 ), | ||||
|                                                 onPressed: () { | ||||
|                                                   redirectUris.value = | ||||
|                                                       redirectUris.value | ||||
|                                                           .where( | ||||
|                                                             (u) => u != uri, | ||||
|                                                           ) | ||||
|                                                           .toList(); | ||||
|                                                 }, | ||||
|                                               ), | ||||
|                                             ), | ||||
|                                           ), | ||||
|                                           if (redirectUris.value.isNotEmpty) | ||||
|                                             const Divider(height: 1), | ||||
|                                           ListTile( | ||||
|                                             leading: const Icon(Symbols.add), | ||||
|                                             title: Text('addRedirectUri'.tr()), | ||||
|                                             onTap: showAddRedirectUriDialog, | ||||
|                                             shape: RoundedRectangleBorder( | ||||
|                                               borderRadius: | ||||
|                                                   BorderRadius.circular(8), | ||||
|                                             ), | ||||
|                                           ), | ||||
|                                         ], | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     const SizedBox(height: 16), | ||||
|                                     Text('allowedScopes'.tr()), | ||||
|                                     Card( | ||||
|                                       margin: const EdgeInsets.symmetric( | ||||
|                                         vertical: 8, | ||||
|                                       ), | ||||
|                                       child: Column( | ||||
|                                         children: [ | ||||
|                                           ...allowedScopes.value.map( | ||||
|                                             (scope) => ListTile( | ||||
|                                               title: Text(scope), | ||||
|                                               trailing: IconButton( | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.delete, | ||||
|                                                 ), | ||||
|                                                 onPressed: () { | ||||
|                                                   allowedScopes.value = | ||||
|                                                       allowedScopes.value | ||||
|                                                           .where( | ||||
|                                                             (s) => s != scope, | ||||
|                                                           ) | ||||
|                                                           .toList(); | ||||
|                                                 }, | ||||
|                                               ), | ||||
|                                             ), | ||||
|                                           ), | ||||
|                                           if (allowedScopes.value.isNotEmpty) | ||||
|                                             const Divider(height: 1), | ||||
|                                           ListTile( | ||||
|                                             leading: const Icon(Symbols.add), | ||||
|                                             title: Text('add').tr(), | ||||
|                                             onTap: showAddScopeDialog, | ||||
|                                           ), | ||||
|                                         ], | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     const SizedBox(height: 16), | ||||
|                                     SwitchListTile( | ||||
|                                       title: Text('requirePkce'.tr()), | ||||
|                                       value: requirePkce.value, | ||||
|                                       onChanged: | ||||
|                                           (value) => requirePkce.value = value, | ||||
|                                     ), | ||||
|                                     SwitchListTile( | ||||
|                                       title: Text('allowOfflineAccess'.tr()), | ||||
|                                       value: allowOfflineAccess.value, | ||||
|                                       onChanged: | ||||
|                                           (value) => | ||||
|                                               allowOfflineAccess.value = value, | ||||
|                                     ), | ||||
|                                   ], | ||||
|                                 ).padding(horizontal: 16, bottom: 24), | ||||
|                                 isExpanded: oauthEnabled.value, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           Align( | ||||
|                             alignment: Alignment.centerRight, | ||||
|                             child: TextButton.icon( | ||||
|   | ||||
							
								
								
									
										142
									
								
								lib/screens/discovery/articles.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								lib/screens/discovery/articles.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/web_article_card.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'articles.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class ArticlesListNotifier extends _$ArticlesListNotifier | ||||
|     with CursorPagingNotifierMixin<SnWebArticle> { | ||||
|   static const int _pageSize = 20; | ||||
|  | ||||
|   Map<String, dynamic> _params = {}; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> build({ | ||||
|     String? feedId, | ||||
|     String? publisherId, | ||||
|   }) async { | ||||
|     _params = { | ||||
|       if (feedId != null) 'feedId': feedId, | ||||
|       if (publisherId != null) 'publisherId': publisherId, | ||||
|     }; | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final queryParams = {'limit': _pageSize, 'offset': offset, ..._params}; | ||||
|  | ||||
|     try { | ||||
|       final response = await client.get( | ||||
|         '/feeds/articles', | ||||
|         queryParameters: queryParams, | ||||
|       ); | ||||
|  | ||||
|       final List<dynamic> data = response.data; | ||||
|       final articles = | ||||
|           data | ||||
|               .map( | ||||
|                 (json) => SnWebArticle.fromJson(json as Map<String, dynamic>), | ||||
|               ) | ||||
|               .toList(); | ||||
|  | ||||
|       final total = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0; | ||||
|       final hasMore = offset + articles.length < total; | ||||
|       final nextCursor = hasMore ? (offset + articles.length).toString() : null; | ||||
|  | ||||
|       return CursorPagingData( | ||||
|         items: articles, | ||||
|         hasMore: hasMore, | ||||
|         nextCursor: nextCursor, | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       debugPrint('Error fetching articles: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SliverArticlesList extends ConsumerWidget { | ||||
|   final String? feedId; | ||||
|   final String? publisherId; | ||||
|   final Color? backgroundColor; | ||||
|   final EdgeInsets? padding; | ||||
|   final Function? onRefresh; | ||||
|  | ||||
|   const SliverArticlesList({ | ||||
|     super.key, | ||||
|     this.feedId, | ||||
|     this.publisherId, | ||||
|     this.backgroundColor, | ||||
|     this.padding, | ||||
|     this.onRefresh, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return PagingHelperSliverView( | ||||
|       provider: articlesListNotifierProvider( | ||||
|         feedId: feedId, | ||||
|         publisherId: publisherId, | ||||
|       ), | ||||
|       futureRefreshable: | ||||
|           articlesListNotifierProvider( | ||||
|             feedId: feedId, | ||||
|             publisherId: publisherId, | ||||
|           ).future, | ||||
|       notifierRefreshable: | ||||
|           articlesListNotifierProvider( | ||||
|             feedId: feedId, | ||||
|             publisherId: publisherId, | ||||
|           ).notifier, | ||||
|       contentBuilder: | ||||
|           (data, widgetCount, endItemView) => SliverList.builder( | ||||
|             itemCount: widgetCount, | ||||
|             itemBuilder: (context, index) { | ||||
|               if (index == widgetCount - 1) { | ||||
|                 return endItemView; | ||||
|               } | ||||
|  | ||||
|               final article = data.items[index]; | ||||
|               return WebArticleCard(article: article, showDetails: true); | ||||
|             }, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ArticlesScreen extends ConsumerWidget { | ||||
|   final String? feedId; | ||||
|   final String? publisherId; | ||||
|   final String? title; | ||||
|  | ||||
|   const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||
|       body: CustomScrollView( | ||||
|         slivers: [ | ||||
|           SliverPadding( | ||||
|             padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|             sliver: SliverArticlesList( | ||||
|               feedId: feedId, | ||||
|               publisherId: publisherId, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										206
									
								
								lib/screens/discovery/articles.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								lib/screens/discovery/articles.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'articles.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$articlesListNotifierHash() => | ||||
|     r'924f2344c3bbf0ff7b92fe69e88d3b64a534b538'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _$ArticlesListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> { | ||||
|   late final String? feedId; | ||||
|   late final String? publisherId; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> build({ | ||||
|     String? feedId, | ||||
|     String? publisherId, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /// See also [ArticlesListNotifier]. | ||||
| @ProviderFor(ArticlesListNotifier) | ||||
| const articlesListNotifierProvider = ArticlesListNotifierFamily(); | ||||
|  | ||||
| /// See also [ArticlesListNotifier]. | ||||
| class ArticlesListNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> { | ||||
|   /// See also [ArticlesListNotifier]. | ||||
|   const ArticlesListNotifierFamily(); | ||||
|  | ||||
|   /// See also [ArticlesListNotifier]. | ||||
|   ArticlesListNotifierProvider call({String? feedId, String? publisherId}) { | ||||
|     return ArticlesListNotifierProvider( | ||||
|       feedId: feedId, | ||||
|       publisherId: publisherId, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   ArticlesListNotifierProvider getProviderOverride( | ||||
|     covariant ArticlesListNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId, publisherId: provider.publisherId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'articlesListNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [ArticlesListNotifier]. | ||||
| class ArticlesListNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           ArticlesListNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > { | ||||
|   /// See also [ArticlesListNotifier]. | ||||
|   ArticlesListNotifierProvider({String? feedId, String? publisherId}) | ||||
|     : this._internal( | ||||
|         () => | ||||
|             ArticlesListNotifier() | ||||
|               ..feedId = feedId | ||||
|               ..publisherId = publisherId, | ||||
|         from: articlesListNotifierProvider, | ||||
|         name: r'articlesListNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$articlesListNotifierHash, | ||||
|         dependencies: ArticlesListNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             ArticlesListNotifierFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|         publisherId: publisherId, | ||||
|       ); | ||||
|  | ||||
|   ArticlesListNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|     required this.publisherId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? feedId; | ||||
|   final String? publisherId; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild( | ||||
|     covariant ArticlesListNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(feedId: feedId, publisherId: publisherId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(ArticlesListNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: ArticlesListNotifierProvider._internal( | ||||
|         () => | ||||
|             create() | ||||
|               ..feedId = feedId | ||||
|               ..publisherId = publisherId, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|         publisherId: publisherId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     ArticlesListNotifier, | ||||
|     CursorPagingData<SnWebArticle> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _ArticlesListNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is ArticlesListNotifierProvider && | ||||
|         other.feedId == feedId && | ||||
|         other.publisherId == publisherId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, publisherId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin ArticlesListNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String? get feedId; | ||||
|  | ||||
|   /// The parameter `publisherId` of this provider. | ||||
|   String? get publisherId; | ||||
| } | ||||
|  | ||||
| class _ArticlesListNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           ArticlesListNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > | ||||
|     with ArticlesListNotifierRef { | ||||
|   _ArticlesListNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String? get feedId => (origin as ArticlesListNotifierProvider).feedId; | ||||
|   @override | ||||
|   String? get publisherId => | ||||
|       (origin as ArticlesListNotifierProvider).publisherId; | ||||
| } | ||||
|  | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
| @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/activity.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/models/realm.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -21,6 +22,7 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/realm/realm_card.dart'; | ||||
| import 'package:island/widgets/publisher/publisher_card.dart'; | ||||
| import 'package:island/widgets/web_article_card.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'explore.g.dart'; | ||||
| @@ -91,37 +93,69 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|       extendBody: false, // Prevent conflicts with tabs navigation | ||||
|       appBar: AppBar( | ||||
|         toolbarHeight: 0, | ||||
|         bottom: TabBar( | ||||
|           controller: tabController, | ||||
|           tabs: [ | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'explore'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|         bottom: PreferredSize( | ||||
|           preferredSize: const Size.fromHeight(48), | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: TabBar( | ||||
|                   controller: tabController, | ||||
|                   tabAlignment: TabAlignment.start, | ||||
|                   isScrollable: true, | ||||
|                   tabs: [ | ||||
|                     Tab( | ||||
|                       icon: Tooltip( | ||||
|                         message: 'explore'.tr(), | ||||
|                         child: Icon( | ||||
|                           Symbols.explore, | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     Tab( | ||||
|                       icon: Tooltip( | ||||
|                         message: 'exploreFilterSubscriptions'.tr(), | ||||
|                         child: Icon( | ||||
|                           Symbols.subscriptions, | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     Tab( | ||||
|                       icon: Tooltip( | ||||
|                         message: 'exploreFilterFriends'.tr(), | ||||
|                         child: Icon( | ||||
|                           Symbols.people, | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'exploreFilterSubscriptions'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|               Spacer(), | ||||
|               IconButton( | ||||
|                 onPressed: () { | ||||
|                   context.push('/feeds/articles'); | ||||
|                 }, | ||||
|                 icon: Icon( | ||||
|                   Symbols.auto_stories, | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|                 tooltip: 'webArticlesStand'.tr(), | ||||
|               ), | ||||
|             ), | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'exploreFilterFriends'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|               IconButton( | ||||
|                 onPressed: () { | ||||
|                   context.push('/posts/search'); | ||||
|                 }, | ||||
|                 icon: Icon( | ||||
|                   Symbols.search, | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|                 tooltip: 'search'.tr(), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|             ], | ||||
|           ).padding(horizontal: 8), | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
| @@ -196,6 +230,7 @@ class _DiscoveryActivityItem extends StatelessWidget { | ||||
|               (switch (type) { | ||||
|                 'realm' => 'discoverRealms', | ||||
|                 'publisher' => 'discoverPublishers', | ||||
|                 'article' => 'discoverWebArticles', | ||||
|                 _ => 'unknown', | ||||
|               }).tr(), | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
| @@ -221,6 +256,11 @@ class _DiscoveryActivityItem extends StatelessWidget { | ||||
|                     publisher: SnPublisher.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 case 'article': | ||||
|                   return WebArticleCard( | ||||
|                     article: SnWebArticle.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 default: | ||||
|                   return Placeholder(); | ||||
|               } | ||||
| @@ -342,7 +382,7 @@ class ActivityListNotifier extends _$ActivityListNotifier | ||||
|       if (cursor != null) 'cursor': cursor, | ||||
|       'take': take, | ||||
|       if (filter != null) 'filter': filter, | ||||
|       if (kDebugMode) 'debugInclude': 'realms,publishers', | ||||
|       if (kDebugMode) 'debugInclude': 'realms,publishers,articles', | ||||
|     }; | ||||
|  | ||||
|     final response = await client.get( | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'explore.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$activityListNotifierHash() => | ||||
|     r'57e9dcec944a9f88f8508b69fc91342592f5b349'; | ||||
|     r'98b62fb9b958023d2c9e320af7ec1f1244836f49'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/post/draft_manager.dart'; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/widgets/content/attachment_preview.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/markdown.dart'; | ||||
|   | ||||
| @@ -9,10 +9,11 @@ import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| import 'package:island/widgets/post/post_replies.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| 
 | ||||
| part 'detail.g.dart'; | ||||
| part 'post_detail.g.dart'; | ||||
| 
 | ||||
| @riverpod | ||||
| Future<SnPost?> post(Ref ref, String id) async { | ||||
| @@ -21,20 +22,43 @@ Future<SnPost?> post(Ref ref, String id) async { | ||||
|   return SnPost.fromJson(resp.data); | ||||
| } | ||||
| 
 | ||||
| final postStateProvider = StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>( | ||||
|   (ref, id) => PostState(ref, id), | ||||
| ); | ||||
| 
 | ||||
| class PostState extends StateNotifier<AsyncValue<SnPost?>> { | ||||
|   final Ref _ref; | ||||
|   final String _id; | ||||
| 
 | ||||
|   PostState(this._ref, this._id) : super(const AsyncValue.loading()) { | ||||
|     // Initialize with the initial post data | ||||
|     _ref.listen<AsyncValue<SnPost?>>( | ||||
|       postProvider(_id), | ||||
|       (_, next) => state = next, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   void updatePost(SnPost? newPost) { | ||||
|     if (newPost != null) { | ||||
|       state = AsyncData(newPost); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class PostDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostDetailScreen({super.key, required this.id}); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final post = ref.watch(postProvider(id)); | ||||
|     final postState = ref.watch(postStateProvider(id)); | ||||
|     final user = ref.watch(userInfoProvider); | ||||
| 
 | ||||
|     final isWide = isWideScreen(context); | ||||
| 
 | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: const Text('Post')), | ||||
|       body: post.when( | ||||
|       body: postState.when( | ||||
|         data: (post) { | ||||
|           return Stack( | ||||
|             fit: StackFit.expand, | ||||
| @@ -49,6 +73,10 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                           isOpenable: false, | ||||
|                           isFullPost: true, | ||||
|                           backgroundColor: isWide ? Colors.transparent : null, | ||||
|                           onUpdate: (newItem) { | ||||
|                             // Update the local state with the new post data | ||||
|                             ref.read(postStateProvider(id).notifier).updatePost(newItem); | ||||
|                           }, | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                       ], | ||||
| @@ -65,11 +93,15 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                   right: 0, | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     child: PostQuickReply( | ||||
|                       parent: post, | ||||
|                       onPosted: () { | ||||
|                         ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                       }, | ||||
|                     child: postState.when( | ||||
|                       data: (post) => PostQuickReply( | ||||
|                         parent: post!, | ||||
|                         onPosted: () { | ||||
|                           ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                         }, | ||||
|                       ), | ||||
|                       loading: () => const SizedBox.shrink(), | ||||
|                       error: (_, __) => const SizedBox.shrink(), | ||||
|                     ).padding( | ||||
|                       bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                       top: 16, | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'detail.dart'; | ||||
| part of 'post_detail.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
							
								
								
									
										165
									
								
								lib/screens/posts/post_search.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								lib/screens/posts/post_search.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import 'dart:async'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| final postSearchNotifierProvider = StateNotifierProvider.autoDispose< | ||||
|   PostSearchNotifier, | ||||
|   AsyncValue<CursorPagingData<SnPost>> | ||||
| >((ref) => PostSearchNotifier(ref)); | ||||
|  | ||||
| class PostSearchNotifier | ||||
|     extends StateNotifier<AsyncValue<CursorPagingData<SnPost>>> { | ||||
|   final AutoDisposeRef ref; | ||||
|   static const int _pageSize = 20; | ||||
|   String _currentQuery = ''; | ||||
|   bool _isLoading = false; | ||||
|  | ||||
|   PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) { | ||||
|     state = const AsyncValue.data( | ||||
|       CursorPagingData(items: [], hasMore: false, nextCursor: null), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> search(String query) async { | ||||
|     if (_isLoading) return; | ||||
|  | ||||
|     _currentQuery = query.trim(); | ||||
|     if (_currentQuery.isEmpty) { | ||||
|       state = AsyncValue.data( | ||||
|         CursorPagingData(items: [], hasMore: false, nextCursor: null), | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     await fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   Future<void> fetch({String? cursor}) async { | ||||
|     if (_isLoading) return; | ||||
|  | ||||
|     _isLoading = true; | ||||
|     state = const AsyncValue.loading(); | ||||
|  | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|       final response = await client.get( | ||||
|         '/posts/search', | ||||
|         queryParameters: { | ||||
|           'query': _currentQuery, | ||||
|           'offset': offset, | ||||
|           'take': _pageSize, | ||||
|           'useVector': true, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
|       final data = response.data as List; | ||||
|       final posts = data.map((json) => SnPost.fromJson(json)).toList(); | ||||
|       final hasMore = posts.length == _pageSize; | ||||
|       final nextCursor = hasMore ? (offset + posts.length).toString() : null; | ||||
|  | ||||
|       state = AsyncValue.data( | ||||
|         CursorPagingData( | ||||
|           items: posts, | ||||
|           hasMore: hasMore, | ||||
|           nextCursor: nextCursor, | ||||
|         ), | ||||
|       ); | ||||
|     } catch (e, stack) { | ||||
|       state = AsyncValue.error(e, stack); | ||||
|     } finally { | ||||
|       _isLoading = false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostSearchScreen extends ConsumerStatefulWidget { | ||||
|   const PostSearchScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState(); | ||||
| } | ||||
|  | ||||
| class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|   final _searchController = TextEditingController(); | ||||
|   final _debounce = Duration(milliseconds: 500); | ||||
|   Timer? _debounceTimer; | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _searchController.dispose(); | ||||
|     _debounceTimer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   void _onSearchChanged(String query) { | ||||
|     if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel(); | ||||
|  | ||||
|     _debounceTimer = Timer(_debounce, () { | ||||
|       ref.read(postSearchNotifierProvider.notifier).search(query); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: TextField( | ||||
|           controller: _searchController, | ||||
|           decoration: InputDecoration( | ||||
|             hintText: 'Search posts...', | ||||
|             border: InputBorder.none, | ||||
|             hintStyle: TextStyle( | ||||
|               color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|             ), | ||||
|           ), | ||||
|           style: TextStyle( | ||||
|             color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|           ), | ||||
|           onChanged: _onSearchChanged, | ||||
|           onSubmitted: (value) { | ||||
|             ref.read(postSearchNotifierProvider.notifier).search(value); | ||||
|           }, | ||||
|           autofocus: true, | ||||
|         ), | ||||
|       ), | ||||
|       body: Consumer( | ||||
|         builder: (context, ref, child) { | ||||
|           final searchState = ref.watch(postSearchNotifierProvider); | ||||
|  | ||||
|           return searchState.when( | ||||
|             data: (data) { | ||||
|               if (data.items.isEmpty && _searchController.text.isNotEmpty) { | ||||
|                 return const Center(child: Text('No results found')); | ||||
|               } | ||||
|  | ||||
|               return ListView.builder( | ||||
|                 itemCount: data.items.length + (data.hasMore ? 1 : 0), | ||||
|                 itemBuilder: (context, index) { | ||||
|                   if (index >= data.items.length) { | ||||
|                     ref | ||||
|                         .read(postSearchNotifierProvider.notifier) | ||||
|                         .fetch(cursor: data.nextCursor); | ||||
|                     return const Center(child: CircularProgressIndicator()); | ||||
|                   } | ||||
|  | ||||
|                   final post = data.items[index]; | ||||
|                   return Column( | ||||
|                     children: [PostItem(item: post), const Divider(height: 1)], | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|             loading: () => const Center(child: CircularProgressIndicator()), | ||||
|             error: (error, stack) => Center(child: Text('Error: $error')), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| 
 | ||||
| part 'detail.g.dart'; | ||||
| part 'realm_detail.g.dart'; | ||||
| 
 | ||||
| @riverpod | ||||
| Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'detail.dart'; | ||||
| part of 'realm_detail.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| @@ -344,7 +344,10 @@ class EditRealmScreen extends HookConsumerWidget { | ||||
|                 const SizedBox(height: 16), | ||||
|                 TextFormField( | ||||
|                   controller: descriptionController, | ||||
|                   decoration: InputDecoration(labelText: 'description'.tr()), | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'description'.tr(), | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|                   onTapOutside: | ||||
|   | ||||
							
								
								
									
										1
									
								
								lib/widgets/article/article_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/widgets/article/article_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
|  | ||||
| @@ -360,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|         ).padding(all: 16), | ||||
|       ), | ||||
|       onTap: () { | ||||
|         context.push('/chat/call/callNotifier.roomId!'); | ||||
|         context.push('/chat/call/${callNotifier.roomId!}'); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:path/path.dart' show extension; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:photo_view/photo_view.dart'; | ||||
| @@ -210,6 +211,124 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Widget buildInfoRow(IconData icon, String label, String value) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             Icon( | ||||
|               icon, | ||||
|               size: 20, | ||||
|               color: Theme.of(context).colorScheme.onSurface, | ||||
|             ), | ||||
|             const SizedBox(width: 12), | ||||
|             Text( | ||||
|               label, | ||||
|               style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                 color: Theme.of(context).textTheme.bodySmall?.color, | ||||
|               ), | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             Flexible( | ||||
|               child: Text( | ||||
|                 value, | ||||
|                 style: Theme.of( | ||||
|                   context, | ||||
|                 ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), | ||||
|                 textAlign: TextAlign.end, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     String _formatFileSize(int bytes) { | ||||
|       if (bytes <= 0) return '0 B'; | ||||
|       if (bytes < 1024) return '$bytes B'; | ||||
|       if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB'; | ||||
|       if (bytes < 1024 * 1024 * 1024) { | ||||
|         return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB'; | ||||
|       } | ||||
|       return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; | ||||
|     } | ||||
|  | ||||
|     void showInfoSheet() { | ||||
|       final theme = Theme.of(context); | ||||
|       final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {}; | ||||
|  | ||||
|       showModalBottomSheet( | ||||
|         useRootNavigator: true, | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'File Information', | ||||
|               child: SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     buildInfoRow(Icons.description, 'Name', item.name), | ||||
|                     const Divider(height: 1), | ||||
|                     buildInfoRow( | ||||
|                       Icons.storage, | ||||
|                       'Size', | ||||
|                       _formatFileSize(item.size), | ||||
|                     ), | ||||
|                     const Divider(height: 1), | ||||
|                     buildInfoRow( | ||||
|                       Icons.category, | ||||
|                       'Type', | ||||
|                       item.mimeType?.toUpperCase() ?? 'UNKNOWN', | ||||
|                     ), | ||||
|                     if (exifData.isNotEmpty) ...[ | ||||
|                       const SizedBox(height: 16), | ||||
|                       Text( | ||||
|                         'EXIF Data', | ||||
|                         style: theme.textTheme.titleMedium?.copyWith( | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                         ), | ||||
|                       ).padding(horizontal: 24), | ||||
|                       const SizedBox(height: 8), | ||||
|                       Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           ...exifData.entries.map( | ||||
|                             (entry) => Padding( | ||||
|                               padding: const EdgeInsets.symmetric(vertical: 4), | ||||
|                               child: Row( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   Text( | ||||
|                                     '• ${entry.key.contains('-') ? entry.key.split('-').last : entry.key}: ', | ||||
|                                     style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                                       fontWeight: FontWeight.w500, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   Expanded( | ||||
|                                     child: Text( | ||||
|                                       '${entry.value}'.isNotEmpty | ||||
|                                           ? '${entry.value}' | ||||
|                                           : 'N/A', | ||||
|                                       style: theme.textTheme.bodyMedium, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ).padding(horizontal: 24), | ||||
|                     ], | ||||
|                     const SizedBox(height: 16), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return DismissiblePage( | ||||
|       isFullScreen: true, | ||||
|       backgroundColor: Colors.transparent, | ||||
| @@ -288,8 +407,22 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             child: Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               children: [ | ||||
|                 IconButton( | ||||
|                   icon: Icon( | ||||
|                     Icons.info_outline, | ||||
|                     color: Colors.white, | ||||
|                     shadows: [ | ||||
|                       Shadow( | ||||
|                         color: Colors.black54, | ||||
|                         blurRadius: 5.0, | ||||
|                         offset: Offset(1.0, 1.0), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   onPressed: showInfoSheet, | ||||
|                 ), | ||||
|                 Spacer(), | ||||
|                 IconButton( | ||||
|                   icon: Icon(Icons.remove, color: Colors.white), | ||||
|                   onPressed: () { | ||||
|   | ||||
| @@ -74,9 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|               final url = Uri.tryParse(href); | ||||
|               if (url != null) { | ||||
|                 if (url.scheme == 'solian') { | ||||
|                   context.push( | ||||
|                     ['', url.host, ...url.pathSegments].join('/'), | ||||
|                   ); | ||||
|                   context.push(['', url.host, ...url.pathSegments].join('/')); | ||||
|                   return; | ||||
|                 } | ||||
|                 final whitelistDomains = ['solian.app', 'solsynth.dev']; | ||||
| @@ -143,17 +141,27 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       generator: MarkdownGenerator( | ||||
|         generators: [latexGenerator], | ||||
|         inlineSyntaxList: [ | ||||
|           _UserNameCardInlineSyntax(), | ||||
|           _StickerInlineSyntax(), | ||||
|           LatexSyntax(isDark), | ||||
|         ], | ||||
|         linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4), | ||||
|       generator: MarkdownTextContent.buildGenerator( | ||||
|         isDark: isDark, | ||||
|         linesMargin: linesMargin, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static MarkdownGenerator buildGenerator({ | ||||
|     bool isDark = false, | ||||
|     EdgeInsets? linesMargin, | ||||
|   }) { | ||||
|     return MarkdownGenerator( | ||||
|       generators: [latexGenerator], | ||||
|       inlineSyntaxList: [ | ||||
|         _UserNameCardInlineSyntax(), | ||||
|         _StickerInlineSyntax(), | ||||
|         LatexSyntax(isDark), | ||||
|       ], | ||||
|       linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _UserNameCardInlineSyntax extends markdown.InlineSyntax { | ||||
|   | ||||
							
								
								
									
										51
									
								
								lib/widgets/empty_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/widgets/empty_state.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class EmptyState extends StatelessWidget { | ||||
|   final IconData icon; | ||||
|   final String title; | ||||
|   final String description; | ||||
|   final Widget? action; | ||||
|  | ||||
|   const EmptyState({ | ||||
|     super.key, | ||||
|     required this.icon, | ||||
|     required this.title, | ||||
|     required this.description, | ||||
|     this.action, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Center( | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.all(24.0), | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             Icon( | ||||
|               icon, | ||||
|               size: 64, | ||||
|               color: Theme.of(context).colorScheme.outline, | ||||
|             ), | ||||
|             const SizedBox(height: 16), | ||||
|             Text( | ||||
|               title, | ||||
|               style: Theme.of(context).textTheme.titleLarge, | ||||
|               textAlign: TextAlign.center, | ||||
|             ), | ||||
|             const SizedBox(height: 8), | ||||
|             Text( | ||||
|               description, | ||||
|               style: Theme.of(context).textTheme.bodyMedium, | ||||
|               textAlign: TextAlign.center, | ||||
|             ), | ||||
|             if (action != null) ...[ | ||||
|               const SizedBox(height: 24), | ||||
|               action!, | ||||
|             ], | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										33
									
								
								lib/widgets/loading_indicator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								lib/widgets/loading_indicator.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| /// A simple loading indicator widget that can be used throughout the app | ||||
| class LoadingIndicator extends StatelessWidget { | ||||
|   /// The size of the loading indicator | ||||
|   final double size; | ||||
|  | ||||
|   /// The color of the loading indicator | ||||
|   final Color? color; | ||||
|  | ||||
|   /// Creates a loading indicator | ||||
|   const LoadingIndicator({ | ||||
|     super.key, | ||||
|     this.size = 24.0, | ||||
|     this.color, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|       width: size, | ||||
|       height: size, | ||||
|       child: CircularProgressIndicator( | ||||
|         strokeWidth: 2.0, | ||||
|         valueColor: color != null | ||||
|             ? AlwaysStoppedAnimation<Color>( | ||||
|                 color!, | ||||
|               ) | ||||
|             : null, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -536,20 +536,20 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | ||||
|         ), | ||||
|       ], | ||||
|     ), | ||||
|   ).gestures(onTap: () => context.push('/posts/referencePost.id')); | ||||
|   ).gestures(onTap: () => context.push('/posts/${referencePost.id}')); | ||||
| } | ||||
|  | ||||
| class PostReactionList extends HookConsumerWidget { | ||||
|   final String parentId; | ||||
|   final Map<String, int> reactions; | ||||
|   final Function(String symbol, int attitude, int delta) onReact; | ||||
|   final Function(String symbol, int attitude, int delta)? onReact; | ||||
|   final EdgeInsets? padding; | ||||
|   const PostReactionList({ | ||||
|     super.key, | ||||
|     required this.parentId, | ||||
|     required this.reactions, | ||||
|     this.padding, | ||||
|     required this.onReact, | ||||
|     this.onReact, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -570,7 +570,7 @@ class PostReactionList extends HookConsumerWidget { | ||||
|           }) | ||||
|           .then((resp) { | ||||
|             var isRemoving = resp.statusCode == 204; | ||||
|             onReact(symbol, attitude, isRemoving ? -1 : 1); | ||||
|             onReact?.call(symbol, attitude, isRemoving ? -1 : 1); | ||||
|             HapticFeedback.heavyImpact(); | ||||
|           }); | ||||
|       submitting.value = false; | ||||
| @@ -582,33 +582,34 @@ class PostReactionList extends HookConsumerWidget { | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         padding: padding ?? EdgeInsets.zero, | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(right: 8), | ||||
|             child: ActionChip( | ||||
|               avatar: Icon(Symbols.add_reaction), | ||||
|               label: Text('react').tr(), | ||||
|               visualDensity: const VisualDensity( | ||||
|                 horizontal: VisualDensity.minimumDensity, | ||||
|                 vertical: VisualDensity.minimumDensity, | ||||
|           if (onReact != null) | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(right: 8), | ||||
|               child: ActionChip( | ||||
|                 avatar: Icon(Symbols.add_reaction), | ||||
|                 label: Text('react').tr(), | ||||
|                 visualDensity: const VisualDensity( | ||||
|                   horizontal: VisualDensity.minimumDensity, | ||||
|                   vertical: VisualDensity.minimumDensity, | ||||
|                 ), | ||||
|                 onPressed: | ||||
|                     submitting.value | ||||
|                         ? null | ||||
|                         : () { | ||||
|                           showModalBottomSheet( | ||||
|                             context: context, | ||||
|                             builder: (BuildContext context) { | ||||
|                               return _PostReactionSheet( | ||||
|                                 reactionsCount: reactions, | ||||
|                                 onReact: (symbol, attitude) { | ||||
|                                   reactPost(symbol, attitude); | ||||
|                                 }, | ||||
|                               ); | ||||
|                             }, | ||||
|                           ); | ||||
|                         }, | ||||
|               ), | ||||
|               onPressed: | ||||
|                   submitting.value | ||||
|                       ? null | ||||
|                       : () { | ||||
|                         showModalBottomSheet( | ||||
|                           context: context, | ||||
|                           builder: (BuildContext context) { | ||||
|                             return _PostReactionSheet( | ||||
|                               reactionsCount: reactions, | ||||
|                               onReact: (symbol, attitude) { | ||||
|                                 reactPost(symbol, attitude); | ||||
|                               }, | ||||
|                             ); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
|             ), | ||||
|           ), | ||||
|           for (final symbol in reactions.keys) | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(right: 8), | ||||
|   | ||||
| @@ -365,11 +365,6 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|           parentId: item.id, | ||||
|           reactions: item.reactionsCount, | ||||
|           padding: EdgeInsets.zero, | ||||
|           onReact: (symbol, attitude, delta) { | ||||
|             final reactionsCount = Map<String, int>.from(item.reactionsCount); | ||||
|             reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta; | ||||
|             onUpdate?.call(item.copyWith(reactionsCount: reactionsCount)); | ||||
|           }, | ||||
|         ), | ||||
|         const Gap(16), | ||||
|       ], | ||||
|   | ||||
							
								
								
									
										139
									
								
								lib/widgets/web_article_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								lib/widgets/web_article_card.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
|  | ||||
| class WebArticleCard extends StatelessWidget { | ||||
|   final SnWebArticle article; | ||||
|   final double? maxWidth; | ||||
|   final bool showDetails; | ||||
|  | ||||
|   const WebArticleCard({ | ||||
|     super.key, | ||||
|     required this.article, | ||||
|     this.maxWidth, | ||||
|     this.showDetails = false, | ||||
|   }); | ||||
|  | ||||
|   void _onTap(BuildContext context) { | ||||
|     context.push('/feeds/articles/${article.id}'); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final theme = Theme.of(context); | ||||
|     final colorScheme = theme.colorScheme; | ||||
|  | ||||
|     return ConstrainedBox( | ||||
|       constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), | ||||
|       child: Card( | ||||
|         clipBehavior: Clip.antiAlias, | ||||
|         child: InkWell( | ||||
|           onTap: () => _onTap(context), | ||||
|           child: AspectRatio( | ||||
|             aspectRatio: 16 / 9, | ||||
|             child: Stack( | ||||
|               fit: StackFit.expand, | ||||
|               children: [ | ||||
|                 // Image or fallback | ||||
|                 article.preview?.imageUrl != null | ||||
|                     ? CachedNetworkImage( | ||||
|                       imageUrl: article.preview!.imageUrl!, | ||||
|                       fit: BoxFit.cover, | ||||
|                       width: double.infinity, | ||||
|                       height: double.infinity, | ||||
|                     ) | ||||
|                     : ColoredBox( | ||||
|                       color: colorScheme.secondaryContainer, | ||||
|                       child: const Center( | ||||
|                         child: Icon( | ||||
|                           Icons.article_outlined, | ||||
|                           size: 48, | ||||
|                           color: Colors.white, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                 // Gradient overlay | ||||
|                 Container( | ||||
|                   decoration: BoxDecoration( | ||||
|                     gradient: LinearGradient( | ||||
|                       begin: Alignment.topCenter, | ||||
|                       end: Alignment.bottomCenter, | ||||
|                       colors: [ | ||||
|                         Colors.transparent, | ||||
|                         Colors.black.withOpacity(0.7), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 // Title | ||||
|                 Align( | ||||
|                   alignment: Alignment.bottomLeft, | ||||
|                   child: Container( | ||||
|                     padding: const EdgeInsets.only( | ||||
|                       left: 12, | ||||
|                       right: 12, | ||||
|                       bottom: 8, | ||||
|                     ), | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       mainAxisAlignment: MainAxisAlignment.end, | ||||
|                       mainAxisSize: MainAxisSize.min, | ||||
|                       children: [ | ||||
|                         if (showDetails) | ||||
|                           const SizedBox(height: 8) | ||||
|                         else | ||||
|                           Spacer(), | ||||
|                         Text( | ||||
|                           article.title, | ||||
|                           style: theme.textTheme.titleSmall?.copyWith( | ||||
|                             color: Colors.white, | ||||
|                             fontWeight: FontWeight.bold, | ||||
|                             height: 1.3, | ||||
|                           ), | ||||
|                           maxLines: showDetails ? 3 : 2, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                         ), | ||||
|                         if (showDetails && | ||||
|                             article.author?.isNotEmpty == true) ...[ | ||||
|                           const SizedBox(height: 4), | ||||
|                           Text( | ||||
|                             article.author!, | ||||
|                             style: TextStyle( | ||||
|                               fontSize: 10, | ||||
|                               color: Colors.white.withOpacity(0.9), | ||||
|                               fontWeight: FontWeight.w500, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                         if (showDetails) const Spacer(), | ||||
|                         if (showDetails && article.publishedAt != null) ...[ | ||||
|                           Text( | ||||
|                             '${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}', | ||||
|                             style: const TextStyle( | ||||
|                               fontSize: 9, | ||||
|                               color: Colors.white70, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(height: 2), | ||||
|                         ], | ||||
|                         Text( | ||||
|                           article.feed?.title ?? 'Unknown Source', | ||||
|                           style: const TextStyle( | ||||
|                             fontSize: 9, | ||||
|                             color: Colors.white70, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -11,32 +11,32 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - file_selector_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - Firebase/CoreOnly (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - Firebase/Messaging (11.13.0): | ||||
|   - Firebase/CoreOnly (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - Firebase/Messaging (11.15.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.13.0) | ||||
|   - firebase_core (3.14.0): | ||||
|     - Firebase/CoreOnly (~> 11.13.0) | ||||
|     - FirebaseMessaging (~> 11.15.0) | ||||
|   - firebase_core (3.15.0): | ||||
|     - Firebase/CoreOnly (~> 11.15.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.2.7): | ||||
|     - Firebase/CoreOnly (~> 11.13.0) | ||||
|     - Firebase/Messaging (~> 11.13.0) | ||||
|   - firebase_messaging (15.2.8): | ||||
|     - Firebase/CoreOnly (~> 11.15.0) | ||||
|     - Firebase/Messaging (~> 11.15.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - FirebaseCore (11.13.0): | ||||
|     - FirebaseCoreInternal (~> 11.13.0) | ||||
|   - FirebaseCore (11.15.0): | ||||
|     - FirebaseCoreInternal (~> 11.15.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreInternal (11.13.0): | ||||
|   - FirebaseCoreInternal (11.15.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseInstallations (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - FirebaseInstallations (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.13.0): | ||||
|     - FirebaseCore (~> 11.13.0) | ||||
|   - FirebaseMessaging (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
| @@ -92,7 +92,7 @@ PODS: | ||||
|     - GoogleUtilities/Privacy | ||||
|   - irondash_engine_context (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - livekit_client (2.4.8): | ||||
|   - livekit_client (2.4.9): | ||||
|     - flutter_webrtc | ||||
|     - FlutterMacOS | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
| @@ -291,13 +291,13 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 | ||||
|   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a | ||||
|   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 | ||||
|   Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327 | ||||
|   firebase_core: 1095fcf33161d99bc34aa10f7c0d89414a208d15 | ||||
|   firebase_messaging: 6417056ffb85141607618ddfef9fec9f3caab3ea | ||||
|   FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0 | ||||
|   FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c | ||||
|   FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02 | ||||
|   FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4 | ||||
|   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||
|   firebase_core: 177f51be1650b15d2d5b9f1abf48792619288070 | ||||
|   firebase_messaging: 8748a5d4bb435993cffa7f5501292f3e914a23d7 | ||||
|   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||
|   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||
|   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||
|   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||
|   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d | ||||
|   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 | ||||
|   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 | ||||
| @@ -309,7 +309,7 @@ SPEC CHECKSUMS: | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||
|   livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888 | ||||
|   livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059 | ||||
|   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 | ||||
|   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 | ||||
|   media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 | ||||
|   | ||||
							
								
								
									
										140
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _flutterfire_internals | ||||
|       sha256: dda4fd7909a732a014239009aa52537b136f8ce568de23c212587097887e2307 | ||||
|       sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.56" | ||||
|     version: "1.3.57" | ||||
|   analyzer: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -629,50 +629,50 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_core | ||||
|       sha256: "420d9111dcf095341f1ea8fdce926eef750cf7b9745d21f38000de780c94f608" | ||||
|       sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.14.0" | ||||
|     version: "3.15.0" | ||||
|   firebase_core_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_core_platform_interface | ||||
|       sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf | ||||
|       sha256: "5d2ab45779d91af2aa0252dec9fe4ee1caa015d83377de255454dcaa1526a0e0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.4.0" | ||||
|     version: "5.4.1" | ||||
|   firebase_core_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_core_web | ||||
|       sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e | ||||
|       sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.23.0" | ||||
|     version: "2.24.0" | ||||
|   firebase_messaging: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_messaging | ||||
|       sha256: "758461f67b96aa5ad27625aaae39882fd6d1961b1c7e005301f9a74b6336100b" | ||||
|       sha256: c6711cf2f455532b84a94022c7aaf85088849763af2f01b775ca79d82d10a01a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.2.7" | ||||
|     version: "15.2.8" | ||||
|   firebase_messaging_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_platform_interface | ||||
|       sha256: "614db1b0df0f53e541e41cc182b6d7ede5763c400f6ba232a5f8d0e1b5e5de32" | ||||
|       sha256: "1c9dacccb1aee1bf17ba519dda5563a16fdd2ec1e79b5f2e421cb4bf75a166f7" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.6.7" | ||||
|     version: "4.6.8" | ||||
|   firebase_messaging_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_web | ||||
|       sha256: b5fbbcdd3e0e7f3fde72b0c119410f22737638fed5fc428b54bba06bc1455d81 | ||||
|       sha256: "54317c26fa92f0d90a2017977ac791cb0504eca29fcf397f06adf727d4a7a2d5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.7" | ||||
|     version: "3.10.8" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -798,6 +798,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.0" | ||||
|   flutter_keyboard_visibility: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility | ||||
|       sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.0" | ||||
|   flutter_keyboard_visibility_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_linux | ||||
|       sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_keyboard_visibility_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_macos | ||||
|       sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_keyboard_visibility_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_platform_interface | ||||
|       sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|   flutter_keyboard_visibility_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_web | ||||
|       sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|   flutter_keyboard_visibility_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_windows | ||||
|       sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -960,6 +1008,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.1" | ||||
|   flutter_typeahead: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_typeahead | ||||
|       sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.2.0" | ||||
|   flutter_udid: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -977,10 +1033,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3 | ||||
|       sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.14.1" | ||||
|     version: "0.14.2" | ||||
|   font_awesome_flutter: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1093,6 +1149,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.15.6" | ||||
|   html2md: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: html2md | ||||
|       sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.2" | ||||
|   http: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1297,10 +1361,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: livekit_client | ||||
|       sha256: c270720a49b935591960c6f3296fd8f00c09b45a70cd64aef78cd0a8f8257913 | ||||
|       sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.8" | ||||
|     version: "2.4.9" | ||||
|   local_auth: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1677,6 +1741,38 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.8" | ||||
|   pointer_interceptor: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor | ||||
|       sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.1+2" | ||||
|   pointer_interceptor_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_ios | ||||
|       sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.1" | ||||
|   pointer_interceptor_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_platform_interface | ||||
|       sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.0+1" | ||||
|   pointer_interceptor_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_web | ||||
|       sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.3" | ||||
|   pool: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1689,10 +1785,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: posix | ||||
|       sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 | ||||
|       sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.2" | ||||
|     version: "6.0.3" | ||||
|   protobuf: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1809,10 +1905,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: record_web | ||||
|       sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4" | ||||
|       sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.8" | ||||
|     version: "1.1.9" | ||||
|   record_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | ||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 3.0.0+109 | ||||
| version: 3.0.0+110 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.7.2 | ||||
| @@ -37,7 +37,7 @@ dependencies: | ||||
|   flutter_hooks: ^0.21.2 | ||||
|   hooks_riverpod: ^2.6.1 | ||||
|   bitsdojo_window: ^0.1.6 | ||||
|   go_router: ^15.2.4 | ||||
|   go_router: ^15.1.3 | ||||
|   styled_widget: ^0.4.1 | ||||
|   shared_preferences: ^2.5.3 | ||||
|   flutter_riverpod: ^2.6.1 | ||||
| @@ -127,6 +127,8 @@ dependencies: | ||||
|       url: https://github.com/lionelmennig/textfield_tags.git | ||||
|       ref: fixes/allow-controller-re-registration | ||||
|   mime: ^2.0.0 | ||||
|   html2md: ^1.3.2 | ||||
|   flutter_typeahead: ^5.2.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user