M android/app/src/main/AndroidManifest.xml => android/app/src/main/AndroidManifest.xml +10 -0
@@ 41,6 41,16 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
+ <data
+ android:scheme="fluent-reader"
+ android:host="import" />
+ </intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
M ios/Podfile.lock => ios/Podfile.lock +6 -0
@@ 14,6 14,8 @@ PODS:
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
+ - uni_links (0.0.1):
+ - Flutter
- url_launcher (0.0.1):
- Flutter
- webview_flutter (0.0.1):
@@ 26,6 28,7 @@ DEPENDENCIES:
- share (from `.symlinks/plugins/share/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
+ - uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
- webview_flutter (from `.symlinks/plugins/webview_flutter/ios`)
@@ 46,6 49,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
+ uni_links:
+ :path: ".symlinks/plugins/uni_links/ios"
url_launcher:
:path: ".symlinks/plugins/url_launcher/ios"
webview_flutter:
@@ 59,6 64,7 @@ SPEC CHECKSUMS:
share: 0b2c3e82132f5888bccca3351c504d0003b3b410
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
+ uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96
M ios/Runner/Info.plist => ios/Runner/Info.plist +15 -2
@@ 2,8 2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
- <key>ITSAppUsesNonExemptEncryption</key>
- <false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ 22,8 20,23 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>me.hyliu.fluent-reader-lite</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>fluent-reader</string>
+ </array>
+ </dict>
+ </array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>ITSAppUsesNonExemptEncryption</key>
+ <false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +2 -1
@@ 90,5 90,6 @@
"errorLog": "Error log",
"unreadSourceTip": "You can long press on the title of this page to toggle between all and unread subscriptions.",
"uncategorized": "Uncategorized",
- "showUncategorized": "Show uncategorized"
+ "showUncategorized": "Show uncategorized",
+ "serviceExists": "A service already exists. Please log out before importing."
}=
\ No newline at end of file
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +2 -1
@@ 90,5 90,6 @@
"errorLog": "错误日志",
"unreadSourceTip": "您可以长按此页面的标题来切换全部订阅源或仅未读订阅源。",
"uncategorized": "未分组",
- "showUncategorized": "显示“未分组”"
+ "showUncategorized": "显示“未分组”",
+ "serviceExists": "已登录至一个服务,请在导入前登出。"
}=
\ No newline at end of file
M lib/models/groups_model.dart => lib/models/groups_model.dart +1 -0
@@ 9,6 9,7 @@ class GroupsModel with ChangeNotifier {
Map<String, List<String>> get groups => _groups;
set groups(Map<String, List<String>> groups) {
_groups = groups;
+ updateUncategorized();
notifyListeners();
Store.setGroups(groups);
}
M lib/models/service.dart => lib/models/service.dart +7 -1
@@ 1,5 1,7 @@
import 'package:fluent_reader_lite/models/item.dart';
import 'package:fluent_reader_lite/models/source.dart';
+import 'package:fluent_reader_lite/utils/global.dart';
+import 'package:fluent_reader_lite/utils/store.dart';
import 'package:tuple/tuple.dart';
enum SyncService {
@@ 7,7 9,11 @@ enum SyncService {
}
abstract class ServiceHandler {
- void remove();
+ void remove() {
+ Store.sp.remove(StoreKeys.SYNC_SERVICE);
+ Global.groupsModel.groups = Map();
+ Global.groupsModel.showUncategorized = false;
+ }
Future<bool> validate();
Future<void> reauthenticate() async { }
Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources();
M lib/models/services/feedbin.dart => lib/models/services/feedbin.dart +1 -1
@@ 49,7 49,7 @@ class FeedbinServiceHandler extends ServiceHandler {
@override
void remove() {
- Store.sp.remove(StoreKeys.SYNC_SERVICE);
+ super.remove();
Store.sp.remove(StoreKeys.ENDPOINT);
Store.sp.remove(StoreKeys.USERNAME);
Store.sp.remove(StoreKeys.PASSWORD);
M lib/models/services/fever.dart => lib/models/services/fever.dart +1 -1
@@ 50,7 50,7 @@ class FeverServiceHandler extends ServiceHandler {
@override
void remove() {
- Store.sp.remove(StoreKeys.SYNC_SERVICE);
+ super.remove();
Store.sp.remove(StoreKeys.ENDPOINT);
Store.sp.remove(StoreKeys.USERNAME);
Store.sp.remove(StoreKeys.PASSWORD);
M lib/models/services/greader.dart => lib/models/services/greader.dart +1 -1
@@ 79,7 79,7 @@ class GReaderServiceHandler extends ServiceHandler {
@override
void remove() {
- Store.sp.remove(StoreKeys.SYNC_SERVICE);
+ super.remove();
Store.sp.remove(StoreKeys.ENDPOINT);
Store.sp.remove(StoreKeys.USERNAME);
Store.sp.remove(StoreKeys.PASSWORD);
A lib/models/services/service_import.dart => lib/models/services/service_import.dart +22 -0
@@ 0,0 1,22 @@
+class ServiceImport {
+ String endpoint;
+ String username;
+ String password;
+ String apiId;
+ String apiKey;
+
+ static const typeMap = {
+ "f": "/settings/service/fever",
+ "g": "/settings/service/greader",
+ "i": "/settings/service/inoreader",
+ "fb": "/settings/service/feedbin"
+ };
+
+ ServiceImport(Map<String, String> params) {
+ endpoint = params["e"];
+ username = params["u"];
+ password = params["p"];
+ apiId = params["i"];
+ apiKey = params["k"];
+ }
+}
M lib/pages/home_page.dart => lib/pages/home_page.dart +57 -0
@@ 1,5 1,8 @@
+import 'dart:async';
+
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/main.dart';
+import 'package:fluent_reader_lite/models/services/service_import.dart';
import 'package:fluent_reader_lite/models/sync_model.dart';
import 'package:fluent_reader_lite/pages/setup_page.dart';
import 'package:fluent_reader_lite/pages/subscription_list_page.dart';
@@ 9,10 12,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';
+import 'package:uni_links/uni_links.dart';
import 'item_list_page.dart';
class HomePage extends StatefulWidget {
+ HomePage() : super(key: Key("home"));
+
@override
_HomePageState createState() => _HomePageState();
}
@@ 33,6 39,57 @@ class _HomePageState extends State<HomePage> {
final List<GlobalKey<NavigatorState>> _tabNavigatorKeys = [
GlobalKey(), GlobalKey(),
];
+ StreamSubscription _uriSub;
+
+ void _uriStreamListener(Uri uri) {
+ if (uri == null) return;
+ if (uri.host == "import") {
+ if (Global.syncModel.hasService) {
+ showCupertinoDialog(
+ context: context,
+ builder: (context) => CupertinoAlertDialog(
+ title: Text(S.of(context).serviceExists),
+ actions: [
+ CupertinoDialogAction(
+ child: Text(S.of(context).confirm),
+ onPressed: () { Navigator.of(context).pop(); },
+ ),
+ ],
+ ),
+ );
+ } else if (!Global.syncModel.syncing) {
+ final import = ServiceImport(uri.queryParameters);
+ final route = ServiceImport.typeMap[uri.queryParameters["t"]];
+ if (route != null) {
+ final navigator = Navigator.of(context);
+ while (navigator.canPop()) navigator.pop();
+ navigator.pushNamed(route, arguments: import);
+ }
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _uriSub = getUriLinksStream().listen(_uriStreamListener);
+ Future.delayed(Duration.zero, () async {
+ try {
+ final uri = await getInitialUri();
+ if (uri != null) {
+ _uriStreamListener(uri);
+ }
+ } catch(exp) {
+ print(exp);
+ }
+ });
+ }
+
+ @override
+ dispose() {
+ _uriSub.cancel();
+ super.dispose();
+ }
Widget _constructPage(Widget page, bool isMobile) {
return isMobile
M lib/pages/settings/services/feedbin_page.dart => lib/pages/settings/services/feedbin_page.dart +22 -0
@@ 1,9 1,11 @@
+import 'dart:convert';
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/service_import.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';
@@ 26,6 28,26 @@ class _FeedbinPageState extends State<FeedbinPage> {
int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250;
bool _validating = false;
+ @override
+ void initState() {
+ super.initState();
+ Future.delayed(Duration.zero, () {
+ ServiceImport import = ModalRoute.of(context).settings.arguments;
+ if (import == null) return;
+ if (Utils.testUrl(import.endpoint)) {
+ setState(() { _endpoint = import.endpoint; });
+ }
+ if (Utils.notEmpty(import.username)) {
+ setState(() { _username = import.username; });
+ }
+ if (Utils.notEmpty(import.password)) {
+ final bytes = base64.decode(import.password);
+ final password = utf8.decode(bytes);
+ setState(() { _password = password; });
+ }
+ });
+ }
+
void _editEndpoint() async {
final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
M lib/pages/settings/services/fever_page.dart => lib/pages/settings/services/fever_page.dart +33 -5
@@ 6,6 6,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/models/services/fever.dart';
+import 'package:fluent_reader_lite/models/services/service_import.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';
@@ 24,10 25,29 @@ class FeverPage extends StatefulWidget {
class _FeverPageState extends State<FeverPage> {
String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "";
String _username = Store.sp.getString(StoreKeys.USERNAME) ?? "";
+ String _apiKey = Store.sp.getString(StoreKeys.API_KEY);
String _password = Store.sp.getString(StoreKeys.PASSWORD) ?? "";
int _fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 250;
bool _validating = false;
+ @override
+ void initState() {
+ super.initState();
+ Future.delayed(Duration.zero, () {
+ ServiceImport import = ModalRoute.of(context).settings.arguments;
+ if (import == null) return;
+ if (Utils.testUrl(import.endpoint)) {
+ setState(() { _endpoint = import.endpoint; });
+ }
+ if (Utils.notEmpty(import.username)) {
+ setState(() { _username = import.username; });
+ }
+ if (Utils.notEmpty(import.apiKey)) {
+ setState(() { _apiKey = import.apiKey; });
+ }
+ });
+ }
+
void _editEndpoint() async {
final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
@@ 50,7 70,10 @@ class _FeverPageState extends State<FeverPage> {
),
));
if (username == null) return;
- setState(() { _username = username; });
+ setState(() {
+ _username = username;
+ _apiKey = null;
+ });
}
void _editPassword() async {
@@ 62,16 85,21 @@ class _FeverPageState extends State<FeverPage> {
),
));
if (password == null) return;
- setState(() { _password = password; });
+ setState(() {
+ _password = password;
+ _apiKey = null;
+ });
}
bool _canSave() {
if (_validating) return false;
- return _endpoint.length > 0 && _username.length > 0 && _password.length > 0;
+ return _endpoint.length > 0 &&
+ ((_username.length > 0 && _password.length > 0) || _apiKey != null);
}
void _save() async {
- final apiKey = md5.convert(utf8.encode("$_username:$_password")).toString();
+ final apiKey = _apiKey
+ ?? md5.convert(utf8.encode("$_username:$_password")).toString();
final handler = FeverServiceHandler.fromValues(
_endpoint,
apiKey,
@@ 154,7 182,7 @@ class _FeverPageState extends State<FeverPage> {
),
MyListTile(
title: Text(S.of(context).password),
- trailing: Text(_password.length == 0
+ trailing: Text(_password.length == 0 && _apiKey == null
? S.of(context).enter
: S.of(context).entered),
onTap: _editPassword,
M lib/pages/settings/services/greader_page.dart => lib/pages/settings/services/greader_page.dart +22 -0
@@ 1,9 1,11 @@
+import 'dart:convert';
import 'dart:io';
import 'package:fluent_reader_lite/components/list_tile_group.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/models/services/greader.dart';
+import 'package:fluent_reader_lite/models/services/service_import.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';
@@ 27,6 29,26 @@ class _GReaderPageState extends State<GReaderPage> {
bool _validating = false;
+ @override
+ void initState() {
+ super.initState();
+ Future.delayed(Duration.zero, () {
+ ServiceImport import = ModalRoute.of(context).settings.arguments;
+ if (import == null) return;
+ if (Utils.testUrl(import.endpoint)) {
+ setState(() { _endpoint = import.endpoint; });
+ }
+ if (Utils.notEmpty(import.username)) {
+ setState(() { _username = import.username; });
+ }
+ if (Utils.notEmpty(import.password)) {
+ final bytes = base64.decode(import.password);
+ final password = utf8.decode(bytes);
+ setState(() { _password = password; });
+ }
+ });
+ }
+
void _editEndpoint() async {
final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
M lib/pages/settings/services/inoreader_page.dart => lib/pages/settings/services/inoreader_page.dart +29 -1
@@ 1,9 1,11 @@
+import 'dart:convert';
import 'dart:io';
import 'package:fluent_reader_lite/components/list_tile_group.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/models/services/greader.dart';
+import 'package:fluent_reader_lite/models/services/service_import.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';
@@ 26,7 28,7 @@ class _InoreaderPageState extends State<InoreaderPage> {
"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) ?? "";
@@ 38,6 40,32 @@ class _InoreaderPageState extends State<InoreaderPage> {
bool _validating = false;
+ @override
+ void initState() {
+ super.initState();
+ Future.delayed(Duration.zero, () {
+ ServiceImport import = ModalRoute.of(context).settings.arguments;
+ if (import == null) return;
+ if (_endpointOptions.contains(import.endpoint)) {
+ setState(() { _endpoint = import.endpoint; });
+ }
+ if (Utils.notEmpty(import.username)) {
+ setState(() { _username = import.username; });
+ }
+ if (Utils.notEmpty(import.password)) {
+ final bytes = base64.decode(import.password);
+ final password = utf8.decode(bytes);
+ setState(() { _password = password; });
+ }
+ if (Utils.notEmpty(import.apiId)) {
+ setState(() { _apiId = import.apiId; });
+ }
+ if (Utils.notEmpty(import.apiKey)) {
+ setState(() { _apiKey = import.apiKey; });
+ }
+ });
+ }
+
void _editUsername() async {
final String username = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
M lib/utils/global.dart => lib/utils/global.dart +1 -1
@@ 59,7 59,7 @@ abstract class Global {
db = await DatabaseHelper.getDatabase();
await db.delete(
"items",
- where: "date < ?",
+ where: "date < ? AND starred = 0",
whereArgs: [
DateTime.now()
.subtract(Duration(days: globalModel.keepItemsDays))
M lib/utils/utils.dart => lib/utils/utils.dart +2 -2
@@ 47,9 47,9 @@ abstract class Utils {
r"^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*$)",
caseSensitive: false,
);
- static bool testUrl(String url) => _urlRegex.hasMatch(url.trim());
+ static bool testUrl(String url) => url != null && _urlRegex.hasMatch(url.trim());
- static bool notEmpty(String text) => text.trim().length > 0;
+ static bool notEmpty(String text) => text != null && text.trim().length > 0;
static void showServiceFailureDialog(BuildContext context) {
showCupertinoDialog(
M pubspec.lock => pubspec.lock +7 -0
@@ 544,6 544,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0-nullsafety.5"
+ uni_links:
+ dependency: "direct main"
+ description:
+ name: uni_links
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.4.0"
url_launcher:
dependency: "direct main"
description:
M pubspec.yaml => pubspec.yaml +1 -0
@@ 44,6 44,7 @@ dependencies:
cached_network_image: ^2.5.0
flutter_cache_manager: ^2.1.0
lpinyin: ^1.1.0
+ uni_links: ^0.4.0
modal_bottom_sheet: ^1.0.0+1
overlay_dialog: ^0.0.3