♻️ 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',
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',

View File

@ -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',

View File

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

View File

@ -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<ChatRoomScreen> {
}
}
Future<void> _onCallJoin() async {
Future<void> _joinCall() async {
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) {
return await _joinCallWeb();
}
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final meet = JitsiMeet();
@ -156,6 +162,14 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
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) {
if (a == null || b == null) return false;
if (a.sender.accountId != b.sender.accountId) return false;
@ -237,7 +251,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (_currentMember != null)
IconButton(
icon: const Icon(Symbols.video_call),
onPressed: _onCallJoin,
onPressed: _joinCall,
onLongPress: _joinCallWeb,
),
IconButton(
icon: const Icon(Symbols.more_vert),

View File

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

View File

@ -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<NewsDetailScreen> createState() => _NewsDetailScreenState();
State<ReaderPageScreen> createState() => _ReaderPageScreenState();
}
class _NewsDetailScreenState extends State<NewsDetailScreen> {
class _ReaderPageScreenState extends State<ReaderPageScreen> {
SnSubscriptionItem? _article;
Future<void> _fetchArticle() async {
try {
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);
} catch (err) {
if (!mounted) return;

View File

@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
}
Future<void> _doCheckIn() async {
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
final captchaTk = await Navigator.of(context).push(
MaterialPageRoute(
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';
@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<String, dynamic> json) =>
_$SnNewsSourceFromJson(json);
factory SnSubscriptionFeed.fromJson(Map<String, dynamic> 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;

View File

@ -14,149 +14,224 @@ part of 'news.dart';
T _$identity<T>(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<SnNewsSource> get copyWith =>
_$SnNewsSourceCopyWithImpl<SnNewsSource>(
this as SnNewsSource, _$identity);
$SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith =>
_$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>(
this as SnSubscriptionFeed, _$identity);
/// Serializes this SnNewsSource to a JSON map.
/// Serializes this SnSubscriptionFeed to a JSON map.
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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

View File

@ -6,24 +6,38 @@ part of 'news.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) =>
Map<String, dynamic> _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) =>
<String, dynamic>{
'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<String, dynamic> json) =>
@ -41,6 +55,7 @@ _SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> 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<String, dynamic>),
publishedAt: json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
@ -59,5 +74,6 @@ Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
'url': instance.url,
'hash': instance.hash,
'feed_id': instance.feedId,
'feed': instance.feed.toJson(),
'published_at': instance.publishedAt?.toIso8601String(),
};

View File

@ -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(),
});
},
);
}
}