✨ News reader
This commit is contained in:
parent
963e538ae5
commit
5377161fb0
@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
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
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
@ -13,5 +13,5 @@ get {
|
|||||||
params:query {
|
params:query {
|
||||||
take: 10
|
take: 10
|
||||||
offset: 0
|
offset: 0
|
||||||
source: taiwan-pts
|
source: shadiao
|
||||||
}
|
}
|
||||||
|
@ -563,5 +563,6 @@
|
|||||||
"newsAllSources": "All News",
|
"newsAllSources": "All News",
|
||||||
"newsReadingProviderSwap": "Swap",
|
"newsReadingProviderSwap": "Swap",
|
||||||
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
"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": "所有新闻",
|
"newsAllSources": "所有新闻",
|
||||||
"newsReadingProviderSwap": "切换",
|
"newsReadingProviderSwap": "切换",
|
||||||
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||||
"newsReadingFromOriginal": "你正在阅读原始文章"
|
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||||
|
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。"
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@ -194,6 +195,10 @@
|
|||||||
"settingsFeatures": "功能",
|
"settingsFeatures": "功能",
|
||||||
"settingsNotifyWithHaptic": "新通知時振動",
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@ -552,5 +557,10 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"newsAllSources": "所有新聞",
|
||||||
|
"newsReadingProviderSwap": "切換",
|
||||||
|
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||||
|
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||||
|
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。"
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@ -194,6 +195,10 @@
|
|||||||
"settingsFeatures": "功能",
|
"settingsFeatures": "功能",
|
||||||
"settingsNotifyWithHaptic": "新通知時振動",
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@ -552,5 +557,10 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"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:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.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: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/providers/sn_network.dart';
|
||||||
import 'package:surface/types/news.dart';
|
import 'package:surface/types/news.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.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 {
|
class NewsDetailScreen extends StatefulWidget {
|
||||||
final String hash;
|
final String hash;
|
||||||
@ -18,12 +26,14 @@ class NewsDetailScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||||
SnNewsArticle? _article;
|
SnNewsArticle? _article;
|
||||||
|
dom.Document? _articleFragment;
|
||||||
|
|
||||||
Future<void> _fetchArticle() async {
|
Future<void> _fetchArticle() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||||
_article = SnNewsArticle.fromJson(resp.data);
|
_article = SnNewsArticle.fromJson(resp.data);
|
||||||
|
_articleFragment = parse(_article!.content);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err).then((_) {
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -43,27 +143,6 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
|||||||
|
|
||||||
bool _isReadingFromReader = true;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@ -82,30 +161,68 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
|||||||
child: Text('newsReadingProviderSwap').tr(),
|
child: Text('newsReadingProviderSwap').tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _isReadingFromReader = !_isReadingFromReader);
|
setState(() => _isReadingFromReader = !_isReadingFromReader);
|
||||||
if (!_isReadingFromReader) {
|
|
||||||
_webViewController?.goTo(historyItem: WebHistoryItem(url: WebUri(_article!.url)));
|
|
||||||
} else {
|
|
||||||
_webViewController?.goBack();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
if (_articleFragment != null && _isReadingFromReader)
|
||||||
child: InAppWebView(
|
Expanded(
|
||||||
key: Key('news-detail-webview-${widget.hash}-$_isReadingFromReader'),
|
child: SingleChildScrollView(
|
||||||
onWebViewCreated: (controller) {
|
child: Column(
|
||||||
_webViewController = controller;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
},
|
spacing: 8,
|
||||||
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
|
children: [
|
||||||
onLoadStop: (controller, url) {
|
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
print("Loaded: $url");
|
Builder(builder: (context) {
|
||||||
},
|
final htmlDescription = parse(_article!.description);
|
||||||
onLoadError: (controller, url, code, message) {
|
return Text(
|
||||||
print("Error loading $url: $message ($code)");
|
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:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/news.dart';
|
import 'package:surface/types/news.dart';
|
||||||
@ -81,8 +83,12 @@ class _NewsScreenState extends State<NewsScreen> {
|
|||||||
},
|
},
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_NewsArticleListWidget(),
|
_NewsArticleListWidget(allSources: _sources!),
|
||||||
for (final source in _sources!) _NewsArticleListWidget(source: source.id),
|
for (final source in _sources!)
|
||||||
|
_NewsArticleListWidget(
|
||||||
|
source: source.id,
|
||||||
|
allSources: _sources!,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -93,8 +99,9 @@ class _NewsScreenState extends State<NewsScreen> {
|
|||||||
|
|
||||||
class _NewsArticleListWidget extends StatefulWidget {
|
class _NewsArticleListWidget extends StatefulWidget {
|
||||||
final String? source;
|
final String? source;
|
||||||
|
final List<SnNewsSource> allSources;
|
||||||
|
|
||||||
const _NewsArticleListWidget({this.source});
|
const _NewsArticleListWidget({this.source, required this.allSources});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
||||||
@ -154,6 +161,9 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
|||||||
final baseUri = Uri.parse(article.url);
|
final baseUri = Uri.parse(article.url);
|
||||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||||
|
|
||||||
|
final htmlDescription = parse(article.description);
|
||||||
|
final date = article.publishedAt ?? article.createdAt;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
radius: 8,
|
radius: 8,
|
||||||
@ -164,21 +174,43 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
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!),
|
const Gap(16),
|
||||||
Text(article.description).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||||
const Gap(8),
|
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 url,
|
||||||
required String hash,
|
required String hash,
|
||||||
required String source,
|
required String source,
|
||||||
required dynamic publishedAt,
|
required DateTime? publishedAt,
|
||||||
}) = _SnNewsArticle;
|
}) = _SnNewsArticle;
|
||||||
|
|
||||||
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
||||||
|
@ -285,7 +285,7 @@ mixin _$SnNewsArticle {
|
|||||||
String get url => throw _privateConstructorUsedError;
|
String get url => throw _privateConstructorUsedError;
|
||||||
String get hash => throw _privateConstructorUsedError;
|
String get hash => throw _privateConstructorUsedError;
|
||||||
String get source => throw _privateConstructorUsedError;
|
String get source => throw _privateConstructorUsedError;
|
||||||
dynamic get publishedAt => throw _privateConstructorUsedError;
|
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnNewsArticle to a JSON map.
|
/// Serializes this SnNewsArticle to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@ -315,7 +315,7 @@ abstract class $SnNewsArticleCopyWith<$Res> {
|
|||||||
String url,
|
String url,
|
||||||
String hash,
|
String hash,
|
||||||
String source,
|
String source,
|
||||||
dynamic publishedAt});
|
DateTime? publishedAt});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -394,7 +394,7 @@ class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
|
|||||||
publishedAt: freezed == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _value.publishedAt
|
? _value.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,7 +419,7 @@ abstract class _$$SnNewsArticleImplCopyWith<$Res>
|
|||||||
String url,
|
String url,
|
||||||
String hash,
|
String hash,
|
||||||
String source,
|
String source,
|
||||||
dynamic publishedAt});
|
DateTime? publishedAt});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -496,7 +496,7 @@ class __$$SnNewsArticleImplCopyWithImpl<$Res>
|
|||||||
publishedAt: freezed == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _value.publishedAt
|
? _value.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -544,7 +544,7 @@ class _$SnNewsArticleImpl implements _SnNewsArticle {
|
|||||||
@override
|
@override
|
||||||
final String source;
|
final String source;
|
||||||
@override
|
@override
|
||||||
final dynamic publishedAt;
|
final DateTime? publishedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -571,8 +571,8 @@ class _$SnNewsArticleImpl implements _SnNewsArticle {
|
|||||||
(identical(other.url, url) || other.url == url) &&
|
(identical(other.url, url) || other.url == url) &&
|
||||||
(identical(other.hash, hash) || other.hash == hash) &&
|
(identical(other.hash, hash) || other.hash == hash) &&
|
||||||
(identical(other.source, source) || other.source == source) &&
|
(identical(other.source, source) || other.source == source) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.publishedAt, publishedAt) ||
|
||||||
.equals(other.publishedAt, publishedAt));
|
other.publishedAt == publishedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@ -590,7 +590,7 @@ class _$SnNewsArticleImpl implements _SnNewsArticle {
|
|||||||
url,
|
url,
|
||||||
hash,
|
hash,
|
||||||
source,
|
source,
|
||||||
const DeepCollectionEquality().hash(publishedAt));
|
publishedAt);
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnNewsArticle
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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 url,
|
||||||
required final String hash,
|
required final String hash,
|
||||||
required final String source,
|
required final String source,
|
||||||
required final dynamic publishedAt}) = _$SnNewsArticleImpl;
|
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
|
||||||
|
|
||||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
|
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
|
||||||
_$SnNewsArticleImpl.fromJson;
|
_$SnNewsArticleImpl.fromJson;
|
||||||
@ -649,7 +649,7 @@ abstract class _SnNewsArticle implements SnNewsArticle {
|
|||||||
@override
|
@override
|
||||||
String get source;
|
String get source;
|
||||||
@override
|
@override
|
||||||
dynamic get publishedAt;
|
DateTime? get publishedAt;
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnNewsArticle
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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,
|
url: json['url'] as String,
|
||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
source: json['source'] 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) =>
|
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
||||||
@ -55,5 +57,5 @@ Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
|||||||
'url': instance.url,
|
'url': instance.url,
|
||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'source': instance.source,
|
'source': instance.source,
|
||||||
'published_at': instance.publishedAt,
|
'published_at': instance.publishedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
@ -939,7 +939,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||||
|
@ -116,6 +116,7 @@ dependencies:
|
|||||||
video_compress: ^3.1.3
|
video_compress: ^3.1.3
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
|
html: ^0.15.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user