Compare commits
	
		
			10 Commits
		
	
	
		
			666a2dfbf5
			...
			3.0.0+110
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 007acedf29 | |||
| 8e903ec6c1 | |||
| b55e56c3c4 | |||
| 6f9de431b1 | |||
| a8efd26262 | |||
| e367fc3f5c | |||
| 8a1af120ea | |||
| f03f0181f8 | |||
| 6c7d42c31a | |||
| d6c829c26a | 
| @@ -675,5 +675,8 @@ | |||||||
|   "publisherFeatureDevelop": "Developer Program", |   "publisherFeatureDevelop": "Developer Program", | ||||||
|   "publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.", |   "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.", |   "publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.", | ||||||
|   "learnMore": "Learn More" |   "learnMore": "Learn More", | ||||||
|  |   "discoverWebArticles": "Articles from external sites", | ||||||
|  |   "webArticlesStand": "Article Stand", | ||||||
|  |   "about": "About" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,31 +40,31 @@ PODS: | |||||||
|   - file_picker (0.0.1): |   - file_picker (0.0.1): | ||||||
|     - DKImagePickerController/PhotoGallery |     - DKImagePickerController/PhotoGallery | ||||||
|     - Flutter |     - Flutter | ||||||
|   - Firebase/CoreOnly (11.13.0): |   - Firebase/CoreOnly (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|   - Firebase/Messaging (11.13.0): |   - Firebase/Messaging (11.15.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 11.13.0) |     - FirebaseMessaging (~> 11.15.0) | ||||||
|   - firebase_core (3.14.0): |   - firebase_core (3.15.0): | ||||||
|     - Firebase/CoreOnly (= 11.13.0) |     - Firebase/CoreOnly (= 11.15.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_messaging (15.2.7): |   - firebase_messaging (15.2.8): | ||||||
|     - Firebase/Messaging (= 11.13.0) |     - Firebase/Messaging (= 11.15.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - FirebaseCore (11.13.0): |   - FirebaseCore (11.15.0): | ||||||
|     - FirebaseCoreInternal (~> 11.13.0) |     - FirebaseCoreInternal (~> 11.15.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|     - GoogleUtilities/Logger (~> 8.1) |     - GoogleUtilities/Logger (~> 8.1) | ||||||
|   - FirebaseCoreInternal (11.13.0): |   - FirebaseCoreInternal (11.15.0): | ||||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|   - FirebaseInstallations (11.13.0): |   - FirebaseInstallations (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|     - GoogleUtilities/UserDefaults (~> 8.1) |     - GoogleUtilities/UserDefaults (~> 8.1) | ||||||
|     - PromisesObjC (~> 2.4) |     - PromisesObjC (~> 2.4) | ||||||
|   - FirebaseMessaging (11.13.0): |   - FirebaseMessaging (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleDataTransport (~> 10.0) |     - GoogleDataTransport (~> 10.0) | ||||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
| @@ -80,6 +80,8 @@ PODS: | |||||||
|   - flutter_inappwebview_ios/Core (0.0.1): |   - flutter_inappwebview_ios/Core (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - OrderedSet (~> 6.0.3) |     - OrderedSet (~> 6.0.3) | ||||||
|  |   - flutter_keyboard_visibility (0.0.1): | ||||||
|  |     - Flutter | ||||||
|   - flutter_native_splash (2.4.3): |   - flutter_native_splash (2.4.3): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - flutter_platform_alert (0.0.1): |   - flutter_platform_alert (0.0.1): | ||||||
| @@ -128,8 +130,8 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|   - irondash_engine_context (0.0.1): |   - irondash_engine_context (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - Kingfisher (8.3.2) |   - Kingfisher (8.3.3) | ||||||
|   - livekit_client (2.4.8): |   - livekit_client (2.4.9): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
|     - WebRTC-SDK (= 125.6422.07) |     - WebRTC-SDK (= 125.6422.07) | ||||||
| @@ -155,6 +157,8 @@ PODS: | |||||||
|   - path_provider_foundation (0.0.1): |   - path_provider_foundation (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|  |   - pointer_interceptor_ios (0.0.1): | ||||||
|  |     - Flutter | ||||||
|   - PromisesObjC (2.4.0) |   - PromisesObjC (2.4.0) | ||||||
|   - receive_sharing_intent (1.8.1): |   - receive_sharing_intent (1.8.1): | ||||||
|     - Flutter |     - Flutter | ||||||
| @@ -217,6 +221,7 @@ DEPENDENCIES: | |||||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) |   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||||
|   - Flutter (from `Flutter`) |   - Flutter (from `Flutter`) | ||||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) |   - 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_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||||
|   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) |   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) | ||||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/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`) |   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) | ||||||
|   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) |   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) | ||||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) |   - 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`) |   - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) | ||||||
|   - record_ios (from `.symlinks/plugins/record_ios/ios`) |   - record_ios (from `.symlinks/plugins/record_ios/ios`) | ||||||
|   - share_plus (from `.symlinks/plugins/share_plus/ios`) |   - share_plus (from `.symlinks/plugins/share_plus/ios`) | ||||||
| @@ -286,6 +292,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter |     :path: Flutter | ||||||
|   flutter_inappwebview_ios: |   flutter_inappwebview_ios: | ||||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" |     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||||
|  |   flutter_keyboard_visibility: | ||||||
|  |     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" | ||||||
|   flutter_native_splash: |   flutter_native_splash: | ||||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" |     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||||
|   flutter_platform_alert: |   flutter_platform_alert: | ||||||
| @@ -320,6 +328,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/pasteboard/ios" |     :path: ".symlinks/plugins/pasteboard/ios" | ||||||
|   path_provider_foundation: |   path_provider_foundation: | ||||||
|     :path: ".symlinks/plugins/path_provider_foundation/darwin" |     :path: ".symlinks/plugins/path_provider_foundation/darwin" | ||||||
|  |   pointer_interceptor_ios: | ||||||
|  |     :path: ".symlinks/plugins/pointer_interceptor_ios/ios" | ||||||
|   receive_sharing_intent: |   receive_sharing_intent: | ||||||
|     :path: ".symlinks/plugins/receive_sharing_intent/ios" |     :path: ".symlinks/plugins/receive_sharing_intent/ios" | ||||||
|   record_ios: |   record_ios: | ||||||
| @@ -351,15 +361,16 @@ SPEC CHECKSUMS: | |||||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c |   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 |   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be |   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||||
|   Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327 |   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||||
|   firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450 |   firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492 | ||||||
|   firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954 |   firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c | ||||||
|   FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0 |   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||||
|   FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c |   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||||
|   FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02 |   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||||
|   FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4 |   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 |   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 |   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||||
|  |   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf |   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||||
|   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 |   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 | ||||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 |   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||||
| @@ -371,8 +382,8 @@ SPEC CHECKSUMS: | |||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 |   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||||
|   Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5 |   Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be | ||||||
|   livekit_client: 9e901890552514206e5ff828903ed271531da264 |   livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e | ||||||
|   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 |   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 | ||||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 |   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 |   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||||
| @@ -382,6 +393,7 @@ SPEC CHECKSUMS: | |||||||
|   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 |   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||||
|   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c |   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c | ||||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|  |   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed | ||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 |   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||||
|   record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b |   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:flutter/material.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.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/apps.dart'; | ||||||
| import 'package:island/screens/developers/edit_app.dart'; | import 'package:island/screens/developers/edit_app.dart'; | ||||||
| import 'package:island/screens/developers/new_app.dart'; | import 'package:island/screens/developers/new_app.dart'; | ||||||
| import 'package:island/screens/developers/hub.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/widgets/app_wrapper.dart'; | ||||||
| import 'package:island/screens/tabs.dart'; | import 'package:island/screens/tabs.dart'; | ||||||
|  |  | ||||||
| import 'package:island/screens/explore.dart'; | import 'package:island/screens/explore.dart'; | ||||||
|  | import 'package:island/screens/article_detail_screen.dart'; | ||||||
| import 'package:island/screens/account.dart'; | import 'package:island/screens/account.dart'; | ||||||
| import 'package:island/screens/notification.dart'; | import 'package:island/screens/notification.dart'; | ||||||
| import 'package:island/screens/wallet.dart'; | import 'package:island/screens/wallet.dart'; | ||||||
| @@ -22,18 +25,20 @@ import 'package:island/screens/chat/room.dart'; | |||||||
| import 'package:island/screens/chat/room_detail.dart'; | import 'package:island/screens/chat/room_detail.dart'; | ||||||
| import 'package:island/screens/chat/call.dart'; | import 'package:island/screens/chat/call.dart'; | ||||||
| import 'package:island/screens/creators/hub.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/stickers.dart'; | ||||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||||
| import 'package:island/screens/creators/publishers.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/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/posts/pub_profile.dart'; | ||||||
| import 'package:island/screens/auth/login.dart'; | import 'package:island/screens/auth/login.dart'; | ||||||
| import 'package:island/screens/auth/create_account.dart'; | import 'package:island/screens/auth/create_account.dart'; | ||||||
| import 'package:island/screens/settings.dart'; | import 'package:island/screens/settings.dart'; | ||||||
| import 'package:island/screens/realm/realms.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/account/event_calendar.dart'; | ||||||
| import 'package:island/screens/discovery/realms.dart'; | import 'package:island/screens/discovery/realms.dart'; | ||||||
|  |  | ||||||
| @@ -91,6 +96,33 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|                 path: '/creators', |                 path: '/creators', | ||||||
|                 builder: (context, state) => const CreatorHubScreen(), |                 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( |               GoRoute( | ||||||
|                 path: '/creators/:name/posts', |                 path: '/creators/:name/posts', | ||||||
|                 builder: (context, state) { |                 builder: (context, state) { | ||||||
| @@ -167,19 +199,22 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|               ), |               ), | ||||||
|               GoRoute( |               GoRoute( | ||||||
|                 path: '/developers/:name/apps', |                 path: '/developers/:name/apps', | ||||||
|                 builder: (context, state) => CustomAppsScreen( |                 builder: | ||||||
|  |                     (context, state) => CustomAppsScreen( | ||||||
|                       publisherName: state.pathParameters['name']!, |                       publisherName: state.pathParameters['name']!, | ||||||
|                     ), |                     ), | ||||||
|               ), |               ), | ||||||
|               GoRoute( |               GoRoute( | ||||||
|                 path: '/developers/:name/apps/new', |                 path: '/developers/:name/apps/new', | ||||||
|                 builder: (context, state) => NewCustomAppScreen( |                 builder: | ||||||
|  |                     (context, state) => NewCustomAppScreen( | ||||||
|                       publisherName: state.pathParameters['name']!, |                       publisherName: state.pathParameters['name']!, | ||||||
|                     ), |                     ), | ||||||
|               ), |               ), | ||||||
|               GoRoute( |               GoRoute( | ||||||
|                 path: '/developers/:name/apps/:id', |                 path: '/developers/:name/apps/:id', | ||||||
|                 builder: (context, state) => EditAppScreen( |                 builder: | ||||||
|  |                     (context, state) => EditAppScreen( | ||||||
|                       publisherName: state.pathParameters['name']!, |                       publisherName: state.pathParameters['name']!, | ||||||
|                       id: state.pathParameters['id']!, |                       id: state.pathParameters['id']!, | ||||||
|                     ), |                     ), | ||||||
| @@ -187,6 +222,19 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|  |  | ||||||
|  |           // 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 |           // Auth routes | ||||||
|           GoRoute( |           GoRoute( | ||||||
|             path: '/auth/login', |             path: '/auth/login', | ||||||
| @@ -202,6 +250,10 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|             path: '/settings', |             path: '/settings', | ||||||
|             builder: (context, state) => const SettingsScreen(), |             builder: (context, state) => const SettingsScreen(), | ||||||
|           ), |           ), | ||||||
|  |           GoRoute( | ||||||
|  |             path: '/about', | ||||||
|  |             builder: (context, state) => const AboutScreen(), | ||||||
|  |           ), | ||||||
|  |  | ||||||
|           // Main tabs with TabsScreen shell |           // Main tabs with TabsScreen shell | ||||||
|           ShellRoute( |           ShellRoute( | ||||||
| @@ -219,6 +271,10 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|                     path: '/', |                     path: '/', | ||||||
|                     builder: (context, state) => const ExploreScreen(), |                     builder: (context, state) => const ExploreScreen(), | ||||||
|                   ), |                   ), | ||||||
|  |                   GoRoute( | ||||||
|  |                     path: '/posts/search', | ||||||
|  |                     builder: (context, state) => const PostSearchScreen(), | ||||||
|  |                   ), | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     path: '/posts/:id', |                     path: '/posts/:id', | ||||||
|                     builder: (context, state) { |                     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), |             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( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.logout), |               leading: const Icon(Symbols.logout), | ||||||
|   | |||||||
| @@ -341,7 +341,10 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|  |  | ||||||
|                   TextFormField( |                   TextFormField( | ||||||
|                     decoration: InputDecoration(labelText: 'bio'.tr()), |                     decoration: InputDecoration( | ||||||
|  |                       labelText: 'bio'.tr(), | ||||||
|  |                       alignLabelWithHint: true, | ||||||
|  |                     ), | ||||||
|                     maxLines: null, |                     maxLines: null, | ||||||
|                     minLines: 3, |                     minLines: 3, | ||||||
|                     controller: bioController, |                     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), |                 const SizedBox(height: 16), | ||||||
|                 TextFormField( |                 TextFormField( | ||||||
|                   controller: descriptionController, |                   controller: descriptionController, | ||||||
|                   decoration: const InputDecoration(labelText: 'Description'), |                   decoration: const InputDecoration( | ||||||
|  |                     labelText: 'Description', | ||||||
|  |                     alignLabelWithHint: true, | ||||||
|  |                   ), | ||||||
|                   minLines: 3, |                   minLines: 3, | ||||||
|                   maxLines: null, |                   maxLines: null, | ||||||
|                   onTapOutside: |                   onTapOutside: | ||||||
|   | |||||||
| @@ -370,9 +370,9 @@ class CreatorHubScreen extends HookConsumerWidget { | |||||||
|                           ListTile( |                           ListTile( | ||||||
|                             minTileHeight: 48, |                             minTileHeight: 48, | ||||||
|                             title: Text('publisherMembers').tr(), |                             title: Text('publisherMembers').tr(), | ||||||
|                             trailing: Icon(Symbols.chevron_right), |                             trailing: const Icon(Symbols.chevron_right), | ||||||
|                             leading: const Icon(Symbols.group), |                             leading: const Icon(Symbols.group), | ||||||
|                             contentPadding: EdgeInsets.symmetric( |                             contentPadding: const EdgeInsets.symmetric( | ||||||
|                               horizontal: 24, |                               horizontal: 24, | ||||||
|                             ), |                             ), | ||||||
|                             onTap: () { |                             onTap: () { | ||||||
| @@ -387,6 +387,20 @@ 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( |                           ExpansionTile( | ||||||
|                             title: Text('publisherFeatures').tr(), |                             title: Text('publisherFeatures').tr(), | ||||||
|                             leading: const Icon(Symbols.flag), |                             leading: const Icon(Symbols.flag), | ||||||
|   | |||||||
| @@ -270,7 +270,10 @@ class EditPublisherScreen extends HookConsumerWidget { | |||||||
|                     ), |                     ), | ||||||
|                     TextFormField( |                     TextFormField( | ||||||
|                       controller: bioController, |                       controller: bioController, | ||||||
|                       decoration: InputDecoration(labelText: 'bio'.tr()), |                       decoration: InputDecoration( | ||||||
|  |                         labelText: 'bio'.tr(), | ||||||
|  |                         alignLabelWithHint: true, | ||||||
|  |                       ), | ||||||
|                       minLines: 3, |                       minLines: 3, | ||||||
|                       maxLines: null, |                       maxLines: null, | ||||||
|                       onTapOutside: |                       onTapOutside: | ||||||
|   | |||||||
| @@ -71,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget { | |||||||
|                 subtitle: Text(sticker.description), |                 subtitle: Text(sticker.description), | ||||||
|                 trailing: const Icon(Symbols.chevron_right), |                 trailing: const Icon(Symbols.chevron_right), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   context.push( |                   context.push('/creators/$pubName/stickers/${sticker.id}'); | ||||||
|                     '/creators/$pubName/stickers/${sticker.id}', |  | ||||||
|                   ); |  | ||||||
|                 }, |                 }, | ||||||
|               ); |               ); | ||||||
|             }, |             }, | ||||||
| @@ -230,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | |||||||
|                   decoration: InputDecoration( |                   decoration: InputDecoration( | ||||||
|                     labelText: 'description'.tr(), |                     labelText: 'description'.tr(), | ||||||
|                     border: const UnderlineInputBorder(), |                     border: const UnderlineInputBorder(), | ||||||
|  |                     alignLabelWithHint: true, | ||||||
|                   ), |                   ), | ||||||
|                   minLines: 3, |                   minLines: 3, | ||||||
|                   maxLines: null, |                   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')), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -378,6 +378,7 @@ class EditAppScreen extends HookConsumerWidget { | |||||||
|                             controller: descriptionController, |                             controller: descriptionController, | ||||||
|                             decoration: InputDecoration( |                             decoration: InputDecoration( | ||||||
|                               labelText: 'description'.tr(), |                               labelText: 'description'.tr(), | ||||||
|  |                               alignLabelWithHint: true, | ||||||
|                             ), |                             ), | ||||||
|                             maxLines: 3, |                             maxLines: 3, | ||||||
|                             onTapOutside: |                             onTapOutside: | ||||||
|   | |||||||
							
								
								
									
										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/activity.dart'; | ||||||
| import 'package:island/models/publisher.dart'; | import 'package:island/models/publisher.dart'; | ||||||
| import 'package:island/models/realm.dart'; | import 'package:island/models/realm.dart'; | ||||||
|  | import 'package:island/models/webfeed.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.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/pods/network.dart'; | ||||||
| import 'package:island/widgets/realm/realm_card.dart'; | import 'package:island/widgets/realm/realm_card.dart'; | ||||||
| import 'package:island/widgets/publisher/publisher_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'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| part 'explore.g.dart'; | part 'explore.g.dart'; | ||||||
| @@ -91,32 +93,39 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|       extendBody: false, // Prevent conflicts with tabs navigation |       extendBody: false, // Prevent conflicts with tabs navigation | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         toolbarHeight: 0, |         toolbarHeight: 0, | ||||||
|         bottom: TabBar( |         bottom: PreferredSize( | ||||||
|  |           preferredSize: const Size.fromHeight(48), | ||||||
|  |           child: Row( | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: TabBar( | ||||||
|                   controller: tabController, |                   controller: tabController, | ||||||
|  |                   tabAlignment: TabAlignment.start, | ||||||
|  |                   isScrollable: true, | ||||||
|                   tabs: [ |                   tabs: [ | ||||||
|                     Tab( |                     Tab( | ||||||
|               child: Text( |                       icon: Tooltip( | ||||||
|                 'explore'.tr(), |                         message: 'explore'.tr(), | ||||||
|                 textAlign: TextAlign.center, |                         child: Icon( | ||||||
|                 style: TextStyle( |                           Symbols.explore, | ||||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, |                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     Tab( |                     Tab( | ||||||
|               child: Text( |                       icon: Tooltip( | ||||||
|                 'exploreFilterSubscriptions'.tr(), |                         message: 'exploreFilterSubscriptions'.tr(), | ||||||
|                 textAlign: TextAlign.center, |                         child: Icon( | ||||||
|                 style: TextStyle( |                           Symbols.subscriptions, | ||||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, |                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     Tab( |                     Tab( | ||||||
|               child: Text( |                       icon: Tooltip( | ||||||
|                 'exploreFilterFriends'.tr(), |                         message: 'exploreFilterFriends'.tr(), | ||||||
|                 textAlign: TextAlign.center, |                         child: Icon( | ||||||
|                 style: TextStyle( |                           Symbols.people, | ||||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, |                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
| @@ -124,6 +133,31 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  |               Spacer(), | ||||||
|  |               IconButton( | ||||||
|  |                 onPressed: () { | ||||||
|  |                   context.push('/feeds/articles'); | ||||||
|  |                 }, | ||||||
|  |                 icon: Icon( | ||||||
|  |                   Symbols.auto_stories, | ||||||
|  |                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|  |                 ), | ||||||
|  |                 tooltip: 'webArticlesStand'.tr(), | ||||||
|  |               ), | ||||||
|  |               IconButton( | ||||||
|  |                 onPressed: () { | ||||||
|  |                   context.push('/posts/search'); | ||||||
|  |                 }, | ||||||
|  |                 icon: Icon( | ||||||
|  |                   Symbols.search, | ||||||
|  |                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|  |                 ), | ||||||
|  |                 tooltip: 'search'.tr(), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 8), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|       floatingActionButton: FloatingActionButton( |       floatingActionButton: FloatingActionButton( | ||||||
|         heroTag: Key("explore-page-fab"), |         heroTag: Key("explore-page-fab"), | ||||||
|         onPressed: () { |         onPressed: () { | ||||||
| @@ -196,6 +230,7 @@ class _DiscoveryActivityItem extends StatelessWidget { | |||||||
|               (switch (type) { |               (switch (type) { | ||||||
|                 'realm' => 'discoverRealms', |                 'realm' => 'discoverRealms', | ||||||
|                 'publisher' => 'discoverPublishers', |                 'publisher' => 'discoverPublishers', | ||||||
|  |                 'article' => 'discoverWebArticles', | ||||||
|                 _ => 'unknown', |                 _ => 'unknown', | ||||||
|               }).tr(), |               }).tr(), | ||||||
|               style: Theme.of(context).textTheme.titleMedium, |               style: Theme.of(context).textTheme.titleMedium, | ||||||
| @@ -221,6 +256,11 @@ class _DiscoveryActivityItem extends StatelessWidget { | |||||||
|                     publisher: SnPublisher.fromJson(item['data']), |                     publisher: SnPublisher.fromJson(item['data']), | ||||||
|                     maxWidth: 280, |                     maxWidth: 280, | ||||||
|                   ); |                   ); | ||||||
|  |                 case 'article': | ||||||
|  |                   return WebArticleCard( | ||||||
|  |                     article: SnWebArticle.fromJson(item['data']), | ||||||
|  |                     maxWidth: 280, | ||||||
|  |                   ); | ||||||
|                 default: |                 default: | ||||||
|                   return Placeholder(); |                   return Placeholder(); | ||||||
|               } |               } | ||||||
| @@ -342,7 +382,7 @@ class ActivityListNotifier extends _$ActivityListNotifier | |||||||
|       if (cursor != null) 'cursor': cursor, |       if (cursor != null) 'cursor': cursor, | ||||||
|       'take': take, |       'take': take, | ||||||
|       if (filter != null) 'filter': filter, |       if (filter != null) 'filter': filter, | ||||||
|       if (kDebugMode) 'debugInclude': 'realms,publishers', |       if (kDebugMode) 'debugInclude': 'realms,publishers,articles', | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     final response = await client.get( |     final response = await client.get( | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'explore.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$activityListNotifierHash() => | String _$activityListNotifierHash() => | ||||||
|     r'57e9dcec944a9f88f8508b69fc91342592f5b349'; |     r'98b62fb9b958023d2c9e320af7ec1f1244836f49'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | 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/compose_shared.dart'; | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
| import 'package:island/widgets/post/publishers_modal.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/widgets/post/compose_settings_sheet.dart'; | ||||||
| import 'package:island/services/compose_storage_db.dart'; | import 'package:island/services/compose_storage_db.dart'; | ||||||
| import 'package:island/widgets/post/draft_manager.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/screens/creators/publishers.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.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/attachment_preview.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/content/markdown.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_item.dart'; | ||||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | import 'package:island/widgets/post/post_quick_reply.dart'; | ||||||
| import 'package:island/widgets/post/post_replies.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:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| 
 | 
 | ||||||
| part 'detail.g.dart'; | part 'post_detail.g.dart'; | ||||||
| 
 | 
 | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnPost?> post(Ref ref, String id) async { | 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); |   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 { | class PostDetailScreen extends HookConsumerWidget { | ||||||
|   final String id; |   final String id; | ||||||
|   const PostDetailScreen({super.key, required this.id}); |   const PostDetailScreen({super.key, required this.id}); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final post = ref.watch(postProvider(id)); |     final postState = ref.watch(postStateProvider(id)); | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
| 
 | 
 | ||||||
|     final isWide = isWideScreen(context); |     final isWide = isWideScreen(context); | ||||||
| 
 | 
 | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar(title: const Text('Post')), |       appBar: AppBar(title: const Text('Post')), | ||||||
|       body: post.when( |       body: postState.when( | ||||||
|         data: (post) { |         data: (post) { | ||||||
|           return Stack( |           return Stack( | ||||||
|             fit: StackFit.expand, |             fit: StackFit.expand, | ||||||
| @@ -49,6 +73,10 @@ class PostDetailScreen extends HookConsumerWidget { | |||||||
|                           isOpenable: false, |                           isOpenable: false, | ||||||
|                           isFullPost: true, |                           isFullPost: true, | ||||||
|                           backgroundColor: isWide ? Colors.transparent : null, |                           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), |                         const Divider(height: 1), | ||||||
|                       ], |                       ], | ||||||
| @@ -65,11 +93,15 @@ class PostDetailScreen extends HookConsumerWidget { | |||||||
|                   right: 0, |                   right: 0, | ||||||
|                   child: Material( |                   child: Material( | ||||||
|                     elevation: 2, |                     elevation: 2, | ||||||
|                     child: PostQuickReply( |                     child: postState.when( | ||||||
|                       parent: post, |                       data: (post) => PostQuickReply( | ||||||
|  |                         parent: post!, | ||||||
|                         onPosted: () { |                         onPosted: () { | ||||||
|                           ref.invalidate(postRepliesNotifierProvider(id)); |                           ref.invalidate(postRepliesNotifierProvider(id)); | ||||||
|                         }, |                         }, | ||||||
|  |                       ), | ||||||
|  |                       loading: () => const SizedBox.shrink(), | ||||||
|  |                       error: (_, __) => const SizedBox.shrink(), | ||||||
|                     ).padding( |                     ).padding( | ||||||
|                       bottom: MediaQuery.of(context).padding.bottom + 16, |                       bottom: MediaQuery.of(context).padding.bottom + 16, | ||||||
|                       top: 16, |                       top: 16, | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'detail.dart'; | part of 'post_detail.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // 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:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| 
 | 
 | ||||||
| part 'detail.g.dart'; | part 'realm_detail.g.dart'; | ||||||
| 
 | 
 | ||||||
| @riverpod | @riverpod | ||||||
| Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'detail.dart'; | part of 'realm_detail.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| @@ -344,7 +344,10 @@ class EditRealmScreen extends HookConsumerWidget { | |||||||
|                 const SizedBox(height: 16), |                 const SizedBox(height: 16), | ||||||
|                 TextFormField( |                 TextFormField( | ||||||
|                   controller: descriptionController, |                   controller: descriptionController, | ||||||
|                   decoration: InputDecoration(labelText: 'description'.tr()), |                   decoration: InputDecoration( | ||||||
|  |                     labelText: 'description'.tr(), | ||||||
|  |                     alignLabelWithHint: true, | ||||||
|  |                   ), | ||||||
|                   minLines: 3, |                   minLines: 3, | ||||||
|                   maxLines: null, |                   maxLines: null, | ||||||
|                   onTapOutside: |                   onTapOutside: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								lib/widgets/article/article_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/widgets/article/article_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  |  | ||||||
| @@ -13,6 +13,7 @@ import 'package:island/pods/config.dart'; | |||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:path/path.dart' show extension; | import 'package:path/path.dart' show extension; | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:photo_view/photo_view.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( |     return DismissiblePage( | ||||||
|       isFullScreen: true, |       isFullScreen: true, | ||||||
|       backgroundColor: Colors.transparent, |       backgroundColor: Colors.transparent, | ||||||
| @@ -288,8 +407,22 @@ class CloudFileZoomIn extends HookConsumerWidget { | |||||||
|             left: 16, |             left: 16, | ||||||
|             right: 16, |             right: 16, | ||||||
|             child: Row( |             child: Row( | ||||||
|               mainAxisAlignment: MainAxisAlignment.end, |  | ||||||
|               children: [ |               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( |                 IconButton( | ||||||
|                   icon: Icon(Icons.remove, color: Colors.white), |                   icon: Icon(Icons.remove, color: Colors.white), | ||||||
|                   onPressed: () { |                   onPressed: () { | ||||||
|   | |||||||
| @@ -74,9 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|               final url = Uri.tryParse(href); |               final url = Uri.tryParse(href); | ||||||
|               if (url != null) { |               if (url != null) { | ||||||
|                 if (url.scheme == 'solian') { |                 if (url.scheme == 'solian') { | ||||||
|                   context.push( |                   context.push(['', url.host, ...url.pathSegments].join('/')); | ||||||
|                     ['', url.host, ...url.pathSegments].join('/'), |  | ||||||
|                   ); |  | ||||||
|                   return; |                   return; | ||||||
|                 } |                 } | ||||||
|                 final whitelistDomains = ['solian.app', 'solsynth.dev']; |                 final whitelistDomains = ['solian.app', 'solsynth.dev']; | ||||||
| @@ -143,7 +141,18 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       generator: MarkdownGenerator( |       generator: MarkdownTextContent.buildGenerator( | ||||||
|  |         isDark: isDark, | ||||||
|  |         linesMargin: linesMargin, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static MarkdownGenerator buildGenerator({ | ||||||
|  |     bool isDark = false, | ||||||
|  |     EdgeInsets? linesMargin, | ||||||
|  |   }) { | ||||||
|  |     return MarkdownGenerator( | ||||||
|       generators: [latexGenerator], |       generators: [latexGenerator], | ||||||
|       inlineSyntaxList: [ |       inlineSyntaxList: [ | ||||||
|         _UserNameCardInlineSyntax(), |         _UserNameCardInlineSyntax(), | ||||||
| @@ -151,7 +160,6 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|         LatexSyntax(isDark), |         LatexSyntax(isDark), | ||||||
|       ], |       ], | ||||||
|       linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4), |       linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4), | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -542,14 +542,14 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | |||||||
| class PostReactionList extends HookConsumerWidget { | class PostReactionList extends HookConsumerWidget { | ||||||
|   final String parentId; |   final String parentId; | ||||||
|   final Map<String, int> reactions; |   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; |   final EdgeInsets? padding; | ||||||
|   const PostReactionList({ |   const PostReactionList({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.parentId, |     required this.parentId, | ||||||
|     required this.reactions, |     required this.reactions, | ||||||
|     this.padding, |     this.padding, | ||||||
|     required this.onReact, |     this.onReact, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -570,7 +570,7 @@ class PostReactionList extends HookConsumerWidget { | |||||||
|           }) |           }) | ||||||
|           .then((resp) { |           .then((resp) { | ||||||
|             var isRemoving = resp.statusCode == 204; |             var isRemoving = resp.statusCode == 204; | ||||||
|             onReact(symbol, attitude, isRemoving ? -1 : 1); |             onReact?.call(symbol, attitude, isRemoving ? -1 : 1); | ||||||
|             HapticFeedback.heavyImpact(); |             HapticFeedback.heavyImpact(); | ||||||
|           }); |           }); | ||||||
|       submitting.value = false; |       submitting.value = false; | ||||||
| @@ -582,6 +582,7 @@ class PostReactionList extends HookConsumerWidget { | |||||||
|         scrollDirection: Axis.horizontal, |         scrollDirection: Axis.horizontal, | ||||||
|         padding: padding ?? EdgeInsets.zero, |         padding: padding ?? EdgeInsets.zero, | ||||||
|         children: [ |         children: [ | ||||||
|  |           if (onReact != null) | ||||||
|             Padding( |             Padding( | ||||||
|               padding: const EdgeInsets.only(right: 8), |               padding: const EdgeInsets.only(right: 8), | ||||||
|               child: ActionChip( |               child: ActionChip( | ||||||
|   | |||||||
| @@ -365,11 +365,6 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|           parentId: item.id, |           parentId: item.id, | ||||||
|           reactions: item.reactionsCount, |           reactions: item.reactionsCount, | ||||||
|           padding: EdgeInsets.zero, |           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), |         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 |     - FlutterMacOS | ||||||
|   - file_selector_macos (0.0.1): |   - file_selector_macos (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - Firebase/CoreOnly (11.13.0): |   - Firebase/CoreOnly (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|   - Firebase/Messaging (11.13.0): |   - Firebase/Messaging (11.15.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 11.13.0) |     - FirebaseMessaging (~> 11.15.0) | ||||||
|   - firebase_core (3.14.0): |   - firebase_core (3.15.0): | ||||||
|     - Firebase/CoreOnly (~> 11.13.0) |     - Firebase/CoreOnly (~> 11.15.0) | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - firebase_messaging (15.2.7): |   - firebase_messaging (15.2.8): | ||||||
|     - Firebase/CoreOnly (~> 11.13.0) |     - Firebase/CoreOnly (~> 11.15.0) | ||||||
|     - Firebase/Messaging (~> 11.13.0) |     - Firebase/Messaging (~> 11.15.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - FirebaseCore (11.13.0): |   - FirebaseCore (11.15.0): | ||||||
|     - FirebaseCoreInternal (~> 11.13.0) |     - FirebaseCoreInternal (~> 11.15.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|     - GoogleUtilities/Logger (~> 8.1) |     - GoogleUtilities/Logger (~> 8.1) | ||||||
|   - FirebaseCoreInternal (11.13.0): |   - FirebaseCoreInternal (11.15.0): | ||||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" |     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||||
|   - FirebaseInstallations (11.13.0): |   - FirebaseInstallations (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.1) |     - GoogleUtilities/Environment (~> 8.1) | ||||||
|     - GoogleUtilities/UserDefaults (~> 8.1) |     - GoogleUtilities/UserDefaults (~> 8.1) | ||||||
|     - PromisesObjC (~> 2.4) |     - PromisesObjC (~> 2.4) | ||||||
|   - FirebaseMessaging (11.13.0): |   - FirebaseMessaging (11.15.0): | ||||||
|     - FirebaseCore (~> 11.13.0) |     - FirebaseCore (~> 11.15.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleDataTransport (~> 10.0) |     - GoogleDataTransport (~> 10.0) | ||||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) |     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||||
| @@ -92,7 +92,7 @@ PODS: | |||||||
|     - GoogleUtilities/Privacy |     - GoogleUtilities/Privacy | ||||||
|   - irondash_engine_context (0.0.1): |   - irondash_engine_context (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - livekit_client (2.4.8): |   - livekit_client (2.4.9): | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|     - WebRTC-SDK (= 125.6422.07) |     - WebRTC-SDK (= 125.6422.07) | ||||||
| @@ -291,13 +291,13 @@ SPEC CHECKSUMS: | |||||||
|   device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 |   device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 | ||||||
|   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a |   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a | ||||||
|   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 |   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 | ||||||
|   Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327 |   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||||
|   firebase_core: 1095fcf33161d99bc34aa10f7c0d89414a208d15 |   firebase_core: 177f51be1650b15d2d5b9f1abf48792619288070 | ||||||
|   firebase_messaging: 6417056ffb85141607618ddfef9fec9f3caab3ea |   firebase_messaging: 8748a5d4bb435993cffa7f5501292f3e914a23d7 | ||||||
|   FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0 |   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||||
|   FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c |   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||||
|   FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02 |   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||||
|   FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4 |   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||||
|   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d |   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d | ||||||
|   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 |   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 | ||||||
|   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 |   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 | ||||||
| @@ -309,7 +309,7 @@ SPEC CHECKSUMS: | |||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba |   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||||
|   livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888 |   livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059 | ||||||
|   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 |   local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 | ||||||
|   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 |   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 | ||||||
|   media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 |   media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 | ||||||
|   | |||||||
							
								
								
									
										140
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: _flutterfire_internals |       name: _flutterfire_internals | ||||||
|       sha256: dda4fd7909a732a014239009aa52537b136f8ce568de23c212587097887e2307 |       sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.56" |     version: "1.3.57" | ||||||
|   analyzer: |   analyzer: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -629,50 +629,50 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: firebase_core |       name: firebase_core | ||||||
|       sha256: "420d9111dcf095341f1ea8fdce926eef750cf7b9745d21f38000de780c94f608" |       sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.14.0" |     version: "3.15.0" | ||||||
|   firebase_core_platform_interface: |   firebase_core_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_core_platform_interface |       name: firebase_core_platform_interface | ||||||
|       sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf |       sha256: "5d2ab45779d91af2aa0252dec9fe4ee1caa015d83377de255454dcaa1526a0e0" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.4.0" |     version: "5.4.1" | ||||||
|   firebase_core_web: |   firebase_core_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_core_web |       name: firebase_core_web | ||||||
|       sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e |       sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.23.0" |     version: "2.24.0" | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: firebase_messaging |       name: firebase_messaging | ||||||
|       sha256: "758461f67b96aa5ad27625aaae39882fd6d1961b1c7e005301f9a74b6336100b" |       sha256: c6711cf2f455532b84a94022c7aaf85088849763af2f01b775ca79d82d10a01a | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "15.2.7" |     version: "15.2.8" | ||||||
|   firebase_messaging_platform_interface: |   firebase_messaging_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_messaging_platform_interface |       name: firebase_messaging_platform_interface | ||||||
|       sha256: "614db1b0df0f53e541e41cc182b6d7ede5763c400f6ba232a5f8d0e1b5e5de32" |       sha256: "1c9dacccb1aee1bf17ba519dda5563a16fdd2ec1e79b5f2e421cb4bf75a166f7" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.6.7" |     version: "4.6.8" | ||||||
|   firebase_messaging_web: |   firebase_messaging_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_messaging_web |       name: firebase_messaging_web | ||||||
|       sha256: b5fbbcdd3e0e7f3fde72b0c119410f22737638fed5fc428b54bba06bc1455d81 |       sha256: "54317c26fa92f0d90a2017977ac791cb0504eca29fcf397f06adf727d4a7a2d5" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.10.7" |     version: "3.10.8" | ||||||
|   fixnum: |   fixnum: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -798,6 +798,54 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.6.0" |     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: |   flutter_launcher_icons: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
| @@ -960,6 +1008,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.1.1" |     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: |   flutter_udid: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -977,10 +1033,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_webrtc |       name: flutter_webrtc | ||||||
|       sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3 |       sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.14.1" |     version: "0.14.2" | ||||||
|   font_awesome_flutter: |   font_awesome_flutter: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1093,6 +1149,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.15.6" |     version: "0.15.6" | ||||||
|  |   html2md: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: html2md | ||||||
|  |       sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.3.2" | ||||||
|   http: |   http: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1297,10 +1361,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: livekit_client |       name: livekit_client | ||||||
|       sha256: c270720a49b935591960c6f3296fd8f00c09b45a70cd64aef78cd0a8f8257913 |       sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.8" |     version: "2.4.9" | ||||||
|   local_auth: |   local_auth: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1677,6 +1741,38 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.8" |     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: |   pool: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1689,10 +1785,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: posix |       name: posix | ||||||
|       sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 |       sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.2" |     version: "6.0.3" | ||||||
|   protobuf: |   protobuf: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1809,10 +1905,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_web |       name: record_web | ||||||
|       sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4" |       sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.8" |     version: "1.1.9" | ||||||
|   record_windows: |   record_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     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 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.0.0+109 | version: 3.0.0+110 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
| @@ -37,7 +37,7 @@ dependencies: | |||||||
|   flutter_hooks: ^0.21.2 |   flutter_hooks: ^0.21.2 | ||||||
|   hooks_riverpod: ^2.6.1 |   hooks_riverpod: ^2.6.1 | ||||||
|   bitsdojo_window: ^0.1.6 |   bitsdojo_window: ^0.1.6 | ||||||
|   go_router: ^15.2.4 |   go_router: ^15.1.3 | ||||||
|   styled_widget: ^0.4.1 |   styled_widget: ^0.4.1 | ||||||
|   shared_preferences: ^2.5.3 |   shared_preferences: ^2.5.3 | ||||||
|   flutter_riverpod: ^2.6.1 |   flutter_riverpod: ^2.6.1 | ||||||
| @@ -127,6 +127,8 @@ dependencies: | |||||||
|       url: https://github.com/lionelmennig/textfield_tags.git |       url: https://github.com/lionelmennig/textfield_tags.git | ||||||
|       ref: fixes/allow-controller-re-registration |       ref: fixes/allow-controller-re-registration | ||||||
|   mime: ^2.0.0 |   mime: ^2.0.0 | ||||||
|  |   html2md: ^1.3.2 | ||||||
|  |   flutter_typeahead: ^5.2.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user