✨ Web feed
This commit is contained in:
parent
666a2dfbf5
commit
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,10 +22,12 @@ 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/detail.dart';
|
||||||
import 'package:island/screens/posts/pub_profile.dart';
|
import 'package:island/screens/posts/pub_profile.dart';
|
||||||
@ -91,6 +93,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,22 +196,25 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/developers/:name/apps',
|
path: '/developers/:name/apps',
|
||||||
builder: (context, state) => CustomAppsScreen(
|
builder:
|
||||||
publisherName: state.pathParameters['name']!,
|
(context, state) => CustomAppsScreen(
|
||||||
),
|
publisherName: state.pathParameters['name']!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/developers/:name/apps/new',
|
path: '/developers/:name/apps/new',
|
||||||
builder: (context, state) => NewCustomAppScreen(
|
builder:
|
||||||
publisherName: state.pathParameters['name']!,
|
(context, state) => NewCustomAppScreen(
|
||||||
),
|
publisherName: state.pathParameters['name']!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/developers/:name/apps/:id',
|
path: '/developers/:name/apps/:id',
|
||||||
builder: (context, state) => EditAppScreen(
|
builder:
|
||||||
publisherName: state.pathParameters['name']!,
|
(context, state) => EditAppScreen(
|
||||||
id: state.pathParameters['id']!,
|
publisherName: state.pathParameters['name']!,
|
||||||
),
|
id: state.pathParameters['id']!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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 = 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,
|
controller: descriptionController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'description'.tr(),
|
labelText: 'description'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
|
@ -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:
|
||||||
|
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 {
|
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,33 +582,34 @@ class PostReactionList extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: padding ?? EdgeInsets.zero,
|
padding: padding ?? EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
if (onReact != null)
|
||||||
padding: const EdgeInsets.only(right: 8),
|
Padding(
|
||||||
child: ActionChip(
|
padding: const EdgeInsets.only(right: 8),
|
||||||
avatar: Icon(Symbols.add_reaction),
|
child: ActionChip(
|
||||||
label: Text('react').tr(),
|
avatar: Icon(Symbols.add_reaction),
|
||||||
visualDensity: const VisualDensity(
|
label: Text('react').tr(),
|
||||||
horizontal: VisualDensity.minimumDensity,
|
visualDensity: const VisualDensity(
|
||||||
vertical: VisualDensity.minimumDensity,
|
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)
|
for (final symbol in reactions.keys)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user