From 438ed4ce0837581b637fa6778f9b2fbc8eb9649e Mon Sep 17 00:00:00 2001 From: Bruce Liu Date: Sun, 31 Jan 2021 19:01:06 +0800 Subject: [PATCH] add service import through uri scheme --- android/app/src/main/AndroidManifest.xml | 10 ++++ ios/Podfile.lock | 6 ++ ios/Runner/Info.plist | 17 +++++- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh.arb | 3 +- lib/models/groups_model.dart | 1 + lib/models/service.dart | 8 ++- lib/models/services/feedbin.dart | 2 +- lib/models/services/fever.dart | 2 +- lib/models/services/greader.dart | 2 +- lib/models/services/service_import.dart | 22 +++++++ lib/pages/home_page.dart | 57 +++++++++++++++++++ lib/pages/settings/services/feedbin_page.dart | 22 +++++++ lib/pages/settings/services/fever_page.dart | 38 +++++++++++-- lib/pages/settings/services/greader_page.dart | 22 +++++++ .../settings/services/inoreader_page.dart | 30 +++++++++- lib/utils/global.dart | 2 +- lib/utils/utils.dart | 4 +- pubspec.lock | 7 +++ pubspec.yaml | 1 + 20 files changed, 242 insertions(+), 17 deletions(-) create mode 100644 lib/models/services/service_import.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5acdde9..15966b3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,6 +41,16 @@ + + + + + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ff34946..0e52529 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -14,6 +14,8 @@ PODS: - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - uni_links (0.0.1): + - Flutter - url_launcher (0.0.1): - Flutter - webview_flutter (0.0.1): @@ -26,6 +28,7 @@ DEPENDENCIES: - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) @@ -46,6 +49,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + uni_links: + :path: ".symlinks/plugins/uni_links/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" webview_flutter: @@ -59,6 +64,7 @@ SPEC CHECKSUMS: share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ec7c27b..cb023d0 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,8 +2,6 @@ - ITSAppUsesNonExemptEncryption - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -22,8 +20,23 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + me.hyliu.fluent-reader-lite + CFBundleURLSchemes + + fluent-reader + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2cadb8c..8d0933b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -90,5 +90,6 @@ "errorLog": "Error log", "unreadSourceTip": "You can long press on the title of this page to toggle between all and unread subscriptions.", "uncategorized": "Uncategorized", - "showUncategorized": "Show uncategorized" + "showUncategorized": "Show uncategorized", + "serviceExists": "A service already exists. Please log out before importing." } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 88ad962..bf988dc 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -90,5 +90,6 @@ "errorLog": "错误日志", "unreadSourceTip": "您可以长按此页面的标题来切换全部订阅源或仅未读订阅源。", "uncategorized": "未分组", - "showUncategorized": "显示“未分组”" + "showUncategorized": "显示“未分组”", + "serviceExists": "已登录至一个服务,请在导入前登出。" } \ No newline at end of file diff --git a/lib/models/groups_model.dart b/lib/models/groups_model.dart index 268e36a..b0dbe3c 100644 --- a/lib/models/groups_model.dart +++ b/lib/models/groups_model.dart @@ -9,6 +9,7 @@ class GroupsModel with ChangeNotifier { Map> get groups => _groups; set groups(Map> groups) { _groups = groups; + updateUncategorized(); notifyListeners(); Store.setGroups(groups); } diff --git a/lib/models/service.dart b/lib/models/service.dart index b88121a..e7af583 100644 --- a/lib/models/service.dart +++ b/lib/models/service.dart @@ -1,5 +1,7 @@ import 'package:fluent_reader_lite/models/item.dart'; import 'package:fluent_reader_lite/models/source.dart'; +import 'package:fluent_reader_lite/utils/global.dart'; +import 'package:fluent_reader_lite/utils/store.dart'; import 'package:tuple/tuple.dart'; enum SyncService { @@ -7,7 +9,11 @@ enum SyncService { } abstract class ServiceHandler { - void remove(); + void remove() { + Store.sp.remove(StoreKeys.SYNC_SERVICE); + Global.groupsModel.groups = Map(); + Global.groupsModel.showUncategorized = false; + } Future validate(); Future reauthenticate() async { } Future, Map>>> getSources(); diff --git a/lib/models/services/feedbin.dart b/lib/models/services/feedbin.dart index 6befab1..c1d896e 100644 --- a/lib/models/services/feedbin.dart +++ b/lib/models/services/feedbin.dart @@ -49,7 +49,7 @@ class FeedbinServiceHandler extends ServiceHandler { @override void remove() { - Store.sp.remove(StoreKeys.SYNC_SERVICE); + super.remove(); Store.sp.remove(StoreKeys.ENDPOINT); Store.sp.remove(StoreKeys.USERNAME); Store.sp.remove(StoreKeys.PASSWORD); diff --git a/lib/models/services/fever.dart b/lib/models/services/fever.dart index 24dc91d..236a58f 100644 --- a/lib/models/services/fever.dart +++ b/lib/models/services/fever.dart @@ -50,7 +50,7 @@ class FeverServiceHandler extends ServiceHandler { @override void remove() { - Store.sp.remove(StoreKeys.SYNC_SERVICE); + super.remove(); Store.sp.remove(StoreKeys.ENDPOINT); Store.sp.remove(StoreKeys.USERNAME); Store.sp.remove(StoreKeys.PASSWORD); diff --git a/lib/models/services/greader.dart b/lib/models/services/greader.dart index 67a6bad..6b97388 100644 --- a/lib/models/services/greader.dart +++ b/lib/models/services/greader.dart @@ -79,7 +79,7 @@ class GReaderServiceHandler extends ServiceHandler { @override void remove() { - Store.sp.remove(StoreKeys.SYNC_SERVICE); + super.remove(); Store.sp.remove(StoreKeys.ENDPOINT); Store.sp.remove(StoreKeys.USERNAME); Store.sp.remove(StoreKeys.PASSWORD); diff --git a/lib/models/services/service_import.dart b/lib/models/services/service_import.dart new file mode 100644 index 0000000..a99e7a0 --- /dev/null +++ b/lib/models/services/service_import.dart @@ -0,0 +1,22 @@ +class ServiceImport { + String endpoint; + String username; + String password; + String apiId; + String apiKey; + + static const typeMap = { + "f": "/settings/service/fever", + "g": "/settings/service/greader", + "i": "/settings/service/inoreader", + "fb": "/settings/service/feedbin" + }; + + ServiceImport(Map params) { + endpoint = params["e"]; + username = params["u"]; + password = params["p"]; + apiId = params["i"]; + apiKey = params["k"]; + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 01feb91..07550a6 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:fluent_reader_lite/generated/l10n.dart'; import 'package:fluent_reader_lite/main.dart'; +import 'package:fluent_reader_lite/models/services/service_import.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/setup_page.dart'; import 'package:fluent_reader_lite/pages/subscription_list_page.dart'; @@ -9,10 +12,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:responsive_builder/responsive_builder.dart'; +import 'package:uni_links/uni_links.dart'; import 'item_list_page.dart'; class HomePage extends StatefulWidget { + HomePage() : super(key: Key("home")); + @override _HomePageState createState() => _HomePageState(); } @@ -33,6 +39,57 @@ class _HomePageState extends State { final List> _tabNavigatorKeys = [ GlobalKey(), GlobalKey(), ]; + StreamSubscription _uriSub; + + void _uriStreamListener(Uri uri) { + if (uri == null) return; + if (uri.host == "import") { + if (Global.syncModel.hasService) { + showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: Text(S.of(context).serviceExists), + actions: [ + CupertinoDialogAction( + child: Text(S.of(context).confirm), + onPressed: () { Navigator.of(context).pop(); }, + ), + ], + ), + ); + } else if (!Global.syncModel.syncing) { + final import = ServiceImport(uri.queryParameters); + final route = ServiceImport.typeMap[uri.queryParameters["t"]]; + if (route != null) { + final navigator = Navigator.of(context); + while (navigator.canPop()) navigator.pop(); + navigator.pushNamed(route, arguments: import); + } + } + } + } + + @override + void initState() { + super.initState(); + _uriSub = getUriLinksStream().listen(_uriStreamListener); + Future.delayed(Duration.zero, () async { + try { + final uri = await getInitialUri(); + if (uri != null) { + _uriStreamListener(uri); + } + } catch(exp) { + print(exp); + } + }); + } + + @override + dispose() { + _uriSub.cancel(); + super.dispose(); + } Widget _constructPage(Widget page, bool isMobile) { return isMobile diff --git a/lib/pages/settings/services/feedbin_page.dart b/lib/pages/settings/services/feedbin_page.dart index 0fd8457..0d01168 100644 --- a/lib/pages/settings/services/feedbin_page.dart +++ b/lib/pages/settings/services/feedbin_page.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'package:fluent_reader_lite/components/list_tile_group.dart'; import 'package:fluent_reader_lite/components/my_list_tile.dart'; import 'package:fluent_reader_lite/generated/l10n.dart'; import 'package:fluent_reader_lite/models/services/feedbin.dart'; +import 'package:fluent_reader_lite/models/services/service_import.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; import 'package:fluent_reader_lite/utils/colors.dart'; @@ -26,6 +28,26 @@ class _FeedbinPageState extends State { int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250; bool _validating = false; + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + ServiceImport import = ModalRoute.of(context).settings.arguments; + if (import == null) return; + if (Utils.testUrl(import.endpoint)) { + setState(() { _endpoint = import.endpoint; }); + } + if (Utils.notEmpty(import.username)) { + setState(() { _username = import.username; }); + } + if (Utils.notEmpty(import.password)) { + final bytes = base64.decode(import.password); + final password = utf8.decode(bytes); + setState(() { _password = password; }); + } + }); + } + void _editEndpoint() async { final String endpoint = await Navigator.of(context).push(CupertinoPageRoute( builder: (context) => TextEditorPage( diff --git a/lib/pages/settings/services/fever_page.dart b/lib/pages/settings/services/fever_page.dart index c3e3454..b110f88 100644 --- a/lib/pages/settings/services/fever_page.dart +++ b/lib/pages/settings/services/fever_page.dart @@ -6,6 +6,7 @@ import 'package:fluent_reader_lite/components/list_tile_group.dart'; import 'package:fluent_reader_lite/components/my_list_tile.dart'; import 'package:fluent_reader_lite/generated/l10n.dart'; import 'package:fluent_reader_lite/models/services/fever.dart'; +import 'package:fluent_reader_lite/models/services/service_import.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; import 'package:fluent_reader_lite/utils/colors.dart'; @@ -24,10 +25,29 @@ class FeverPage extends StatefulWidget { class _FeverPageState extends State { String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? ""; String _username = Store.sp.getString(StoreKeys.USERNAME) ?? ""; + String _apiKey = Store.sp.getString(StoreKeys.API_KEY); String _password = Store.sp.getString(StoreKeys.PASSWORD) ?? ""; int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250; bool _validating = false; + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + ServiceImport import = ModalRoute.of(context).settings.arguments; + if (import == null) return; + if (Utils.testUrl(import.endpoint)) { + setState(() { _endpoint = import.endpoint; }); + } + if (Utils.notEmpty(import.username)) { + setState(() { _username = import.username; }); + } + if (Utils.notEmpty(import.apiKey)) { + setState(() { _apiKey = import.apiKey; }); + } + }); + } + void _editEndpoint() async { final String endpoint = await Navigator.of(context).push(CupertinoPageRoute( builder: (context) => TextEditorPage( @@ -50,7 +70,10 @@ class _FeverPageState extends State { ), )); if (username == null) return; - setState(() { _username = username; }); + setState(() { + _username = username; + _apiKey = null; + }); } void _editPassword() async { @@ -62,16 +85,21 @@ class _FeverPageState extends State { ), )); if (password == null) return; - setState(() { _password = password; }); + setState(() { + _password = password; + _apiKey = null; + }); } bool _canSave() { if (_validating) return false; - return _endpoint.length > 0 && _username.length > 0 && _password.length > 0; + return _endpoint.length > 0 && + ((_username.length > 0 && _password.length > 0) || _apiKey != null); } void _save() async { - final apiKey = md5.convert(utf8.encode("$_username:$_password")).toString(); + final apiKey = _apiKey + ?? md5.convert(utf8.encode("$_username:$_password")).toString(); final handler = FeverServiceHandler.fromValues( _endpoint, apiKey, @@ -154,7 +182,7 @@ class _FeverPageState extends State { ), MyListTile( title: Text(S.of(context).password), - trailing: Text(_password.length == 0 + trailing: Text(_password.length == 0 && _apiKey == null ? S.of(context).enter : S.of(context).entered), onTap: _editPassword, diff --git a/lib/pages/settings/services/greader_page.dart b/lib/pages/settings/services/greader_page.dart index 6585747..7e3d57f 100644 --- a/lib/pages/settings/services/greader_page.dart +++ b/lib/pages/settings/services/greader_page.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'package:fluent_reader_lite/components/list_tile_group.dart'; import 'package:fluent_reader_lite/components/my_list_tile.dart'; import 'package:fluent_reader_lite/generated/l10n.dart'; import 'package:fluent_reader_lite/models/services/greader.dart'; +import 'package:fluent_reader_lite/models/services/service_import.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; import 'package:fluent_reader_lite/utils/colors.dart'; @@ -27,6 +29,26 @@ class _GReaderPageState extends State { bool _validating = false; + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + ServiceImport import = ModalRoute.of(context).settings.arguments; + if (import == null) return; + if (Utils.testUrl(import.endpoint)) { + setState(() { _endpoint = import.endpoint; }); + } + if (Utils.notEmpty(import.username)) { + setState(() { _username = import.username; }); + } + if (Utils.notEmpty(import.password)) { + final bytes = base64.decode(import.password); + final password = utf8.decode(bytes); + setState(() { _password = password; }); + } + }); + } + void _editEndpoint() async { final String endpoint = await Navigator.of(context).push(CupertinoPageRoute( builder: (context) => TextEditorPage( diff --git a/lib/pages/settings/services/inoreader_page.dart b/lib/pages/settings/services/inoreader_page.dart index 1590f04..06e0e27 100644 --- a/lib/pages/settings/services/inoreader_page.dart +++ b/lib/pages/settings/services/inoreader_page.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'package:fluent_reader_lite/components/list_tile_group.dart'; import 'package:fluent_reader_lite/components/my_list_tile.dart'; import 'package:fluent_reader_lite/generated/l10n.dart'; import 'package:fluent_reader_lite/models/services/greader.dart'; +import 'package:fluent_reader_lite/models/services/service_import.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; import 'package:fluent_reader_lite/utils/colors.dart'; @@ -26,7 +28,7 @@ class _InoreaderPageState extends State { "https://www.inoreader.com", "https://www.innoreader.com", "https://jp.inoreader.com" -]; + ]; String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? _endpointOptions[0]; String _username = Store.sp.getString(StoreKeys.USERNAME) ?? ""; @@ -38,6 +40,32 @@ class _InoreaderPageState extends State { bool _validating = false; + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + ServiceImport import = ModalRoute.of(context).settings.arguments; + if (import == null) return; + if (_endpointOptions.contains(import.endpoint)) { + setState(() { _endpoint = import.endpoint; }); + } + if (Utils.notEmpty(import.username)) { + setState(() { _username = import.username; }); + } + if (Utils.notEmpty(import.password)) { + final bytes = base64.decode(import.password); + final password = utf8.decode(bytes); + setState(() { _password = password; }); + } + if (Utils.notEmpty(import.apiId)) { + setState(() { _apiId = import.apiId; }); + } + if (Utils.notEmpty(import.apiKey)) { + setState(() { _apiKey = import.apiKey; }); + } + }); + } + void _editUsername() async { final String username = await Navigator.of(context).push(CupertinoPageRoute( builder: (context) => TextEditorPage( diff --git a/lib/utils/global.dart b/lib/utils/global.dart index 0f6a5e2..ff10675 100644 --- a/lib/utils/global.dart +++ b/lib/utils/global.dart @@ -59,7 +59,7 @@ abstract class Global { db = await DatabaseHelper.getDatabase(); await db.delete( "items", - where: "date < ?", + where: "date < ? AND starred = 0", whereArgs: [ DateTime.now() .subtract(Duration(days: globalModel.keepItemsDays)) diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 3461641..24f54a7 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -47,9 +47,9 @@ abstract class Utils { r"^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*$)", caseSensitive: false, ); - static bool testUrl(String url) => _urlRegex.hasMatch(url.trim()); + static bool testUrl(String url) => url != null && _urlRegex.hasMatch(url.trim()); - static bool notEmpty(String text) => text.trim().length > 0; + static bool notEmpty(String text) => text != null && text.trim().length > 0; static void showServiceFailureDialog(BuildContext context) { showCupertinoDialog( diff --git a/pubspec.lock b/pubspec.lock index 7aca0ae..f05b0ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -544,6 +544,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0-nullsafety.5" + uni_links: + dependency: "direct main" + description: + name: uni_links + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7921732..f9f1c30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: cached_network_image: ^2.5.0 flutter_cache_manager: ^2.1.0 lpinyin: ^1.1.0 + uni_links: ^0.4.0 modal_bottom_sheet: ^1.0.0+1 overlay_dialog: ^0.0.3 -- 2.38.5