M lib/components/favicon.dart => lib/components/favicon.dart +1 -1
@@ 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,
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +2 -1
@@ 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
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +2 -1
@@ 78,5 78,6 @@
"logOut": "登出",
"logOutWarning": "这将移除所有本地数据,是否继续?",
"confirm": "确定",
- "allLoaded": "已全部加载"
+ "allLoaded": "已全部加载",
+ "removeAd": "移除广告"
}=
\ 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/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();
M lib/models/items_model.dart => lib/models/items_model.dart +1 -1
@@ 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);
}
M lib/models/service.dart => lib/models/service.dart +2 -1
@@ 9,12 9,13 @@ enum SyncService {
abstract class ServiceHandler {
void remove();
Future<bool> validate();
+ Future<void> reauthenticate() async { }
Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources();
Future<List<RSSItem>> fetchItems();
Future<Tuple2<Set<String>, Set<String>>> syncItems();
Future<void> markAllRead(Set<String> sids, DateTime date, bool before);
Future<void> markRead(RSSItem item);
- Future<void> markUnead(RSSItem item);
+ Future<void> markUnread(RSSItem item);
Future<void> star(RSSItem item);
Future<void> unstar(RSSItem item);
}
M lib/models/services/feedbin.dart => lib/models/services/feedbin.dart +6 -1
@@ 214,6 214,7 @@ class FeedbinServiceHandler extends ServiceHandler {
return _lastSynced;
}
+ @override
Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
List<String> predicates = ["hasRead = 0"];
if (sids.length > 0) {
@@ 232,18 233,22 @@ class FeedbinServiceHandler extends ServiceHandler {
await _markItems("unread", "DELETE", List.from(iids));
}
+ @override
Future<void> markRead(RSSItem item) async{
await _markItems("unread", "DELETE", [item.id]);
}
- Future<void> markUnead(RSSItem item) async {
+ @override
+ Future<void> markUnread(RSSItem item) async {
await _markItems("unread", "POST", [item.id]);
}
+ @override
Future<void> star(RSSItem item) async {
await _markItems("starred", "POST", [item.id]);
}
+ @override
Future<void> unstar(RSSItem item) async {
await _markItems("starred", "DELETE", [item.id]);
}
M lib/models/services/fever.dart => lib/models/services/fever.dart +1 -1
@@ 218,7 218,7 @@ class FeverServiceHandler extends ServiceHandler {
}
@override
- Future<void> markUnead(RSSItem item) async {
+ Future<void> markUnread(RSSItem item) async {
await _markItem(item, "unread");
}
A lib/models/services/greader.dart => lib/models/services/greader.dart +350 -0
@@ 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<http.Response> _fetchAPI(String params,
+ {dynamic body}) async {
+ final headers = Map<String, String>();
+ 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<Set<String>> _fetchAll(String params) async {
+ final results = List<String>.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<http.Response> _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<bool> 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<void> 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<Tuple2<List<RSSSource>, Map<String, List<String>>>> 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<String, List<String>>();
+ 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<RSSSource>((s) {
+ return RSSSource(s["id"], s["url"] ?? s["htmlUrl"], s["title"]);
+ }).toList();
+ return Tuple2(sources, groupsMap);
+ }
+
+ @override
+ Future<List<RSSItem>> 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<RSSItem>((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<Tuple2<Set<String>, Set<String>>> syncItems() async {
+ List<Set<String>> 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<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
+ if (date != null) {
+ List<String> 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<String> 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<void> markRead(RSSItem item) async {
+ await _editTag(item.id, _READ_TAG);
+ }
+
+ @override
+ Future<void> markUnread(RSSItem item) async {
+ await _editTag(item.id, _READ_TAG, add: false);
+ }
+
+ @override
+ Future<void> star(RSSItem item) async {
+ await _editTag(item.id, _STAR_TAG);
+ }
+
+ @override
+ Future<void> unstar(RSSItem item) async {
+ await _editTag(item.id, _STAR_TAG, add: false);
+ }
+}
M lib/pages/settings/services/feedbin_page.dart => lib/pages/settings/services/feedbin_page.dart +4 -6
@@ 189,12 189,10 @@ class _FeedbinPageState extends State<FeedbinPage> {
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,
A lib/pages/settings/services/inoreader_page.dart => lib/pages/settings/services/inoreader_page.dart +297 -0
@@ 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<InoreaderPage> {
+ 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<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("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;
+ }
+ }
+}
M lib/pages/setup_page.dart => lib/pages/setup_page.dart +1 -0
@@ 38,6 38,7 @@ class SetupPage extends StatelessWidget {
),
MyListTile(
title: Text("Inoreader"),
+ onTap: () { _configure(context, "/settings/service/inoreader"); },
),
MyListTile(
title: Text("Feedbin"),
M lib/utils/global.dart => lib/utils/global.dart +2 -3
@@ 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();
M lib/utils/store.dart => lib/utils/store.dart +5 -0
@@ 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 {