From 06e740abdfab1698e03bc0d4ad921b2c5a7161af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9=E8=BF=9C?= Date: Mon, 18 Jan 2021 21:51:23 +0800 Subject: [PATCH] add inoreader support --- lib/components/favicon.dart | 2 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh.arb | 3 +- lib/main.dart | 4 +- lib/models/items_model.dart | 2 +- lib/models/service.dart | 3 +- lib/models/services/feedbin.dart | 7 +- lib/models/services/fever.dart | 2 +- lib/models/services/greader.dart | 350 ++++++++++++++++++ lib/pages/settings/services/feedbin_page.dart | 10 +- .../settings/services/inoreader_page.dart | 297 +++++++++++++++ lib/pages/setup_page.dart | 1 + lib/utils/global.dart | 5 +- lib/utils/store.dart | 5 + 14 files changed, 677 insertions(+), 17 deletions(-) create mode 100644 lib/models/services/greader.dart create mode 100644 lib/pages/settings/services/inoreader_page.dart diff --git a/lib/components/favicon.dart b/lib/components/favicon.dart index ff8ba37..aa6d4f8 100644 --- a/lib/components/favicon.dart +++ b/lib/components/favicon.dart @@ -25,7 +25,7 @@ class Favicon extends StatelessWidget { return Container( width: size, height: size, - color: CupertinoColors.systemGrey2, + color: CupertinoColors.systemGrey.resolveFrom(context), child: Center(child: Text( source.name.length > 0 ? source.name[0] : "?", style: _textStyle, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f98510f..c8392c4 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -78,5 +78,6 @@ "logOut": "Log out", "logOutWarning": "All local data will be deleted. Are you sure?", "confirm": "Confirm", - "allLoaded": "All loaded" + "allLoaded": "All loaded", + "removeAd": "Remove Ad" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index a71f7d3..6fadda6 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -78,5 +78,6 @@ "logOut": "登出", "logOutWarning": "这将移除所有本地数据,是否继续?", "confirm": "确定", - "allLoaded": "已全部加载" + "allLoaded": "已全部加载", + "removeAd": "移除广告" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e3763bf..2a50e23 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/inoreader_page.dart'; import 'package:fluent_reader_lite/pages/settings/source_edit_page.dart'; import 'package:fluent_reader_lite/pages/settings/sources_page.dart'; import 'package:fluent_reader_lite/pages/settings_page.dart'; @@ -60,6 +61,7 @@ class MyApp extends StatelessWidget { "/settings/about": (context) => AboutPage(), "/settings/service/fever": (context) => FeverPage(), "/settings/service/feedbin": (context) => FeedbinPage(), + "/settings/service/inoreader": (context) => InoreaderPage(), "/settings/service": (context) { var serviceType = SyncService.values[Store.sp.getInt(StoreKeys.SYNC_SERVICE) ?? 0]; switch (serviceType) { @@ -73,7 +75,7 @@ class MyApp extends StatelessWidget { // TODO: Handle this case. break; case SyncService.Inoreader: - // TODO: Handle this case. + return InoreaderPage(); break; } return AboutPage(); diff --git a/lib/models/items_model.dart b/lib/models/items_model.dart index 0d4a28a..6a03e01 100644 --- a/lib/models/items_model.dart +++ b/lib/models/items_model.dart @@ -26,7 +26,7 @@ class ItemsModel with ChangeNotifier { item.hasRead = read; if (!local) { if (read) Global.service.markRead(item); - else Global.service.markUnead(item); + else Global.service.markUnread(item); } Global.sourcesModel.updateUnreadCount(item.source, read ? -1 : 1); } diff --git a/lib/models/service.dart b/lib/models/service.dart index 80c5cd0..b88121a 100644 --- a/lib/models/service.dart +++ b/lib/models/service.dart @@ -9,12 +9,13 @@ enum SyncService { abstract class ServiceHandler { void remove(); Future validate(); + Future reauthenticate() async { } Future, Map>>> getSources(); Future> fetchItems(); Future, Set>> syncItems(); Future markAllRead(Set sids, DateTime date, bool before); Future markRead(RSSItem item); - Future markUnead(RSSItem item); + Future markUnread(RSSItem item); Future star(RSSItem item); Future unstar(RSSItem item); } diff --git a/lib/models/services/feedbin.dart b/lib/models/services/feedbin.dart index 63df065..6befab1 100644 --- a/lib/models/services/feedbin.dart +++ b/lib/models/services/feedbin.dart @@ -214,6 +214,7 @@ class FeedbinServiceHandler extends ServiceHandler { return _lastSynced; } + @override Future markAllRead(Set sids, DateTime date, bool before) async { List predicates = ["hasRead = 0"]; if (sids.length > 0) { @@ -232,18 +233,22 @@ class FeedbinServiceHandler extends ServiceHandler { await _markItems("unread", "DELETE", List.from(iids)); } + @override Future markRead(RSSItem item) async{ await _markItems("unread", "DELETE", [item.id]); } - Future markUnead(RSSItem item) async { + @override + Future markUnread(RSSItem item) async { await _markItems("unread", "POST", [item.id]); } + @override Future star(RSSItem item) async { await _markItems("starred", "POST", [item.id]); } + @override Future unstar(RSSItem item) async { await _markItems("starred", "DELETE", [item.id]); } diff --git a/lib/models/services/fever.dart b/lib/models/services/fever.dart index 052bd8c..12e7e33 100644 --- a/lib/models/services/fever.dart +++ b/lib/models/services/fever.dart @@ -218,7 +218,7 @@ class FeverServiceHandler extends ServiceHandler { } @override - Future markUnead(RSSItem item) async { + Future markUnread(RSSItem item) async { await _markItem(item, "unread"); } diff --git a/lib/models/services/greader.dart b/lib/models/services/greader.dart new file mode 100644 index 0000000..b753045 --- /dev/null +++ b/lib/models/services/greader.dart @@ -0,0 +1,350 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:fluent_reader_lite/models/item.dart'; +import 'package:fluent_reader_lite/models/service.dart'; +import 'package:fluent_reader_lite/utils/global.dart'; +import 'package:fluent_reader_lite/utils/store.dart'; +import 'package:html/parser.dart'; +import 'package:tuple/tuple.dart'; +import 'package:http/http.dart' as http; +import 'package:fluent_reader_lite/models/source.dart'; + +class GReaderServiceHandler extends ServiceHandler { + static const _ALL_TAG = "user/-/state/com.google/reading-list"; + static const _READ_TAG = "user/-/state/com.google/read"; + static const _STAR_TAG = "user/-/state/com.google/starred"; + + String endpoint; + String username; + String password; + int fetchLimit; + int _lastFetched; + String _lastId; + String _auth; + bool useInt64; + String inoreaderId; + String inoreaderKey; + bool removeInoreaderAd; + + GReaderServiceHandler() { + endpoint = Store.sp.getString(StoreKeys.ENDPOINT); + username = Store.sp.getString(StoreKeys.USERNAME); + password = Store.sp.getString(StoreKeys.PASSWORD); + fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT); + _lastFetched = Store.sp.getInt(StoreKeys.LAST_FETCHED); + _lastId = Store.sp.getString(StoreKeys.LAST_ID); + _auth = Store.sp.getString(StoreKeys.AUTH); + useInt64 = Store.sp.getBool(StoreKeys.USE_INT_64); + inoreaderId = Store.sp.getString(StoreKeys.API_ID); + inoreaderKey = Store.sp.getString(StoreKeys.API_KEY); + removeInoreaderAd = Store.sp.getBool(StoreKeys.INOREADER_REMOVE_AD); + } + + GReaderServiceHandler.fromValues( + this.endpoint, + this.username, + this.password, + this.fetchLimit, + { + this.inoreaderId, + this.inoreaderKey, + this.removeInoreaderAd, + } + ) { + _lastFetched = Store.sp.getInt(StoreKeys.LAST_FETCHED); + _lastId = Store.sp.getString(StoreKeys.LAST_ID); + _auth = Store.sp.getString(StoreKeys.AUTH); + useInt64 = Store.sp.getBool(StoreKeys.USE_INT_64) + ?? !endpoint.endsWith("theoldreader.com"); + } + + void persist() { + Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Inoreader.index); + Store.sp.setString(StoreKeys.ENDPOINT, endpoint); + Store.sp.setString(StoreKeys.USERNAME, username); + Store.sp.setString(StoreKeys.PASSWORD, password); + Store.sp.setInt(StoreKeys.FETCH_LIMIT, fetchLimit); + Store.sp.setBool(StoreKeys.USE_INT_64, useInt64); + if (inoreaderId != null) { + Store.sp.setString(StoreKeys.API_ID, inoreaderId); + Store.sp.setString(StoreKeys.API_KEY, inoreaderKey); + Store.sp.setBool(StoreKeys.INOREADER_REMOVE_AD, removeInoreaderAd); + } + Global.service = this; + } + + @override + void remove() { + Store.sp.remove(StoreKeys.SYNC_SERVICE); + Store.sp.remove(StoreKeys.ENDPOINT); + Store.sp.remove(StoreKeys.USERNAME); + Store.sp.remove(StoreKeys.PASSWORD); + Store.sp.remove(StoreKeys.FETCH_LIMIT); + Store.sp.remove(StoreKeys.LAST_FETCHED); + Store.sp.remove(StoreKeys.LAST_ID); + Store.sp.remove(StoreKeys.AUTH); + Store.sp.remove(StoreKeys.USE_INT_64); + Store.sp.remove(StoreKeys.API_ID); + Store.sp.remove(StoreKeys.API_KEY); + Store.sp.remove(StoreKeys.INOREADER_REMOVE_AD); + Global.service = null; + } + + int get lastFetched => _lastFetched; + set lastFetched(int value) { + _lastFetched = value; + Store.sp.setInt(StoreKeys.LAST_FETCHED, value); + } + + String get lastId => _lastId; + set lastId(String value) { + _lastId = value; + Store.sp.setString(StoreKeys.LAST_ID, value); + } + + String get auth => _auth; + set auth(String value) { + _auth = value; + Store.sp.setString(StoreKeys.AUTH, value); + } + + Future _fetchAPI(String params, + {dynamic body}) async { + final headers = Map(); + if (auth != null) headers["Authorization"] = auth; + if (inoreaderId != null) { + headers["AppId"] = inoreaderId; + headers["AppKey"] = inoreaderKey; + } + if (body == null) { + return await http.get(endpoint + params, headers: headers); + } else { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + return await http.post(endpoint + params, headers: headers, body: body); + } + } + + Future> _fetchAll(String params) async { + final results = List.empty(growable: true); + List fetched; + String continuation; + do { + var p = params; + if (continuation != null) p += "&c=$continuation"; + final response = await _fetchAPI(p); + assert(response.statusCode == 200); + final parsed = jsonDecode(response.body); + fetched = parsed["itemRefs"]; + if (fetched != null && fetched.length > 0) { + for (var i in fetched) { + results.add(i["id"]); + } + } + continuation = parsed["continuation"]; + } while (continuation != null && fetched != null && fetched.length >= 1000); + return new Set.from(results); + } + + Future _editTag(String ref, String tag, {add: true}) async { + final body = "i=$ref&${add?"a":"r"}=$tag"; + return await _fetchAPI("/reader/api/0/edit-tag", body: body); + } + + String _compactId(String longId) { + final last = longId.split("/").last; + if (!useInt64) return last; + return int.parse(last, radix: 16).toString(); + } + + @override + Future validate() async { + try { + final result = await _fetchAPI("/reader/api/0/user-info"); + return result.statusCode == 200; + } catch(exp) { + return false; + } + } + + static final _authRegex = RegExp(r"Auth=(\S+)"); + @override + Future reauthenticate() async { + if (!await validate()) { + final body = { + "Email": username, + "Passwd": password, + }; + final result = await _fetchAPI("/accounts/ClientLogin", body: body); + assert(result.statusCode == 200); + final match = _authRegex.firstMatch(result.body); + if (match != null && match.groupCount > 0) { + auth = "GoogleLogin auth=${match.group(1)}"; + } + } + } + + @override + Future, Map>>> getSources() async { + final response = await _fetchAPI("/reader/api/0/subscription/list?output=json"); + assert(response.statusCode == 200); + List subscriptions = jsonDecode(response.body)["subscriptions"]; + final groupsMap = Map>(); + for (var s in subscriptions) { + final categories = s["categories"]; + if (categories != null) { + for (var c in categories) { + groupsMap.putIfAbsent(c["label"], () => []); + groupsMap[c["label"]].add(s["id"]); + } + } + } + final sources = subscriptions.map((s) { + return RSSSource(s["id"], s["url"] ?? s["htmlUrl"], s["title"]); + }).toList(); + return Tuple2(sources, groupsMap); + } + + @override + Future> fetchItems() async { + List items = []; + List fetchedItems; + String continuation; + do { + try { + final limit = min(fetchLimit - items.length, 1000); + var params = "/reader/api/0/stream/contents?output=json&n=$limit"; + if (lastFetched != null) params += "&ot=$lastFetched"; + if (continuation != null) params += "&c=$continuation"; + final response = await _fetchAPI(params); + assert(response.statusCode == 200); + final fetched = jsonDecode(response.body); + fetchedItems = fetched["items"]; + for (var i in fetchedItems) { + i["id"] = _compactId(i["id"]); + if (i["id"] == lastId || items.length >= fetchLimit) { + break; + } else { + items.add(i); + } + } + continuation = fetched["continuation"]; + } catch(exp) { + break; + } + } while (continuation != null && items.length < fetchLimit); + if (items.length > 0) { + lastId = items[0]["id"]; + lastFetched = int.parse(items[0]["crawlTimeMsec"]) ~/ 1000; + } + final parsedItems = items.map((i) { + final dom = parse(i["summary"]["content"]); + if (removeInoreaderAd) { + if (dom.documentElement.text.trim().startsWith("Ads from Inoreader")) { + dom.body.firstChild.remove(); + } + } + final item = RSSItem( + id: i["id"], + source: i["origin"]["streamId"], + title: i["title"], + link: i["canonical"][0]["href"], + date: DateTime.fromMillisecondsSinceEpoch(i["published"] * 1000), + content: dom.body.innerHtml, + snippet: dom.documentElement.text.trim(), + creator: i["author"], + hasRead: false, + starred: false, + ); + if (inoreaderId != null) { + final titleDom = parse(item.title); + item.title = titleDom.documentElement.text; + } + var img = dom.querySelector("img"); + if (img != null && img.attributes["src"] != null) { + var thumb = img.attributes["src"]; + if (thumb.startsWith("http")) { + item.thumb = thumb; + } + } + for (var c in i["categories"]) { + if (!item.hasRead && c.endsWith("/state/com.google/read")) item.hasRead = true; + else if (!item.starred && c.endsWith("/state/com.google/starred")) item.starred = true; + } + return item; + }).toList(); + return parsedItems; + } + + @override + Future, Set>> syncItems() async { + List> results; + if (inoreaderId != null) { + results = await Future.wait([ + _fetchAll("/reader/api/0/stream/items/ids?output=json&xt=$_READ_TAG&n=1000"), + _fetchAll("/reader/api/0/stream/items/ids?output=json&it=$_STAR_TAG&n=1000"), + ]); + } else { + results = await Future.wait([ + _fetchAll("/reader/api/0/stream/items/ids?output=json&s=$_ALL_TAG&xt=$_READ_TAG&n=1000"), + _fetchAll("/reader/api/0/stream/items/ids?output=json&s=$_STAR_TAG&n=1000"), + ]); + } + return Tuple2.fromList(results); + } + + @override + Future markAllRead(Set sids, DateTime date, bool before) async { + if (date != null) { + List predicates = ["hasRead = 0"]; + if (sids.length > 0) { + predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})"); + } + if (date != null) { + predicates.add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}"); + } + final rows = await Global.db.query( + "items", + columns: ["iid"], + where: predicates.join(" AND "), + whereArgs: sids.toList(), + ); + final iids = rows.map((r) => r["iid"]).iterator; + List refs = []; + while (iids.moveNext()) { + refs.add(iids.current); + if (refs.length >= 1000) { + _editTag(refs.join("&i="), _READ_TAG); + refs = []; + } + } + if (refs.length > 0) _editTag(refs.join("&i="), _READ_TAG); + } else { + for (var sid in sids) { + final body = { "s": sid }; + _fetchAPI("/reader/api/0/mark-all-as-read", body: body); + } + } + } + + @override + Future markRead(RSSItem item) async { + await _editTag(item.id, _READ_TAG); + } + + @override + Future markUnread(RSSItem item) async { + await _editTag(item.id, _READ_TAG, add: false); + } + + @override + Future star(RSSItem item) async { + await _editTag(item.id, _STAR_TAG); + } + + @override + Future unstar(RSSItem item) async { + await _editTag(item.id, _STAR_TAG, add: false); + } +} diff --git a/lib/pages/settings/services/feedbin_page.dart b/lib/pages/settings/services/feedbin_page.dart index eb5c372..17562f3 100644 --- a/lib/pages/settings/services/feedbin_page.dart +++ b/lib/pages/settings/services/feedbin_page.dart @@ -189,12 +189,10 @@ class _FeedbinPageState extends State { return ListTileGroup([ MyListTile( title: Expanded(child: Center( - child: _validating - ? CupertinoActivityIndicator() - : Text( - S.of(context).save, - style: saveStyle, - ) + child: Text( + S.of(context).save, + style: saveStyle, + ) )), onTap: canSave ? _save : null, trailingChevron: false, diff --git a/lib/pages/settings/services/inoreader_page.dart b/lib/pages/settings/services/inoreader_page.dart new file mode 100644 index 0000000..023fb2c --- /dev/null +++ b/lib/pages/settings/services/inoreader_page.dart @@ -0,0 +1,297 @@ +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'; +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'; +import 'package:tuple/tuple.dart'; + +class InoreaderPage extends StatefulWidget { + @override + _InoreaderPageState createState() => _InoreaderPageState(); +} + +class _InoreaderPageState extends State { + static const _endpointOptions = [ + "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) ?? ""; + String _password = Store.sp.getString(StoreKeys.PASSWORD) ?? ""; + String _apiId = Store.sp.getString(StoreKeys.API_ID) ?? ""; + String _apiKey = Store.sp.getString(StoreKeys.API_KEY) ?? ""; + int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250; + bool _removeAd = Store.sp.getBool(StoreKeys.INOREADER_REMOVE_AD) ?? true; + + bool _validating = false; + + 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; }); + } + + void _editAPIId() async { + final String apiId = await Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => TextEditorPage( + "API ID", + Utils.notEmpty, + initialValue: _apiId, + ), + )); + if (apiId == null) return; + setState(() { _apiId = apiId; }); + } + + void _editAPIKey() async { + final String apiKey = await Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => TextEditorPage( + "API Key", + Utils.notEmpty, + initialValue: _apiKey, + ), + )); + if (apiKey == null) return; + setState(() { _apiKey = apiKey; }); + } + + bool _canSave() { + if (_validating) return false; + return _endpoint.length > 0 && _username.length > 0 && _password.length > 0 + && _apiId.length > 0 && _apiKey.length > 0; + } + + void _save() async { + final handler = GReaderServiceHandler.fromValues( + _endpoint, + _username, + _password, + _fetchLimit, + inoreaderId: _apiId, + inoreaderKey: _apiKey, + removeInoreaderAd: _removeAd, + ); + 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 endpointItems = ListTileGroup.fromOptions( + _endpointOptions.map((e) => Tuple2(e, e)).toList(), + _endpoint, + (e) { setState(() { _endpoint = e; } ); }, + title: S.of(context).endpoint, + ); + final inputs = ListTileGroup([ + 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, + ), + MyListTile( + title: Text("API ID"), + trailing: Text(_apiId.length == 0 + ? S.of(context).enter + : S.of(context).entered), + onTap: _editAPIId, + ), + MyListTile( + title: Text("API Key"), + trailing: Text(_apiKey.length == 0 + ? S.of(context).enter + : S.of(context).entered), + onTap: _editAPIKey, + withDivider: false, + ), + ], title: S.of(context).credentials); + final syncItems = ListTileGroup([ + MyListTile( + title: Text(S.of(context).removeAd), + trailing: CupertinoSwitch( + value: _removeAd, + onChanged: (v) { setState(() { _removeAd = v; }); }, + ), + trailingChevron: false, + ), + 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("Inoreader"), + ), + child: ListView(children: [ + endpointItems, + 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/setup_page.dart b/lib/pages/setup_page.dart index d31642f..6454d59 100644 --- a/lib/pages/setup_page.dart +++ b/lib/pages/setup_page.dart @@ -38,6 +38,7 @@ class SetupPage extends StatelessWidget { ), MyListTile( title: Text("Inoreader"), + onTap: () { _configure(context, "/settings/service/inoreader"); }, ), MyListTile( title: Text("Feedbin"), diff --git a/lib/utils/global.dart b/lib/utils/global.dart index 33d63d0..0f6a5e2 100644 --- a/lib/utils/global.dart +++ b/lib/utils/global.dart @@ -5,6 +5,7 @@ import 'package:fluent_reader_lite/models/items_model.dart'; import 'package:fluent_reader_lite/models/service.dart'; import 'package:fluent_reader_lite/models/services/feedbin.dart'; import 'package:fluent_reader_lite/models/services/fever.dart'; +import 'package:fluent_reader_lite/models/services/greader.dart'; import 'package:fluent_reader_lite/models/sources_model.dart'; import 'package:fluent_reader_lite/models/sync_model.dart'; import 'package:fluent_reader_lite/utils/db.dart'; @@ -46,10 +47,8 @@ abstract class Global { service = FeedbinServiceHandler(); break; case SyncService.GReader: - // TODO: Handle this case. - break; case SyncService.Inoreader: - // TODO: Handle this case. + service = GReaderServiceHandler(); break; } syncModel = SyncModel(); diff --git a/lib/utils/store.dart b/lib/utils/store.dart index 85e219b..d57a10d 100644 --- a/lib/utils/store.dart +++ b/lib/utils/store.dart @@ -35,9 +35,14 @@ abstract class StoreKeys { static const ENDPOINT = "endpoint"; static const USERNAME = "username"; static const PASSWORD = "password"; + static const API_ID = "apiId"; static const API_KEY = "apiKey"; static const FETCH_LIMIT = "fetchLimit"; static const FEVER_INT_32 = "feverInt32"; + static const LAST_FETCHED = "lastFetched"; + static const AUTH = "auth"; + static const USE_INT_64 = "useInt64"; + static const INOREADER_REMOVE_AD = "inoRemoveAd"; } class Store { -- 2.38.5