M assets/article/article.css => assets/article/article.css +2 -2
@@ 42,7 42,7 @@ html.light {
}
h1, h2, h3, h4, h5, h6, b, strong {
- font-weight: 600;
+ font-weight: 800;
}
a, a:hover, a:active {
color: var(--primary);
@@ 80,7 80,7 @@ a, a:hover, a:active {
#main > p#title {
font-size: 1.375rem;
line-height: 1.75rem;
- font-weight: 600;
+ font-weight: 800;
margin: 0;
}
#main > p#date {
M lib/components/sync_control.dart => lib/components/sync_control.dart +4 -2
@@ 13,8 13,10 @@ class _SyncControlState extends State<SyncControl> {
var completer = Completer();
Function listener;
listener = () {
- completer.complete();
- Global.syncModel.removeListener(listener);
+ if (!Global.syncModel.syncing) {
+ completer.complete();
+ Global.syncModel.removeListener(listener);
+ }
};
Global.syncModel.addListener(listener);
Global.syncModel.syncWithService();
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +2 -1
@@ 76,5 76,6 @@
"serviceFailureHint": "Please check the service configuration or network status.",
"logOut": "Log out",
"logOutWarning": "All local data will be deleted. Are you sure?",
- "confirm": "Confirm"
+ "confirm": "Confirm",
+ "allLoaded": "All loaded"
}=
\ No newline at end of file
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +2 -1
@@ 76,5 76,6 @@
"serviceFailureHint": "请检查服务配置或网络连接",
"logOut": "登出",
"logOutWarning": "这将移除所有本地数据,是否继续?",
- "confirm": "确定"
+ "confirm": "确定",
+ "allLoaded": "已全部加载"
}=
\ No newline at end of file
M lib/main.dart => lib/main.dart +3 -2
@@ 7,6 7,7 @@ import 'package:fluent_reader_lite/pages/home_page.dart';
import 'package:fluent_reader_lite/pages/settings/feed_page.dart';
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/source_edit_page.dart';
import 'package:fluent_reader_lite/pages/settings/sources_page.dart';
@@ 55,6 56,7 @@ class MyApp extends StatelessWidget {
"/settings/general": (context) => GeneralPage(),
"/settings/about": (context) => AboutPage(),
"/settings/service/fever": (context) => FeverPage(),
+ "/settings/service/feedbin": (context) => FeedbinPage(),
"/settings/service": (context) {
var serviceType = SyncService.values[Store.sp.getInt(StoreKeys.SYNC_SERVICE) ?? 0];
switch (serviceType) {
@@ 63,8 65,7 @@ class MyApp extends StatelessWidget {
case SyncService.Fever:
return FeverPage();
case SyncService.Feedbin:
- // TODO: Handle this case.
- break;
+ return FeedbinPage();
case SyncService.GReader:
// TODO: Handle this case.
break;
M lib/models/feed.dart => lib/models/feed.dart +1 -1
@@ 29,7 29,7 @@ class RSSFeed {
: StoreKeys.FEED_FILTER_SOURCE;
Tuple2<String, List<String>> _getPredicates() {
- List<String> where = ["TRUE"];
+ List<String> where = ["1 = 1"];
List<String> whereArgs = [];
if (sids.length > 0) {
var placeholders = List.filled(sids.length, "?").join(" , ");
M lib/models/items_model.dart => lib/models/items_model.dart +1 -1
@@ 51,7 51,7 @@ class ItemsModel with ChangeNotifier {
Future<void> markAllRead(Set<String> sids, {DateTime date, before = true}) async {
Global.service.markAllRead(sids, date, before);
- List<String> predicates = [];
+ List<String> predicates = ["hasRead = 0"];
if (sids.length > 0) {
predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})");
}
A lib/models/services/feedbin.dart => lib/models/services/feedbin.dart +250 -0
@@ 0,0 1,250 @@
+import 'dart:convert';
+import 'dart:math';
+
+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:fluent_reader_lite/utils/utils.dart';
+import 'package:html/parser.dart';
+import 'package:tuple/tuple.dart';
+import 'package:http/http.dart' as http;
+
+import '../item.dart';
+import '../source.dart';
+
+class FeedbinServiceHandler extends ServiceHandler {
+ String endpoint;
+ String username;
+ String password;
+ int fetchLimit;
+ int _lastId;
+ Tuple2<Set<String>, Set<String>> _lastSynced;
+
+ FeedbinServiceHandler() {
+ 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);
+ _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
+ }
+
+ FeedbinServiceHandler.fromValues(
+ this.endpoint,
+ this.username,
+ this.password,
+ this.fetchLimit
+ ) {
+ _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
+ }
+
+ void persist() {
+ Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Feedbin.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.setInt(StoreKeys.LAST_ID, _lastId);
+ 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_ID);
+ Global.service = null;
+ }
+
+ String _getApiKey() {
+ final credentials = "$username:$password";
+ final bytes = utf8.encode(credentials);
+ return base64.encode(bytes);
+ }
+
+ Future<http.Response> _fetchAPI(String params) async {
+ return await http.get(
+ endpoint + params,
+ headers: {
+ "Authorization": "Basic ${_getApiKey()}",
+ }
+ );
+ }
+
+ Future<void> _markItems(String type, String method, List<String> refs) async {
+ final auth = "Basic ${_getApiKey()}";
+ final promises = List<Future>.empty(growable: true);
+ final client = http.Client();
+ try {
+ while (refs.length > 0) {
+ final batch = List<int>.empty(growable: true);
+ while (batch.length < 1000 && refs.length > 0) {
+ batch.add(int.parse(refs.removeLast()));
+ }
+ final bodyObject = {
+ "${type}_entries": batch,
+ };
+ final request = http.Request(
+ method,
+ Uri.parse(endpoint + type + "_entries.json"),
+ );
+ request.headers["Authorization"] = auth;
+ request.headers["Content-Type"] = "application/json; charset=utf-8";
+ request.body = jsonEncode(bodyObject);
+ promises.add(client.send(request));
+ }
+ await Future.wait(promises);
+ } finally {
+ client.close();
+ }
+ }
+
+ int get lastId => _lastId;
+ set lastId(int value) {
+ _lastId = value;
+ Store.sp.setInt(StoreKeys.LAST_ID, value);
+ }
+
+ @override
+ Future<bool> validate() async {
+ try {
+ final response = await _fetchAPI("authentication.json");
+ return response.statusCode == 200;
+ } catch(exp) {
+ print(exp);
+ return false;
+ }
+ }
+
+ @override
+ Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources() async {
+ final response = await _fetchAPI("subscriptions.json");
+ assert(response.statusCode == 200);
+ final subscriptions = jsonDecode(response.body);
+ final groupsMap = Map<String, List<String>>();
+ final tagsResponse = await _fetchAPI("taggings.json");
+ assert(tagsResponse.statusCode == 200);
+ final tags = jsonDecode(tagsResponse.body);
+ for (var tag in tags) {
+ final name = tag["name"].trim();
+ groupsMap.putIfAbsent(name, () => []);
+ groupsMap[name].add(tag["feed_id"].toString());
+ }
+ final sources = subscriptions.map<RSSSource>((s) {
+ return RSSSource(s["feed_id"].toString(), s["feed_url"], s["title"]);
+ }).toList();
+ return Tuple2(sources, groupsMap);
+ }
+
+ @override
+ Future<List<RSSItem>> fetchItems() async {
+ var page = 1;
+ var minId = Utils.syncMaxId;
+ var items = [];
+ List lastFetched;
+ do {
+ try {
+ final response = await _fetchAPI("entries.json?mode=extended&per_page=125&page=$page");
+ assert(response.statusCode == 200);
+ lastFetched = jsonDecode(response.body);
+ items.addAll(lastFetched.where((i) => i["id"] > lastId && i["id"] < minId));
+ minId = lastFetched.fold(minId, (m, n) => min(m, n["id"]));
+ page += 1;
+ } catch(exp) {
+ break;
+ }
+ } while (
+ minId > lastId &&
+ lastFetched != null && lastFetched.length >= 125 &&
+ items.length < fetchLimit
+ );
+ lastId = items.fold(lastId, (m, n) => max(m, n["id"]));
+ final parsedItems = List<RSSItem>.empty(growable: true);
+ final unread = _lastSynced.item1;
+ final starred = _lastSynced.item2;
+ for (var i in items) {
+ if (i["content"] == null) continue;
+ final dom = parse(i["content"]);
+ final iid = i["id"].toString();
+ final item = RSSItem(
+ id: iid,
+ source: i["feed_id"].toString(),
+ title: i["title"],
+ link: i["url"],
+ date: DateTime.parse(i["published"]),
+ content: i["content"],
+ snippet: dom.documentElement.text.trim(),
+ creator: i["author"],
+ hasRead: !unread.contains(iid),
+ starred: starred.contains(iid),
+ );
+ if (i["images"] != null && i["images"]["original_url"] != null) {
+ item.thumb = i["images"]["original_url"];
+ } else {
+ var img = dom.querySelector("img");
+ if (img != null && img.attributes["src"] != null) {
+ var thumb = img.attributes["src"];
+ if (thumb.startsWith("http")) {
+ item.thumb = thumb;
+ }
+ }
+ }
+ parsedItems.add(item);
+ }
+ _lastSynced = null;
+ return parsedItems;
+ }
+
+ @override
+ Future<Tuple2<Set<String>, Set<String>>> syncItems() async {
+ final responses = await Future.wait([
+ _fetchAPI("unread_entries.json"),
+ _fetchAPI("starred_entries.json"),
+ ]);
+ assert(responses[0].statusCode == 200);
+ assert(responses[1].statusCode == 200);
+ final unread = jsonDecode(responses[0].body);
+ final starred = jsonDecode(responses[1].body);
+ _lastSynced = Tuple2(
+ Set.from(unread.map((i) => i.toString())),
+ Set.from(starred.map((i) => i.toString())),
+ );
+ return _lastSynced;
+ }
+
+ Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
+ 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"]);
+ await _markItems("unread", "DELETE", List.from(iids));
+ }
+
+ Future<void> markRead(RSSItem item) async{
+ await _markItems("unread", "DELETE", [item.id]);
+ }
+
+ Future<void> markUnead(RSSItem item) async {
+ await _markItems("unread", "POST", [item.id]);
+ }
+
+ Future<void> star(RSSItem item) async {
+ await _markItems("starred", "POST", [item.id]);
+ }
+
+ Future<void> unstar(RSSItem item) async {
+ await _markItems("starred", "DELETE", [item.id]);
+ }
+}<
\ No newline at end of file
M lib/models/services/fever.dart => lib/models/services/fever.dart +4 -3
@@ 4,6 4,7 @@ import 'dart:math';
import 'package:fluent_reader_lite/models/item.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:html/parser.dart';
import 'package:http/http.dart' as http;
import 'package:fluent_reader_lite/models/source.dart';
@@ 117,14 118,14 @@ class FeverServiceHandler extends ServiceHandler {
@override
Future<List<RSSItem>> fetchItems() async {
- var minId = useInt32 ? 2147483647 : 2^50;
+ var minId = useInt32 ? 2147483647 : Utils.syncMaxId;
List<dynamic> response;
List<dynamic> items = [];
do {
response = (await _fetchAPI(params: "&items&max_id=$minId"))["items"];
if (response == null) throw Error();
items.addAll(response.where((i) => i["id"] > lastId));
- if (response.length == 0 && minId == 2^50) {
+ if (response.length == 0 && minId == Utils.syncMaxId) {
useInt32 = true;
minId = 2147483647;
response = null;
@@ 161,7 162,7 @@ class FeverServiceHandler extends ServiceHandler {
var a = dom.querySelector("body>ul>li:first-child>a");
if (a != null && a.text.endsWith(", image\/generic") && a.attributes["href"] != null)
item.thumb = a.attributes["href"];
- }
+ }
return item;
});
lastId = items.fold(lastId, (m, n) => max(m, n["id"]));
M lib/pages/home_page.dart => lib/pages/home_page.dart +13 -1
@@ 29,6 29,10 @@ class ScrollTopNotifier with ChangeNotifier {
class _HomePageState extends State<HomePage> {
final _scrollTopNotifier = ScrollTopNotifier();
+ final _controller = CupertinoTabController();
+ final List<GlobalKey<NavigatorState>> _tabNavigatorKeys = [
+ GlobalKey(), GlobalKey(),
+ ];
Widget _constructPage(Widget page, bool isMobile) {
return isMobile
@@ 49,7 53,8 @@ class _HomePageState extends State<HomePage> {
builder: (context, hasService, child) {
if (!hasService) return SetupPage();
var isMobile = true;
- var left = CupertinoTabScaffold(
+ final leftTabs = CupertinoTabScaffold(
+ controller: _controller,
backgroundColor: CupertinoColors.systemBackground,
tabBar: CupertinoTabBar(
backgroundColor: CupertinoColors.systemBackground,
@@ 67,6 72,7 @@ class _HomePageState extends State<HomePage> {
),
tabBuilder: (context, index) {
return CupertinoTabView(
+ navigatorKey: _tabNavigatorKeys[index],
routes: {
'/feed': (context) {
Widget page = ItemListPage(_scrollTopNotifier);
@@ 82,6 88,12 @@ class _HomePageState extends State<HomePage> {
);
},
);
+ final left = WillPopScope(
+ child: leftTabs,
+ onWillPop: () async {
+ return !(await _tabNavigatorKeys[_controller.index].currentState.maybePop());
+ },
+ );
return ScreenTypeLayout.builder(
mobile: (context) => left,
tablet: (context) {
M lib/pages/item_list_page.dart => lib/pages/item_list_page.dart +2 -2
@@ 352,8 352,8 @@ class _ItemListPageState extends State<ItemListPage> {
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: feed.allLoaded
- ? Text("All loaded", style: TextStyle(
- color: CupertinoColors.secondaryLabel.resolveFrom(context),
+ ? Text(S.of(context).allLoaded, style: TextStyle(
+ color: CupertinoColors.tertiaryLabel.resolveFrom(context),
))
: CupertinoActivityIndicator()
),
M lib/pages/settings/about_page.dart => lib/pages/settings/about_page.dart +3 -0
@@ 2,6 2,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/utils/colors.dart';
+import 'package:fluent_reader_lite/utils/utils.dart';
import 'package:flutter/cupertino.dart';
class AboutPage extends StatelessWidget {
@@ 45,9 46,11 @@ class AboutPage extends StatelessWidget {
ListTileGroup([
MyListTile(
title: Text(S.of(context).openSource),
+ onTap: () { Utils.openExternal("https://github.com/yang991178/fluent-reader-lite"); },
),
MyListTile(
title: Text(S.of(context).feedback),
+ onTap: () { Utils.openExternal("https://github.com/yang991178/fluent-reader-lite/issues"); },
withDivider: false,
),
]),
A lib/pages/settings/services/feedbin_page.dart => lib/pages/settings/services/feedbin_page.dart +231 -0
@@ 0,0 1,231 @@
+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/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:provider/provider.dart';
+
+class FeedbinPage extends StatefulWidget {
+ @override
+ _FeedbinPageState createState() => _FeedbinPageState();
+}
+
+class _FeedbinPageState extends State<FeedbinPage> {
+ String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "https://api.feedbin.me/v2/";
+ 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 = FeedbinServiceHandler.fromValues(
+ _endpoint,
+ _username,
+ _password,
+ _fetchLimit,
+ );
+ setState(() { _validating = true; });
+ final isValid = await handler.validate();
+ if (!mounted) return;
+ if (isValid) {
+ handler.persist();
+ await Global.syncModel.syncWithService();
+ Global.syncModel.checkHasService();
+ _validating = false;
+ if (mounted) Navigator.of(context).pop();
+ } else {
+ setState(() { _validating = false; });
+ 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; });
+ await Global.syncModel.removeService();
+ setState(() { _validating = false; });
+ 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,
+ withDivider: false,
+ ),
+ ], 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: _validating
+ ? CupertinoActivityIndicator()
+ : 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("Feedbin"),
+ ),
+ child: ListView(children: [
+ inputs,
+ syncItems,
+ saveButton,
+ if (Global.service != null) logOutButton,
+ ]),
+ );
+ return WillPopScope(
+ child: page,
+ onWillPop: () async => !_validating,
+ );
+ }
+}
M lib/pages/setup_page.dart => lib/pages/setup_page.dart +1 -0
@@ 41,6 41,7 @@ class SetupPage extends StatelessWidget {
),
MyListTile(
title: Text("Feedbin"),
+ onTap: () { _configure(context, "/settings/service/feedbin"); },
),
], title: S.of(context).service);
final page = CupertinoPageScaffold(
M lib/utils/global.dart => lib/utils/global.dart +2 -1
@@ 3,6 3,7 @@ import 'package:fluent_reader_lite/models/global_model.dart';
import 'package:fluent_reader_lite/models/groups_model.dart';
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/sources_model.dart';
import 'package:fluent_reader_lite/models/sync_model.dart';
@@ 39,7 40,7 @@ abstract class Global {
service = FeverServiceHandler();
break;
case SyncService.Feedbin:
- // TODO: Handle this case.
+ service = FeedbinServiceHandler();
break;
case SyncService.GReader:
// TODO: Handle this case.
M lib/utils/utils.dart => lib/utils/utils.dart +9 -0
@@ 1,9 1,18 @@
+import 'dart:math';
+
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
+import 'package:url_launcher/url_launcher.dart';
abstract class Utils {
+ static final syncMaxId = pow(2, 50);
+
+ static void openExternal(String url) {
+ launch(url, forceSafariVC: false, forceWebView: false);
+ }
+
static int binarySearch<T>(List<T> sortedList, T value,
int Function(T, T) compare) {
var min = 0;