♻️ Refactored news, mixed feed and call

This commit is contained in:
LittleSheep 2025-04-06 14:43:23 +08:00
parent 5c9569ef36
commit 33fc7b287e
12 changed files with 396 additions and 454 deletions

View File

@ -64,11 +64,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'realm', screen: 'realm',
label: 'screenRealm', label: 'screenRealm',
), ),
AppNavDestination(
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
screen: 'news',
label: 'screenNews',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20), icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'settings', screen: 'settings',

View File

@ -29,8 +29,7 @@ import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart'; import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart'; import 'package:surface/screens/home.dart';
import 'package:surface/screens/logging.dart'; import 'package:surface/screens/logging.dart';
import 'package:surface/screens/news/news_detail.dart'; import 'package:surface/screens/feed/feed_detail.dart';
import 'package:surface/screens/news/news_list.dart';
import 'package:surface/screens/notification.dart'; import 'package:surface/screens/notification.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/screens/post/post_draft.dart'; import 'package:surface/screens/post/post_draft.dart';
@ -125,6 +124,13 @@ final _appRoutes = [
preload: state.extra as SnPost?, preload: state.extra as SnPost?,
), ),
), ),
GoRoute(
path: '/pages/:id',
name: 'readerFeedDetail',
builder: (context, state) => ReaderPageScreen(
id: state.pathParameters['id']!,
),
),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
@ -314,20 +320,6 @@ final _appRoutes = [
), ),
], ],
), ),
GoRoute(
path: '/news',
name: 'news',
builder: (context, state) => const NewsScreen(),
routes: [
GoRoute(
path: '/:hash',
name: 'newsDetail',
builder: (context, state) => NewsDetailScreen(
hash: state.pathParameters['hash']!,
),
),
],
),
GoRoute( GoRoute(
path: '/stickers', path: '/stickers',
name: 'stickers', name: 'stickers',

View File

@ -41,7 +41,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
return; return;
} }
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CaptchaScreen(), builder: (context) => CaptchaScreen(),
), ),

View File

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -26,6 +28,7 @@ import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChatRoomScreenExtra { class ChatRoomScreenExtra {
@ -135,7 +138,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
} }
} }
Future<void> _onCallJoin() async { Future<void> _joinCall() async {
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) {
return await _joinCallWeb();
}
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final meet = JitsiMeet(); final meet = JitsiMeet();
@ -156,6 +162,14 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
meet.join(confOpts); meet.join(confOpts);
} }
Future<void> _joinCallWeb() async {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final url =
'${sn.client.options.baseUrl}/meet/${_channel!.id}?tk=${await ua.atk}';
launchUrlString(url);
}
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) { bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
if (a == null || b == null) return false; if (a == null || b == null) return false;
if (a.sender.accountId != b.sender.accountId) return false; if (a.sender.accountId != b.sender.accountId) return false;
@ -237,7 +251,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (_currentMember != null) if (_currentMember != null)
IconButton( IconButton(
icon: const Icon(Symbols.video_call), icon: const Icon(Symbols.video_call),
onPressed: _onCallJoin, onPressed: _joinCall,
onLongPress: _joinCallWeb,
), ),
IconButton( IconButton(
icon: const Icon(Symbols.more_vert), icon: const Icon(Symbols.more_vert),

View File

@ -465,7 +465,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
final pt = context.read<SnPostContentProvider>(); final pt = context.read<SnPostContentProvider>();
final result = await pt.getFeed( final result = await pt.getFeed(
cursor: _feed cursor: _feed
.where((ele) => !['reader.news'].contains(ele.type)) .where((ele) => !['reader.feed'].contains(ele.type))
.lastOrNull .lastOrNull
?.createdAt, ?.createdAt,
); );

View File

@ -14,22 +14,22 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class NewsDetailScreen extends StatefulWidget { class ReaderPageScreen extends StatefulWidget {
final String hash; final String id;
const NewsDetailScreen({super.key, required this.hash}); const ReaderPageScreen({super.key, required this.id});
@override @override
State<NewsDetailScreen> createState() => _NewsDetailScreenState(); State<ReaderPageScreen> createState() => _ReaderPageScreenState();
} }
class _NewsDetailScreenState extends State<NewsDetailScreen> { class _ReaderPageScreenState extends State<ReaderPageScreen> {
SnSubscriptionItem? _article; SnSubscriptionItem? _article;
Future<void> _fetchArticle() async { Future<void> _fetchArticle() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news/${widget.hash}'); final resp = await sn.client.get('/cgi/re/subscriptions/${widget.id}');
_article = SnSubscriptionItem.fromJson(resp.data); _article = SnSubscriptionItem.fromJson(resp.data);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;

View File

@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
} }
Future<void> _doCheckIn() async { Future<void> _doCheckIn() async {
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CaptchaScreen(), builder: (context) => CaptchaScreen(),
), ),

View File

@ -1,260 +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:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/news.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class NewsScreen extends StatefulWidget {
const NewsScreen({super.key});
@override
State<NewsScreen> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
List<SnNewsSource>? _sources;
@override
initState() {
super.initState();
_fetchSources();
}
Future<void> _fetchSources() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/well-known/sources');
_sources = List<SnNewsSource>.from(
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (_sources == null) {
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenNews').tr(),
),
body: Center(
child: CircularProgressIndicator(),
),
);
}
return DefaultTabController(
length: _sources!.length + 1,
child: AppScaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
leading: AutoAppBarLeading(),
title: Text('screenNews').tr(),
floating: true,
snap: true,
bottom: TabBar(
isScrollable: true,
tabs: [
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),
),
],
),
),
),
];
},
body: TabBarView(
children: [
_NewsArticleListWidget(allSources: _sources!),
for (final source in _sources!)
_NewsArticleListWidget(
source: source.id,
allSources: _sources!,
),
],
),
),
),
);
}
}
class _NewsArticleListWidget extends StatefulWidget {
final String? source;
final List<SnNewsSource> allSources;
const _NewsArticleListWidget({this.source, required this.allSources});
@override
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
}
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
bool _isBusy = false;
int? _totalCount;
final List<SnSubscriptionItem> _articles = List.empty(growable: true);
Future<void> _fetchArticles() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
'take': 10,
'offset': _articles.length,
if (widget.source != null) 'source': widget.source,
});
_totalCount = resp.data['count'];
_articles.addAll(List<SnSubscriptionItem>.from(
resp.data['data']?.map((e) => SnSubscriptionItem.fromJson(e)) ?? [],
));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchArticles();
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: RefreshIndicator(
onRefresh: _fetchArticles,
child: InfiniteList(
isLoading: _isBusy,
itemCount: _articles.length,
hasReachedMax:
_totalCount != null && _articles.length >= _totalCount!,
onFetchData: () {
_fetchArticles();
},
itemBuilder: (context, index) {
final article = _articles[index];
final baseUri = Uri.parse(article.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
final htmlDescription = parse(article.description);
final date = article.publishedAt ?? article.createdAt;
return Card(
child: InkWell(
radius: 8,
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': article.hash},
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (article.thumbnail.isNotEmpty &&
!article.thumbnail.endsWith('.svg'))
ClipRRect(
borderRadius: BorderRadius.only(
topRight: Radius.circular(8),
topLeft: Radius.circular(8),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainer,
child: AutoResizeUniversalImage(
article.thumbnail.startsWith('http')
? article.thumbnail
: '$baseUrl/${article.thumbnail}',
),
),
),
),
const Gap(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())
.textStyle(Theme.of(context).textTheme.bodyMedium!)
.padding(horizontal: 16),
const Gap(8),
Row(
spacing: 2,
children: [
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!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
],
),
),
);
},
),
),
),
),
);
}
}

View File

@ -4,18 +4,23 @@ part 'news.freezed.dart';
part 'news.g.dart'; part 'news.g.dart';
@freezed @freezed
abstract class SnNewsSource with _$SnNewsSource { abstract class SnSubscriptionFeed with _$SnSubscriptionFeed {
const factory SnNewsSource({ const factory SnSubscriptionFeed({
required String id, required int id,
required String label, required DateTime createdAt,
required String type, required DateTime updatedAt,
required String source, required DateTime? deletedAt,
required int depth, required String url,
required bool enabled, required bool isEnabled,
}) = _SnNewsSource; required bool isFullContent,
required int pullInterval,
required String adapter,
required int? accountId,
required DateTime? lastFetchedAt,
}) = _SnSubscriptionFeed;
factory SnNewsSource.fromJson(Map<String, dynamic> json) => factory SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
_$SnNewsSourceFromJson(json); _$SnSubscriptionFeedFromJson(json);
} }
@freezed @freezed
@ -32,6 +37,7 @@ abstract class SnSubscriptionItem with _$SnSubscriptionItem {
required String url, required String url,
required String hash, required String hash,
required int feedId, required int feedId,
required SnSubscriptionFeed feed,
required DateTime? publishedAt, required DateTime? publishedAt,
}) = _SnSubscriptionItem; }) = _SnSubscriptionItem;

View File

@ -14,149 +14,224 @@ part of 'news.dart';
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnNewsSource { mixin _$SnSubscriptionFeed {
String get id; int get id;
String get label; DateTime get createdAt;
String get type; DateTime get updatedAt;
String get source; DateTime? get deletedAt;
int get depth; String get url;
bool get enabled; bool get isEnabled;
bool get isFullContent;
int get pullInterval;
String get adapter;
int? get accountId;
DateTime? get lastFetchedAt;
/// Create a copy of SnNewsSource /// Create a copy of SnSubscriptionFeed
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SnNewsSourceCopyWith<SnNewsSource> get copyWith => $SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith =>
_$SnNewsSourceCopyWithImpl<SnNewsSource>( _$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>(
this as SnNewsSource, _$identity); this as SnSubscriptionFeed, _$identity);
/// Serializes this SnNewsSource to a JSON map. /// Serializes this SnSubscriptionFeed to a JSON map.
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is SnNewsSource && other is SnSubscriptionFeed &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.label, label) || other.label == label) && (identical(other.createdAt, createdAt) ||
(identical(other.type, type) || other.type == type) && other.createdAt == createdAt) &&
(identical(other.source, source) || other.source == source) && (identical(other.updatedAt, updatedAt) ||
(identical(other.depth, depth) || other.depth == depth) && other.updatedAt == updatedAt) &&
(identical(other.enabled, enabled) || other.enabled == enabled)); (identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.isEnabled, isEnabled) ||
other.isEnabled == isEnabled) &&
(identical(other.isFullContent, isFullContent) ||
other.isFullContent == isFullContent) &&
(identical(other.pullInterval, pullInterval) ||
other.pullInterval == pullInterval) &&
(identical(other.adapter, adapter) || other.adapter == adapter) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.lastFetchedAt, lastFetchedAt) ||
other.lastFetchedAt == lastFetchedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(runtimeType, id, label, type, source, depth, enabled); runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
url,
isEnabled,
isFullContent,
pullInterval,
adapter,
accountId,
lastFetchedAt);
@override @override
String toString() { String toString() {
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)'; return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class $SnNewsSourceCopyWith<$Res> { abstract mixin class $SnSubscriptionFeedCopyWith<$Res> {
factory $SnNewsSourceCopyWith( factory $SnSubscriptionFeedCopyWith(
SnNewsSource value, $Res Function(SnNewsSource) _then) = SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) =
_$SnNewsSourceCopyWithImpl; _$SnSubscriptionFeedCopyWithImpl;
@useResult @useResult
$Res call( $Res call(
{String id, {int id,
String label, DateTime createdAt,
String type, DateTime updatedAt,
String source, DateTime? deletedAt,
int depth, String url,
bool enabled}); bool isEnabled,
bool isFullContent,
int pullInterval,
String adapter,
int? accountId,
DateTime? lastFetchedAt});
} }
/// @nodoc /// @nodoc
class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> { class _$SnSubscriptionFeedCopyWithImpl<$Res>
_$SnNewsSourceCopyWithImpl(this._self, this._then); implements $SnSubscriptionFeedCopyWith<$Res> {
_$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
final SnNewsSource _self; final SnSubscriptionFeed _self;
final $Res Function(SnNewsSource) _then; final $Res Function(SnSubscriptionFeed) _then;
/// Create a copy of SnNewsSource /// Create a copy of SnSubscriptionFeed
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? label = null, Object? createdAt = null,
Object? type = null, Object? updatedAt = null,
Object? source = null, Object? deletedAt = freezed,
Object? depth = null, Object? url = null,
Object? enabled = null, Object? isEnabled = null,
Object? isFullContent = null,
Object? pullInterval = null,
Object? adapter = null,
Object? accountId = freezed,
Object? lastFetchedAt = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
? _self.id ? _self.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _self.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _self.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _self.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _self.depth
: depth // ignore: cast_nullable_to_non_nullable
as int, as int,
enabled: null == enabled createdAt: null == createdAt
? _self.enabled ? _self.createdAt
: enabled // ignore: cast_nullable_to_non_nullable : 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?,
url: null == url
? _self.url
: url // ignore: cast_nullable_to_non_nullable
as String,
isEnabled: null == isEnabled
? _self.isEnabled
: isEnabled // ignore: cast_nullable_to_non_nullable
as bool, as bool,
isFullContent: null == isFullContent
? _self.isFullContent
: isFullContent // ignore: cast_nullable_to_non_nullable
as bool,
pullInterval: null == pullInterval
? _self.pullInterval
: pullInterval // ignore: cast_nullable_to_non_nullable
as int,
adapter: null == adapter
? _self.adapter
: adapter // ignore: cast_nullable_to_non_nullable
as String,
accountId: freezed == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int?,
lastFetchedAt: freezed == lastFetchedAt
? _self.lastFetchedAt
: lastFetchedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _SnNewsSource implements SnNewsSource { class _SnSubscriptionFeed implements SnSubscriptionFeed {
const _SnNewsSource( const _SnSubscriptionFeed(
{required this.id, {required this.id,
required this.label, required this.createdAt,
required this.type, required this.updatedAt,
required this.source, required this.deletedAt,
required this.depth, required this.url,
required this.enabled}); required this.isEnabled,
factory _SnNewsSource.fromJson(Map<String, dynamic> json) => required this.isFullContent,
_$SnNewsSourceFromJson(json); required this.pullInterval,
required this.adapter,
required this.accountId,
required this.lastFetchedAt});
factory _SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionFeedFromJson(json);
@override @override
final String id; final int id;
@override @override
final String label; final DateTime createdAt;
@override @override
final String type; final DateTime updatedAt;
@override @override
final String source; final DateTime? deletedAt;
@override @override
final int depth; final String url;
@override @override
final bool enabled; final bool isEnabled;
@override
final bool isFullContent;
@override
final int pullInterval;
@override
final String adapter;
@override
final int? accountId;
@override
final DateTime? lastFetchedAt;
/// Create a copy of SnNewsSource /// Create a copy of SnSubscriptionFeed
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$SnNewsSourceCopyWith<_SnNewsSource> get copyWith => _$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith =>
__$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity); __$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$SnNewsSourceToJson( return _$SnSubscriptionFeedToJson(
this, this,
); );
} }
@ -165,88 +240,142 @@ class _SnNewsSource implements SnNewsSource {
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _SnNewsSource && other is _SnSubscriptionFeed &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.label, label) || other.label == label) && (identical(other.createdAt, createdAt) ||
(identical(other.type, type) || other.type == type) && other.createdAt == createdAt) &&
(identical(other.source, source) || other.source == source) && (identical(other.updatedAt, updatedAt) ||
(identical(other.depth, depth) || other.depth == depth) && other.updatedAt == updatedAt) &&
(identical(other.enabled, enabled) || other.enabled == enabled)); (identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.isEnabled, isEnabled) ||
other.isEnabled == isEnabled) &&
(identical(other.isFullContent, isFullContent) ||
other.isFullContent == isFullContent) &&
(identical(other.pullInterval, pullInterval) ||
other.pullInterval == pullInterval) &&
(identical(other.adapter, adapter) || other.adapter == adapter) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.lastFetchedAt, lastFetchedAt) ||
other.lastFetchedAt == lastFetchedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(runtimeType, id, label, type, source, depth, enabled); runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
url,
isEnabled,
isFullContent,
pullInterval,
adapter,
accountId,
lastFetchedAt);
@override @override
String toString() { String toString() {
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)'; return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class _$SnNewsSourceCopyWith<$Res> abstract mixin class _$SnSubscriptionFeedCopyWith<$Res>
implements $SnNewsSourceCopyWith<$Res> { implements $SnSubscriptionFeedCopyWith<$Res> {
factory _$SnNewsSourceCopyWith( factory _$SnSubscriptionFeedCopyWith(
_SnNewsSource value, $Res Function(_SnNewsSource) _then) = _SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) =
__$SnNewsSourceCopyWithImpl; __$SnSubscriptionFeedCopyWithImpl;
@override @override
@useResult @useResult
$Res call( $Res call(
{String id, {int id,
String label, DateTime createdAt,
String type, DateTime updatedAt,
String source, DateTime? deletedAt,
int depth, String url,
bool enabled}); bool isEnabled,
bool isFullContent,
int pullInterval,
String adapter,
int? accountId,
DateTime? lastFetchedAt});
} }
/// @nodoc /// @nodoc
class __$SnNewsSourceCopyWithImpl<$Res> class __$SnSubscriptionFeedCopyWithImpl<$Res>
implements _$SnNewsSourceCopyWith<$Res> { implements _$SnSubscriptionFeedCopyWith<$Res> {
__$SnNewsSourceCopyWithImpl(this._self, this._then); __$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
final _SnNewsSource _self; final _SnSubscriptionFeed _self;
final $Res Function(_SnNewsSource) _then; final $Res Function(_SnSubscriptionFeed) _then;
/// Create a copy of SnNewsSource /// Create a copy of SnSubscriptionFeed
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? label = null, Object? createdAt = null,
Object? type = null, Object? updatedAt = null,
Object? source = null, Object? deletedAt = freezed,
Object? depth = null, Object? url = null,
Object? enabled = null, Object? isEnabled = null,
Object? isFullContent = null,
Object? pullInterval = null,
Object? adapter = null,
Object? accountId = freezed,
Object? lastFetchedAt = freezed,
}) { }) {
return _then(_SnNewsSource( return _then(_SnSubscriptionFeed(
id: null == id id: null == id
? _self.id ? _self.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _self.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _self.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _self.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _self.depth
: depth // ignore: cast_nullable_to_non_nullable
as int, as int,
enabled: null == enabled createdAt: null == createdAt
? _self.enabled ? _self.createdAt
: enabled // ignore: cast_nullable_to_non_nullable : 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?,
url: null == url
? _self.url
: url // ignore: cast_nullable_to_non_nullable
as String,
isEnabled: null == isEnabled
? _self.isEnabled
: isEnabled // ignore: cast_nullable_to_non_nullable
as bool, as bool,
isFullContent: null == isFullContent
? _self.isFullContent
: isFullContent // ignore: cast_nullable_to_non_nullable
as bool,
pullInterval: null == pullInterval
? _self.pullInterval
: pullInterval // ignore: cast_nullable_to_non_nullable
as int,
adapter: null == adapter
? _self.adapter
: adapter // ignore: cast_nullable_to_non_nullable
as String,
accountId: freezed == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int?,
lastFetchedAt: freezed == lastFetchedAt
? _self.lastFetchedAt
: lastFetchedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }
} }
@ -264,6 +393,7 @@ mixin _$SnSubscriptionItem {
String get url; String get url;
String get hash; String get hash;
int get feedId; int get feedId;
SnSubscriptionFeed get feed;
DateTime? get publishedAt; DateTime? get publishedAt;
/// Create a copy of SnSubscriptionItem /// Create a copy of SnSubscriptionItem
@ -298,6 +428,7 @@ mixin _$SnSubscriptionItem {
(identical(other.url, url) || other.url == url) && (identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) && (identical(other.hash, hash) || other.hash == hash) &&
(identical(other.feedId, feedId) || other.feedId == feedId) && (identical(other.feedId, feedId) || other.feedId == feedId) &&
(identical(other.feed, feed) || other.feed == feed) &&
(identical(other.publishedAt, publishedAt) || (identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt)); other.publishedAt == publishedAt));
} }
@ -317,11 +448,12 @@ mixin _$SnSubscriptionItem {
url, url,
hash, hash,
feedId, feedId,
feed,
publishedAt); publishedAt);
@override @override
String toString() { String toString() {
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)'; return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, feed: $feed, publishedAt: $publishedAt)';
} }
} }
@ -343,7 +475,10 @@ abstract mixin class $SnSubscriptionItemCopyWith<$Res> {
String url, String url,
String hash, String hash,
int feedId, int feedId,
SnSubscriptionFeed feed,
DateTime? publishedAt}); DateTime? publishedAt});
$SnSubscriptionFeedCopyWith<$Res> get feed;
} }
/// @nodoc /// @nodoc
@ -370,6 +505,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
Object? url = null, Object? url = null,
Object? hash = null, Object? hash = null,
Object? feedId = null, Object? feedId = null,
Object? feed = null,
Object? publishedAt = freezed, Object? publishedAt = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
@ -417,12 +553,26 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
? _self.feedId ? _self.feedId
: feedId // ignore: cast_nullable_to_non_nullable : feedId // ignore: cast_nullable_to_non_nullable
as int, as int,
feed: null == feed
? _self.feed
: feed // ignore: cast_nullable_to_non_nullable
as SnSubscriptionFeed,
publishedAt: freezed == publishedAt publishedAt: freezed == publishedAt
? _self.publishedAt ? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
)); ));
} }
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnSubscriptionFeedCopyWith<$Res> get feed {
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
return _then(_self.copyWith(feed: value));
});
}
} }
/// @nodoc /// @nodoc
@ -440,6 +590,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
required this.url, required this.url,
required this.hash, required this.hash,
required this.feedId, required this.feedId,
required this.feed,
required this.publishedAt}); required this.publishedAt});
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) => factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionItemFromJson(json); _$SnSubscriptionItemFromJson(json);
@ -467,6 +618,8 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
@override @override
final int feedId; final int feedId;
@override @override
final SnSubscriptionFeed feed;
@override
final DateTime? publishedAt; final DateTime? publishedAt;
/// Create a copy of SnSubscriptionItem /// Create a copy of SnSubscriptionItem
@ -505,6 +658,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
(identical(other.url, url) || other.url == url) && (identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) && (identical(other.hash, hash) || other.hash == hash) &&
(identical(other.feedId, feedId) || other.feedId == feedId) && (identical(other.feedId, feedId) || other.feedId == feedId) &&
(identical(other.feed, feed) || other.feed == feed) &&
(identical(other.publishedAt, publishedAt) || (identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt)); other.publishedAt == publishedAt));
} }
@ -524,11 +678,12 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
url, url,
hash, hash,
feedId, feedId,
feed,
publishedAt); publishedAt);
@override @override
String toString() { String toString() {
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)'; return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, feed: $feed, publishedAt: $publishedAt)';
} }
} }
@ -552,7 +707,11 @@ abstract mixin class _$SnSubscriptionItemCopyWith<$Res>
String url, String url,
String hash, String hash,
int feedId, int feedId,
SnSubscriptionFeed feed,
DateTime? publishedAt}); DateTime? publishedAt});
@override
$SnSubscriptionFeedCopyWith<$Res> get feed;
} }
/// @nodoc /// @nodoc
@ -579,6 +738,7 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
Object? url = null, Object? url = null,
Object? hash = null, Object? hash = null,
Object? feedId = null, Object? feedId = null,
Object? feed = null,
Object? publishedAt = freezed, Object? publishedAt = freezed,
}) { }) {
return _then(_SnSubscriptionItem( return _then(_SnSubscriptionItem(
@ -626,12 +786,26 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
? _self.feedId ? _self.feedId
: feedId // ignore: cast_nullable_to_non_nullable : feedId // ignore: cast_nullable_to_non_nullable
as int, as int,
feed: null == feed
? _self.feed
: feed // ignore: cast_nullable_to_non_nullable
as SnSubscriptionFeed,
publishedAt: freezed == publishedAt publishedAt: freezed == publishedAt
? _self.publishedAt ? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
)); ));
} }
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnSubscriptionFeedCopyWith<$Res> get feed {
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
return _then(_self.copyWith(feed: value));
});
}
} }
// dart format on // dart format on

View File

@ -6,24 +6,38 @@ part of 'news.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) => _SnSubscriptionFeed _$SnSubscriptionFeedFromJson(Map<String, dynamic> json) =>
_SnNewsSource( _SnSubscriptionFeed(
id: json['id'] as String, id: (json['id'] as num).toInt(),
label: json['label'] as String, createdAt: DateTime.parse(json['created_at'] as String),
type: json['type'] as String, updatedAt: DateTime.parse(json['updated_at'] as String),
source: json['source'] as String, deletedAt: json['deleted_at'] == null
depth: (json['depth'] as num).toInt(), ? null
enabled: json['enabled'] as bool, : DateTime.parse(json['deleted_at'] as String),
url: json['url'] as String,
isEnabled: json['is_enabled'] as bool,
isFullContent: json['is_full_content'] as bool,
pullInterval: (json['pull_interval'] as num).toInt(),
adapter: json['adapter'] as String,
accountId: (json['account_id'] as num?)?.toInt(),
lastFetchedAt: json['last_fetched_at'] == null
? null
: DateTime.parse(json['last_fetched_at'] as String),
); );
Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) => Map<String, dynamic> _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'label': instance.label, 'created_at': instance.createdAt.toIso8601String(),
'type': instance.type, 'updated_at': instance.updatedAt.toIso8601String(),
'source': instance.source, 'deleted_at': instance.deletedAt?.toIso8601String(),
'depth': instance.depth, 'url': instance.url,
'enabled': instance.enabled, 'is_enabled': instance.isEnabled,
'is_full_content': instance.isFullContent,
'pull_interval': instance.pullInterval,
'adapter': instance.adapter,
'account_id': instance.accountId,
'last_fetched_at': instance.lastFetchedAt?.toIso8601String(),
}; };
_SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) => _SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) =>
@ -41,6 +55,7 @@ _SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) =>
url: json['url'] as String, url: json['url'] as String,
hash: json['hash'] as String, hash: json['hash'] as String,
feedId: (json['feed_id'] as num).toInt(), feedId: (json['feed_id'] as num).toInt(),
feed: SnSubscriptionFeed.fromJson(json['feed'] as Map<String, dynamic>),
publishedAt: json['published_at'] == null publishedAt: json['published_at'] == null
? null ? null
: DateTime.parse(json['published_at'] as String), : DateTime.parse(json['published_at'] as String),
@ -59,5 +74,6 @@ Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
'url': instance.url, 'url': instance.url,
'hash': instance.hash, 'hash': instance.hash,
'feed_id': instance.feedId, 'feed_id': instance.feedId,
'feed': instance.feed.toJson(),
'published_at': instance.publishedAt?.toIso8601String(), 'published_at': instance.publishedAt?.toIso8601String(),
}; };

View File

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/news.dart'; import 'package:surface/types/news.dart';
@ -15,10 +16,7 @@ class NewsFeedEntry extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ele = SnSubscriptionItem.fromJson(data.data); final ele = SnSubscriptionItem.fromJson(data.data);
return Card( return InkWell(
elevation: 0,
color: Colors.transparent,
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -53,11 +51,17 @@ class NewsFeedEntry extends StatelessWidget {
Text(ele.description), Text(ele.description),
Text(DateFormat().format(ele.createdAt.toLocal())) Text(DateFormat().format(ele.createdAt.toLocal()))
.tr() .tr()
.fontSize(13)
.opacity(0.8), .opacity(0.8),
], ],
).padding(horizontal: 16), ).padding(horizontal: 16, vertical: 4),
], ],
), ),
onTap: () {
GoRouter.of(context).pushNamed('readerFeedDetail', pathParameters: {
'id': ele.id.toString(),
});
},
); );
} }
} }