Compare commits
	
		
			2 Commits
		
	
	
		
			666a2dfbf5
			...
			6c7d42c31a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6c7d42c31a | |||
| d6c829c26a | 
							
								
								
									
										63
									
								
								lib/models/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/models/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| 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 WebFeedConfig with _$WebFeedConfig { | ||||
|   const factory WebFeedConfig({@Default(false) bool scrapPage}) = | ||||
|       _WebFeedConfig; | ||||
|  | ||||
|   factory WebFeedConfig.fromJson(Map<String, dynamic> json) => | ||||
|       _$WebFeedConfigFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class WebFeed with _$WebFeed { | ||||
|   const factory WebFeed({ | ||||
|     required String id, | ||||
|     required String url, | ||||
|     required String title, | ||||
|     String? description, | ||||
|     SnScrappedLink? preview, | ||||
|     @Default(WebFeedConfig()) WebFeedConfig config, | ||||
|     required String publisherId, | ||||
|     @Default([]) List<WebArticle> articles, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     DateTime? deletedAt, | ||||
|   }) = _WebFeed; | ||||
|  | ||||
|   factory WebFeed.fromJson(Map<String, dynamic> json) => | ||||
|       _$WebFeedFromJson(json); | ||||
|  | ||||
|   factory WebFeed.fromJsonString(String jsonString) => | ||||
|       WebFeed.fromJson(jsonDecode(jsonString) as Map<String, dynamic>); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class WebArticle with _$WebArticle { | ||||
|   const factory WebArticle({ | ||||
|     required String id, | ||||
|     required String title, | ||||
|     required String url, | ||||
|     String? author, | ||||
|     Map<String, dynamic>? meta, | ||||
|     SnScrappedLink? preview, | ||||
|     String? content, | ||||
|     DateTime? publishedAt, | ||||
|     required String feedId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     DateTime? deletedAt, | ||||
|   }) = _WebArticle; | ||||
|  | ||||
|   factory WebArticle.fromJson(Map<String, dynamic> json) => | ||||
|       _$WebArticleFromJson(json); | ||||
|  | ||||
|   factory WebArticle.fromJsonString(String jsonString) => | ||||
|       WebArticle.fromJson(jsonDecode(jsonString) as Map<String, dynamic>); | ||||
| } | ||||
							
								
								
									
										557
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										557
									
								
								lib/models/webfeed.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,557 @@ | ||||
| // 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 _$WebFeedConfig { | ||||
|  | ||||
|  bool get scrapPage; | ||||
| /// Create a copy of WebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $WebFeedConfigCopyWith<WebFeedConfig> get copyWith => _$WebFeedConfigCopyWithImpl<WebFeedConfig>(this as WebFeedConfig, _$identity); | ||||
|  | ||||
|   /// Serializes this WebFeedConfig to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is WebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,scrapPage); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'WebFeedConfig(scrapPage: $scrapPage)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $WebFeedConfigCopyWith<$Res>  { | ||||
|   factory $WebFeedConfigCopyWith(WebFeedConfig value, $Res Function(WebFeedConfig) _then) = _$WebFeedConfigCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  bool scrapPage | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$WebFeedConfigCopyWithImpl<$Res> | ||||
|     implements $WebFeedConfigCopyWith<$Res> { | ||||
|   _$WebFeedConfigCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final WebFeedConfig _self; | ||||
|   final $Res Function(WebFeedConfig) _then; | ||||
|  | ||||
| /// Create a copy of WebFeedConfig | ||||
| /// 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 _WebFeedConfig implements WebFeedConfig { | ||||
|   const _WebFeedConfig({this.scrapPage = false}); | ||||
|   factory _WebFeedConfig.fromJson(Map<String, dynamic> json) => _$WebFeedConfigFromJson(json); | ||||
|  | ||||
| @override@JsonKey() final  bool scrapPage; | ||||
|  | ||||
| /// Create a copy of WebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$WebFeedConfigCopyWith<_WebFeedConfig> get copyWith => __$WebFeedConfigCopyWithImpl<_WebFeedConfig>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$WebFeedConfigToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,scrapPage); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'WebFeedConfig(scrapPage: $scrapPage)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$WebFeedConfigCopyWith<$Res> implements $WebFeedConfigCopyWith<$Res> { | ||||
|   factory _$WebFeedConfigCopyWith(_WebFeedConfig value, $Res Function(_WebFeedConfig) _then) = __$WebFeedConfigCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  bool scrapPage | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$WebFeedConfigCopyWithImpl<$Res> | ||||
|     implements _$WebFeedConfigCopyWith<$Res> { | ||||
|   __$WebFeedConfigCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _WebFeedConfig _self; | ||||
|   final $Res Function(_WebFeedConfig) _then; | ||||
|  | ||||
| /// Create a copy of WebFeedConfig | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? scrapPage = null,}) { | ||||
|   return _then(_WebFeedConfig( | ||||
| scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable | ||||
| as bool, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$WebFeed { | ||||
|  | ||||
|  String get id; String get url; String get title; String? get description; SnScrappedLink? get preview; WebFeedConfig get config; String get publisherId; List<WebArticle> get articles; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of WebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $WebFeedCopyWith<WebFeed> get copyWith => _$WebFeedCopyWithImpl<WebFeed>(this as WebFeed, _$identity); | ||||
|  | ||||
|   /// Serializes this WebFeed to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is WebFeed&&(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 'WebFeed(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 $WebFeedCopyWith<$Res>  { | ||||
|   factory $WebFeedCopyWith(WebFeed value, $Res Function(WebFeed) _then) = _$WebFeedCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String url, String title, String? description, SnScrappedLink? preview, WebFeedConfig config, String publisherId, List<WebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview;$WebFeedConfigCopyWith<$Res> get config; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$WebFeedCopyWithImpl<$Res> | ||||
|     implements $WebFeedCopyWith<$Res> { | ||||
|   _$WebFeedCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final WebFeed _self; | ||||
|   final $Res Function(WebFeed) _then; | ||||
|  | ||||
| /// Create a copy of WebFeed | ||||
| /// 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 WebFeedConfig,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<WebArticle>,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 WebFeed | ||||
| /// 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 WebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $WebFeedConfigCopyWith<$Res> get config { | ||||
|    | ||||
|   return $WebFeedConfigCopyWith<$Res>(_self.config, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _WebFeed implements WebFeed { | ||||
|   const _WebFeed({required this.id, required this.url, required this.title, this.description, this.preview, this.config = const WebFeedConfig(), required this.publisherId, final  List<WebArticle> articles = const [], required this.createdAt, required this.updatedAt, this.deletedAt}): _articles = articles; | ||||
|   factory _WebFeed.fromJson(Map<String, dynamic> json) => _$WebFeedFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String url; | ||||
| @override final  String title; | ||||
| @override final  String? description; | ||||
| @override final  SnScrappedLink? preview; | ||||
| @override@JsonKey() final  WebFeedConfig config; | ||||
| @override final  String publisherId; | ||||
|  final  List<WebArticle> _articles; | ||||
| @override@JsonKey() List<WebArticle> 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 WebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$WebFeedCopyWith<_WebFeed> get copyWith => __$WebFeedCopyWithImpl<_WebFeed>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$WebFeedToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebFeed&&(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 'WebFeed(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 _$WebFeedCopyWith<$Res> implements $WebFeedCopyWith<$Res> { | ||||
|   factory _$WebFeedCopyWith(_WebFeed value, $Res Function(_WebFeed) _then) = __$WebFeedCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String url, String title, String? description, SnScrappedLink? preview, WebFeedConfig config, String publisherId, List<WebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnScrappedLinkCopyWith<$Res>? get preview;@override $WebFeedConfigCopyWith<$Res> get config; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$WebFeedCopyWithImpl<$Res> | ||||
|     implements _$WebFeedCopyWith<$Res> { | ||||
|   __$WebFeedCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _WebFeed _self; | ||||
|   final $Res Function(_WebFeed) _then; | ||||
|  | ||||
| /// Create a copy of WebFeed | ||||
| /// 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(_WebFeed( | ||||
| 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 WebFeedConfig,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<WebArticle>,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 WebFeed | ||||
| /// 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 WebFeed | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $WebFeedConfigCopyWith<$Res> get config { | ||||
|    | ||||
|   return $WebFeedConfigCopyWith<$Res>(_self.config, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$WebArticle { | ||||
|  | ||||
|  String get id; String get title; String get url; String? get author; Map<String, dynamic>? get meta; SnScrappedLink? get preview; String? get content; DateTime? get publishedAt; String get feedId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of WebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $WebArticleCopyWith<WebArticle> get copyWith => _$WebArticleCopyWithImpl<WebArticle>(this as WebArticle, _$identity); | ||||
|  | ||||
|   /// Serializes this WebArticle to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is WebArticle&&(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.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,content,publishedAt,feedId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'WebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $WebArticleCopyWith<$Res>  { | ||||
|   factory $WebArticleCopyWith(WebArticle value, $Res Function(WebArticle) _then) = _$WebArticleCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnScrappedLinkCopyWith<$Res>? get preview; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$WebArticleCopyWithImpl<$Res> | ||||
|     implements $WebArticleCopyWith<$Res> { | ||||
|   _$WebArticleCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final WebArticle _self; | ||||
|   final $Res Function(WebArticle) _then; | ||||
|  | ||||
| /// Create a copy of WebArticle | ||||
| /// 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? 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?,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 WebArticle | ||||
| /// 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)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _WebArticle implements WebArticle { | ||||
|   const _WebArticle({required this.id, required this.title, required this.url, this.author, final  Map<String, dynamic>? meta, this.preview, this.content, this.publishedAt, required this.feedId, required this.createdAt, required this.updatedAt, this.deletedAt}): _meta = meta; | ||||
|   factory _WebArticle.fromJson(Map<String, dynamic> json) => _$WebArticleFromJson(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  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 WebArticle | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$WebArticleCopyWith<_WebArticle> get copyWith => __$WebArticleCopyWithImpl<_WebArticle>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$WebArticleToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebArticle&&(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.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,content,publishedAt,feedId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'WebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$WebArticleCopyWith<$Res> implements $WebArticleCopyWith<$Res> { | ||||
|   factory _$WebArticleCopyWith(_WebArticle value, $Res Function(_WebArticle) _then) = __$WebArticleCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnScrappedLinkCopyWith<$Res>? get preview; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$WebArticleCopyWithImpl<$Res> | ||||
|     implements _$WebArticleCopyWith<$Res> { | ||||
|   __$WebArticleCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _WebArticle _self; | ||||
|   final $Res Function(_WebArticle) _then; | ||||
|  | ||||
| /// Create a copy of WebArticle | ||||
| /// 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? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_WebArticle( | ||||
| 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?,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 WebArticle | ||||
| /// 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)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										94
									
								
								lib/models/webfeed.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								lib/models/webfeed.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'webfeed.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _WebFeedConfig _$WebFeedConfigFromJson(Map<String, dynamic> json) => | ||||
|     _WebFeedConfig(scrapPage: json['scrap_page'] as bool? ?? false); | ||||
|  | ||||
| Map<String, dynamic> _$WebFeedConfigToJson(_WebFeedConfig instance) => | ||||
|     <String, dynamic>{'scrap_page': instance.scrapPage}; | ||||
|  | ||||
| _WebFeed _$WebFeedFromJson(Map<String, dynamic> json) => _WebFeed( | ||||
|   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 WebFeedConfig() | ||||
|           : WebFeedConfig.fromJson(json['config'] as Map<String, dynamic>), | ||||
|   publisherId: json['publisher_id'] as String, | ||||
|   articles: | ||||
|       (json['articles'] as List<dynamic>?) | ||||
|           ?.map((e) => WebArticle.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> _$WebFeedToJson(_WebFeed 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(), | ||||
| }; | ||||
|  | ||||
| _WebArticle _$WebArticleFromJson(Map<String, dynamic> json) => _WebArticle( | ||||
|   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>), | ||||
|   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> _$WebArticleToJson(_WebArticle instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'title': instance.title, | ||||
|       'url': instance.url, | ||||
|       'author': instance.author, | ||||
|       'meta': instance.meta, | ||||
|       'preview': instance.preview?.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(), | ||||
|     }; | ||||
							
								
								
									
										114
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/pods/webfeed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
|  | ||||
| final webFeedListProvider = FutureProvider.family<List<WebFeed>, String>(( | ||||
|   ref, | ||||
|   pubName, | ||||
| ) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final response = await client.get('/publishers/$pubName/feeds'); | ||||
|   return (response.data as List).map((json) => WebFeed.fromJson(json)).toList(); | ||||
| }); | ||||
|  | ||||
| class WebFeedNotifier | ||||
|     extends | ||||
|         AutoDisposeFamilyAsyncNotifier< | ||||
|           WebFeed, | ||||
|           ({String pubName, String? feedId}) | ||||
|         > { | ||||
|   @override | ||||
|   FutureOr<WebFeed> build(({String pubName, String? feedId}) arg) async { | ||||
|     if (arg.feedId == null || arg.feedId!.isEmpty) { | ||||
|       return WebFeed( | ||||
|         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 WebFeed.fromJson(response.data); | ||||
|     } catch (e) { | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> saveFeed(WebFeed 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(WebFeed.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( | ||||
|         WebFeed( | ||||
|           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'); | ||||
|  | ||||
|       // Reload the feed | ||||
|       final response = await client.get( | ||||
|         '/publishers/${arg.pubName}/feeds/$feedId', | ||||
|       ); | ||||
|       state = AsyncValue.data(WebFeed.fromJson(response.data)); | ||||
|     } catch (error, stackTrace) { | ||||
|       state = AsyncValue.error(error, stackTrace); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| final webFeedNotifierProvider = AsyncNotifierProvider.autoDispose | ||||
|     .family<WebFeedNotifier, WebFeed, ({String pubName, String? feedId})>( | ||||
|       WebFeedNotifier.new, | ||||
|     ); | ||||
| @@ -22,18 +22,20 @@ import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/screens/chat/room_detail.dart'; | ||||
| import 'package:island/screens/chat/call.dart'; | ||||
| import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/list.dart'; | ||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/screens/posts/pub_profile.dart'; | ||||
| import 'package:island/screens/auth/login.dart'; | ||||
| import 'package:island/screens/auth/create_account.dart'; | ||||
| import 'package:island/screens/settings.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/screens/realm/detail.dart'; | ||||
| import 'package:island/screens/realm/realm_detail.dart'; | ||||
| import 'package:island/screens/account/event_calendar.dart'; | ||||
| import 'package:island/screens/discovery/realms.dart'; | ||||
|  | ||||
| @@ -91,6 +93,33 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                 path: '/creators', | ||||
|                 builder: (context, state) => const CreatorHubScreen(), | ||||
|               ), | ||||
|               // Web Feed Routes | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/feeds', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return WebFeedListScreen(pubName: name); | ||||
|                 }, | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'new', | ||||
|                     builder: (context, state) { | ||||
|                       return WebFeedNewScreen( | ||||
|                         pubName: state.pathParameters['name']!, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':feedId', | ||||
|                     builder: (context, state) { | ||||
|                       return WebFeedEditScreen( | ||||
|                         pubName: state.pathParameters['name']!, | ||||
|                         feedId: state.pathParameters['feedId'], | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/creators/:name/posts', | ||||
|                 builder: (context, state) { | ||||
| @@ -167,22 +196,25 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps', | ||||
|                 builder: (context, state) => CustomAppsScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => CustomAppsScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                     ), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps/new', | ||||
|                 builder: (context, state) => NewCustomAppScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => NewCustomAppScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                     ), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: '/developers/:name/apps/:id', | ||||
|                 builder: (context, state) => EditAppScreen( | ||||
|                   publisherName: state.pathParameters['name']!, | ||||
|                   id: state.pathParameters['id']!, | ||||
|                 ), | ||||
|                 builder: | ||||
|                     (context, state) => EditAppScreen( | ||||
|                       publisherName: state.pathParameters['name']!, | ||||
|                       id: state.pathParameters['id']!, | ||||
|                     ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|   | ||||
| @@ -341,7 +341,10 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                   ), | ||||
|  | ||||
|                   TextFormField( | ||||
|                     decoration: InputDecoration(labelText: 'bio'.tr()), | ||||
|                     decoration: InputDecoration( | ||||
|                       labelText: 'bio'.tr(), | ||||
|                       alignLabelWithHint: true, | ||||
|                     ), | ||||
|                     maxLines: null, | ||||
|                     minLines: 3, | ||||
|                     controller: bioController, | ||||
|   | ||||
| @@ -669,7 +669,10 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|                 const SizedBox(height: 16), | ||||
|                 TextFormField( | ||||
|                   controller: descriptionController, | ||||
|                   decoration: const InputDecoration(labelText: 'Description'), | ||||
|                   decoration: const InputDecoration( | ||||
|                     labelText: 'Description', | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|                   onTapOutside: | ||||
|   | ||||
| @@ -370,9 +370,9 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|                             title: Text('publisherMembers').tr(), | ||||
|                             trailing: Icon(Symbols.chevron_right), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             leading: const Icon(Symbols.group), | ||||
|                             contentPadding: EdgeInsets.symmetric( | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
| @@ -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( | ||||
|                             title: Text('publisherFeatures').tr(), | ||||
|                             leading: const Icon(Symbols.flag), | ||||
|   | ||||
| @@ -270,7 +270,10 @@ class EditPublisherScreen extends HookConsumerWidget { | ||||
|                     ), | ||||
|                     TextFormField( | ||||
|                       controller: bioController, | ||||
|                       decoration: InputDecoration(labelText: 'bio'.tr()), | ||||
|                       decoration: InputDecoration( | ||||
|                         labelText: 'bio'.tr(), | ||||
|                         alignLabelWithHint: true, | ||||
|                       ), | ||||
|                       minLines: 3, | ||||
|                       maxLines: null, | ||||
|                       onTapOutside: | ||||
|   | ||||
| @@ -71,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget { | ||||
|                 subtitle: Text(sticker.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   context.push( | ||||
|                     '/creators/$pubName/stickers/${sticker.id}', | ||||
|                   ); | ||||
|                   context.push('/creators/$pubName/stickers/${sticker.id}'); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
| @@ -230,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'description'.tr(), | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|   | ||||
							
								
								
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								lib/screens/creators/webfeed/webfeed_edit.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/webfeed.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class WebFeedNewScreen extends StatelessWidget { | ||||
|   final String pubName; | ||||
|   const WebFeedNewScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return WebFeedEditScreen(pubName: pubName, feedId: null); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class WebFeedEditScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   final String? feedId; | ||||
|  | ||||
|   const WebFeedEditScreen({super.key, required this.pubName, this.feedId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final formKey = useMemoized(() => GlobalKey<FormState>()); | ||||
|     final titleController = useTextEditingController(); | ||||
|     final urlController = useTextEditingController(); | ||||
|     final descriptionController = useTextEditingController(); | ||||
|     final isLoading = useState(false); | ||||
|     final isScrapEnabled = useState(false); | ||||
|  | ||||
|     final saveFeed = useCallback(() async { | ||||
|       if (!formKey.currentState!.validate()) return; | ||||
|  | ||||
|       isLoading.value = true; | ||||
|  | ||||
|       try { | ||||
|         final feed = WebFeed( | ||||
|           id: feedId ?? '', | ||||
|           title: titleController.text, | ||||
|           url: urlController.text, | ||||
|           description: descriptionController.text, | ||||
|           config: WebFeedConfig(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, | ||||
|                             decoration: InputDecoration( | ||||
|                               labelText: 'description'.tr(), | ||||
|                               alignLabelWithHint: true, | ||||
|                             ), | ||||
|                             maxLines: 3, | ||||
|                             onTapOutside: | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/post/draft_manager.dart'; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/widgets/content/attachment_preview.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/markdown.dart'; | ||||
|   | ||||
| @@ -9,10 +9,11 @@ import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| import 'package:island/widgets/post/post_replies.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| 
 | ||||
| part 'detail.g.dart'; | ||||
| part 'post_detail.g.dart'; | ||||
| 
 | ||||
| @riverpod | ||||
| Future<SnPost?> post(Ref ref, String id) async { | ||||
| @@ -21,20 +22,43 @@ Future<SnPost?> post(Ref ref, String id) async { | ||||
|   return SnPost.fromJson(resp.data); | ||||
| } | ||||
| 
 | ||||
| final postStateProvider = StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>( | ||||
|   (ref, id) => PostState(ref, id), | ||||
| ); | ||||
| 
 | ||||
| class PostState extends StateNotifier<AsyncValue<SnPost?>> { | ||||
|   final Ref _ref; | ||||
|   final String _id; | ||||
| 
 | ||||
|   PostState(this._ref, this._id) : super(const AsyncValue.loading()) { | ||||
|     // Initialize with the initial post data | ||||
|     _ref.listen<AsyncValue<SnPost?>>( | ||||
|       postProvider(_id), | ||||
|       (_, next) => state = next, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   void updatePost(SnPost? newPost) { | ||||
|     if (newPost != null) { | ||||
|       state = AsyncData(newPost); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class PostDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostDetailScreen({super.key, required this.id}); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final post = ref.watch(postProvider(id)); | ||||
|     final postState = ref.watch(postStateProvider(id)); | ||||
|     final user = ref.watch(userInfoProvider); | ||||
| 
 | ||||
|     final isWide = isWideScreen(context); | ||||
| 
 | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: const Text('Post')), | ||||
|       body: post.when( | ||||
|       body: postState.when( | ||||
|         data: (post) { | ||||
|           return Stack( | ||||
|             fit: StackFit.expand, | ||||
| @@ -49,6 +73,10 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                           isOpenable: false, | ||||
|                           isFullPost: true, | ||||
|                           backgroundColor: isWide ? Colors.transparent : null, | ||||
|                           onUpdate: (newItem) { | ||||
|                             // Update the local state with the new post data | ||||
|                             ref.read(postStateProvider(id).notifier).updatePost(newItem); | ||||
|                           }, | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                       ], | ||||
| @@ -65,11 +93,15 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                   right: 0, | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     child: PostQuickReply( | ||||
|                       parent: post, | ||||
|                       onPosted: () { | ||||
|                         ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                       }, | ||||
|                     child: postState.when( | ||||
|                       data: (post) => PostQuickReply( | ||||
|                         parent: post!, | ||||
|                         onPosted: () { | ||||
|                           ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                         }, | ||||
|                       ), | ||||
|                       loading: () => const SizedBox.shrink(), | ||||
|                       error: (_, __) => const SizedBox.shrink(), | ||||
|                     ).padding( | ||||
|                       bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                       top: 16, | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'detail.dart'; | ||||
| part of 'post_detail.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| @@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| 
 | ||||
| part 'detail.g.dart'; | ||||
| part 'realm_detail.g.dart'; | ||||
| 
 | ||||
| @riverpod | ||||
| Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'detail.dart'; | ||||
| part of 'realm_detail.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| @@ -344,7 +344,10 @@ class EditRealmScreen extends HookConsumerWidget { | ||||
|                 const SizedBox(height: 16), | ||||
|                 TextFormField( | ||||
|                   controller: descriptionController, | ||||
|                   decoration: InputDecoration(labelText: 'description'.tr()), | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'description'.tr(), | ||||
|                     alignLabelWithHint: true, | ||||
|                   ), | ||||
|                   minLines: 3, | ||||
|                   maxLines: null, | ||||
|                   onTapOutside: | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| @@ -13,6 +14,7 @@ import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:path/path.dart' show extension; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:photo_view/photo_view.dart'; | ||||
| @@ -210,6 +212,35 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void showInfoSheet() { | ||||
|       final exifData = item.fileMeta?['exif'] ?? {}; | ||||
|       showModalBottomSheet( | ||||
|         useRootNavigator: true, | ||||
|         context: context, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'File Info', | ||||
|               child: SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     Text('Name: ${item.name}'), | ||||
|                     Text('Size: ${item.size} bytes'), | ||||
|                     Text('Type: ${item.mimeType ?? 'Unknown'}'), | ||||
|                     if (exifData.isNotEmpty) ...[ | ||||
|                       const Gap(16), | ||||
|                       Text('EXIF Data:'), | ||||
|                       const Gap(8), | ||||
|                       for (var entry in exifData.entries) | ||||
|                         Text('${entry.key}: ${entry.value}'), | ||||
|                     ], | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return DismissiblePage( | ||||
|       isFullScreen: true, | ||||
|       backgroundColor: Colors.transparent, | ||||
| @@ -288,8 +319,22 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             child: Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               children: [ | ||||
|                 IconButton( | ||||
|                   icon: Icon( | ||||
|                     Icons.info_outline, | ||||
|                     color: Colors.white, | ||||
|                     shadows: [ | ||||
|                       Shadow( | ||||
|                         color: Colors.black54, | ||||
|                         blurRadius: 5.0, | ||||
|                         offset: Offset(1.0, 1.0), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   onPressed: showInfoSheet, | ||||
|                 ), | ||||
|                 Spacer(), | ||||
|                 IconButton( | ||||
|                   icon: Icon(Icons.remove, color: Colors.white), | ||||
|                   onPressed: () { | ||||
|   | ||||
							
								
								
									
										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!, | ||||
|             ], | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -542,14 +542,14 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | ||||
| class PostReactionList extends HookConsumerWidget { | ||||
|   final String parentId; | ||||
|   final Map<String, int> reactions; | ||||
|   final Function(String symbol, int attitude, int delta) onReact; | ||||
|   final Function(String symbol, int attitude, int delta)? onReact; | ||||
|   final EdgeInsets? padding; | ||||
|   const PostReactionList({ | ||||
|     super.key, | ||||
|     required this.parentId, | ||||
|     required this.reactions, | ||||
|     this.padding, | ||||
|     required this.onReact, | ||||
|     this.onReact, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -570,7 +570,7 @@ class PostReactionList extends HookConsumerWidget { | ||||
|           }) | ||||
|           .then((resp) { | ||||
|             var isRemoving = resp.statusCode == 204; | ||||
|             onReact(symbol, attitude, isRemoving ? -1 : 1); | ||||
|             onReact?.call(symbol, attitude, isRemoving ? -1 : 1); | ||||
|             HapticFeedback.heavyImpact(); | ||||
|           }); | ||||
|       submitting.value = false; | ||||
| @@ -582,33 +582,34 @@ class PostReactionList extends HookConsumerWidget { | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         padding: padding ?? EdgeInsets.zero, | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(right: 8), | ||||
|             child: ActionChip( | ||||
|               avatar: Icon(Symbols.add_reaction), | ||||
|               label: Text('react').tr(), | ||||
|               visualDensity: const VisualDensity( | ||||
|                 horizontal: VisualDensity.minimumDensity, | ||||
|                 vertical: VisualDensity.minimumDensity, | ||||
|           if (onReact != null) | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(right: 8), | ||||
|               child: ActionChip( | ||||
|                 avatar: Icon(Symbols.add_reaction), | ||||
|                 label: Text('react').tr(), | ||||
|                 visualDensity: const VisualDensity( | ||||
|                   horizontal: VisualDensity.minimumDensity, | ||||
|                   vertical: VisualDensity.minimumDensity, | ||||
|                 ), | ||||
|                 onPressed: | ||||
|                     submitting.value | ||||
|                         ? null | ||||
|                         : () { | ||||
|                           showModalBottomSheet( | ||||
|                             context: context, | ||||
|                             builder: (BuildContext context) { | ||||
|                               return _PostReactionSheet( | ||||
|                                 reactionsCount: reactions, | ||||
|                                 onReact: (symbol, attitude) { | ||||
|                                   reactPost(symbol, attitude); | ||||
|                                 }, | ||||
|                               ); | ||||
|                             }, | ||||
|                           ); | ||||
|                         }, | ||||
|               ), | ||||
|               onPressed: | ||||
|                   submitting.value | ||||
|                       ? null | ||||
|                       : () { | ||||
|                         showModalBottomSheet( | ||||
|                           context: context, | ||||
|                           builder: (BuildContext context) { | ||||
|                             return _PostReactionSheet( | ||||
|                               reactionsCount: reactions, | ||||
|                               onReact: (symbol, attitude) { | ||||
|                                 reactPost(symbol, attitude); | ||||
|                               }, | ||||
|                             ); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
|             ), | ||||
|           ), | ||||
|           for (final symbol in reactions.keys) | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(right: 8), | ||||
|   | ||||
| @@ -365,11 +365,6 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|           parentId: item.id, | ||||
|           reactions: item.reactionsCount, | ||||
|           padding: EdgeInsets.zero, | ||||
|           onReact: (symbol, attitude, delta) { | ||||
|             final reactionsCount = Map<String, int>.from(item.reactionsCount); | ||||
|             reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta; | ||||
|             onUpdate?.call(item.copyWith(reactionsCount: reactionsCount)); | ||||
|           }, | ||||
|         ), | ||||
|         const Gap(16), | ||||
|       ], | ||||
|   | ||||
		Reference in New Issue
	
	Block a user