From 4a21d3259885f835d064df292029e7fe164db960 Mon Sep 17 00:00:00 2001 From: Bruce Liu Date: Tue, 19 Jan 2021 11:03:43 +0800 Subject: [PATCH] add google reader api support --- ios/Runner.xcodeproj/project.pbxproj | 21 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh.arb | 3 +- lib/main.dart | 4 +- lib/models/services/greader.dart | 10 +- lib/models/sync_model.dart | 1 + lib/pages/settings/services/greader_page.dart | 247 ++++++++++++++++++ .../settings/services/inoreader_page.dart | 10 +- lib/pages/settings_page.dart | 2 - lib/pages/setup_page.dart | 1 + 10 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 lib/pages/settings/services/greader_page.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9ece2dd..91f1aec 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -354,8 +354,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = P2CG8QD3BP; + DEVELOPMENT_TEAM = EM8VE646TZ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -370,8 +372,9 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite; + PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -492,8 +495,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = P2CG8QD3BP; + DEVELOPMENT_TEAM = EM8VE646TZ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -508,8 +513,9 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite; + PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -524,8 +530,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = P2CG8QD3BP; + DEVELOPMENT_TEAM = EM8VE646TZ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -540,8 +548,9 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite; + PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c8392c4..94ca7b9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -79,5 +79,6 @@ "logOutWarning": "All local data will be deleted. Are you sure?", "confirm": "Confirm", "allLoaded": "All loaded", - "removeAd": "Remove Ad" + "removeAd": "Remove Ad", + "getApiKey": "Get API ID & Key" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 6fadda6..8c4b5fa 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -79,5 +79,6 @@ "logOutWarning": "这将移除所有本地数据,是否继续?", "confirm": "确定", "allLoaded": "已全部加载", - "removeAd": "移除广告" + "removeAd": "移除广告", + "getApiKey": "获取 API ID & KEY" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2a50e23..3977d29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:fluent_reader_lite/pages/settings/general_page.dart'; import 'package:fluent_reader_lite/pages/settings/reading_page.dart'; import 'package:fluent_reader_lite/pages/settings/services/feedbin_page.dart'; import 'package:fluent_reader_lite/pages/settings/services/fever_page.dart'; +import 'package:fluent_reader_lite/pages/settings/services/greader_page.dart'; import 'package:fluent_reader_lite/pages/settings/services/inoreader_page.dart'; import 'package:fluent_reader_lite/pages/settings/source_edit_page.dart'; import 'package:fluent_reader_lite/pages/settings/sources_page.dart'; @@ -62,6 +63,7 @@ class MyApp extends StatelessWidget { "/settings/service/fever": (context) => FeverPage(), "/settings/service/feedbin": (context) => FeedbinPage(), "/settings/service/inoreader": (context) => InoreaderPage(), + "/settings/service/greader": (context) => GReaderPage(), "/settings/service": (context) { var serviceType = SyncService.values[Store.sp.getInt(StoreKeys.SYNC_SERVICE) ?? 0]; switch (serviceType) { @@ -72,7 +74,7 @@ class MyApp extends StatelessWidget { case SyncService.Feedbin: return FeedbinPage(); case SyncService.GReader: - // TODO: Handle this case. + return GReaderPage(); break; case SyncService.Inoreader: return InoreaderPage(); diff --git a/lib/models/services/greader.dart b/lib/models/services/greader.dart index b753045..67a6bad 100644 --- a/lib/models/services/greader.dart +++ b/lib/models/services/greader.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'dart:math'; import 'package:fluent_reader_lite/models/item.dart'; @@ -61,7 +60,10 @@ class GReaderServiceHandler extends ServiceHandler { } void persist() { - Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Inoreader.index); + Store.sp.setInt( + StoreKeys.SYNC_SERVICE, + inoreaderId != null ? SyncService.Inoreader.index : SyncService.GReader.index + ); Store.sp.setString(StoreKeys.ENDPOINT, endpoint); Store.sp.setString(StoreKeys.USERNAME, username); Store.sp.setString(StoreKeys.PASSWORD, password); @@ -240,7 +242,7 @@ class GReaderServiceHandler extends ServiceHandler { } final parsedItems = items.map((i) { final dom = parse(i["summary"]["content"]); - if (removeInoreaderAd) { + if (removeInoreaderAd == true) { if (dom.documentElement.text.trim().startsWith("Ads from Inoreader")) { dom.body.firstChild.remove(); } @@ -321,6 +323,8 @@ class GReaderServiceHandler extends ServiceHandler { } if (refs.length > 0) _editTag(refs.join("&i="), _READ_TAG); } else { + if (sids.length == 0) + sids = Set.from(Global.sourcesModel.getSources().map((s) => s.id)); for (var sid in sids) { final body = { "s": sid }; _fetchAPI("/reader/api/0/mark-all-as-read", body: body); diff --git a/lib/models/sync_model.dart b/lib/models/sync_model.dart index ca9b453..5aae2f6 100644 --- a/lib/models/sync_model.dart +++ b/lib/models/sync_model.dart @@ -49,6 +49,7 @@ class SyncModel with ChangeNotifier { syncing = true; notifyListeners(); try { + await Global.service.reauthenticate(); await Global.sourcesModel.updateSources(); await Global.itemsModel.syncItems(); await Global.itemsModel.fetchItems(); diff --git a/lib/pages/settings/services/greader_page.dart b/lib/pages/settings/services/greader_page.dart new file mode 100644 index 0000000..25f8fbc --- /dev/null +++ b/lib/pages/settings/services/greader_page.dart @@ -0,0 +1,247 @@ +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/sync_model.dart'; +import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; +import 'package:fluent_reader_lite/utils/colors.dart'; +import 'package:fluent_reader_lite/utils/global.dart'; +import 'package:fluent_reader_lite/utils/store.dart'; +import 'package:fluent_reader_lite/utils/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:overlay_dialog/overlay_dialog.dart'; +import 'package:provider/provider.dart'; + +class GReaderPage extends StatefulWidget { + @override + _GReaderPageState createState() => _GReaderPageState(); +} + +class _GReaderPageState extends State { + String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? ""; + String _username = Store.sp.getString(StoreKeys.USERNAME) ?? ""; + String _password = Store.sp.getString(StoreKeys.PASSWORD) ?? ""; + int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250; + + bool _validating = false; + + void _editEndpoint() async { + final String endpoint = await Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => TextEditorPage( + S.of(context).endpoint, + Utils.testUrl, + initialValue: _endpoint, + ), + )); + if (endpoint == null) return; + setState(() { _endpoint = endpoint; }); + } + + void _editUsername() async { + final String username = await Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => TextEditorPage( + S.of(context).username, + Utils.notEmpty, + initialValue: _username, + ), + )); + if (username == null) return; + setState(() { _username = username; }); + } + + void _editPassword() async { + final String password = await Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => TextEditorPage( + S.of(context).password, + Utils.notEmpty, + isPassword: true, + ), + )); + if (password == null) return; + setState(() { _password = password; }); + } + + bool _canSave() { + if (_validating) return false; + return _endpoint.length > 0 && _username.length > 0 && _password.length > 0; + } + + void _save() async { + final handler = GReaderServiceHandler.fromValues( + _endpoint, + _username, + _password, + _fetchLimit, + ); + setState(() { _validating = true; }); + DialogHelper().show( + context, + DialogWidget.progress(style: DialogStyle.cupertino), + ); + try { + await handler.reauthenticate(); + final isValid = await handler.validate(); + if (!mounted) return; + assert (isValid); + handler.persist(); + await Global.syncModel.syncWithService(); + Global.syncModel.checkHasService(); + _validating = false; + DialogHelper().hide(context); + if (mounted) Navigator.of(context).pop(); + } catch(exp) { + handler.remove(); + setState(() { _validating = false; }); + DialogHelper().hide(context); + Utils.showServiceFailureDialog(context); + } + } + + void _logOut() async { + final bool confirmed = await showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: Text(S.of(context).logOutWarning), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + child: Text(S.of(context).cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + CupertinoDialogAction( + isDestructiveAction: true, + child: Text(S.of(context).confirm), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ); + if (confirmed != null) { + setState(() { _validating = true; }); + DialogHelper().show( + context, + DialogWidget.progress(style: DialogStyle.cupertino), + ); + await Global.syncModel.removeService(); + _validating = false; + DialogHelper().hide(context); + final navigator = Navigator.of(context); + while (navigator.canPop()) navigator.pop(); + } + } + + @override + Widget build(BuildContext context) { + final inputs = ListTileGroup([ + MyListTile( + title: Text(S.of(context).endpoint), + trailing: Text(_endpoint.length == 0 + ? S.of(context).enter + : S.of(context).entered), + onTap: _editEndpoint, + ), + MyListTile( + title: Text(S.of(context).username), + trailing: Text(_username.length == 0 + ? S.of(context).enter + : S.of(context).entered), + onTap: _editUsername, + ), + MyListTile( + title: Text(S.of(context).password), + trailing: Text(_password.length == 0 + ? S.of(context).enter + : S.of(context).entered), + onTap: _editPassword, + ), + ], title: S.of(context).credentials); + final syncItems = ListTileGroup([ + MyListTile( + title: Text(S.of(context).fetchLimit), + trailing: Text(_fetchLimit.toString()), + trailingChevron: false, + withDivider: false, + ), + MyListTile( + title: Expanded(child: CupertinoSlider( + min: 250, + max: 1500, + divisions: 5, + value: _fetchLimit.toDouble(), + onChanged: (v) { setState(() { _fetchLimit = v.toInt(); }); }, + )), + trailingChevron: false, + withDivider: false, + ), + ], title: S.of(context).sync); + final saveButton = Selector( + selector: (context, syncModel) => syncModel.syncing, + builder: (context, syncing, child) { + var canSave = !syncing && _canSave(); + final saveStyle = TextStyle( + color: canSave + ? CupertinoColors.activeBlue.resolveFrom(context) + : CupertinoColors.secondaryLabel.resolveFrom(context), + ); + return ListTileGroup([ + MyListTile( + title: Expanded(child: Center( + child: Text( + S.of(context).save, + style: saveStyle, + ) + )), + onTap: canSave ? _save : null, + trailingChevron: false, + withDivider: false, + ), + ], title: ""); + }, + ); + final logOutButton = Selector( + selector: (context, syncModel) => syncModel.syncing, + builder: (context, syncing, child) { + return ListTileGroup([ + MyListTile( + title: Expanded(child: Center( + child: Text( + S.of(context).logOut, + style: TextStyle( + color: (_validating || syncing) + ? CupertinoColors.secondaryLabel.resolveFrom(context) + : CupertinoColors.destructiveRed, + ), + ) + )), + onTap: (_validating || syncing) ? null : _logOut, + trailingChevron: false, + withDivider: false, + ), + ], title: ""); + }, + ); + final page = CupertinoPageScaffold( + backgroundColor: MyColors.background, + navigationBar: CupertinoNavigationBar( + middle: Text("Google Reader API"), + ), + child: ListView(children: [ + inputs, + syncItems, + saveButton, + if (Global.service != null) logOutButton, + ]), + ); + if (Platform.isAndroid) { + return WillPopScope(child: page, onWillPop: () async => !_validating); + } else { + return page; + } + } +} diff --git a/lib/pages/settings/services/inoreader_page.dart b/lib/pages/settings/services/inoreader_page.dart index 023fb2c..8df5c12 100644 --- a/lib/pages/settings/services/inoreader_page.dart +++ b/lib/pages/settings/services/inoreader_page.dart @@ -3,7 +3,6 @@ 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/greader.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart'; @@ -15,6 +14,7 @@ import 'package:flutter/cupertino.dart'; import 'package:overlay_dialog/overlay_dialog.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; +import 'package:url_launcher/url_launcher.dart'; class InoreaderPage extends StatefulWidget { @override @@ -163,6 +163,10 @@ class _InoreaderPageState extends State { } } + void _getKey() { + launch(_endpoint + "/all_articles#preferences-developer", forceSafariVC: false, forceWebView: false); + } + @override Widget build(BuildContext context) { final endpointItems = ListTileGroup.fromOptions( @@ -199,6 +203,10 @@ class _InoreaderPageState extends State { ? S.of(context).enter : S.of(context).entered), onTap: _editAPIKey, + ), + MyListTile( + title: Text(S.of(context).getApiKey), + onTap: _getKey, withDivider: false, ), ], title: S.of(context).credentials); diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index eb34fa6..bb8bc1f 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,5 +1,3 @@ -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'; diff --git a/lib/pages/setup_page.dart b/lib/pages/setup_page.dart index 6454d59..35af26f 100644 --- a/lib/pages/setup_page.dart +++ b/lib/pages/setup_page.dart @@ -35,6 +35,7 @@ class SetupPage extends StatelessWidget { ), MyListTile( title: Text("Google Reader API"), + onTap: () { _configure(context, "/settings/service/greader"); }, ), MyListTile( title: Text("Inoreader"), -- 2.38.5