diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5b9a4a6..6d1ed51 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -83,5 +83,9 @@ "getApiKey": "Get API ID & Key", "getApiKeyHint": "In \"Preferences\" > \"Developer\"", "prev": "Previous", - "next": "Next" + "next": "Next", + "wentWrong": "Something went wrong.", + "retry": "Retry", + "copy": "Copy", + "errorLog": "Error log" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index bd780ca..e3dc545 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -83,5 +83,9 @@ "getApiKey": "获取 API ID & KEY", "getApiKeyHint": "在 “偏好设置” > “开发者” 下", "prev": "前一项", - "next": "后一项" + "next": "后一项", + "wentWrong": "发生错误", + "retry": "重试", + "copy": "复制", + "errorLog": "错误日志" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 86edd77..dfa3bb8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fluent_reader_lite/models/service.dart'; import 'package:fluent_reader_lite/pages/article_page.dart'; +import 'package:fluent_reader_lite/pages/error_log_page.dart'; import 'package:fluent_reader_lite/pages/settings/about_page.dart'; import 'package:fluent_reader_lite/pages/home_page.dart'; import 'package:fluent_reader_lite/pages/settings/feed_page.dart'; @@ -53,6 +54,7 @@ void main() async { class MyApp extends StatelessWidget { static final Map baseRoutes = { "/article": (context) => ArticlePage(), + "/error-log": (context) => ErrorLogPage(), "/settings": (context) => SettingsPage(), "/settings/sources": (context) => SourcesPage(), "/settings/sources/edit": (context) => SourceEditPage(), diff --git a/lib/models/sync_model.dart b/lib/models/sync_model.dart index 5aae2f6..722e78d 100644 --- a/lib/models/sync_model.dart +++ b/lib/models/sync_model.dart @@ -56,6 +56,7 @@ class SyncModel with ChangeNotifier { lastSyncSuccess = true; } catch(exp) { lastSyncSuccess = false; + Store.setErrorLog(exp.toString()); print(exp); } lastSynced = DateTime.now(); diff --git a/lib/pages/article_page.dart b/lib/pages/article_page.dart index 91af60a..9af9063 100644 --- a/lib/pages/article_page.dart +++ b/lib/pages/article_page.dart @@ -11,6 +11,7 @@ import 'package:fluent_reader_lite/utils/global.dart'; import 'package:fluent_reader_lite/utils/store.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; @@ -29,10 +30,14 @@ class ArticlePage extends StatefulWidget { ArticlePageState createState() => ArticlePageState(); } +enum _ArticleLoadState { + Loading, Success, Failure +} + class ArticlePageState extends State { WebViewController _controller; int requestId = 0; - bool loaded = false; + _ArticleLoadState loaded = _ArticleLoadState.Loading; bool navigated = false; SourceOpenTarget _target; String iid; @@ -44,7 +49,7 @@ class ArticlePageState extends State { } setState(() { iid = id; - loaded = false; + loaded = _ArticleLoadState.Loading; navigated = false; _target = null; if (isSource != null) isSourceFeed = isSource; @@ -70,12 +75,15 @@ class ArticlePageState extends State { var html = (await http.get(item.link)).body; a = Uri.encodeComponent(html); } catch(exp) { - setState(() { loaded = true; }); + if (mounted && currId == requestId) { + setState(() { loaded = _ArticleLoadState.Failure; }); + } return; } } else { a = Uri.encodeComponent(item.content); } + if (!mounted || currId != requestId) return; var h = '

${source.name}${(item.creator!=null&&item.creator.length>0)?' / '+item.creator:''}

'; h += '

${item.title}

'; h += '

${DateFormat.yMd(Localizations.localeOf(context).toString()).add_Hm().format(item.date)}

'; @@ -87,20 +95,20 @@ class ArticlePageState extends State { var brightness = Global.currentBrightness(context); localUrl += "&t=${brightness.index}"; } - if (currId == requestId) _controller.loadUrl(localUrl); + _controller.loadUrl(localUrl); } void _onPageReady(_) async { if (Platform.isAndroid || Global.globalModel.getBrightness() != null) { await Future.delayed(Duration(milliseconds: 300)); } - setState(() { loaded = true; }); + setState(() { loaded = _ArticleLoadState.Success; }); if (_target == SourceOpenTarget.Local || _target == SourceOpenTarget.FullContent) { navigated = true; } } void _onWebpageReady(_) { - if (loaded) navigated = true; + if (loaded == _ArticleLoadState.Success) navigated = true; } void _setOpenTarget(RSSSource source, {SourceOpenTarget target}) { @@ -112,7 +120,7 @@ class ArticlePageState extends State { void _loadOpenTarget(RSSItem item, RSSSource source) { setState(() { requestId += 1; - loaded = false; + loaded = _ArticleLoadState.Loading; navigated = false; }); switch (_target) { @@ -166,7 +174,7 @@ class ArticlePageState extends State { var source = tuple.item2; if (_target == null) _target = source.openTarget; final body = SafeArea(child: IndexedStack( - index: !loaded ? 0 : 1, + index: loaded.index, children: [ Center( child: CupertinoActivityIndicator() @@ -182,6 +190,21 @@ class ArticlePageState extends State { onPageFinished: _onWebpageReady, navigationDelegate: _onNavigate, ), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).wentWrong, + style: TextStyle(color: CupertinoColors.label.resolveFrom(context)), + ), + CupertinoButton( + child: Text(S.of(context).retry), + onPressed: () { _loadOpenTarget(item, source); }, + ), + ], + ), + ) ], ), bottom: false,); return CupertinoPageScaffold( @@ -208,6 +231,7 @@ class ArticlePageState extends State { ? S.of(context).markUnread : S.of(context).markRead, onPressed: () { + HapticFeedback.mediumImpact(); Global.itemsModel.updateItem(item.id, read: !item.hasRead); }, ), @@ -219,13 +243,25 @@ class ArticlePageState extends State { ? S.of(context).star : S.of(context).unstar, onPressed: () { + HapticFeedback.mediumImpact(); Global.itemsModel.updateItem(item.id, starred: !item.starred); }, ), CupertinoToolbarItem( icon: CupertinoIcons.share, semanticLabel: S.of(context).share, - onPressed: () { Share.share(item.link); }, + onPressed: () { + final media = MediaQuery.of(context); + Share.share( + item.link, + sharePositionOrigin: Rect.fromLTWH( + media.size.width - ArticlePage.state.currentContext.size.width / 2, + media.size.height - media.padding.bottom - 54, + 0, + 0 + ) + ); + }, ), CupertinoToolbarItem( icon: CupertinoIcons.chevron_up, diff --git a/lib/pages/error_log_page.dart b/lib/pages/error_log_page.dart new file mode 100644 index 0000000..dd27d85 --- /dev/null +++ b/lib/pages/error_log_page.dart @@ -0,0 +1,35 @@ +import 'package:fluent_reader_lite/components/list_tile_group.dart'; +import 'package:fluent_reader_lite/generated/l10n.dart'; +import 'package:fluent_reader_lite/utils/colors.dart'; +import 'package:fluent_reader_lite/utils/store.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ErrorLogPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final errorLog = Store.getErrorLog(); + return CupertinoPageScaffold( + backgroundColor: MyColors.background, + navigationBar: CupertinoNavigationBar( + middle: Text(S.of(context).errorLog), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + child: Text(S.of(context).copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: errorLog)); + }, + ), + ), + child: ListView(children: [ + ListTileGroup([ + SelectableText( + errorLog, + style: TextStyle(color: CupertinoColors.label.resolveFrom(context)), + ), + ]), + ]), + ); + } +} \ No newline at end of file diff --git a/lib/pages/item_list_page.dart b/lib/pages/item_list_page.dart index 18cc411..99094b0 100644 --- a/lib/pages/item_list_page.dart +++ b/lib/pages/item_list_page.dart @@ -241,7 +241,13 @@ class _ItemListPageState extends State { ], mainAxisAlignment: MainAxisAlignment.spaceBetween), onPressed: () { Navigator.of(context, rootNavigator: true).pop(); - Share.share(item.link); + final media = MediaQuery.of(context); + Share.share( + item.link, + sharePositionOrigin: Rect.fromLTWH( + 160, media.size.height - media.padding.bottom, 0, 0 + ), + ); }, ), ], diff --git a/lib/pages/settings/source_edit_page.dart b/lib/pages/settings/source_edit_page.dart index 87ee24c..1f4f159 100644 --- a/lib/pages/settings/source_edit_page.dart +++ b/lib/pages/settings/source_edit_page.dart @@ -58,7 +58,10 @@ class SourceEditPage extends StatelessWidget { final urlTile = ListTileGroup([ MyListTile( title: Flexible(child: Text(source.url, style: urlStyle, overflow: TextOverflow.ellipsis)), - trailing: Icon(CupertinoIcons.doc_on_clipboard), + trailing: Icon( + CupertinoIcons.doc_on_clipboard, + semanticLabel: S.of(context).copy, + ), onTap: () { Clipboard.setData(ClipboardData(text: source.url)); }, trailingChevron: false, withDivider: false, diff --git a/lib/pages/subscription_list_page.dart b/lib/pages/subscription_list_page.dart index 5856e0f..a92953b 100644 --- a/lib/pages/subscription_list_page.dart +++ b/lib/pages/subscription_list_page.dart @@ -13,6 +13,7 @@ import 'package:fluent_reader_lite/utils/colors.dart'; import 'package:fluent_reader_lite/utils/global.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:provider/provider.dart'; @@ -98,6 +99,13 @@ class _SubscriptionListPageState extends State { Navigator.of(context, rootNavigator: true).pushNamed("/settings"); } + void _openErrorLog() { + if (!Global.syncModel.lastSyncSuccess) { + HapticFeedback.mediumImpact(); + Navigator.of(context, rootNavigator: true).pushNamed("/error-log"); + } + } + @override Widget build(BuildContext context) { final navigationBar = CupertinoSliverNavigationBar( @@ -166,23 +174,26 @@ class _SubscriptionListPageState extends State { final syncInfo = Consumer( builder: (context, syncModel, child) { return SliverToBoxAdapter( - child: Container( - padding: EdgeInsets.all(12), - child: Column( - children: [ - Text( - syncModel.lastSyncSuccess - ? S.of(context).lastSyncSuccess - : S.of(context).lastSyncFailure, - style: syncStyle, - ), - Text( - DateFormat - .Md(Localizations.localeOf(context).toString()) - .add_Hm().format(syncModel.lastSynced), - style: syncStyle, - ), - ], + child: GestureDetector( + onLongPress: _openErrorLog, + child: Container( + padding: EdgeInsets.all(12), + child: Column( + children: [ + Text( + syncModel.lastSyncSuccess + ? S.of(context).lastSyncSuccess + : S.of(context).lastSyncFailure, + style: syncStyle, + ), + Text( + DateFormat + .Md(Localizations.localeOf(context).toString()) + .add_Hm().format(syncModel.lastSynced), + style: syncStyle, + ), + ], + ), ), ), ); diff --git a/lib/utils/store.dart b/lib/utils/store.dart index d57a10d..3ecc734 100644 --- a/lib/utils/store.dart +++ b/lib/utils/store.dart @@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; abstract class StoreKeys { static const GROUPS = "groups"; + static const ERROR_LOG = "errorLog"; // General static const THEME = "theme"; @@ -97,4 +98,12 @@ class Store { static void setArticleFontSize(int value) { sp.setInt(StoreKeys.ARTICLE_FONT_SIZE, value); } + + static String getErrorLog() { + return sp.getString(StoreKeys.ERROR_LOG) ?? ""; + } + + static void setErrorLog(String value) { + sp.setString(StoreKeys.ERROR_LOG, value); + } } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3f3baa2..612f7d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+3 +version: 1.0.0+4 environment: sdk: ">=2.7.0 <3.0.0"