✨ News reader
This commit is contained in:
		| @@ -5,7 +5,7 @@ meta { | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=taiwan-pts | ||||
|   url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
| @@ -13,5 +13,5 @@ get { | ||||
| params:query { | ||||
|   take: 10 | ||||
|   offset: 0 | ||||
|   source: taiwan-pts | ||||
|   source: shadiao | ||||
| } | ||||
|   | ||||
| @@ -563,5 +563,6 @@ | ||||
|   "newsAllSources": "All News", | ||||
|   "newsReadingProviderSwap": "Swap", | ||||
|   "newsReadingFromReader": "You're reading from HyperNet.Reader", | ||||
|   "newsReadingFromOriginal": "You're reading the original article" | ||||
|   "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." | ||||
| } | ||||
|   | ||||
| @@ -561,5 +561,6 @@ | ||||
|   "newsAllSources": "所有新闻", | ||||
|   "newsReadingProviderSwap": "切换", | ||||
|   "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章", | ||||
|   "newsReadingFromOriginal": "你正在阅读原始文章" | ||||
|   "newsReadingFromOriginal": "你正在阅读原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。" | ||||
| } | ||||
|   | ||||
| @@ -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 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。" | ||||
| } | ||||
|   | ||||
| @@ -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 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。" | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,19 @@ | ||||
| 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; | ||||
| @@ -18,12 +26,14 @@ class NewsDetailScreen extends StatefulWidget { | ||||
|  | ||||
| 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((_) { | ||||
| @@ -35,6 +45,96 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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(); | ||||
| @@ -43,27 +143,6 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> { | ||||
|  | ||||
|   bool _isReadingFromReader = true; | ||||
|  | ||||
|   String get _htmlContent => """ | ||||
|       <!DOCTYPE html> | ||||
|       <html> | ||||
|       <head> | ||||
|         <style> | ||||
|           body { | ||||
|             background-color: #000000; | ||||
|             color: #fff; | ||||
|             font-family: Arial, sans-serif; | ||||
|             padding: 20px; | ||||
|           } | ||||
|         </style> | ||||
|       </head> | ||||
|       <body> | ||||
|         ${_article?.content ?? ''} | ||||
|       </body> | ||||
|       </html> | ||||
|     """; | ||||
|  | ||||
|   InAppWebViewController? _webViewController; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
| @@ -82,30 +161,68 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> { | ||||
|                 child: Text('newsReadingProviderSwap').tr(), | ||||
|                 onPressed: () { | ||||
|                   setState(() => _isReadingFromReader = !_isReadingFromReader); | ||||
|                   if (!_isReadingFromReader) { | ||||
|                     _webViewController?.goTo(historyItem: WebHistoryItem(url: WebUri(_article!.url))); | ||||
|                   } else { | ||||
|                     _webViewController?.goBack(); | ||||
|                   } | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Expanded( | ||||
|             child: InAppWebView( | ||||
|               key: Key('news-detail-webview-${widget.hash}-$_isReadingFromReader'), | ||||
|               onWebViewCreated: (controller) { | ||||
|                 _webViewController = controller; | ||||
|               }, | ||||
|               initialUrlRequest: URLRequest(url: WebUri(_article!.url)), | ||||
|               onLoadStop: (controller, url) { | ||||
|                 print("Loaded: $url"); | ||||
|               }, | ||||
|               onLoadError: (controller, url, code, message) { | ||||
|                 print("Error loading $url: $message ($code)"); | ||||
|               }, | ||||
|           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)), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -2,7 +2,9 @@ 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'; | ||||
| @@ -81,8 +83,12 @@ class _NewsScreenState extends State<NewsScreen> { | ||||
|           }, | ||||
|           body: TabBarView( | ||||
|             children: [ | ||||
|               _NewsArticleListWidget(), | ||||
|               for (final source in _sources!) _NewsArticleListWidget(source: source.id), | ||||
|               _NewsArticleListWidget(allSources: _sources!), | ||||
|               for (final source in _sources!) | ||||
|                 _NewsArticleListWidget( | ||||
|                   source: source.id, | ||||
|                   allSources: _sources!, | ||||
|                 ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
| @@ -93,8 +99,9 @@ class _NewsScreenState extends State<NewsScreen> { | ||||
|  | ||||
| class _NewsArticleListWidget extends StatefulWidget { | ||||
|   final String? source; | ||||
|   final List<SnNewsSource> allSources; | ||||
|  | ||||
|   const _NewsArticleListWidget({this.source}); | ||||
|   const _NewsArticleListWidget({this.source, required this.allSources}); | ||||
|  | ||||
|   @override | ||||
|   State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState(); | ||||
| @@ -154,6 +161,9 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> { | ||||
|             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, | ||||
| @@ -164,21 +174,43 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> { | ||||
|                   ); | ||||
|                 }, | ||||
|                 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('$baseUrl/${article.thumbnail}'), | ||||
|                           child: AutoResizeUniversalImage( | ||||
|                             article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}', | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|                     Text(article.description).textStyle(Theme.of(context).textTheme.bodyMedium!), | ||||
|                     const Gap(16), | ||||
|                     Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16), | ||||
|                     const Gap(8), | ||||
|                     Text(article.source).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                     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), | ||||
|                   ], | ||||
|                 ).padding(all: 8), | ||||
|                 ), | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class SnNewsArticle with _$SnNewsArticle { | ||||
|     required String url, | ||||
|     required String hash, | ||||
|     required String source, | ||||
|     required dynamic publishedAt, | ||||
|     required DateTime? publishedAt, | ||||
|   }) = _SnNewsArticle; | ||||
|  | ||||
|   factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json); | ||||
|   | ||||
| @@ -285,7 +285,7 @@ mixin _$SnNewsArticle { | ||||
|   String get url => throw _privateConstructorUsedError; | ||||
|   String get hash => throw _privateConstructorUsedError; | ||||
|   String get source => throw _privateConstructorUsedError; | ||||
|   dynamic get publishedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get publishedAt => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnNewsArticle to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
| @@ -315,7 +315,7 @@ abstract class $SnNewsArticleCopyWith<$Res> { | ||||
|       String url, | ||||
|       String hash, | ||||
|       String source, | ||||
|       dynamic publishedAt}); | ||||
|       DateTime? publishedAt}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @@ -394,7 +394,7 @@ class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle> | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|               as DateTime?, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
| @@ -419,7 +419,7 @@ abstract class _$$SnNewsArticleImplCopyWith<$Res> | ||||
|       String url, | ||||
|       String hash, | ||||
|       String source, | ||||
|       dynamic publishedAt}); | ||||
|       DateTime? publishedAt}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @@ -496,7 +496,7 @@ class __$$SnNewsArticleImplCopyWithImpl<$Res> | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|               as DateTime?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| @@ -544,7 +544,7 @@ class _$SnNewsArticleImpl implements _SnNewsArticle { | ||||
|   @override | ||||
|   final String source; | ||||
|   @override | ||||
|   final dynamic publishedAt; | ||||
|   final DateTime? publishedAt; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -571,8 +571,8 @@ class _$SnNewsArticleImpl implements _SnNewsArticle { | ||||
|             (identical(other.url, url) || other.url == url) && | ||||
|             (identical(other.hash, hash) || other.hash == hash) && | ||||
|             (identical(other.source, source) || other.source == source) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.publishedAt, publishedAt)); | ||||
|             (identical(other.publishedAt, publishedAt) || | ||||
|                 other.publishedAt == publishedAt)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -590,7 +590,7 @@ class _$SnNewsArticleImpl implements _SnNewsArticle { | ||||
|       url, | ||||
|       hash, | ||||
|       source, | ||||
|       const DeepCollectionEquality().hash(publishedAt)); | ||||
|       publishedAt); | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -621,7 +621,7 @@ abstract class _SnNewsArticle implements SnNewsArticle { | ||||
|       required final String url, | ||||
|       required final String hash, | ||||
|       required final String source, | ||||
|       required final dynamic publishedAt}) = _$SnNewsArticleImpl; | ||||
|       required final DateTime? publishedAt}) = _$SnNewsArticleImpl; | ||||
|  | ||||
|   factory _SnNewsArticle.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnNewsArticleImpl.fromJson; | ||||
| @@ -649,7 +649,7 @@ abstract class _SnNewsArticle implements SnNewsArticle { | ||||
|   @override | ||||
|   String get source; | ||||
|   @override | ||||
|   dynamic get publishedAt; | ||||
|   DateTime? get publishedAt; | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   | ||||
| @@ -39,7 +39,9 @@ _$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) => | ||||
|       url: json['url'] as String, | ||||
|       hash: json['hash'] as String, | ||||
|       source: json['source'] as String, | ||||
|       publishedAt: json['published_at'], | ||||
|       publishedAt: json['published_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['published_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) => | ||||
| @@ -55,5 +57,5 @@ Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) => | ||||
|       'url': instance.url, | ||||
|       'hash': instance.hash, | ||||
|       'source': instance.source, | ||||
|       'published_at': instance.publishedAt, | ||||
|       'published_at': instance.publishedAt?.toIso8601String(), | ||||
|     }; | ||||
|   | ||||
| @@ -939,7 +939,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "0.7.0" | ||||
|   html: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: html | ||||
|       sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" | ||||
|   | ||||
| @@ -116,6 +116,7 @@ dependencies: | ||||
|   video_compress: ^3.1.3 | ||||
|   cached_network_image: ^3.4.1 | ||||
|   flutter_inappwebview: ^6.1.5 | ||||
|   html: ^0.15.5 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user