diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index faacbec..c4a7068 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -64,11 +64,6 @@ class NavigationProvider extends ChangeNotifier { screen: 'realm', label: 'screenRealm', ), - AppNavDestination( - icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20), - screen: 'news', - label: 'screenNews', - ), AppNavDestination( icon: Icon(Symbols.settings, weight: 400, opticalSize: 20), screen: 'settings', diff --git a/lib/router.dart b/lib/router.dart index 378b0d3..ab7a43a 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -29,8 +29,7 @@ import 'package:surface/screens/explore.dart'; import 'package:surface/screens/friend.dart'; import 'package:surface/screens/home.dart'; import 'package:surface/screens/logging.dart'; -import 'package:surface/screens/news/news_detail.dart'; -import 'package:surface/screens/news/news_list.dart'; +import 'package:surface/screens/feed/feed_detail.dart'; import 'package:surface/screens/notification.dart'; import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_draft.dart'; @@ -125,6 +124,13 @@ final _appRoutes = [ preload: state.extra as SnPost?, ), ), + GoRoute( + path: '/pages/:id', + name: 'readerFeedDetail', + builder: (context, state) => ReaderPageScreen( + id: state.pathParameters['id']!, + ), + ), GoRoute( path: '/publishers/:name', 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( path: '/stickers', name: 'stickers', diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index eb18c37..6ed2194 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -41,7 +41,7 @@ class _RegisterScreenState extends State { return; } - final captchaTk = await Navigator.of(context, rootNavigator: true).push( + final captchaTk = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => CaptchaScreen(), ), diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 853d0d1..e9d7d42 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.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/loading_indicator.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'; class ChatRoomScreenExtra { @@ -135,7 +138,10 @@ class _ChatRoomScreenState extends State { } } - Future _onCallJoin() async { + Future _joinCall() async { + if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) { + return await _joinCallWeb(); + } final sn = context.read(); final ua = context.read(); final meet = JitsiMeet(); @@ -156,6 +162,14 @@ class _ChatRoomScreenState extends State { meet.join(confOpts); } + Future _joinCallWeb() async { + final sn = context.read(); + final ua = context.read(); + final url = + '${sn.client.options.baseUrl}/meet/${_channel!.id}?tk=${await ua.atk}'; + launchUrlString(url); + } + bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) { if (a == null || b == null) return false; if (a.sender.accountId != b.sender.accountId) return false; @@ -237,7 +251,8 @@ class _ChatRoomScreenState extends State { if (_currentMember != null) IconButton( icon: const Icon(Symbols.video_call), - onPressed: _onCallJoin, + onPressed: _joinCall, + onLongPress: _joinCallWeb, ), IconButton( icon: const Icon(Symbols.more_vert), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index bfe9f82..508c60b 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -465,7 +465,7 @@ class _PostListWidgetState extends State<_PostListWidget> { final pt = context.read(); final result = await pt.getFeed( cursor: _feed - .where((ele) => !['reader.news'].contains(ele.type)) + .where((ele) => !['reader.feed'].contains(ele.type)) .lastOrNull ?.createdAt, ); diff --git a/lib/screens/news/news_detail.dart b/lib/screens/feed/feed_detail.dart similarity index 94% rename from lib/screens/news/news_detail.dart rename to lib/screens/feed/feed_detail.dart index 41aec50..0afafcb 100644 --- a/lib/screens/news/news_detail.dart +++ b/lib/screens/feed/feed_detail.dart @@ -14,22 +14,22 @@ import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class NewsDetailScreen extends StatefulWidget { - final String hash; +class ReaderPageScreen extends StatefulWidget { + final String id; - const NewsDetailScreen({super.key, required this.hash}); + const ReaderPageScreen({super.key, required this.id}); @override - State createState() => _NewsDetailScreenState(); + State createState() => _ReaderPageScreenState(); } -class _NewsDetailScreenState extends State { +class _ReaderPageScreenState extends State { SnSubscriptionItem? _article; Future _fetchArticle() async { try { final sn = context.read(); - 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); } catch (err) { if (!mounted) return; diff --git a/lib/screens/home.dart b/lib/screens/home.dart index b36f92c..48bfbc5 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { } Future _doCheckIn() async { - final captchaTk = await Navigator.of(context, rootNavigator: true).push( + final captchaTk = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => CaptchaScreen(), ), diff --git a/lib/screens/news/news_list.dart b/lib/screens/news/news_list.dart deleted file mode 100644 index 621b35f..0000000 --- a/lib/screens/news/news_list.dart +++ /dev/null @@ -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 createState() => _NewsScreenState(); -} - -class _NewsScreenState extends State { - List? _sources; - - @override - initState() { - super.initState(); - _fetchSources(); - } - - Future _fetchSources() async { - try { - final sn = context.read(); - final resp = await sn.client.get('/cgi/re/well-known/sources'); - _sources = List.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 [ - 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 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 _articles = List.empty(growable: true); - - Future _fetchArticles() async { - setState(() => _isBusy = true); - - try { - final sn = context.read(); - 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.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), - ], - ), - ), - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/lib/types/news.dart b/lib/types/news.dart index 06d17ec..1db174f 100644 --- a/lib/types/news.dart +++ b/lib/types/news.dart @@ -4,18 +4,23 @@ part 'news.freezed.dart'; part 'news.g.dart'; @freezed -abstract class SnNewsSource with _$SnNewsSource { - const factory SnNewsSource({ - required String id, - required String label, - required String type, - required String source, - required int depth, - required bool enabled, - }) = _SnNewsSource; +abstract class SnSubscriptionFeed with _$SnSubscriptionFeed { + const factory SnSubscriptionFeed({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required String url, + required bool isEnabled, + required bool isFullContent, + required int pullInterval, + required String adapter, + required int? accountId, + required DateTime? lastFetchedAt, + }) = _SnSubscriptionFeed; - factory SnNewsSource.fromJson(Map json) => - _$SnNewsSourceFromJson(json); + factory SnSubscriptionFeed.fromJson(Map json) => + _$SnSubscriptionFeedFromJson(json); } @freezed @@ -32,6 +37,7 @@ abstract class SnSubscriptionItem with _$SnSubscriptionItem { required String url, required String hash, required int feedId, + required SnSubscriptionFeed feed, required DateTime? publishedAt, }) = _SnSubscriptionItem; diff --git a/lib/types/news.freezed.dart b/lib/types/news.freezed.dart index a995cc1..f63ff28 100644 --- a/lib/types/news.freezed.dart +++ b/lib/types/news.freezed.dart @@ -14,149 +14,224 @@ part of 'news.dart'; T _$identity(T value) => value; /// @nodoc -mixin _$SnNewsSource { - String get id; - String get label; - String get type; - String get source; - int get depth; - bool get enabled; +mixin _$SnSubscriptionFeed { + int get id; + DateTime get createdAt; + DateTime get updatedAt; + DateTime? get deletedAt; + String get url; + 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. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $SnNewsSourceCopyWith get copyWith => - _$SnNewsSourceCopyWithImpl( - this as SnNewsSource, _$identity); + $SnSubscriptionFeedCopyWith get copyWith => + _$SnSubscriptionFeedCopyWithImpl( + this as SnSubscriptionFeed, _$identity); - /// Serializes this SnNewsSource to a JSON map. + /// Serializes this SnSubscriptionFeed to a JSON map. Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is SnNewsSource && + other is SnSubscriptionFeed && (identical(other.id, id) || other.id == id) && - (identical(other.label, label) || other.label == label) && - (identical(other.type, type) || other.type == type) && - (identical(other.source, source) || other.source == source) && - (identical(other.depth, depth) || other.depth == depth) && - (identical(other.enabled, enabled) || other.enabled == enabled)); + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (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) @override - int get hashCode => - Object.hash(runtimeType, id, label, type, source, depth, enabled); + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + url, + isEnabled, + isFullContent, + pullInterval, + adapter, + accountId, + lastFetchedAt); @override 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 -abstract mixin class $SnNewsSourceCopyWith<$Res> { - factory $SnNewsSourceCopyWith( - SnNewsSource value, $Res Function(SnNewsSource) _then) = - _$SnNewsSourceCopyWithImpl; +abstract mixin class $SnSubscriptionFeedCopyWith<$Res> { + factory $SnSubscriptionFeedCopyWith( + SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) = + _$SnSubscriptionFeedCopyWithImpl; @useResult $Res call( - {String id, - String label, - String type, - String source, - int depth, - bool enabled}); + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String url, + bool isEnabled, + bool isFullContent, + int pullInterval, + String adapter, + int? accountId, + DateTime? lastFetchedAt}); } /// @nodoc -class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> { - _$SnNewsSourceCopyWithImpl(this._self, this._then); +class _$SnSubscriptionFeedCopyWithImpl<$Res> + implements $SnSubscriptionFeedCopyWith<$Res> { + _$SnSubscriptionFeedCopyWithImpl(this._self, this._then); - final SnNewsSource _self; - final $Res Function(SnNewsSource) _then; + final SnSubscriptionFeed _self; + 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. @pragma('vm:prefer-inline') @override $Res call({ Object? id = null, - Object? label = null, - Object? type = null, - Object? source = null, - Object? depth = null, - Object? enabled = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? url = null, + Object? isEnabled = null, + Object? isFullContent = null, + Object? pullInterval = null, + Object? adapter = null, + Object? accountId = freezed, + Object? lastFetchedAt = freezed, }) { return _then(_self.copyWith( id: null == id ? _self.id : 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, - enabled: null == enabled - ? _self.enabled - : enabled // ignore: cast_nullable_to_non_nullable + 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?, + 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, + 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 @JsonSerializable() -class _SnNewsSource implements SnNewsSource { - const _SnNewsSource( +class _SnSubscriptionFeed implements SnSubscriptionFeed { + const _SnSubscriptionFeed( {required this.id, - required this.label, - required this.type, - required this.source, - required this.depth, - required this.enabled}); - factory _SnNewsSource.fromJson(Map json) => - _$SnNewsSourceFromJson(json); + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.url, + required this.isEnabled, + required this.isFullContent, + required this.pullInterval, + required this.adapter, + required this.accountId, + required this.lastFetchedAt}); + factory _SnSubscriptionFeed.fromJson(Map json) => + _$SnSubscriptionFeedFromJson(json); @override - final String id; + final int id; @override - final String label; + final DateTime createdAt; @override - final String type; + final DateTime updatedAt; @override - final String source; + final DateTime? deletedAt; @override - final int depth; + final String url; @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. @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - _$SnNewsSourceCopyWith<_SnNewsSource> get copyWith => - __$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity); + _$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith => + __$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity); @override Map toJson() { - return _$SnNewsSourceToJson( + return _$SnSubscriptionFeedToJson( this, ); } @@ -165,88 +240,142 @@ class _SnNewsSource implements SnNewsSource { bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _SnNewsSource && + other is _SnSubscriptionFeed && (identical(other.id, id) || other.id == id) && - (identical(other.label, label) || other.label == label) && - (identical(other.type, type) || other.type == type) && - (identical(other.source, source) || other.source == source) && - (identical(other.depth, depth) || other.depth == depth) && - (identical(other.enabled, enabled) || other.enabled == enabled)); + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (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) @override - int get hashCode => - Object.hash(runtimeType, id, label, type, source, depth, enabled); + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + url, + isEnabled, + isFullContent, + pullInterval, + adapter, + accountId, + lastFetchedAt); @override 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 -abstract mixin class _$SnNewsSourceCopyWith<$Res> - implements $SnNewsSourceCopyWith<$Res> { - factory _$SnNewsSourceCopyWith( - _SnNewsSource value, $Res Function(_SnNewsSource) _then) = - __$SnNewsSourceCopyWithImpl; +abstract mixin class _$SnSubscriptionFeedCopyWith<$Res> + implements $SnSubscriptionFeedCopyWith<$Res> { + factory _$SnSubscriptionFeedCopyWith( + _SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) = + __$SnSubscriptionFeedCopyWithImpl; @override @useResult $Res call( - {String id, - String label, - String type, - String source, - int depth, - bool enabled}); + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String url, + bool isEnabled, + bool isFullContent, + int pullInterval, + String adapter, + int? accountId, + DateTime? lastFetchedAt}); } /// @nodoc -class __$SnNewsSourceCopyWithImpl<$Res> - implements _$SnNewsSourceCopyWith<$Res> { - __$SnNewsSourceCopyWithImpl(this._self, this._then); +class __$SnSubscriptionFeedCopyWithImpl<$Res> + implements _$SnSubscriptionFeedCopyWith<$Res> { + __$SnSubscriptionFeedCopyWithImpl(this._self, this._then); - final _SnNewsSource _self; - final $Res Function(_SnNewsSource) _then; + final _SnSubscriptionFeed _self; + 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. @override @pragma('vm:prefer-inline') $Res call({ Object? id = null, - Object? label = null, - Object? type = null, - Object? source = null, - Object? depth = null, - Object? enabled = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? url = 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 ? _self.id : 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, - enabled: null == enabled - ? _self.enabled - : enabled // ignore: cast_nullable_to_non_nullable + 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?, + 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, + 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 hash; int get feedId; + SnSubscriptionFeed get feed; DateTime? get publishedAt; /// Create a copy of SnSubscriptionItem @@ -298,6 +428,7 @@ mixin _$SnSubscriptionItem { (identical(other.url, url) || other.url == url) && (identical(other.hash, hash) || other.hash == hash) && (identical(other.feedId, feedId) || other.feedId == feedId) && + (identical(other.feed, feed) || other.feed == feed) && (identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)); } @@ -317,11 +448,12 @@ mixin _$SnSubscriptionItem { url, hash, feedId, + feed, publishedAt); @override 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 hash, int feedId, + SnSubscriptionFeed feed, DateTime? publishedAt}); + + $SnSubscriptionFeedCopyWith<$Res> get feed; } /// @nodoc @@ -370,6 +505,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res> Object? url = null, Object? hash = null, Object? feedId = null, + Object? feed = null, Object? publishedAt = freezed, }) { return _then(_self.copyWith( @@ -417,12 +553,26 @@ class _$SnSubscriptionItemCopyWithImpl<$Res> ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable as int, + feed: null == feed + ? _self.feed + : feed // ignore: cast_nullable_to_non_nullable + as SnSubscriptionFeed, publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable 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 @@ -440,6 +590,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem { required this.url, required this.hash, required this.feedId, + required this.feed, required this.publishedAt}); factory _SnSubscriptionItem.fromJson(Map json) => _$SnSubscriptionItemFromJson(json); @@ -467,6 +618,8 @@ class _SnSubscriptionItem implements SnSubscriptionItem { @override final int feedId; @override + final SnSubscriptionFeed feed; + @override final DateTime? publishedAt; /// Create a copy of SnSubscriptionItem @@ -505,6 +658,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem { (identical(other.url, url) || other.url == url) && (identical(other.hash, hash) || other.hash == hash) && (identical(other.feedId, feedId) || other.feedId == feedId) && + (identical(other.feed, feed) || other.feed == feed) && (identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)); } @@ -524,11 +678,12 @@ class _SnSubscriptionItem implements SnSubscriptionItem { url, hash, feedId, + feed, publishedAt); @override 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 hash, int feedId, + SnSubscriptionFeed feed, DateTime? publishedAt}); + + @override + $SnSubscriptionFeedCopyWith<$Res> get feed; } /// @nodoc @@ -579,6 +738,7 @@ class __$SnSubscriptionItemCopyWithImpl<$Res> Object? url = null, Object? hash = null, Object? feedId = null, + Object? feed = null, Object? publishedAt = freezed, }) { return _then(_SnSubscriptionItem( @@ -626,12 +786,26 @@ class __$SnSubscriptionItemCopyWithImpl<$Res> ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable as int, + feed: null == feed + ? _self.feed + : feed // ignore: cast_nullable_to_non_nullable + as SnSubscriptionFeed, publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable 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 diff --git a/lib/types/news.g.dart b/lib/types/news.g.dart index b03451e..bf7588d 100644 --- a/lib/types/news.g.dart +++ b/lib/types/news.g.dart @@ -6,24 +6,38 @@ part of 'news.dart'; // JsonSerializableGenerator // ************************************************************************** -_SnNewsSource _$SnNewsSourceFromJson(Map json) => - _SnNewsSource( - id: json['id'] as String, - label: json['label'] as String, - type: json['type'] as String, - source: json['source'] as String, - depth: (json['depth'] as num).toInt(), - enabled: json['enabled'] as bool, +_SnSubscriptionFeed _$SnSubscriptionFeedFromJson(Map json) => + _SnSubscriptionFeed( + 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), + 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 _$SnNewsSourceToJson(_SnNewsSource instance) => +Map _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) => { 'id': instance.id, - 'label': instance.label, - 'type': instance.type, - 'source': instance.source, - 'depth': instance.depth, - 'enabled': instance.enabled, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'url': instance.url, + '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 json) => @@ -41,6 +55,7 @@ _SnSubscriptionItem _$SnSubscriptionItemFromJson(Map json) => url: json['url'] as String, hash: json['hash'] as String, feedId: (json['feed_id'] as num).toInt(), + feed: SnSubscriptionFeed.fromJson(json['feed'] as Map), publishedAt: json['published_at'] == null ? null : DateTime.parse(json['published_at'] as String), @@ -59,5 +74,6 @@ Map _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) => 'url': instance.url, 'hash': instance.hash, 'feed_id': instance.feedId, + 'feed': instance.feed.toJson(), 'published_at': instance.publishedAt?.toIso8601String(), }; diff --git a/lib/widgets/feed/feed_reader.dart b/lib/widgets/feed/feed_reader.dart index 9bff7bf..a2f02bc 100644 --- a/lib/widgets/feed/feed_reader.dart +++ b/lib/widgets/feed/feed_reader.dart @@ -1,6 +1,7 @@ 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:styled_widget/styled_widget.dart'; import 'package:surface/types/news.dart'; @@ -15,10 +16,7 @@ class NewsFeedEntry extends StatelessWidget { Widget build(BuildContext context) { final ele = SnSubscriptionItem.fromJson(data.data); - return Card( - elevation: 0, - color: Colors.transparent, - margin: EdgeInsets.zero, + return InkWell( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -53,11 +51,17 @@ class NewsFeedEntry extends StatelessWidget { Text(ele.description), Text(DateFormat().format(ele.createdAt.toLocal())) .tr() + .fontSize(13) .opacity(0.8), ], - ).padding(horizontal: 16), + ).padding(horizontal: 16, vertical: 4), ], ), + onTap: () { + GoRouter.of(context).pushNamed('readerFeedDetail', pathParameters: { + 'id': ele.id.toString(), + }); + }, ); } }