~cytrogen/fluent-reader-mobile

438ed4ce0837581b637fa6778f9b2fbc8eb9649e — Bruce Liu 5 years ago 8864d2f
add service import through uri scheme
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