👽 Support new mixed feed

This commit is contained in:
LittleSheep 2025-04-06 01:20:55 +08:00
parent 151f917b07
commit 48f40099f4
13 changed files with 194 additions and 1179 deletions

View File

@ -11,8 +11,5 @@ post {
}
body:json {
{
"sources": ["taiwan-pts"],
"eager": true
}
{}
}

View File

@ -17,10 +17,9 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/feed/feed_news.dart';
import 'package:surface/widgets/feed/feed_reader.dart';
import 'package:surface/widgets/feed/feed_unknown.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/fediverse_post_item.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -549,12 +548,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
refreshPosts();
},
);
case 'fediverse.post':
return FediversePostWidget(
data: SnFediversePost.fromJson(ele.data),
maxWidth: 640,
);
case 'reader.news':
case 'reader.feed':
return Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),

View File

@ -24,13 +24,13 @@ class NewsDetailScreen extends StatefulWidget {
}
class _NewsDetailScreenState extends State<NewsDetailScreen> {
SnNewsArticle? _article;
SnSubscriptionItem? _article;
Future<void> _fetchArticle() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
_article = SnNewsArticle.fromJson(resp.data);
_article = SnSubscriptionItem.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err).then((_) {

View File

@ -66,7 +66,8 @@ class _NewsScreenState extends State<NewsScreen> {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
leading: AutoAppBarLeading(),
title: Text('screenNews').tr(),
@ -75,10 +76,13 @@ class _NewsScreenState extends State<NewsScreen> {
bottom: TabBar(
isScrollable: true,
tabs: [
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
Tab(
child: Text('newsAllSources'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor)),
for (final source in _sources!)
Tab(
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
child: Text(source.label).textColor(
Theme.of(context).appBarTheme.foregroundColor),
),
],
),
@ -116,7 +120,7 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
bool _isBusy = false;
int? _totalCount;
final List<SnNewsArticle> _articles = List.empty(growable: true);
final List<SnSubscriptionItem> _articles = List.empty(growable: true);
Future<void> _fetchArticles() async {
setState(() => _isBusy = true);
@ -129,8 +133,8 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
if (widget.source != null) 'source': widget.source,
});
_totalCount = resp.data['count'];
_articles.addAll(List<SnNewsArticle>.from(
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
_articles.addAll(List<SnSubscriptionItem>.from(
resp.data['data']?.map((e) => SnSubscriptionItem.fromJson(e)) ?? [],
));
} catch (err) {
if (!mounted) return;
@ -159,7 +163,8 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
child: InfiniteList(
isLoading: _isBusy,
itemCount: _articles.length,
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
hasReachedMax:
_totalCount != null && _articles.length >= _totalCount!,
onFetchData: () {
_fetchArticles();
},
@ -184,7 +189,8 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
if (article.thumbnail.isNotEmpty &&
!article.thumbnail.endsWith('.svg'))
ClipRRect(
borderRadius: BorderRadius.only(
topRight: Radius.circular(8),
@ -193,7 +199,9 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
color: Theme.of(context)
.colorScheme
.surfaceContainer,
child: AutoResizeUniversalImage(
article.thumbnail.startsWith('http')
? article.thumbnail
@ -203,25 +211,38 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
),
),
const Gap(16),
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
Text(article.title)
.textStyle(Theme.of(context).textTheme.titleLarge!)
.padding(horizontal: 16),
const Gap(8),
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
Text(htmlDescription.children
.map((ele) => ele.text.trim())
.join())
.textStyle(Theme.of(context).textTheme.bodyMedium!)
.padding(horizontal: 16),
const Gap(8),
Row(
spacing: 2,
children: [
Text(widget.allSources.where((x) => x.id == article.source).first.label)
.textStyle(Theme.of(context).textTheme.bodySmall!),
Text(widget.allSources
.where((x) => x.id == article.feedId)
.first
.label)
.textStyle(
Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
Row(
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(DateFormat().format(date)).textStyle(
Theme.of(context).textTheme.bodySmall!),
Text(' · ')
.textStyle(
Theme.of(context).textTheme.bodySmall!)
.bold(),
Text(RelativeTime(context).format(date)).textStyle(
Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),

View File

@ -14,25 +14,27 @@ abstract class SnNewsSource with _$SnNewsSource {
required bool enabled,
}) = _SnNewsSource;
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
factory SnNewsSource.fromJson(Map<String, dynamic> json) =>
_$SnNewsSourceFromJson(json);
}
@freezed
abstract class SnNewsArticle with _$SnNewsArticle {
const factory SnNewsArticle({
abstract class SnSubscriptionItem with _$SnSubscriptionItem {
const factory SnSubscriptionItem({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required dynamic deletedAt,
required DateTime? deletedAt,
required String thumbnail,
required String title,
required String description,
required String content,
required String url,
required String hash,
required String source,
required int feedId,
required DateTime? publishedAt,
}) = _SnNewsArticle;
}) = _SnSubscriptionItem;
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
factory SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionItemFromJson(json);
}

View File

@ -252,42 +252,43 @@ class __$SnNewsSourceCopyWithImpl<$Res>
}
/// @nodoc
mixin _$SnNewsArticle {
mixin _$SnSubscriptionItem {
int get id;
DateTime get createdAt;
DateTime get updatedAt;
dynamic get deletedAt;
DateTime? get deletedAt;
String get thumbnail;
String get title;
String get description;
String get content;
String get url;
String get hash;
String get source;
int get feedId;
DateTime? get publishedAt;
/// Create a copy of SnNewsArticle
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
_$SnNewsArticleCopyWithImpl<SnNewsArticle>(
this as SnNewsArticle, _$identity);
$SnSubscriptionItemCopyWith<SnSubscriptionItem> get copyWith =>
_$SnSubscriptionItemCopyWithImpl<SnSubscriptionItem>(
this as SnSubscriptionItem, _$identity);
/// Serializes this SnNewsArticle to a JSON map.
/// Serializes this SnSubscriptionItem to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnNewsArticle &&
other is SnSubscriptionItem &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) &&
@ -296,7 +297,7 @@ mixin _$SnNewsArticle {
(identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.feedId, feedId) || other.feedId == feedId) &&
(identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt));
}
@ -308,52 +309,52 @@ mixin _$SnNewsArticle {
id,
createdAt,
updatedAt,
const DeepCollectionEquality().hash(deletedAt),
deletedAt,
thumbnail,
title,
description,
content,
url,
hash,
source,
feedId,
publishedAt);
@override
String toString() {
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, publishedAt: $publishedAt)';
}
}
/// @nodoc
abstract mixin class $SnNewsArticleCopyWith<$Res> {
factory $SnNewsArticleCopyWith(
SnNewsArticle value, $Res Function(SnNewsArticle) _then) =
_$SnNewsArticleCopyWithImpl;
abstract mixin class $SnSubscriptionItemCopyWith<$Res> {
factory $SnSubscriptionItemCopyWith(
SnSubscriptionItem value, $Res Function(SnSubscriptionItem) _then) =
_$SnSubscriptionItemCopyWithImpl;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
DateTime? deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
int feedId,
DateTime? publishedAt});
}
/// @nodoc
class _$SnNewsArticleCopyWithImpl<$Res>
implements $SnNewsArticleCopyWith<$Res> {
_$SnNewsArticleCopyWithImpl(this._self, this._then);
class _$SnSubscriptionItemCopyWithImpl<$Res>
implements $SnSubscriptionItemCopyWith<$Res> {
_$SnSubscriptionItemCopyWithImpl(this._self, this._then);
final SnNewsArticle _self;
final $Res Function(SnNewsArticle) _then;
final SnSubscriptionItem _self;
final $Res Function(SnSubscriptionItem) _then;
/// Create a copy of SnNewsArticle
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
@ -368,7 +369,7 @@ class _$SnNewsArticleCopyWithImpl<$Res>
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? feedId = null,
Object? publishedAt = freezed,
}) {
return _then(_self.copyWith(
@ -387,7 +388,7 @@ class _$SnNewsArticleCopyWithImpl<$Res>
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
as DateTime?,
thumbnail: null == thumbnail
? _self.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
@ -412,10 +413,10 @@ class _$SnNewsArticleCopyWithImpl<$Res>
? _self.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _self.source
: source // ignore: cast_nullable_to_non_nullable
as String,
feedId: null == feedId
? _self.feedId
: feedId // ignore: cast_nullable_to_non_nullable
as int,
publishedAt: freezed == publishedAt
? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable
@ -426,8 +427,8 @@ class _$SnNewsArticleCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _SnNewsArticle implements SnNewsArticle {
const _SnNewsArticle(
class _SnSubscriptionItem implements SnSubscriptionItem {
const _SnSubscriptionItem(
{required this.id,
required this.createdAt,
required this.updatedAt,
@ -438,10 +439,10 @@ class _SnNewsArticle implements SnNewsArticle {
required this.content,
required this.url,
required this.hash,
required this.source,
required this.feedId,
required this.publishedAt});
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =>
_$SnNewsArticleFromJson(json);
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionItemFromJson(json);
@override
final int id;
@ -450,7 +451,7 @@ class _SnNewsArticle implements SnNewsArticle {
@override
final DateTime updatedAt;
@override
final dynamic deletedAt;
final DateTime? deletedAt;
@override
final String thumbnail;
@override
@ -464,21 +465,21 @@ class _SnNewsArticle implements SnNewsArticle {
@override
final String hash;
@override
final String source;
final int feedId;
@override
final DateTime? publishedAt;
/// Create a copy of SnNewsArticle
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnNewsArticleCopyWith<_SnNewsArticle> get copyWith =>
__$SnNewsArticleCopyWithImpl<_SnNewsArticle>(this, _$identity);
_$SnSubscriptionItemCopyWith<_SnSubscriptionItem> get copyWith =>
__$SnSubscriptionItemCopyWithImpl<_SnSubscriptionItem>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnNewsArticleToJson(
return _$SnSubscriptionItemToJson(
this,
);
}
@ -487,13 +488,14 @@ class _SnNewsArticle implements SnNewsArticle {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnNewsArticle &&
other is _SnSubscriptionItem &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) &&
@ -502,7 +504,7 @@ class _SnNewsArticle implements SnNewsArticle {
(identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.feedId, feedId) || other.feedId == feedId) &&
(identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt));
}
@ -514,54 +516,54 @@ class _SnNewsArticle implements SnNewsArticle {
id,
createdAt,
updatedAt,
const DeepCollectionEquality().hash(deletedAt),
deletedAt,
thumbnail,
title,
description,
content,
url,
hash,
source,
feedId,
publishedAt);
@override
String toString() {
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, publishedAt: $publishedAt)';
}
}
/// @nodoc
abstract mixin class _$SnNewsArticleCopyWith<$Res>
implements $SnNewsArticleCopyWith<$Res> {
factory _$SnNewsArticleCopyWith(
_SnNewsArticle value, $Res Function(_SnNewsArticle) _then) =
__$SnNewsArticleCopyWithImpl;
abstract mixin class _$SnSubscriptionItemCopyWith<$Res>
implements $SnSubscriptionItemCopyWith<$Res> {
factory _$SnSubscriptionItemCopyWith(
_SnSubscriptionItem value, $Res Function(_SnSubscriptionItem) _then) =
__$SnSubscriptionItemCopyWithImpl;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
DateTime? deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
int feedId,
DateTime? publishedAt});
}
/// @nodoc
class __$SnNewsArticleCopyWithImpl<$Res>
implements _$SnNewsArticleCopyWith<$Res> {
__$SnNewsArticleCopyWithImpl(this._self, this._then);
class __$SnSubscriptionItemCopyWithImpl<$Res>
implements _$SnSubscriptionItemCopyWith<$Res> {
__$SnSubscriptionItemCopyWithImpl(this._self, this._then);
final _SnNewsArticle _self;
final $Res Function(_SnNewsArticle) _then;
final _SnSubscriptionItem _self;
final $Res Function(_SnSubscriptionItem) _then;
/// Create a copy of SnNewsArticle
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@ -576,10 +578,10 @@ class __$SnNewsArticleCopyWithImpl<$Res>
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? feedId = null,
Object? publishedAt = freezed,
}) {
return _then(_SnNewsArticle(
return _then(_SnSubscriptionItem(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
@ -595,7 +597,7 @@ class __$SnNewsArticleCopyWithImpl<$Res>
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
as DateTime?,
thumbnail: null == thumbnail
? _self.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
@ -620,10 +622,10 @@ class __$SnNewsArticleCopyWithImpl<$Res>
? _self.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _self.source
: source // ignore: cast_nullable_to_non_nullable
as String,
feedId: null == feedId
? _self.feedId
: feedId // ignore: cast_nullable_to_non_nullable
as int,
publishedAt: freezed == publishedAt
? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable

View File

@ -26,36 +26,38 @@ Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) =>
'enabled': instance.enabled,
};
_SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) =>
_SnNewsArticle(
_SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) =>
_SnSubscriptionItem(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'],
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
thumbnail: json['thumbnail'] as String,
title: json['title'] as String,
description: json['description'] as String,
content: json['content'] as String,
url: json['url'] as String,
hash: json['hash'] as String,
source: json['source'] as String,
feedId: (json['feed_id'] as num).toInt(),
publishedAt: json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
);
Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt,
'deleted_at': instance.deletedAt?.toIso8601String(),
'thumbnail': instance.thumbnail,
'title': instance.title,
'description': instance.description,
'content': instance.content,
'url': instance.url,
'hash': instance.hash,
'source': instance.source,
'feed_id': instance.feedId,
'published_at': instance.publishedAt?.toIso8601String(),
};

View File

@ -181,41 +181,3 @@ abstract class SnFeedEntry with _$SnFeedEntry {
factory SnFeedEntry.fromJson(Map<String, dynamic> json) =>
_$SnFeedEntryFromJson(json);
}
@freezed
abstract class SnFediversePost with _$SnFediversePost {
const factory SnFediversePost({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String identifier,
required String origin,
required String content,
required String language,
required List<String> images,
required SnFediverseUser user,
required int userId,
}) = _SnFediversePost;
factory SnFediversePost.fromJson(Map<String, Object?> json) =>
_$SnFediversePostFromJson(json);
}
@freezed
abstract class SnFediverseUser with _$SnFediverseUser {
const factory SnFediverseUser({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String identifier,
required String origin,
required String avatar,
required String name,
required String nick,
}) = _SnFediverseUser;
factory SnFediverseUser.fromJson(Map<String, dynamic> json) =>
_$SnFediverseUserFromJson(json);
}

View File

@ -3400,698 +3400,4 @@ class __$SnFeedEntryCopyWithImpl<$Res> implements _$SnFeedEntryCopyWith<$Res> {
}
}
/// @nodoc
mixin _$SnFediversePost {
int get id;
DateTime get createdAt;
DateTime get updatedAt;
DateTime? get deletedAt;
String get identifier;
String get origin;
String get content;
String get language;
List<String> get images;
SnFediverseUser get user;
int get userId;
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnFediversePostCopyWith<SnFediversePost> get copyWith =>
_$SnFediversePostCopyWithImpl<SnFediversePost>(
this as SnFediversePost, _$identity);
/// Serializes this SnFediversePost to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnFediversePost &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.identifier, identifier) ||
other.identifier == identifier) &&
(identical(other.origin, origin) || other.origin == origin) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.language, language) ||
other.language == language) &&
const DeepCollectionEquality().equals(other.images, images) &&
(identical(other.user, user) || other.user == user) &&
(identical(other.userId, userId) || other.userId == userId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
identifier,
origin,
content,
language,
const DeepCollectionEquality().hash(images),
user,
userId);
@override
String toString() {
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
}
}
/// @nodoc
abstract mixin class $SnFediversePostCopyWith<$Res> {
factory $SnFediversePostCopyWith(
SnFediversePost value, $Res Function(SnFediversePost) _then) =
_$SnFediversePostCopyWithImpl;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String identifier,
String origin,
String content,
String language,
List<String> images,
SnFediverseUser user,
int userId});
$SnFediverseUserCopyWith<$Res> get user;
}
/// @nodoc
class _$SnFediversePostCopyWithImpl<$Res>
implements $SnFediversePostCopyWith<$Res> {
_$SnFediversePostCopyWithImpl(this._self, this._then);
final SnFediversePost _self;
final $Res Function(SnFediversePost) _then;
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? identifier = null,
Object? origin = null,
Object? content = null,
Object? language = null,
Object? images = null,
Object? user = null,
Object? userId = null,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
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?,
identifier: null == identifier
? _self.identifier
: identifier // ignore: cast_nullable_to_non_nullable
as String,
origin: null == origin
? _self.origin
: origin // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _self.content
: content // ignore: cast_nullable_to_non_nullable
as String,
language: null == language
? _self.language
: language // ignore: cast_nullable_to_non_nullable
as String,
images: null == images
? _self.images
: images // ignore: cast_nullable_to_non_nullable
as List<String>,
user: null == user
? _self.user
: user // ignore: cast_nullable_to_non_nullable
as SnFediverseUser,
userId: null == userId
? _self.userId
: userId // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnFediverseUserCopyWith<$Res> get user {
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnFediversePost implements SnFediversePost {
const _SnFediversePost(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.identifier,
required this.origin,
required this.content,
required this.language,
required final List<String> images,
required this.user,
required this.userId})
: _images = images;
factory _SnFediversePost.fromJson(Map<String, dynamic> json) =>
_$SnFediversePostFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String identifier;
@override
final String origin;
@override
final String content;
@override
final String language;
final List<String> _images;
@override
List<String> get images {
if (_images is EqualUnmodifiableListView) return _images;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_images);
}
@override
final SnFediverseUser user;
@override
final int userId;
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnFediversePostCopyWith<_SnFediversePost> get copyWith =>
__$SnFediversePostCopyWithImpl<_SnFediversePost>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnFediversePostToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnFediversePost &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.identifier, identifier) ||
other.identifier == identifier) &&
(identical(other.origin, origin) || other.origin == origin) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.language, language) ||
other.language == language) &&
const DeepCollectionEquality().equals(other._images, _images) &&
(identical(other.user, user) || other.user == user) &&
(identical(other.userId, userId) || other.userId == userId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
identifier,
origin,
content,
language,
const DeepCollectionEquality().hash(_images),
user,
userId);
@override
String toString() {
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
}
}
/// @nodoc
abstract mixin class _$SnFediversePostCopyWith<$Res>
implements $SnFediversePostCopyWith<$Res> {
factory _$SnFediversePostCopyWith(
_SnFediversePost value, $Res Function(_SnFediversePost) _then) =
__$SnFediversePostCopyWithImpl;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String identifier,
String origin,
String content,
String language,
List<String> images,
SnFediverseUser user,
int userId});
@override
$SnFediverseUserCopyWith<$Res> get user;
}
/// @nodoc
class __$SnFediversePostCopyWithImpl<$Res>
implements _$SnFediversePostCopyWith<$Res> {
__$SnFediversePostCopyWithImpl(this._self, this._then);
final _SnFediversePost _self;
final $Res Function(_SnFediversePost) _then;
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? identifier = null,
Object? origin = null,
Object? content = null,
Object? language = null,
Object? images = null,
Object? user = null,
Object? userId = null,
}) {
return _then(_SnFediversePost(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
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?,
identifier: null == identifier
? _self.identifier
: identifier // ignore: cast_nullable_to_non_nullable
as String,
origin: null == origin
? _self.origin
: origin // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _self.content
: content // ignore: cast_nullable_to_non_nullable
as String,
language: null == language
? _self.language
: language // ignore: cast_nullable_to_non_nullable
as String,
images: null == images
? _self._images
: images // ignore: cast_nullable_to_non_nullable
as List<String>,
user: null == user
? _self.user
: user // ignore: cast_nullable_to_non_nullable
as SnFediverseUser,
userId: null == userId
? _self.userId
: userId // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of SnFediversePost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnFediverseUserCopyWith<$Res> get user {
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
/// @nodoc
mixin _$SnFediverseUser {
int get id;
DateTime get createdAt;
DateTime get updatedAt;
DateTime? get deletedAt;
String get identifier;
String get origin;
String get avatar;
String get name;
String get nick;
/// Create a copy of SnFediverseUser
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnFediverseUserCopyWith<SnFediverseUser> get copyWith =>
_$SnFediverseUserCopyWithImpl<SnFediverseUser>(
this as SnFediverseUser, _$identity);
/// Serializes this SnFediverseUser to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnFediverseUser &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.identifier, identifier) ||
other.identifier == identifier) &&
(identical(other.origin, origin) || other.origin == origin) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.nick, nick) || other.nick == nick));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, identifier, origin, avatar, name, nick);
@override
String toString() {
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
}
}
/// @nodoc
abstract mixin class $SnFediverseUserCopyWith<$Res> {
factory $SnFediverseUserCopyWith(
SnFediverseUser value, $Res Function(SnFediverseUser) _then) =
_$SnFediverseUserCopyWithImpl;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String identifier,
String origin,
String avatar,
String name,
String nick});
}
/// @nodoc
class _$SnFediverseUserCopyWithImpl<$Res>
implements $SnFediverseUserCopyWith<$Res> {
_$SnFediverseUserCopyWithImpl(this._self, this._then);
final SnFediverseUser _self;
final $Res Function(SnFediverseUser) _then;
/// Create a copy of SnFediverseUser
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? identifier = null,
Object? origin = null,
Object? avatar = null,
Object? name = null,
Object? nick = null,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
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?,
identifier: null == identifier
? _self.identifier
: identifier // ignore: cast_nullable_to_non_nullable
as String,
origin: null == origin
? _self.origin
: origin // ignore: cast_nullable_to_non_nullable
as String,
avatar: null == avatar
? _self.avatar
: avatar // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String,
nick: null == nick
? _self.nick
: nick // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnFediverseUser implements SnFediverseUser {
const _SnFediverseUser(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.identifier,
required this.origin,
required this.avatar,
required this.name,
required this.nick});
factory _SnFediverseUser.fromJson(Map<String, dynamic> json) =>
_$SnFediverseUserFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String identifier;
@override
final String origin;
@override
final String avatar;
@override
final String name;
@override
final String nick;
/// Create a copy of SnFediverseUser
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnFediverseUserCopyWith<_SnFediverseUser> get copyWith =>
__$SnFediverseUserCopyWithImpl<_SnFediverseUser>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnFediverseUserToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnFediverseUser &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.identifier, identifier) ||
other.identifier == identifier) &&
(identical(other.origin, origin) || other.origin == origin) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.nick, nick) || other.nick == nick));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, identifier, origin, avatar, name, nick);
@override
String toString() {
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
}
}
/// @nodoc
abstract mixin class _$SnFediverseUserCopyWith<$Res>
implements $SnFediverseUserCopyWith<$Res> {
factory _$SnFediverseUserCopyWith(
_SnFediverseUser value, $Res Function(_SnFediverseUser) _then) =
__$SnFediverseUserCopyWithImpl;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String identifier,
String origin,
String avatar,
String name,
String nick});
}
/// @nodoc
class __$SnFediverseUserCopyWithImpl<$Res>
implements _$SnFediverseUserCopyWith<$Res> {
__$SnFediverseUserCopyWithImpl(this._self, this._then);
final _SnFediverseUser _self;
final $Res Function(_SnFediverseUser) _then;
/// Create a copy of SnFediverseUser
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? identifier = null,
Object? origin = null,
Object? avatar = null,
Object? name = null,
Object? nick = null,
}) {
return _then(_SnFediverseUser(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
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?,
identifier: null == identifier
? _self.identifier
: identifier // ignore: cast_nullable_to_non_nullable
as String,
origin: null == origin
? _self.origin
: origin // ignore: cast_nullable_to_non_nullable
as String,
avatar: null == avatar
? _self.avatar
: avatar // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String,
nick: null == nick
? _self.nick
: nick // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@ -303,64 +303,3 @@ Map<String, dynamic> _$SnFeedEntryToJson(_SnFeedEntry instance) =>
'data': instance.data,
'created_at': instance.createdAt.toIso8601String(),
};
_SnFediversePost _$SnFediversePostFromJson(Map<String, dynamic> json) =>
_SnFediversePost(
id: (json['id'] as num).toInt(),
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),
identifier: json['identifier'] as String,
origin: json['origin'] as String,
content: json['content'] as String,
language: json['language'] as String,
images:
(json['images'] as List<dynamic>).map((e) => e as String).toList(),
user: SnFediverseUser.fromJson(json['user'] as Map<String, dynamic>),
userId: (json['user_id'] as num).toInt(),
);
Map<String, dynamic> _$SnFediversePostToJson(_SnFediversePost instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'identifier': instance.identifier,
'origin': instance.origin,
'content': instance.content,
'language': instance.language,
'images': instance.images,
'user': instance.user.toJson(),
'user_id': instance.userId,
};
_SnFediverseUser _$SnFediverseUserFromJson(Map<String, dynamic> json) =>
_SnFediverseUser(
id: (json['id'] as num).toInt(),
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),
identifier: json['identifier'] as String,
origin: json['origin'] as String,
avatar: json['avatar'] as String,
name: json['name'] as String,
nick: json['nick'] as String,
);
Map<String, dynamic> _$SnFediverseUserToJson(_SnFediverseUser instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'identifier': instance.identifier,
'origin': instance.origin,
'avatar': instance.avatar,
'name': instance.name,
'nick': instance.nick,
};

View File

@ -1,105 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/news.dart';
import 'package:surface/types/post.dart';
class NewsFeedEntry extends StatelessWidget {
final SnFeedEntry data;
const NewsFeedEntry({super.key, required this.data});
@override
Widget build(BuildContext context) {
final List<SnNewsArticle> news = data.data
.map((ele) => SnNewsArticle.fromJson(ele))
.cast<SnNewsArticle>()
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Symbols.newspaper),
const Gap(8),
Text(
'newsToday',
style: Theme.of(context).textTheme.titleLarge,
).tr()
],
).padding(horizontal: 18, top: 12, bottom: 8),
Container(
margin: const EdgeInsets.only(bottom: 12),
height: 150,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: news.length,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemBuilder: (context, idx) {
return Container(
width: 360,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Material(
elevation: 0,
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
news[idx].title,
maxLines: 2,
style: Theme.of(context).textTheme.titleMedium,
).padding(horizontal: 16, top: 12, bottom: 4),
Text(
news[idx].description,
maxLines: 2,
style: Theme.of(context).textTheme.bodyMedium,
).padding(horizontal: 16, vertical: 4),
const Gap(4),
Row(
children: [
Text(
DateFormat('y/M/d HH:mm')
.format(news[idx].createdAt.toLocal()),
style: Theme.of(context).textTheme.bodySmall,
),
const Gap(4),
Text(
RelativeTime(context)
.format(news[idx].createdAt.toLocal()),
style: Theme.of(context).textTheme.bodySmall,
),
],
).opacity(0.8).padding(horizontal: 16),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': news[idx].hash},
);
},
),
),
);
},
separatorBuilder: (_, __) => const Gap(12),
),
),
],
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/news.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/universal_image.dart';
class NewsFeedEntry extends StatelessWidget {
final SnFeedEntry data;
const NewsFeedEntry({super.key, required this.data});
@override
Widget build(BuildContext context) {
final ele = SnSubscriptionItem.fromJson(data.data);
return Card(
elevation: 0,
color: Colors.transparent,
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (ele.thumbnail.isNotEmpty && ele.thumbnail.startsWith('http'))
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoResizeUniversalImage(ele.thumbnail),
),
),
).padding(horizontal: 16, bottom: 8, top: 4),
Row(
children: [
const Icon(Symbols.globe),
const Gap(8),
Expanded(
child: Text(
ele.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
).padding(horizontal: 18, vertical: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(ele.description),
Text(DateFormat().format(ele.createdAt.toLocal()))
.tr()
.opacity(0.8),
],
).padding(horizontal: 16),
],
),
);
}
}

View File

@ -1,168 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/universal_image.dart';
class FediversePostWidget extends StatelessWidget {
final SnFediversePost data;
final double maxWidth;
const FediversePostWidget({
super.key,
required this.data,
required this.maxWidth,
});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
AccountImage(
content: data.user.avatar,
radius: 20,
),
const Gap(12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.user.nick.isNotEmpty
? data.user.nick
: '@${data.user.name}',
maxLines: 1,
).bold(),
Row(
children: [
Text(
data.user.identifier.contains('@')
? data.user.identifier
: '${data.user.identifier}@${data.user.origin}',
maxLines: 1,
).fontSize(13),
const Gap(4),
Text(
RelativeTime(context)
.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
],
),
],
).padding(horizontal: 12, vertical: 8),
MarkdownTextContent(
isAutoWarp: true,
content: html2md.convert(data.content),
).padding(horizontal: 16, bottom: 6),
if (data.images.isNotEmpty)
_FediversePostImageList(
data: data,
maxWidth: maxWidth,
),
],
),
),
);
}
}
class _FediversePostImageList extends StatelessWidget {
const _FediversePostImageList({
required this.data,
required this.maxWidth,
});
final SnFediversePost data;
final double maxWidth;
@override
Widget build(BuildContext context) {
final borderSide =
BorderSide(width: 1, color: Theme.of(context).dividerColor);
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
if (data.images.length == 1) {
return AspectRatio(
aspectRatio: 1,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AutoResizeUniversalImage(
data.images.first,
),
),
),
).padding(horizontal: 8);
}
return AspectRatio(
aspectRatio: 1,
child: ScrollConfiguration(
behavior: AttachmentListScrollBehavior(),
child: ListView.separated(
shrinkWrap: true,
itemCount: data.images.length,
itemBuilder: (context, idx) {
return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
child: AspectRatio(
aspectRatio: 1,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AutoResizeUniversalImage(
data.images[idx],
),
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${data.images.length}'),
),
),
],
),
),
);
},
separatorBuilder: (context, index) => const Gap(8),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
),
),
);
}
}