Compare commits

...

2 Commits

Author SHA1 Message Date
5377161fb0 News reader 2025-01-26 12:38:43 +08:00
963e538ae5 News Reader Basis 2025-01-26 02:12:03 +08:00
22 changed files with 1428 additions and 72 deletions

View File

@ -19,6 +19,10 @@
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -0,0 +1,11 @@
meta {
name: List News Sources
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/re/well-known/sources
body: none
auth: none
}

17
api/Reader/List News.bru Normal file
View File

@ -0,0 +1,17 @@
meta {
name: List News
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
body: none
auth: none
}
params:query {
take: 10
offset: 0
source: shadiao
}

View File

@ -0,0 +1,17 @@
meta {
name: Trigger Scan News
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/re/admin/scan
body: json
auth: inherit
}
body:json {
{
"eager": true
}
}

View File

@ -17,6 +17,7 @@
"screenAccountProfileEdit": "Edit Profile",
"screenAbuseReport": "Abuse Reports",
"screenSettings": "Settings",
"screenNews": "News",
"screenAlbum": "Album",
"screenChat": "Chat",
"screenChatManage": "Edit Channel",
@ -558,5 +559,10 @@
"postCategoryKnowledge": "Knowledge",
"postCategoryLiterature": "Literature",
"postCategoryFunny": "Funny",
"postCategoryUncategorized": "Uncategorized"
"postCategoryUncategorized": "Uncategorized",
"newsAllSources": "All News",
"newsReadingProviderSwap": "Swap",
"newsReadingFromReader": "You're reading from HyperNet.Reader",
"newsReadingFromOriginal": "You're reading the original article",
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author."
}

View File

@ -15,6 +15,7 @@
"screenAccountProfileEdit": "编辑资料",
"screenAbuseReport": "滥用检举",
"screenSettings": "设置",
"screenNews": "新闻",
"screenAlbum": "相册",
"screenChat": "聊天",
"screenChatManage": "编辑聊天频道",
@ -556,5 +557,10 @@
"postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类"
"postCategoryUncategorized": "未分类",
"newsAllSources": "所有新闻",
"newsReadingProviderSwap": "切换",
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
"newsReadingFromOriginal": "你正在阅读原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。"
}

View File

@ -15,6 +15,7 @@
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設置",
"screenNews": "新聞",
"screenAlbum": "相冊",
"screenChat": "聊天",
"screenChatManage": "編輯聊天頻道",
@ -194,6 +195,10 @@
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -552,5 +557,10 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類"
"postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
"newsReadingFromOriginal": "你正在閲讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。"
}

View File

@ -15,6 +15,7 @@
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設置",
"screenNews": "新聞",
"screenAlbum": "相冊",
"screenChat": "聊天",
"screenChatManage": "編輯聊天頻道",
@ -194,6 +195,10 @@
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -552,5 +557,10 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類"
"postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
"newsReadingFromOriginal": "你正在閱讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。"
}

View File

@ -105,6 +105,13 @@ PODS:
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_native_splash (2.4.3):
- Flutter
- flutter_udid (0.0.1):
@ -188,6 +195,7 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
@ -239,6 +247,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
@ -282,6 +291,7 @@ SPEC REPOS:
- GoogleUtilities
- Kingfisher
- nanopb
- OrderedSet
- PromisesObjC
- SAMKeychain
- SDWebImage
@ -309,6 +319,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_udid:
@ -380,6 +392,7 @@ SPEC CHECKSUMS:
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
@ -396,6 +409,7 @@ SPEC CHECKSUMS:
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46

View File

@ -58,6 +58,11 @@ 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.photo_library, weight: 400, opticalSize: 20),
screen: 'album',
@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
List<AppNavDestination> destinations = [];
int get pinnedDestinationCount =>
destinations.where((ele) => ele.isPinned).length;
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
NavigationProvider() {
buildDestinations(kDefaultPinnedDestination);
@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
}
bool isIndexInRange(int min, int max) {
return _currentIndex != null &&
_currentIndex! >= min &&
_currentIndex! < max;
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
}
void autoDetectIndex(GoRouter? state) {
if (state == null) return;
final idx = destinations.indexWhere(
(ele) =>
ele.screen ==
state.routerDelegate.currentConfiguration.last.route.name,
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
);
_currentIndex = idx == -1 ? null : idx;
notifyListeners();

View File

@ -19,6 +19,8 @@ import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart';
import 'package:surface/screens/news/news_detail.dart';
import 'package:surface/screens/news/news_list.dart';
import 'package:surface/screens/notification.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/screens/post/post_editor.dart';
@ -31,7 +33,6 @@ import 'package:surface/screens/settings.dart';
import 'package:surface/screens/sharing.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(
@ -48,18 +49,12 @@ final _appRoutes = [
GoRoute(
path: '/',
name: 'home',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: const HomeScreen(),
),
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/posts',
name: 'explore',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: const ExploreScreen(),
),
builder: (context, state) => const ExploreScreen(),
routes: [
GoRoute(
path: '/write/:mode',
@ -104,64 +99,42 @@ final _appRoutes = [
GoRoute(
path: '/account',
name: 'account',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: const AccountScreen(),
),
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/chat',
name: 'chat',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: const ChatScreen(),
),
builder: (context, state) => const ChatScreen(),
routes: [
GoRoute(
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => AppBackground(
child: ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
builder: (context, state) => ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/:scope/:alias/call',
name: 'chatCallRoom',
builder: (context, state) => AppBackground(
child: CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
builder: (context, state) => CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => AppBackground(
child: ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
builder: (context, state) => ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
pageBuilder: (context, state) => CustomTransitionPage(
child: ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
builder: (context, state) => ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
),
],
@ -182,36 +155,40 @@ final _appRoutes = [
GoRoute(
path: '/manage',
name: 'realmManage',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
),
builder: (context, state) => RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
),
),
],
),
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: '/album',
name: 'album',
pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: _fadeThroughTransition,
child: const AlbumScreen(),
),
builder: (context, state) => const AlbumScreen(),
),
GoRoute(
path: '/friend',
name: 'friend',
pageBuilder: (context, state) => NoTransitionPage(
child: const FriendScreen(),
),
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/notification',
name: 'notification',
pageBuilder: (context, state) => NoTransitionPage(
child: const NotificationScreen(),
),
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: '/auth/login',

View File

@ -0,0 +1,230 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:html/dom.dart' as dom;
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/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
class NewsDetailScreen extends StatefulWidget {
final String hash;
const NewsDetailScreen({super.key, required this.hash});
@override
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
}
class _NewsDetailScreenState extends State<NewsDetailScreen> {
SnNewsArticle? _article;
dom.Document? _articleFragment;
Future<void> _fetchArticle() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
_article = SnNewsArticle.fromJson(resp.data);
_articleFragment = parse(_article!.content);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err).then((_) {
if (!mounted) return;
Navigator.pop(context);
});
} finally {
setState(() {});
}
}
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
if (elements == null) return [];
final List<Widget> widgets = [];
for (final node in elements) {
switch (node.localName) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
break;
case 'p':
if (node.text.trim().isEmpty) continue;
widgets.add(
Text.rich(
TextSpan(
text: node.text.trim(),
children: [
for (final child in node.children)
switch (child.localName) {
'a' => TextSpan(
text: child.text.trim(),
style: const TextStyle(decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString(child.attributes['href']!);
},
),
_ => TextSpan(text: child.text.trim()),
},
],
),
style: Theme.of(context).textTheme.bodyLarge,
),
);
break;
case 'a':
// drop single link
break;
case 'div':
// ignore div text, normally it is not meaningful
widgets.addAll(_parseHtmlToWidgets(node.children));
break;
case 'hr':
widgets.add(const Divider());
break;
case 'img':
var src = node.attributes['src'];
if (src == null) break;
final width = double.tryParse(node.attributes['width'] ?? 'null');
final height = double.tryParse(node.attributes['height'] ?? 'null');
final ratio = width != null && height != null ? width / height : 1.0;
if (!src.startsWith('http')) {
final baseUri = Uri.parse(_article!.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
src = '$baseUrl/$src';
}
widgets.add(
AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
height: height ?? double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(src, fit: BoxFit.cover),
),
),
),
);
break;
default:
widgets.addAll(_parseHtmlToWidgets(node.children));
break;
}
}
return widgets;
}
@override
void initState() {
super.initState();
_fetchArticle();
}
bool _isReadingFromReader = true;
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text(_article?.title ?? 'loading'.tr()),
),
body: Column(
children: [
MaterialBanner(
dividerColor: Colors.transparent,
leading: const Icon(Icons.info),
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
actions: [
TextButton(
child: Text('newsReadingProviderSwap').tr(),
onPressed: () {
setState(() => _isReadingFromReader = !_isReadingFromReader);
},
),
],
),
if (_articleFragment != null && _isReadingFromReader)
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
Builder(builder: (context) {
final htmlDescription = parse(_article!.description);
return Text(
htmlDescription.children.map((ele) => ele.text.trim()).join(),
style: Theme.of(context).textTheme.bodyMedium,
);
}),
Builder(builder: (context) {
final date = _article!.publishedAt ?? _article!.createdAt;
return 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);
}),
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
const Divider(),
..._parseHtmlToWidgets(_articleFragment!.children),
const Divider(),
InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Reference from original website',
style: TextStyle(decoration: TextDecoration.underline),
),
const Gap(4),
Icon(Icons.launch, size: 16),
],
).opacity(0.85),
onTap: () {
launchUrlString(_article!.url);
},
),
Gap(MediaQuery.of(context).padding.bottom),
],
).padding(horizontal: 12, vertical: 16),
),
)
else if (_article != null)
Expanded(
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
),
),
],
),
);
}
}

View File

@ -0,0 +1,221 @@
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(),
bottom: TabBar(
isScrollable: true,
tabs: [
Tab(child: Text('newsAllSources'.tr())),
for (final source in _sources!) Tab(child: Text(source.label)),
],
),
),
),
];
},
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<SnNewsArticle> _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<SnNewsArticle>.from(
resp.data['data']?.map((e) => SnNewsArticle.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: 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.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
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.source).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),
],
),
),
);
},
),
),
);
}
}

38
lib/types/news.dart Normal file
View File

@ -0,0 +1,38 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'news.freezed.dart';
part 'news.g.dart';
@freezed
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;
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
}
@freezed
class SnNewsArticle with _$SnNewsArticle {
const factory SnNewsArticle({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required dynamic deletedAt,
required String thumbnail,
required String title,
required String description,
required String content,
required String url,
required String hash,
required String source,
required DateTime? publishedAt,
}) = _SnNewsArticle;
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
}

660
lib/types/news.freezed.dart Normal file
View File

@ -0,0 +1,660 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'news.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
return _SnNewsSource.fromJson(json);
}
/// @nodoc
mixin _$SnNewsSource {
String get id => throw _privateConstructorUsedError;
String get label => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
String get source => throw _privateConstructorUsedError;
int get depth => throw _privateConstructorUsedError;
bool get enabled => throw _privateConstructorUsedError;
/// Serializes this SnNewsSource to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnNewsSourceCopyWith<$Res> {
factory $SnNewsSourceCopyWith(
SnNewsSource value, $Res Function(SnNewsSource) then) =
_$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
@useResult
$Res call(
{String id,
String label,
String type,
String source,
int depth,
bool enabled});
}
/// @nodoc
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
implements $SnNewsSourceCopyWith<$Res> {
_$SnNewsSourceCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnNewsSource
/// 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,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _value.depth
: depth // ignore: cast_nullable_to_non_nullable
as int,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnNewsSourceImplCopyWith<$Res>
implements $SnNewsSourceCopyWith<$Res> {
factory _$$SnNewsSourceImplCopyWith(
_$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
__$$SnNewsSourceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String label,
String type,
String source,
int depth,
bool enabled});
}
/// @nodoc
class __$$SnNewsSourceImplCopyWithImpl<$Res>
extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
implements _$$SnNewsSourceImplCopyWith<$Res> {
__$$SnNewsSourceImplCopyWithImpl(
_$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
: super(_value, _then);
/// Create a copy of SnNewsSource
/// 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,
}) {
return _then(_$SnNewsSourceImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _value.depth
: depth // ignore: cast_nullable_to_non_nullable
as int,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnNewsSourceImpl implements _SnNewsSource {
const _$SnNewsSourceImpl(
{required this.id,
required this.label,
required this.type,
required this.source,
required this.depth,
required this.enabled});
factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
_$$SnNewsSourceImplFromJson(json);
@override
final String id;
@override
final String label;
@override
final String type;
@override
final String source;
@override
final int depth;
@override
final bool enabled;
@override
String toString() {
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnNewsSourceImpl &&
(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));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, id, label, type, source, depth, enabled);
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
__$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnNewsSourceImplToJson(
this,
);
}
}
abstract class _SnNewsSource implements SnNewsSource {
const factory _SnNewsSource(
{required final String id,
required final String label,
required final String type,
required final String source,
required final int depth,
required final bool enabled}) = _$SnNewsSourceImpl;
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
_$SnNewsSourceImpl.fromJson;
@override
String get id;
@override
String get label;
@override
String get type;
@override
String get source;
@override
int get depth;
@override
bool get enabled;
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
return _SnNewsArticle.fromJson(json);
}
/// @nodoc
mixin _$SnNewsArticle {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
dynamic get deletedAt => throw _privateConstructorUsedError;
String get thumbnail => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
String get content => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
String get hash => throw _privateConstructorUsedError;
String get source => throw _privateConstructorUsedError;
DateTime? get publishedAt => throw _privateConstructorUsedError;
/// Serializes this SnNewsArticle to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnNewsArticleCopyWith<$Res> {
factory $SnNewsArticleCopyWith(
SnNewsArticle value, $Res Function(SnNewsArticle) then) =
_$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
DateTime? publishedAt});
}
/// @nodoc
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
implements $SnNewsArticleCopyWith<$Res> {
_$SnNewsArticleCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? thumbnail = null,
Object? title = null,
Object? description = null,
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? publishedAt = freezed,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
hash: null == hash
? _value.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
publishedAt: freezed == publishedAt
? _value.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnNewsArticleImplCopyWith<$Res>
implements $SnNewsArticleCopyWith<$Res> {
factory _$$SnNewsArticleImplCopyWith(
_$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
__$$SnNewsArticleImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
DateTime? publishedAt});
}
/// @nodoc
class __$$SnNewsArticleImplCopyWithImpl<$Res>
extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
implements _$$SnNewsArticleImplCopyWith<$Res> {
__$$SnNewsArticleImplCopyWithImpl(
_$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
: super(_value, _then);
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? thumbnail = null,
Object? title = null,
Object? description = null,
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? publishedAt = freezed,
}) {
return _then(_$SnNewsArticleImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
hash: null == hash
? _value.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
publishedAt: freezed == publishedAt
? _value.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnNewsArticleImpl implements _SnNewsArticle {
const _$SnNewsArticleImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.thumbnail,
required this.title,
required this.description,
required this.content,
required this.url,
required this.hash,
required this.source,
required this.publishedAt});
factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
_$$SnNewsArticleImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final dynamic deletedAt;
@override
final String thumbnail;
@override
final String title;
@override
final String description;
@override
final String content;
@override
final String url;
@override
final String hash;
@override
final String source;
@override
final DateTime? publishedAt;
@override
String toString() {
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnNewsArticleImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
const DeepCollectionEquality().hash(deletedAt),
thumbnail,
title,
description,
content,
url,
hash,
source,
publishedAt);
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
__$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnNewsArticleImplToJson(
this,
);
}
}
abstract class _SnNewsArticle implements SnNewsArticle {
const factory _SnNewsArticle(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final dynamic deletedAt,
required final String thumbnail,
required final String title,
required final String description,
required final String content,
required final String url,
required final String hash,
required final String source,
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
_$SnNewsArticleImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
dynamic get deletedAt;
@override
String get thumbnail;
@override
String get title;
@override
String get description;
@override
String get content;
@override
String get url;
@override
String get hash;
@override
String get source;
@override
DateTime? get publishedAt;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
throw _privateConstructorUsedError;
}

61
lib/types/news.g.dart Normal file
View File

@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'news.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
_$SnNewsSourceImpl(
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,
);
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
<String, dynamic>{
'id': instance.id,
'label': instance.label,
'type': instance.type,
'source': instance.source,
'depth': instance.depth,
'enabled': instance.enabled,
};
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
_$SnNewsArticleImpl(
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'],
thumbnail: json['thumbnail'] as String,
title: json['title'] as String,
description: json['description'] as String,
content: json['content'] as String,
url: json['url'] as String,
hash: json['hash'] as String,
source: json['source'] as String,
publishedAt: json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
);
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt,
'thumbnail': instance.thumbnail,
'title': instance.title,
'description': instance.description,
'content': instance.content,
'url': instance.url,
'hash': instance.hash,
'source': instance.source,
'published_at': instance.publishedAt?.toIso8601String(),
};

View File

@ -13,6 +13,7 @@ import file_selector_macos
import firebase_analytics
import firebase_core
import firebase_messaging
import flutter_inappwebview_macos
import flutter_udid
import flutter_webrtc
import gal
@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))

View File

@ -675,6 +675,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
url: "https://pub.dev"
source: hosted
version: "1.3.0+1"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_windows:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -875,7 +939,7 @@ packages:
source: hosted
version: "0.7.0"
html:
dependency: transitive
dependency: "direct main"
description:
name: html
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"

View File

@ -115,6 +115,8 @@ dependencies:
slide_countdown: ^2.0.2
video_compress: ^3.1.3
cached_network_image: ^3.4.1
flutter_inappwebview: ^6.1.5
html: ^0.15.5
dev_dependencies:
flutter_test:

View File

@ -16,6 +16,8 @@
-->
<base href="$FLUTTER_BASE_HREF">
<script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">

View File

@ -11,6 +11,7 @@
#include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h>
@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
FlutterWebRTCPluginRegisterWithRegistrar(

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_saver
file_selector_windows
firebase_core
flutter_inappwebview_windows
flutter_udid
flutter_webrtc
gal