~cytrogen/fluent-reader-mobile

4a21d3259885f835d064df292029e7fe164db960 — Bruce Liu 5 years ago 06e740a
add google reader api support
M ios/Runner.xcodeproj/project.pbxproj => ios/Runner.xcodeproj/project.pbxproj +15 -6
@@ 354,8 354,10 @@
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_IDENTITY = "iPhone Developer";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				DEVELOPMENT_TEAM = P2CG8QD3BP;
				DEVELOPMENT_TEAM = EM8VE646TZ;
				ENABLE_BITCODE = NO;
				FRAMEWORK_SEARCH_PATHS = (
					"$(inherited)",


@@ 370,8 372,9 @@
					"$(inherited)",
					"$(PROJECT_DIR)/Flutter",
				);
				PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite;
				PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite";
				PRODUCT_NAME = "$(TARGET_NAME)";
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";


@@ 492,8 495,10 @@
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_IDENTITY = "iPhone Developer";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				DEVELOPMENT_TEAM = P2CG8QD3BP;
				DEVELOPMENT_TEAM = EM8VE646TZ;
				ENABLE_BITCODE = NO;
				FRAMEWORK_SEARCH_PATHS = (
					"$(inherited)",


@@ 508,8 513,9 @@
					"$(inherited)",
					"$(PROJECT_DIR)/Flutter",
				);
				PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite;
				PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite";
				PRODUCT_NAME = "$(TARGET_NAME)";
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
				SWIFT_VERSION = 5.0;


@@ 524,8 530,10 @@
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_IDENTITY = "iPhone Developer";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				DEVELOPMENT_TEAM = P2CG8QD3BP;
				DEVELOPMENT_TEAM = EM8VE646TZ;
				ENABLE_BITCODE = NO;
				FRAMEWORK_SEARCH_PATHS = (
					"$(inherited)",


@@ 540,8 548,9 @@
					"$(inherited)",
					"$(PROJECT_DIR)/Flutter",
				);
				PRODUCT_BUNDLE_IDENTIFIER = me.hyliu.FluentReaderLite;
				PRODUCT_BUNDLE_IDENTIFIER = "me.hyliu.fluent-reader-lite";
				PRODUCT_NAME = "$(TARGET_NAME)";
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";

M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +2 -1
@@ 79,5 79,6 @@
    "logOutWarning": "All local data will be deleted. Are you sure?",
    "confirm": "Confirm",
    "allLoaded": "All loaded",
    "removeAd": "Remove Ad"
    "removeAd": "Remove Ad",
    "getApiKey": "Get API ID & Key"
  }
\ No newline at end of file

M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +2 -1
@@ 79,5 79,6 @@
    "logOutWarning": "这将移除所有本地数据,是否继续?",
    "confirm": "确定",
    "allLoaded": "已全部加载",
    "removeAd": "移除广告"
    "removeAd": "移除广告",
    "getApiKey": "获取 API ID & KEY"
  }
\ 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/greader_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';


@@ 62,6 63,7 @@ class MyApp extends StatelessWidget {
    "/settings/service/fever": (context) => FeverPage(),
    "/settings/service/feedbin": (context) => FeedbinPage(),
    "/settings/service/inoreader": (context) => InoreaderPage(),
    "/settings/service/greader": (context) => GReaderPage(),
    "/settings/service": (context) {
      var serviceType = SyncService.values[Store.sp.getInt(StoreKeys.SYNC_SERVICE) ?? 0];
      switch (serviceType) {


@@ 72,7 74,7 @@ class MyApp extends StatelessWidget {
        case SyncService.Feedbin:
          return FeedbinPage();
        case SyncService.GReader:
          // TODO: Handle this case.
          return GReaderPage();
          break;
        case SyncService.Inoreader:
          return InoreaderPage();

M lib/models/services/greader.dart => lib/models/services/greader.dart +7 -3
@@ 1,5 1,4 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:fluent_reader_lite/models/item.dart';


@@ 61,7 60,10 @@ class GReaderServiceHandler extends ServiceHandler {
  }

  void persist() {
    Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Inoreader.index);
    Store.sp.setInt(
      StoreKeys.SYNC_SERVICE,
      inoreaderId != null ? SyncService.Inoreader.index : SyncService.GReader.index
    );
    Store.sp.setString(StoreKeys.ENDPOINT, endpoint);
    Store.sp.setString(StoreKeys.USERNAME, username);
    Store.sp.setString(StoreKeys.PASSWORD, password);


@@ 240,7 242,7 @@ class GReaderServiceHandler extends ServiceHandler {
    }
    final parsedItems = items.map<RSSItem>((i) {
      final dom = parse(i["summary"]["content"]);
      if (removeInoreaderAd) {
      if (removeInoreaderAd == true) {
        if (dom.documentElement.text.trim().startsWith("Ads from Inoreader")) {
          dom.body.firstChild.remove();
        }


@@ 321,6 323,8 @@ class GReaderServiceHandler extends ServiceHandler {
      }
      if (refs.length > 0)  _editTag(refs.join("&i="), _READ_TAG);
    } else {
      if (sids.length == 0)
        sids = Set.from(Global.sourcesModel.getSources().map((s) => s.id));
      for (var sid in sids) {
        final body = { "s": sid };
        _fetchAPI("/reader/api/0/mark-all-as-read", body: body);

M lib/models/sync_model.dart => lib/models/sync_model.dart +1 -0
@@ 49,6 49,7 @@ class SyncModel with ChangeNotifier {
    syncing = true;
    notifyListeners();
    try {
      await Global.service.reauthenticate();
      await Global.sourcesModel.updateSources();
      await Global.itemsModel.syncItems();
      await Global.itemsModel.fetchItems();

A lib/pages/settings/services/greader_page.dart => lib/pages/settings/services/greader_page.dart +247 -0
@@ 0,0 1,247 @@
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/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';

class GReaderPage extends StatefulWidget {
  @override
  _GReaderPageState createState() => _GReaderPageState();
}

class _GReaderPageState extends State<GReaderPage> {
  String _endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "";
  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 = GReaderServiceHandler.fromValues(
      _endpoint,
      _username,
      _password,
      _fetchLimit,
    );
    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 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,
      ),
    ], 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: 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("Google Reader API"),
      ),
      child: ListView(children: [
        inputs,
        syncItems,
        saveButton,
        if (Global.service != null) logOutButton,
      ]),
    );
    if (Platform.isAndroid) {
      return WillPopScope(child: page, onWillPop: () async => !_validating);
    } else {
      return page;
    }
  }
}

M lib/pages/settings/services/inoreader_page.dart => lib/pages/settings/services/inoreader_page.dart +9 -1
@@ 3,7 3,6 @@ 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';


@@ 15,6 14,7 @@ import 'package:flutter/cupertino.dart';
import 'package:overlay_dialog/overlay_dialog.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';

class InoreaderPage extends StatefulWidget {
  @override


@@ 163,6 163,10 @@ class _InoreaderPageState extends State<InoreaderPage> {
    }
  }

  void _getKey() {
    launch(_endpoint + "/all_articles#preferences-developer", forceSafariVC: false, forceWebView: false);
  }

  @override
  Widget build(BuildContext context) {
    final endpointItems = ListTileGroup.fromOptions(


@@ 199,6 203,10 @@ class _InoreaderPageState extends State<InoreaderPage> {
          ? S.of(context).enter
          : S.of(context).entered),
        onTap: _editAPIKey,
      ),
      MyListTile(
        title: Text(S.of(context).getApiKey),
        onTap: _getKey,
        withDivider: false,
      ),
    ], title: S.of(context).credentials);

M lib/pages/settings_page.dart => lib/pages/settings_page.dart +0 -2
@@ 1,5 1,3 @@
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';

M lib/pages/setup_page.dart => lib/pages/setup_page.dart +1 -0
@@ 35,6 35,7 @@ class SetupPage extends StatelessWidget {
      ),
      MyListTile(
        title: Text("Google Reader API"),
        onTap: () { _configure(context, "/settings/service/greader"); },
      ),
      MyListTile(
        title: Text("Inoreader"),