From 0b77b4664f92ca4fce2f347c33fc4298f7bf9c26 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 09:31:10 +0800 Subject: [PATCH] add feedbin support --- assets/article/article.css | 4 +- lib/components/sync_control.dart | 6 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh.arb | 3 +- lib/main.dart | 5 +- lib/models/feed.dart | 2 +- lib/models/items_model.dart | 2 +- lib/models/services/feedbin.dart | 250 ++++++++++++++++++ lib/models/services/fever.dart | 7 +- lib/pages/home_page.dart | 14 +- lib/pages/item_list_page.dart | 4 +- lib/pages/settings/about_page.dart | 3 + lib/pages/settings/services/feedbin_page.dart | 231 ++++++++++++++++ lib/pages/setup_page.dart | 1 + lib/utils/global.dart | 3 +- lib/utils/utils.dart | 9 + 16 files changed, 530 insertions(+), 17 deletions(-) create mode 100644 lib/models/services/feedbin.dart create mode 100644 lib/pages/settings/services/feedbin_page.dart diff --git a/assets/article/article.css b/assets/article/article.css index 1a21f73..8572084 100644 --- a/assets/article/article.css +++ b/assets/article/article.css @@ -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 { diff --git a/lib/components/sync_control.dart b/lib/components/sync_control.dart index 5cfc333..3b58b3a 100644 --- a/lib/components/sync_control.dart +++ b/lib/components/sync_control.dart @@ -13,8 +13,10 @@ class _SyncControlState extends State { 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(); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 25f2180..43d1d08 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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 diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 3a3c00d..8e20764 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -76,5 +76,6 @@ "serviceFailureHint": "请检查服务配置或网络连接", "logOut": "登出", "logOutWarning": "这将移除所有本地数据,是否继续?", - "confirm": "确定" + "confirm": "确定", + "allLoaded": "已全部加载" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 22304d3..fa8bc08 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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; diff --git a/lib/models/feed.dart b/lib/models/feed.dart index 2f80d32..70240a5 100644 --- a/lib/models/feed.dart +++ b/lib/models/feed.dart @@ -29,7 +29,7 @@ class RSSFeed { : StoreKeys.FEED_FILTER_SOURCE; Tuple2> _getPredicates() { - List where = ["TRUE"]; + List where = ["1 = 1"]; List whereArgs = []; if (sids.length > 0) { var placeholders = List.filled(sids.length, "?").join(" , "); diff --git a/lib/models/items_model.dart b/lib/models/items_model.dart index abc9cf2..0d4a28a 100644 --- a/lib/models/items_model.dart +++ b/lib/models/items_model.dart @@ -51,7 +51,7 @@ class ItemsModel with ChangeNotifier { Future markAllRead(Set sids, {DateTime date, before = true}) async { Global.service.markAllRead(sids, date, before); - List predicates = []; + List predicates = ["hasRead = 0"]; if (sids.length > 0) { predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})"); } diff --git a/lib/models/services/feedbin.dart b/lib/models/services/feedbin.dart new file mode 100644 index 0000000..63df065 --- /dev/null +++ b/lib/models/services/feedbin.dart @@ -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> _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 _fetchAPI(String params) async { + return await http.get( + endpoint + params, + headers: { + "Authorization": "Basic ${_getApiKey()}", + } + ); + } + + Future _markItems(String type, String method, List refs) async { + final auth = "Basic ${_getApiKey()}"; + final promises = List.empty(growable: true); + final client = http.Client(); + try { + while (refs.length > 0) { + final batch = List.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 validate() async { + try { + final response = await _fetchAPI("authentication.json"); + return response.statusCode == 200; + } catch(exp) { + print(exp); + return false; + } + } + + @override + Future, Map>>> getSources() async { + final response = await _fetchAPI("subscriptions.json"); + assert(response.statusCode == 200); + final subscriptions = jsonDecode(response.body); + final groupsMap = Map>(); + 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((s) { + return RSSSource(s["feed_id"].toString(), s["feed_url"], s["title"]); + }).toList(); + return Tuple2(sources, groupsMap); + } + + @override + Future> 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.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, Set>> 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 markAllRead(Set sids, DateTime date, bool before) async { + 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"]); + await _markItems("unread", "DELETE", List.from(iids)); + } + + Future markRead(RSSItem item) async{ + await _markItems("unread", "DELETE", [item.id]); + } + + Future markUnead(RSSItem item) async { + await _markItems("unread", "POST", [item.id]); + } + + Future star(RSSItem item) async { + await _markItems("starred", "POST", [item.id]); + } + + Future unstar(RSSItem item) async { + await _markItems("starred", "DELETE", [item.id]); + } +} \ No newline at end of file diff --git a/lib/models/services/fever.dart b/lib/models/services/fever.dart index e68b087..052bd8c 100644 --- a/lib/models/services/fever.dart +++ b/lib/models/services/fever.dart @@ -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> fetchItems() async { - var minId = useInt32 ? 2147483647 : 2^50; + var minId = useInt32 ? 2147483647 : Utils.syncMaxId; List response; List 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"])); diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 920805a..b9b0e98 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -29,6 +29,10 @@ class ScrollTopNotifier with ChangeNotifier { class _HomePageState extends State { final _scrollTopNotifier = ScrollTopNotifier(); + final _controller = CupertinoTabController(); + final List> _tabNavigatorKeys = [ + GlobalKey(), GlobalKey(), + ]; Widget _constructPage(Widget page, bool isMobile) { return isMobile @@ -49,7 +53,8 @@ class _HomePageState extends State { 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 { ), tabBuilder: (context, index) { return CupertinoTabView( + navigatorKey: _tabNavigatorKeys[index], routes: { '/feed': (context) { Widget page = ItemListPage(_scrollTopNotifier); @@ -82,6 +88,12 @@ class _HomePageState extends State { ); }, ); + final left = WillPopScope( + child: leftTabs, + onWillPop: () async { + return !(await _tabNavigatorKeys[_controller.index].currentState.maybePop()); + }, + ); return ScreenTypeLayout.builder( mobile: (context) => left, tablet: (context) { diff --git a/lib/pages/item_list_page.dart b/lib/pages/item_list_page.dart index a62c0b5..7a7d492 100644 --- a/lib/pages/item_list_page.dart +++ b/lib/pages/item_list_page.dart @@ -352,8 +352,8 @@ class _ItemListPageState extends State { 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() ), diff --git a/lib/pages/settings/about_page.dart b/lib/pages/settings/about_page.dart index 764728d..ff9bd76 100644 --- a/lib/pages/settings/about_page.dart +++ b/lib/pages/settings/about_page.dart @@ -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, ), ]), diff --git a/lib/pages/settings/services/feedbin_page.dart b/lib/pages/settings/services/feedbin_page.dart new file mode 100644 index 0000000..c373b6b --- /dev/null +++ b/lib/pages/settings/services/feedbin_page.dart @@ -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 { + 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( + 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( + 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, + ); + } +} diff --git a/lib/pages/setup_page.dart b/lib/pages/setup_page.dart index d3fcb12..d31642f 100644 --- a/lib/pages/setup_page.dart +++ b/lib/pages/setup_page.dart @@ -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( diff --git a/lib/utils/global.dart b/lib/utils/global.dart index 86d848e..f7c1c08 100644 --- a/lib/utils/global.dart +++ b/lib/utils/global.dart @@ -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. diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index f1a3b6b..160b1f8 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -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(List sortedList, T value, int Function(T, T) compare) { var min = 0; -- 2.38.5