M ios/Runner.xcodeproj/project.pbxproj => ios/Runner.xcodeproj/project.pbxproj +15 -6
@@ 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";
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +2 -1
@@ 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
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +2 -1
@@ 79,5 79,6 @@
"logOutWarning": "这将移除所有本地数据,是否继续?",
"confirm": "确定",
"allLoaded": "已全部加载",
- "removeAd": "移除广告"
+ "removeAd": "移除广告",
+ "getApiKey": "获取 API ID & KEY"
}=
\ No newline at end of file
M lib/main.dart => lib/main.dart +3 -1
@@ 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();
M lib/models/services/greader.dart => lib/models/services/greader.dart +7 -3
@@ 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<RSSItem>((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);
M lib/models/sync_model.dart => lib/models/sync_model.dart +1 -0
@@ 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();
A lib/pages/settings/services/greader_page.dart => lib/pages/settings/services/greader_page.dart +247 -0
@@ 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<GReaderPage> {
+ 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<SyncModel, bool>(
+ 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<SyncModel, bool>(
+ 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;
+ }
+ }
+}
M lib/pages/settings/services/inoreader_page.dart => lib/pages/settings/services/inoreader_page.dart +9 -1
@@ 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<InoreaderPage> {
}
}
+ 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<InoreaderPage> {
? 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);
M lib/pages/settings_page.dart => lib/pages/settings_page.dart +0 -2
@@ 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';
M lib/pages/setup_page.dart => lib/pages/setup_page.dart +1 -0
@@ 35,6 35,7 @@ class SetupPage extends StatelessWidget {
),
MyListTile(
title: Text("Google Reader API"),
+ onTap: () { _configure(context, "/settings/service/greader"); },
),
MyListTile(
title: Text("Inoreader"),