diff --git a/api/Reader/List News Sources.bru b/api/Reader/List News Sources.bru index efa209c..d91f743 100644 --- a/api/Reader/List News Sources.bru +++ b/api/Reader/List News Sources.bru @@ -7,5 +7,5 @@ meta { get { url: {{endpoint}}/cgi/re/well-known/sources body: none - auth: none + auth: inherit } diff --git a/api/Reader/Trigger Scan News.bru b/api/Reader/Trigger Scan News.bru index 18b86b4..631df8a 100644 --- a/api/Reader/Trigger Scan News.bru +++ b/api/Reader/Trigger Scan News.bru @@ -12,7 +12,7 @@ post { body:json { { - "sources": ["taiwan-ltn"], + "sources": ["taiwan-pts"], "eager": true } } diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 01aa3e5..c28758c 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -794,5 +794,6 @@ "accountStatusPositive": "Positive", "mixedFeed": "Mixed Feed", "mixedFeedDescription": "The Explore screen may not only display the user's posts, but may also contain other content. However, this mode does not apply to classification and filtering.", - "filterFeed": "Exploring Adjust" + "filterFeed": "Exploring Adjust", + "feedUnknownItem": "Unable to display this content, the current version of the client does not support the type of content, please try to update the application afterwards." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index bc2a59c..f697c3f 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -792,5 +792,6 @@ "accountStatusPositive": "正面", "mixedFeed": "混合推荐流", "mixedFeedDescription": "探索页面可能不只会展示用户的帖子,更可能包含其他的内容。但该模式不适用分类和过滤。", - "filterFeed": "探索队列调整" + "filterFeed": "探索队列调整", + "feedUnknownItem": "无法显示该内容,当前版本客户端不支持该类型的内容,请尝试更新应用程序后再试。" } diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index f219b87..e9f66de 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -17,6 +17,8 @@ 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_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'; @@ -459,7 +461,12 @@ class _PostListWidgetState extends State<_PostListWidget> { setState(() => _isBusy = true); final pt = context.read(); - final result = await pt.getFeed(cursor: _feed.lastOrNull?.createdAt); + final result = await pt.getFeed( + cursor: _feed + .where((ele) => !['reader.news'].contains(ele.type)) + .lastOrNull + ?.createdAt, + ); if (!mounted) return; @@ -498,7 +505,12 @@ class _PostListWidgetState extends State<_PostListWidget> { @override void initState() { super.initState(); - _fetchPosts(); + final cfg = context.read(); + if (cfg.mixedFeed) { + _fetchFeed(); + } else { + _fetchPosts(); + } } @override @@ -538,8 +550,10 @@ class _PostListWidgetState extends State<_PostListWidget> { data: SnFediversePost.fromJson(ele.data), maxWidth: 640, ); + case 'reader.news': + return NewsFeedEntry(data: ele); default: - return Placeholder(); + return FeedUnknownEntry(data: ele); } }, separatorBuilder: (_, __) => const Gap(8), diff --git a/lib/types/post.dart b/lib/types/post.dart index 80b611b..c5b44d9 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -171,7 +171,7 @@ abstract class SnSubscription with _$SnSubscription { abstract class SnFeedEntry with _$SnFeedEntry { const factory SnFeedEntry({ required String type, - required Map data, + required dynamic data, required DateTime createdAt, }) = _SnFeedEntry; diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 6b9b93c..85e9366 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -3123,7 +3123,7 @@ class __$SnSubscriptionCopyWithImpl<$Res> /// @nodoc mixin _$SnFeedEntry { String get type; - Map get data; + dynamic get data; DateTime get createdAt; /// Create a copy of SnFeedEntry @@ -3164,7 +3164,7 @@ abstract mixin class $SnFeedEntryCopyWith<$Res> { SnFeedEntry value, $Res Function(SnFeedEntry) _then) = _$SnFeedEntryCopyWithImpl; @useResult - $Res call({String type, Map data, DateTime createdAt}); + $Res call({String type, dynamic data, DateTime createdAt}); } /// @nodoc @@ -3180,7 +3180,7 @@ class _$SnFeedEntryCopyWithImpl<$Res> implements $SnFeedEntryCopyWith<$Res> { @override $Res call({ Object? type = null, - Object? data = null, + Object? data = freezed, Object? createdAt = null, }) { return _then(_self.copyWith( @@ -3188,10 +3188,10 @@ class _$SnFeedEntryCopyWithImpl<$Res> implements $SnFeedEntryCopyWith<$Res> { ? _self.type : type // ignore: cast_nullable_to_non_nullable as String, - data: null == data + data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable - as Map, + as dynamic, createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable @@ -3204,23 +3204,14 @@ class _$SnFeedEntryCopyWithImpl<$Res> implements $SnFeedEntryCopyWith<$Res> { @JsonSerializable() class _SnFeedEntry implements SnFeedEntry { const _SnFeedEntry( - {required this.type, - required final Map data, - required this.createdAt}) - : _data = data; + {required this.type, required this.data, required this.createdAt}); factory _SnFeedEntry.fromJson(Map json) => _$SnFeedEntryFromJson(json); @override final String type; - final Map _data; @override - Map get data { - if (_data is EqualUnmodifiableMapView) return _data; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_data); - } - + final dynamic data; @override final DateTime createdAt; @@ -3245,7 +3236,7 @@ class _SnFeedEntry implements SnFeedEntry { (other.runtimeType == runtimeType && other is _SnFeedEntry && (identical(other.type, type) || other.type == type) && - const DeepCollectionEquality().equals(other._data, _data) && + const DeepCollectionEquality().equals(other.data, data) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt)); } @@ -3253,7 +3244,7 @@ class _SnFeedEntry implements SnFeedEntry { @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( - runtimeType, type, const DeepCollectionEquality().hash(_data), createdAt); + runtimeType, type, const DeepCollectionEquality().hash(data), createdAt); @override String toString() { @@ -3269,7 +3260,7 @@ abstract mixin class _$SnFeedEntryCopyWith<$Res> __$SnFeedEntryCopyWithImpl; @override @useResult - $Res call({String type, Map data, DateTime createdAt}); + $Res call({String type, dynamic data, DateTime createdAt}); } /// @nodoc @@ -3285,7 +3276,7 @@ class __$SnFeedEntryCopyWithImpl<$Res> implements _$SnFeedEntryCopyWith<$Res> { @pragma('vm:prefer-inline') $Res call({ Object? type = null, - Object? data = null, + Object? data = freezed, Object? createdAt = null, }) { return _then(_SnFeedEntry( @@ -3293,10 +3284,10 @@ class __$SnFeedEntryCopyWithImpl<$Res> implements _$SnFeedEntryCopyWith<$Res> { ? _self.type : type // ignore: cast_nullable_to_non_nullable as String, - data: null == data - ? _self._data + data: freezed == data + ? _self.data : data // ignore: cast_nullable_to_non_nullable - as Map, + as dynamic, createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index af6985f..8dc28c2 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -285,7 +285,7 @@ Map _$SnSubscriptionToJson(_SnSubscription instance) => _SnFeedEntry _$SnFeedEntryFromJson(Map json) => _SnFeedEntry( type: json['type'] as String, - data: json['data'] as Map, + data: json['data'], createdAt: DateTime.parse(json['created_at'] as String), ); diff --git a/lib/widgets/feed/feed_news.dart b/lib/widgets/feed/feed_news.dart new file mode 100644 index 0000000..b699474 --- /dev/null +++ b/lib/widgets/feed/feed_news.dart @@ -0,0 +1,107 @@ +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 news = data.data + .map((ele) => SnNewsArticle.fromJson(ele)) + .cast() + .toList(); + + return Card( + child: 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), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/feed/feed_unknown.dart b/lib/widgets/feed/feed_unknown.dart new file mode 100644 index 0000000..dc48852 --- /dev/null +++ b/lib/widgets/feed/feed_unknown.dart @@ -0,0 +1,27 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/types/post.dart'; + +class FeedUnknownEntry extends StatelessWidget { + final SnFeedEntry data; + const FeedUnknownEntry({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Symbols.help, size: 36), + const Gap(4), + Text('feedUnknownItem').tr(), + Text(data.type, style: GoogleFonts.robotoMono()), + ], + ).padding(horizontal: 12, vertical: 8), + ); + } +}