~cytrogen/fluent-reader-mobile

2f8d3710d368f61ea1ffd02396e04ac291c3f4cc — Bruce Liu 4 years ago 64c6aee
update packages and use uri for http
M ios/Podfile.lock => ios/Podfile.lock +2 -2
@@ 66,8 66,8 @@ SPEC CHECKSUMS:
  sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
  uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
  url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
  webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96
  webview_flutter: 3603125dfd3bcbc9d8d418c3f80aeecf331c068b

PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

COCOAPODS: 1.10.0
COCOAPODS: 1.10.1

M lib/models/services/feedbin.dart => lib/models/services/feedbin.dart +24 -26
@@ 29,11 29,7 @@ class FeedbinServiceHandler extends ServiceHandler {
  }

  FeedbinServiceHandler.fromValues(
    this.endpoint,
    this.username,
    this.password,
    this.fetchLimit
  ) {
      this.endpoint, this.username, this.password, this.fetchLimit) {
    _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
  }



@@ 65,12 61,10 @@ class FeedbinServiceHandler extends ServiceHandler {
  }

  Future<http.Response> _fetchAPI(String params) async {
    return await http.get(
      endpoint + params,
      headers: {
        "Authorization": "Basic ${_getApiKey()}",
      }
    );
    var uri = Uri.parse(endpoint + params);
    return await http.get(uri, headers: {
      "Authorization": "Basic ${_getApiKey()}",
    });
  }

  Future<void> _markItems(String type, String method, List<String> refs) async {


@@ 112,14 106,15 @@ class FeedbinServiceHandler extends ServiceHandler {
    try {
      final response = await _fetchAPI("authentication.json");
      return response.statusCode == 200;
    } catch(exp) {
    } catch (exp) {
      print(exp);
      return false;
    }
  }

  @override
  Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources() async {
  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);


@@ 146,20 141,21 @@ class FeedbinServiceHandler extends ServiceHandler {
    List lastFetched;
    do {
      try {
        final response = await _fetchAPI("entries.json?mode=extended&per_page=125&page=$page");
        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));
        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) {
      } catch (exp) {
        break;
      }
    } while (
      minId > lastId &&
      lastFetched != null && lastFetched.length >= 125 &&
      items.length < fetchLimit
    );
    } 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;


@@ 184,7 180,7 @@ class FeedbinServiceHandler extends ServiceHandler {
        item.thumb = i["images"]["original_url"];
      } else {
        var img = dom.querySelector("img");
        if (img != null && img.attributes["src"] != null) { 
        if (img != null && img.attributes["src"] != null) {
          var thumb = img.attributes["src"];
          if (thumb.startsWith("http")) {
            item.thumb = thumb;


@@ 218,10 214,12 @@ class FeedbinServiceHandler extends ServiceHandler {
  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(" , ")})");
      predicates
          .add("source IN (${List.filled(sids.length, "?").join(" , ")})");
    }
    if (date != null) {
      predicates.add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}");
      predicates
          .add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}");
    }
    final rows = await Global.db.query(
      "items",


@@ 234,7 232,7 @@ class FeedbinServiceHandler extends ServiceHandler {
  }

  @override
  Future<void> markRead(RSSItem item) async{
  Future<void> markRead(RSSItem item) async {
    await _markItems("unread", "DELETE", [item.id]);
  }



@@ 252,4 250,4 @@ class FeedbinServiceHandler extends ServiceHandler {
  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 +34 -30
@@ 62,9 62,10 @@ class FeverServiceHandler extends ServiceHandler {
  }

  Future<Map<String, dynamic>> _fetchAPI({params: "", postparams: ""}) async {
    var uri = Uri.parse(endpoint + "?api" + params);
    final response = await http.post(
      endpoint + "?api" + params,
      headers: { "content-type": "application/x-www-form-urlencoded" },
      uri,
      headers: {"content-type": "application/x-www-form-urlencoded"},
      body: "api_key=$apiKey$postparams",
    );
    final body = Utf8Decoder().convert(response.bodyBytes);


@@ 82,18 83,19 @@ class FeverServiceHandler extends ServiceHandler {
    _useInt32 = value;
    Store.sp.setBool(StoreKeys.FEVER_INT_32, value);
  }
  

  @override
  Future<bool> validate() async {
    try {
      return (await _fetchAPI())["auth"] == 1;
    } catch(exp) {
    } catch (exp) {
      return false;
    }
  }

  @override
  Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources() async {
  Future<Tuple2<List<RSSSource>, Map<String, List<String>>>>
      getSources() async {
    var response = await _fetchAPI(params: "&feeds");
    var sources = response["feeds"].map<RSSSource>((f) {
      return RSSSource(f["id"].toString(), f["url"], f["title"]);


@@ 104,8 106,8 @@ class FeverServiceHandler extends ServiceHandler {
    if (groups == null || feedGroups == null) throw Error();
    var groupsIdMap = Map<int, String>();
    for (var group in groups) {
        var title = group["title"].trim();
        groupsIdMap[group["id"]] = title;
      var title = group["title"].trim();
      groupsIdMap[group["id"]] = title;
    }
    for (var group in feedGroups) {
      var name = groupsIdMap[group["group_id"]];


@@ 126,7 128,7 @@ class FeverServiceHandler extends ServiceHandler {
      response = (await _fetchAPI(params: "&items&max_id=$minId"))["items"];
      if (response == null) throw Error();
      for (var i in response) {
        if (i["id"] is String)  i["id"] = int.parse(i["id"]);
        if (i["id"] is String) i["id"] = int.parse(i["id"]);
        if (i["id"] > lastId) items.add(i);
      }
      if (response.length == 0 && minId == Utils.syncMaxId) {


@@ 136,11 138,9 @@ class FeverServiceHandler extends ServiceHandler {
      } else {
        minId = response.fold(minId, (m, n) => min<int>(m, n["id"]));
      }
    } while (
      minId > lastId && 
      (response == null || response.length >= 50) && 
      items.length < fetchLimit
    );
    } while (minId > lastId &&
        (response == null || response.length >= 50) &&
        items.length < fetchLimit);
    var parsedItems = items.map<RSSItem>((i) {
      var dom = parse(i["html"]);
      var item = RSSItem(


@@ 157,15 157,17 @@ class FeverServiceHandler extends ServiceHandler {
      );
      // Try to get the thumbnail of the item
      var img = dom.querySelector("img");
      if (img != null && img.attributes["src"] != null) { 
      if (img != null && img.attributes["src"] != null) {
        var thumb = img.attributes["src"];
        if (thumb.startsWith("http")) {
          item.thumb = thumb;
        }
      } else if (useInt32) { // TTRSS Fever Plugin attachments
      } else if (useInt32) {
        // TTRSS Fever Plugin attachments
        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"];
        if (a != null &&
            a.text.endsWith(", image\/generic") &&
            a.attributes["href"] != null) item.thumb = a.attributes["href"];
      }
      return item;
    });


@@ 182,15 184,13 @@ class FeverServiceHandler extends ServiceHandler {
    final unreadIds = responses[0]["unread_item_ids"];
    final starredIds = responses[1]["saved_item_ids"];
    return Tuple2(
      Set.from(unreadIds.split(",")),
      Set.from(starredIds.split(","))
    );
        Set.from(unreadIds.split(",")), Set.from(starredIds.split(",")));
  }

  Future<void> _markItem(RSSItem item, String asType) async {
    try {
      await _fetchAPI(postparams: "&mark=item&as=$asType&id=${item.id}");
    } catch(exp) {
    } catch (exp) {
      print(exp);
    }
  }


@@ 199,18 199,22 @@ class FeverServiceHandler extends ServiceHandler {
  Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
    if (date != null && !before) {
      var items = Global.itemsModel.getItems().where((i) =>
        (sids.length == 0 || sids.contains(i.source)) && i.date.compareTo(date) >= 0
      );
          (sids.length == 0 || sids.contains(i.source)) &&
          i.date.compareTo(date) >= 0);
      await Future.wait(items.map((i) => markRead(i)));
    } else {
      var timestamp = date != null ? date.millisecondsSinceEpoch : DateTime.now().millisecondsSinceEpoch;
      var timestamp = date != null
          ? date.millisecondsSinceEpoch
          : DateTime.now().millisecondsSinceEpoch;
      timestamp = timestamp ~/ 1000 + 1;
      try {
        await Future.wait(Global.sourcesModel.getSources()
          .where((s) => sids.length == 0 || sids.contains(s.id))
          .map((s) => _fetchAPI(postparams: "&mark=feed&as=read&id=${s.id}&before=$timestamp"))
        );
      } catch(exp) {
        await Future.wait(Global.sourcesModel
            .getSources()
            .where((s) => sids.length == 0 || sids.contains(s.id))
            .map((s) => _fetchAPI(
                postparams:
                    "&mark=feed&as=read&id=${s.id}&before=$timestamp")));
      } catch (exp) {
        print(exp);
      }
    }


@@ 235,4 239,4 @@ class FeverServiceHandler extends ServiceHandler {
  Future<void> unstar(RSSItem item) async {
    await _markItem(item, "unsaved");
  }
}
\ No newline at end of file
}

M lib/models/services/greader.dart => lib/models/services/greader.dart +42 -33
@@ 45,25 45,24 @@ class GReaderServiceHandler extends ServiceHandler {
    this.endpoint,
    this.username,
    this.password,
    this.fetchLimit,
    {
      this.inoreaderId,
      this.inoreaderKey,
      this.removeInoreaderAd,
    }
  ) {
    this.fetchLimit, {
    this.inoreaderId,
    this.inoreaderKey,
    this.removeInoreaderAd,
  }) {
    _lastFetched = Store.sp.getInt(StoreKeys.LAST_FETCHED);
    _lastId = Store.sp.getString(StoreKeys.LAST_ID);
    _auth = Store.sp.getString(StoreKeys.AUTH);
    useInt64 = Store.sp.getBool(StoreKeys.USE_INT_64)
      ?? !endpoint.endsWith("theoldreader.com");
    useInt64 = Store.sp.getBool(StoreKeys.USE_INT_64) ??
        !endpoint.endsWith("theoldreader.com");
  }

  void persist() {
    Store.sp.setInt(
      StoreKeys.SYNC_SERVICE,
      inoreaderId != null ? SyncService.Inoreader.index : SyncService.GReader.index
    );
        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);


@@ 112,19 111,19 @@ class GReaderServiceHandler extends ServiceHandler {
    Store.sp.setString(StoreKeys.AUTH, value);
  }

  Future<http.Response> _fetchAPI(String params,
      {dynamic body}) async {
  Future<http.Response> _fetchAPI(String params, {dynamic body}) async {
    final headers = Map<String, String>();
    if (auth != null) headers["Authorization"] = auth;
    if (inoreaderId != null) {
      headers["AppId"] = inoreaderId;
      headers["AppKey"] = inoreaderKey;
    }
    var uri = Uri.parse(endpoint + params);
    if (body == null) {
      return await http.get(endpoint + params, headers: headers);
      return await http.get(uri, headers: headers);
    } else {
      headers["Content-Type"] = "application/x-www-form-urlencoded";
      return await http.post(endpoint + params, headers: headers, body: body);
      return await http.post(uri, headers: headers, body: body);
    }
  }



@@ 150,7 149,7 @@ class GReaderServiceHandler extends ServiceHandler {
  }

  Future<http.Response> _editTag(String ref, String tag, {add: true}) async {
    final body = "i=$ref&${add?"a":"r"}=$tag";
    final body = "i=$ref&${add ? "a" : "r"}=$tag";
    return await _fetchAPI("/reader/api/0/edit-tag", body: body);
  }



@@ 165,7 164,7 @@ class GReaderServiceHandler extends ServiceHandler {
    try {
      final result = await _fetchAPI("/reader/api/0/user-info");
      return result.statusCode == 200;
    } catch(exp) {
    } catch (exp) {
      return false;
    }
  }


@@ 188,11 187,13 @@ class GReaderServiceHandler extends ServiceHandler {
  }

  @override
  Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources() async {
    final response = await _fetchAPI("/reader/api/0/subscription/list?output=json");
  Future<Tuple2<List<RSSSource>, Map<String, List<String>>>>
      getSources() async {
    final response =
        await _fetchAPI("/reader/api/0/subscription/list?output=json");
    assert(response.statusCode == 200);
    List subscriptions = jsonDecode(response.body)["subscriptions"];
    final groupsMap =  Map<String, List<String>>();
    final groupsMap = Map<String, List<String>>();
    for (var s in subscriptions) {
      final categories = s["categories"];
      if (categories != null) {


@@ 232,7 233,7 @@ class GReaderServiceHandler extends ServiceHandler {
          }
        }
        continuation = fetched["continuation"];
      } catch(exp) {
      } catch (exp) {
        break;
      }
    } while (continuation != null && items.length < fetchLimit);


@@ 264,15 265,17 @@ class GReaderServiceHandler extends ServiceHandler {
        item.title = titleDom.documentElement.text;
      }
      var img = dom.querySelector("img");
      if (img != null && img.attributes["src"] != null) { 
      if (img != null && img.attributes["src"] != null) {
        var thumb = img.attributes["src"];
        if (thumb.startsWith("http")) {
          item.thumb = thumb;
        }
      }
      for (var c in i["categories"]) {
        if (!item.hasRead && c.endsWith("/state/com.google/read")) item.hasRead = true;
        else if (!item.starred && c.endsWith("/state/com.google/starred")) item.starred = true;
        if (!item.hasRead && c.endsWith("/state/com.google/read"))
          item.hasRead = true;
        else if (!item.starred && c.endsWith("/state/com.google/starred"))
          item.starred = true;
      }
      return item;
    }).toList();


@@ 284,13 287,17 @@ class GReaderServiceHandler extends ServiceHandler {
    List<Set<String>> results;
    if (inoreaderId != null) {
      results = await Future.wait([
        _fetchAll("/reader/api/0/stream/items/ids?output=json&xt=$_READ_TAG&n=1000"),
        _fetchAll("/reader/api/0/stream/items/ids?output=json&it=$_STAR_TAG&n=1000"),
        _fetchAll(
            "/reader/api/0/stream/items/ids?output=json&xt=$_READ_TAG&n=1000"),
        _fetchAll(
            "/reader/api/0/stream/items/ids?output=json&it=$_STAR_TAG&n=1000"),
      ]);
    } else {
      results = await Future.wait([
        _fetchAll("/reader/api/0/stream/items/ids?output=json&s=$_ALL_TAG&xt=$_READ_TAG&n=1000"),
        _fetchAll("/reader/api/0/stream/items/ids?output=json&s=$_STAR_TAG&n=1000"),
        _fetchAll(
            "/reader/api/0/stream/items/ids?output=json&s=$_ALL_TAG&xt=$_READ_TAG&n=1000"),
        _fetchAll(
            "/reader/api/0/stream/items/ids?output=json&s=$_STAR_TAG&n=1000"),
      ]);
    }
    return Tuple2.fromList(results);


@@ 301,10 308,12 @@ class GReaderServiceHandler extends ServiceHandler {
    if (date != null) {
      List<String> predicates = ["hasRead = 0"];
      if (sids.length > 0) {
        predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})");
        predicates
            .add("source IN (${List.filled(sids.length, "?").join(" , ")})");
      }
      if (date != null) {
        predicates.add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}");
        predicates
            .add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}");
      }
      final rows = await Global.db.query(
        "items",


@@ 321,12 330,12 @@ class GReaderServiceHandler extends ServiceHandler {
          refs = [];
        }
      }
      if (refs.length > 0)  _editTag(refs.join("&i="), _READ_TAG);
      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 };
        final body = {"s": sid};
        _fetchAPI("/reader/api/0/mark-all-as-read", body: body);
      }
    }

M lib/models/sources_model.dart => lib/models/sources_model.dart +33 -32
@@ 1,5 1,3 @@
import 'dart:collection';

import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/utils/global.dart';
import 'package:fluent_reader_lite/utils/store.dart';


@@ 42,8 40,7 @@ class SourcesModel with ChangeNotifier {

  Future<void> updateUnreadCounts() async {
    final rows = await Global.db.rawQuery(
      "SELECT source, COUNT(iid) FROM items WHERE hasRead=0 GROUP BY source;"
    );
        "SELECT source, COUNT(iid) FROM items WHERE hasRead=0 GROUP BY source;");
    for (var source in _sources.values) {
      var cloned = source.clone();
      _sources[source.id] = cloned;


@@ 65,7 62,8 @@ class SourcesModel with ChangeNotifier {
    for (var item in items) {
      var source = _sources[item.source];
      if (!item.hasRead) source.unreadCount += 1;
      if (item.date.compareTo(source.latest) > 0 || source.lastTitle.length == 0) {
      if (item.date.compareTo(source.latest) > 0 ||
          source.lastTitle.length == 0) {
        source.latest = item.date;
        source.lastTitle = item.title;
        changed.add(source.id);


@@ 78,8 76,8 @@ class SourcesModel with ChangeNotifier {
        var source = _sources[sid];
        batch.update(
          "sources",
          { 
            "latest": source.latest.millisecondsSinceEpoch, 
          {
            "latest": source.latest.millisecondsSinceEpoch,
            "lastTitle": source.lastTitle,
          },
          where: "sid = ?",


@@ 172,33 170,36 @@ class SourcesModel with ChangeNotifier {

  Future<String> _fetchFavicon(String url) async {
    try {
        url = url.split("/").getRange(0, 3).join("/");
        var result = await http.get(url);
        if (result.statusCode == 200) {
            var htmlStr = result.body;
            var dom = parse(htmlStr);
            var links = dom.getElementsByTagName("link");
            for (var link in links) {
                var rel = link.attributes["rel"];
                if ((rel == "icon" || rel == "shortcut icon") && link.attributes.containsKey("href")) {
                    var href = link.attributes["href"];
                    var parsedUrl = Uri.parse(url);
                    if (href.startsWith("//")) return parsedUrl.scheme + ":" + href;
                    else if (href.startsWith("/")) return url + href;
                    else return href;
                }
            }
      url = url.split("/").getRange(0, 3).join("/");
      var uri = Uri.parse(url);
      var result = await http.get(uri);
      if (result.statusCode == 200) {
        var htmlStr = result.body;
        var dom = parse(htmlStr);
        var links = dom.getElementsByTagName("link");
        for (var link in links) {
          var rel = link.attributes["rel"];
          if ((rel == "icon" || rel == "shortcut icon") &&
              link.attributes.containsKey("href")) {
            var href = link.attributes["href"];
            var parsedUrl = Uri.parse(url);
            if (href.startsWith("//"))
              return parsedUrl.scheme + ":" + href;
            else if (href.startsWith("/"))
              return url + href;
            else
              return href;
          }
        }
        url = url + "/favicon.ico";
        if (await Utils.validateFavicon(url)) { 
            return url;
        } else {
            return null;
        }
    } catch(exp) {
      }
      url = url + "/favicon.ico";
      if (await Utils.validateFavicon(url)) {
        return url;
      } else {
        return null;
      }
    } catch (exp) {
      return null;
    }
  }

  
}

M lib/pages/article_page.dart => lib/pages/article_page.dart +98 -79
@@ 30,9 30,7 @@ class ArticlePage extends StatefulWidget {
  ArticlePageState createState() => ArticlePageState();
}

enum _ArticleLoadState {
  Loading, Success, Failure
}
enum _ArticleLoadState { Loading, Success, Failure }

class ArticlePageState extends State<ArticlePage> {
  WebViewController _controller;


@@ 59,7 57,8 @@ class ArticlePageState extends State<ArticlePage> {
  Future<NavigationDecision> _onNavigate(NavigationRequest request) async {
    if (navigated && request.isForMainFrame) {
      final internal = Global.globalModel.inAppBrowser;
      await launch(request.url, forceSafariVC: internal, forceWebView: internal);
      await launch(request.url,
          forceSafariVC: internal, forceWebView: internal);
      return NavigationDecision.prevent;
    } else {
      return NavigationDecision.navigate;


@@ 72,11 71,14 @@ class ArticlePageState extends State<ArticlePage> {
    String a;
    if (loadFull) {
      try {
        var html = (await http.get(item.link)).body;
        var uri = Uri.parse(item.link);
        var html = (await http.get(uri)).body;
        a = Uri.encodeComponent(html);
      } catch(exp) {
      } catch (exp) {
        if (mounted && currId == requestId) {
          setState(() { loaded = _ArticleLoadState.Failure; });
          setState(() {
            loaded = _ArticleLoadState.Failure;
          });
        }
        return;
      }


@@ 84,9 86,11 @@ class ArticlePageState extends State<ArticlePage> {
      a = Uri.encodeComponent(item.content);
    }
    if (!mounted || currId != requestId) return;
    var h = '<p id="source">${source.name}${(item.creator!=null&&item.creator.length>0)?' / '+item.creator:''}</p>';
    var h =
        '<p id="source">${source.name}${(item.creator != null && item.creator.length > 0) ? ' / ' + item.creator : ''}</p>';
    h += '<p id="title">${item.title}</p>';
    h += '<p id="date">${DateFormat.yMd(Localizations.localeOf(context).toString()).add_Hm().format(item.date)}</p>';
    h +=
        '<p id="date">${DateFormat.yMd(Localizations.localeOf(context).toString()).add_Hm().format(item.date)}</p>';
    h += '<article></article>';
    h = Uri.encodeComponent(h);
    var s = Store.getArticleFontSize();


@@ 102,11 106,15 @@ class ArticlePageState extends State<ArticlePage> {
    if (Platform.isAndroid || Global.globalModel.getBrightness() != null) {
      await Future.delayed(Duration(milliseconds: 300));
    }
    setState(() { loaded = _ArticleLoadState.Success; });
    if (_target == SourceOpenTarget.Local || _target == SourceOpenTarget.FullContent) {
    setState(() {
      loaded = _ArticleLoadState.Success;
    });
    if (_target == SourceOpenTarget.Local ||
        _target == SourceOpenTarget.FullContent) {
      navigated = true;
    }
  }

  void _onWebpageReady(_) {
    if (loaded == _ArticleLoadState.Success) navigated = true;
  }


@@ 139,7 147,8 @@ class ArticlePageState extends State<ArticlePage> {

  @override
  Widget build(BuildContext context) {
    final Tuple2<String, bool> arguments = ModalRoute.of(context).settings.arguments;
    final Tuple2<String, bool> arguments =
        ModalRoute.of(context).settings.arguments;
    if (iid == null) iid = arguments.item1;
    if (isSourceFeed == null) isSourceFeed = arguments.item2;
    final resolvedDarkGrey = MyColors.dynamicDarkGrey.resolveFrom(context);


@@ 173,46 182,52 @@ class ArticlePageState extends State<ArticlePage> {
        var item = tuple.item1;
        var source = tuple.item2;
        if (_target == null) _target = source.openTarget;
        final body = SafeArea(child: IndexedStack(
          index: loaded.index,
          children: [
            Center(
              child: CupertinoActivityIndicator()
            ),
            WebView(
              key: Key("a-$iid-${_target.index}"),
              javascriptMode: JavascriptMode.unrestricted,
              onWebViewCreated: (WebViewController webViewController) {
                _controller = webViewController;
                _loadOpenTarget(item, source);
              },
              onPageStarted: _onPageReady,
              onPageFinished: _onWebpageReady,
              navigationDelegate: _onNavigate,
            ),
            Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    S.of(context).wentWrong,
                    style: TextStyle(color: CupertinoColors.label.resolveFrom(context)),
                  ),
                  CupertinoButton(
                    child: Text(S.of(context).retry),
                    onPressed: () { _loadOpenTarget(item, source); },
                  ),
                ],
        final body = SafeArea(
          child: IndexedStack(
            index: loaded.index,
            children: [
              Center(child: CupertinoActivityIndicator()),
              WebView(
                key: Key("a-$iid-${_target.index}"),
                javascriptMode: JavascriptMode.unrestricted,
                onWebViewCreated: (WebViewController webViewController) {
                  _controller = webViewController;
                  _loadOpenTarget(item, source);
                },
                onPageStarted: _onPageReady,
                onPageFinished: _onWebpageReady,
                navigationDelegate: _onNavigate,
              ),
            )
          ],
        ), bottom: false,);
              Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      S.of(context).wentWrong,
                      style: TextStyle(
                          color: CupertinoColors.label.resolveFrom(context)),
                    ),
                    CupertinoButton(
                      child: Text(S.of(context).retry),
                      onPressed: () {
                        _loadOpenTarget(item, source);
                      },
                    ),
                  ],
                ),
              )
            ],
          ),
          bottom: false,
        );
        return CupertinoPageScaffold(
          navigationBar: CupertinoNavigationBar(
            backgroundColor: CupertinoColors.systemBackground,
            middle: CupertinoSlidingSegmentedControl(
              children: viewOptions,
              onValueChanged: (v) { _setOpenTarget(source, target: SourceOpenTarget.values[v]); },
              onValueChanged: (v) {
                _setOpenTarget(source, target: SourceOpenTarget.values[v]);
              },
              groupValue: _target.index,
            ),
          ),


@@ 225,26 240,28 @@ class ArticlePageState extends State<ArticlePage> {
                items: [
                  CupertinoToolbarItem(
                    icon: item.hasRead
                      ? CupertinoIcons.circle
                      : CupertinoIcons.smallcircle_fill_circle,
                        ? CupertinoIcons.circle
                        : CupertinoIcons.smallcircle_fill_circle,
                    semanticLabel: item.hasRead
                      ? S.of(context).markUnread
                      : S.of(context).markRead,
                        ? S.of(context).markUnread
                        : S.of(context).markRead,
                    onPressed: () {
                      HapticFeedback.mediumImpact();
                      Global.itemsModel.updateItem(item.id, read: !item.hasRead);
                      Global.itemsModel
                          .updateItem(item.id, read: !item.hasRead);
                    },
                  ),
                  CupertinoToolbarItem(
                    icon: item.starred
                      ? CupertinoIcons.star_fill
                      : CupertinoIcons.star,
                        ? CupertinoIcons.star_fill
                        : CupertinoIcons.star,
                    semanticLabel: item.starred
                      ? S.of(context).star
                      : S.of(context).unstar,
                        ? S.of(context).star
                        : S.of(context).unstar,
                    onPressed: () {
                      HapticFeedback.mediumImpact();
                      Global.itemsModel.updateItem(item.id, starred: !item.starred);
                      Global.itemsModel
                          .updateItem(item.id, starred: !item.starred);
                    },
                  ),
                  CupertinoToolbarItem(


@@ 252,38 269,40 @@ class ArticlePageState extends State<ArticlePage> {
                    semanticLabel: S.of(context).share,
                    onPressed: () {
                      final media = MediaQuery.of(context);
                      Share.share(
                        item.link,
                        sharePositionOrigin: Rect.fromLTWH(
                          media.size.width - ArticlePage.state.currentContext.size.width / 2,
                          media.size.height - media.padding.bottom - 54,
                          0,
                          0
                        )
                      );
                      Share.share(item.link,
                          sharePositionOrigin: Rect.fromLTWH(
                              media.size.width -
                                  ArticlePage.state.currentContext.size.width /
                                      2,
                              media.size.height - media.padding.bottom - 54,
                              0,
                              0));
                    },
                  ),
                  CupertinoToolbarItem(
                    icon: CupertinoIcons.chevron_up,
                    semanticLabel: S.of(context).prev,
                    onPressed: idx <= 0 ? null : () {
                      loadNewItem(feed.iids[idx - 1]);
                    },
                    onPressed: idx <= 0
                        ? null
                        : () {
                            loadNewItem(feed.iids[idx - 1]);
                          },
                  ),
                  CupertinoToolbarItem(
                    icon: CupertinoIcons.chevron_down,
                    semanticLabel: S.of(context).next,
                    onPressed: (idx == -1 || (idx == feed.iids.length - 1 && feed.allLoaded))
                      ? null
                      : () async {
                        if (idx == feed.iids.length - 1) {
                          await feed.loadMore();
                        }
                        idx = feed.iids.indexOf(iid);
                        if (idx != feed.iids.length - 1) {
                          loadNewItem(feed.iids[idx + 1]);
                        }
                      },
                    onPressed: (idx == -1 ||
                            (idx == feed.iids.length - 1 && feed.allLoaded))
                        ? null
                        : () async {
                            if (idx == feed.iids.length - 1) {
                              await feed.loadMore();
                            }
                            idx = feed.iids.indexOf(iid);
                            if (idx != feed.iids.length - 1) {
                              loadNewItem(feed.iids[idx + 1]);
                            }
                          },
                  ),
                ],
                body: child,

M lib/pages/home_page.dart => lib/pages/home_page.dart +48 -43
@@ 25,19 25,20 @@ class HomePage extends StatefulWidget {

class ScrollTopNotifier with ChangeNotifier {
  int index = 0;
  

  void onTap(int newIndex) {
    var oldIndex = index;
    index = newIndex;
    if (newIndex == oldIndex) notifyListeners();
  } 
  }
}

class _HomePageState extends State<HomePage> {
  final _scrollTopNotifier = ScrollTopNotifier();
  final _controller = CupertinoTabController();
  final List<GlobalKey<NavigatorState>> _tabNavigatorKeys = [
    GlobalKey(), GlobalKey(),
    GlobalKey(),
    GlobalKey(),
  ];
  StreamSubscription _uriSub;



@@ 46,13 47,15 @@ class _HomePageState extends State<HomePage> {
    if (uri.host == "import") {
      if (Global.syncModel.hasService) {
        showCupertinoDialog(
          context: context, 
          context: context,
          builder: (context) => CupertinoAlertDialog(
            title: Text(S.of(context).serviceExists),
            actions: [
              CupertinoDialogAction(
                child: Text(S.of(context).confirm),
                onPressed: () { Navigator.of(context).pop(); },
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          ),


@@ 72,14 75,14 @@ class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    _uriSub = getUriLinksStream().listen(_uriStreamListener);
    _uriSub = uriLinkStream.listen(_uriStreamListener);
    Future.delayed(Duration.zero, () async {
      try {
        final uri = await getInitialUri();
        if (uri != null) {
          _uriStreamListener(uri);
        }
      } catch(exp) {
      } catch (exp) {
        print(exp);
      }
    });


@@ 93,14 96,15 @@ class _HomePageState extends State<HomePage> {

  Widget _constructPage(Widget page, bool isMobile) {
    return isMobile
      ? CupertinoPageScaffold(
        child: page,
        backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
      )
      : Container(
        child: page,
        color: CupertinoColors.systemBackground.resolveFrom(context),
      );
        ? CupertinoPageScaffold(
            child: page,
            backgroundColor:
                CupertinoColors.systemBackground.resolveFrom(context),
          )
        : Container(
            child: page,
            color: CupertinoColors.systemBackground.resolveFrom(context),
          );
  }

  Widget buildLeft(BuildContext context, {isMobile: true}) {


@@ 132,8 136,8 @@ class _HomePageState extends State<HomePage> {
          },
          builder: (context) {
            Widget page = index == 0
              ? ItemListPage(_scrollTopNotifier)
              : SubscriptionListPage(_scrollTopNotifier);
                ? ItemListPage(_scrollTopNotifier)
                : SubscriptionListPage(_scrollTopNotifier);
            return _constructPage(page, isMobile);
          },
        );


@@ 142,7 146,9 @@ class _HomePageState extends State<HomePage> {
    return WillPopScope(
      child: leftTabs,
      onWillPop: () async {
        return !(await _tabNavigatorKeys[_controller.index].currentState.maybePop());
        return !(await _tabNavigatorKeys[_controller.index]
            .currentState
            .maybePop());
      },
    );
  }


@@ 163,32 169,31 @@ class _HomePageState extends State<HomePage> {
          tablet: (context) {
            final left = buildLeft(context, isMobile: false);
            final right = Container(
              decoration: BoxDecoration(),
              clipBehavior: Clip.hardEdge,
              child: CupertinoTabView(
              navigatorKey: Global.tabletPanel,
              routes: {
                "/": (context) => TabletBasePage(),
                ...MyApp.baseRoutes,
              },
            ));
                decoration: BoxDecoration(),
                clipBehavior: Clip.hardEdge,
                child: CupertinoTabView(
                  navigatorKey: Global.tabletPanel,
                  routes: {
                    "/": (context) => TabletBasePage(),
                    ...MyApp.baseRoutes,
                  },
                ));
            return Container(
              color: CupertinoColors.systemBackground.resolveFrom(context),
              child: Row(
                children: [
                  Container(
                    constraints: BoxConstraints(maxWidth: 320),
                    child: left,
                  ),
                  VerticalDivider(
                    width: 1,
                    thickness: 1,
                    color: CupertinoColors.systemGrey4.resolveFrom(context),
                  ),
                  Expanded(child: right),
                ],
              )
            );
                color: CupertinoColors.systemBackground.resolveFrom(context),
                child: Row(
                  children: [
                    Container(
                      constraints: BoxConstraints(maxWidth: 320),
                      child: left,
                    ),
                    VerticalDivider(
                      width: 1,
                      thickness: 1,
                      color: CupertinoColors.systemGrey4.resolveFrom(context),
                    ),
                    Expanded(child: right),
                  ],
                ));
          },
        );
      },

M lib/utils/utils.dart => lib/utils/utils.dart +13 -8
@@ 12,8 12,8 @@ abstract class Utils {
    launch(url, forceSafariVC: false, forceWebView: false);
  }

  static int binarySearch<T>(List<T> sortedList, T value, 
    int Function(T, T) compare) {
  static int binarySearch<T>(
      List<T> sortedList, T value, int Function(T, T) compare) {
    var min = 0;
    var max = sortedList.length;
    while (min < max) {


@@ 33,9 33,11 @@ abstract class Utils {
  static Future<bool> validateFavicon(String url) async {
    var flag = false;
    try {
      var result = await http.get(url);
      var uri = Uri.parse(url);
      var result = await http.get(uri);
      if (result.statusCode == 200) {
        var contentType = result.headers["Content-Type"] ?? result.headers["content-type"];
        var contentType =
            result.headers["Content-Type"] ?? result.headers["content-type"];
        if (contentType != null && contentType.startsWith("image")) flag = true;
      }
    } finally {


@@ 47,7 49,8 @@ abstract class Utils {
    r"^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*$)",
    caseSensitive: false,
  );
  static bool testUrl(String url) => url != null && _urlRegex.hasMatch(url.trim());
  static bool testUrl(String url) =>
      url != null && _urlRegex.hasMatch(url.trim());

  static bool notEmpty(String text) => text != null && text.trim().length > 0;



@@ 60,7 63,9 @@ abstract class Utils {
        actions: [
          CupertinoDialogAction(
            child: Text(S.of(context).close),
            onPressed: () { Navigator.of(context).pop(); },
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      ),


@@ 74,8 79,8 @@ abstract class Utils {
      String ap = PinyinHelper.getShortPinyin(a);
      String bp = PinyinHelper.getShortPinyin(b);
      return ap.compareTo(bp);
    } catch(exp) {
    } catch (exp) {
      return a.compareTo(b);
    }
  }
}
\ No newline at end of file
}

M pubspec.lock => pubspec.lock +176 -211
@@ 1,125 1,104 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  archive:
    dependency: transitive
    description:
      name: archive
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "2.0.13"
  args:
    dependency: transitive
    description:
      name: args
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "1.6.0"
  async:
    dependency: transitive
    description:
      name: async
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.5.0-nullsafety.3"
    version: "2.6.1"
  auth_header:
    dependency: transitive
    description:
      name: auth_header
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.4"
    version: "3.0.1"
  boolean_selector:
    dependency: transitive
    description:
      name: boolean_selector
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.0-nullsafety.3"
    version: "2.1.0"
  cached_network_image:
    dependency: "direct main"
    description:
      name: cached_network_image
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.5.0"
    version: "3.0.0"
  characters:
    dependency: transitive
    description:
      name: characters
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.1.0-nullsafety.5"
    version: "1.1.0"
  charcode:
    dependency: transitive
    description:
      name: charcode
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.2.0-nullsafety.3"
    version: "1.2.0"
  clock:
    dependency: transitive
    description:
      name: clock
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.1.0-nullsafety.3"
    version: "1.1.0"
  collection:
    dependency: transitive
    description:
      name: collection
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "1.15.0-nullsafety.5"
  convert:
    dependency: transitive
    description:
      name: convert
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.1"
    version: "1.15.0"
  crypto:
    dependency: "direct main"
    description:
      name: crypto
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.5"
    version: "3.0.1"
  csslib:
    dependency: transitive
    description:
      name: csslib
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.16.2"
    version: "0.17.0"
  cupertino_icons:
    dependency: "direct main"
    description:
      name: cupertino_icons
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.0"
    version: "1.0.3"
  fake_async:
    dependency: transitive
    description:
      name: fake_async
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.2.0-nullsafety.3"
    version: "1.2.0"
  ffi:
    dependency: transitive
    description:
      name: ffi
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.1.3"
    version: "1.0.0"
  file:
    dependency: transitive
    description:
      name: file
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "6.0.0-nullsafety.4"
    version: "6.1.0"
  flutter:
    dependency: "direct main"
    description: flutter


@@ 129,16 108,16 @@ packages:
    dependency: transitive
    description:
      name: flutter_blurhash
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.5.0"
    version: "0.6.0"
  flutter_cache_manager:
    dependency: "direct main"
    description:
      name: flutter_cache_manager
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.0"
    version: "3.1.2"
  flutter_localizations:
    dependency: "direct main"
    description: flutter


@@ 158,310 137,289 @@ packages:
    dependency: "direct main"
    description:
      name: html
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.14.0+4"
    version: "0.15.0"
  http:
    dependency: "direct main"
    description:
      name: http
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.12.2"
    version: "0.13.3"
  http_parser:
    dependency: transitive
    description:
      name: http_parser
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "3.1.4"
    version: "4.0.0"
  http_server:
    dependency: transitive
    description:
      name: http_server
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "0.9.8+3"
  image:
    dependency: transitive
    description:
      name: image
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.19"
    version: "1.0.0"
  intl:
    dependency: "direct main"
    description:
      name: intl
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.17.0-nullsafety.2"
    version: "0.17.0"
  jaguar:
    dependency: "direct main"
    description:
      name: jaguar
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.4.46"
    version: "3.0.11"
  jaguar_common:
    dependency: transitive
    description:
      name: jaguar_common
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.4"
    version: "3.0.0"
  jaguar_flutter_asset:
    dependency: "direct main"
    description:
      name: jaguar_flutter_asset
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "2.2.0"
  jaguar_serializer:
    dependency: transitive
    description:
      name: jaguar_serializer
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.2.12"
    version: "3.0.0"
  js:
    dependency: transitive
    description:
      name: js
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.6.3-nullsafety.3"
    version: "0.6.3"
  logging:
    dependency: transitive
    description:
      name: logging
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.11.4"
    version: "1.0.1"
  lpinyin:
    dependency: "direct main"
    description:
      name: lpinyin
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.1.0"
    version: "2.0.3"
  matcher:
    dependency: transitive
    description:
      name: matcher
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.12.10-nullsafety.3"
    version: "0.12.10"
  meta:
    dependency: transitive
    description:
      name: meta
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.3.0-nullsafety.6"
    version: "1.3.0"
  mime:
    dependency: transitive
    description:
      name: mime
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.9.7"
    version: "1.0.0"
  modal_bottom_sheet:
    dependency: "direct main"
    description:
      name: modal_bottom_sheet
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.0+1"
    version: "2.0.0"
  nested:
    dependency: transitive
    description:
      name: nested
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.4"
    version: "1.0.0"
  octo_image:
    dependency: transitive
    description:
      name: octo_image
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.3.0"
    version: "1.0.0+1"
  overlay_dialog:
    dependency: "direct main"
    description:
      name: overlay_dialog
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.3"
    version: "0.2.0"
  package_info:
    dependency: "direct main"
    description:
      name: package_info
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.4.3+2"
    version: "2.0.2"
  path:
    dependency: "direct main"
    description:
      name: path
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.8.0-nullsafety.3"
    version: "1.8.0"
  path_provider:
    dependency: transitive
    description:
      name: path_provider
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.6.24"
    version: "2.0.2"
  path_provider_linux:
    dependency: transitive
    description:
      name: path_provider_linux
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+2"
    version: "2.0.0"
  path_provider_macos:
    dependency: transitive
    description:
      name: path_provider_macos
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.4+6"
    version: "2.0.0"
  path_provider_platform_interface:
    dependency: transitive
    description:
      name: path_provider_platform_interface
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.4"
    version: "2.0.0"
  path_provider_windows:
    dependency: transitive
    description:
      name: path_provider_windows
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.4+3"
    version: "2.0.1"
  path_tree:
    dependency: transitive
    description:
      name: path_tree
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.2.4"
    version: "3.0.0"
  pedantic:
    dependency: transitive
    description:
      name: pedantic
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "1.9.2"
  petitparser:
    dependency: transitive
    description:
      name: petitparser
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "3.1.0"
    version: "1.11.0"
  platform:
    dependency: transitive
    description:
      name: platform
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "3.0.0-nullsafety.4"
    version: "3.0.0"
  plugin_platform_interface:
    dependency: transitive
    description:
      name: plugin_platform_interface
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.3"
    version: "2.0.1"
  process:
    dependency: transitive
    description:
      name: process
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "4.0.0-nullsafety.4"
    version: "4.2.1"
  provider:
    dependency: "direct main"
    description:
      name: provider
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "4.3.2+3"
    version: "5.0.0"
  quiver:
    dependency: transitive
    description:
      name: quiver
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.5"
    version: "3.0.1"
  responsive_builder:
    dependency: "direct main"
    description:
      name: responsive_builder
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.3.0"
    version: "0.4.1"
  rxdart:
    dependency: transitive
    description:
      name: rxdart
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.25.0"
    version: "0.27.1"
  share:
    dependency: "direct main"
    description:
      name: share
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.6.5+4"
    version: "2.0.4"
  shared_preferences:
    dependency: "direct main"
    description:
      name: shared_preferences
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.5.12+4"
    version: "2.0.6"
  shared_preferences_linux:
    dependency: transitive
    description:
      name: shared_preferences_linux
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.2+4"
    version: "2.0.0"
  shared_preferences_macos:
    dependency: transitive
    description:
      name: shared_preferences_macos
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+11"
    version: "2.0.0"
  shared_preferences_platform_interface:
    dependency: transitive
    description:
      name: shared_preferences_platform_interface
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.4"
    version: "2.0.0"
  shared_preferences_web:
    dependency: transitive
    description:
      name: shared_preferences_web
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.1.2+7"
    version: "2.0.0"
  shared_preferences_windows:
    dependency: transitive
    description:
      name: shared_preferences_windows
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+3"
    version: "2.0.0"
  sky_engine:
    dependency: transitive
    description: flutter


@@ 471,170 429,177 @@ packages:
    dependency: transitive
    description:
      name: source_span
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.8.0-nullsafety.4"
    version: "1.8.1"
  sqflite:
    dependency: "direct main"
    description:
      name: sqflite
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.3.2+1"
    version: "2.0.0+3"
  sqflite_common:
    dependency: transitive
    description:
      name: sqflite_common
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.2+1"
    version: "2.0.0+2"
  stack_trace:
    dependency: transitive
    description:
      name: stack_trace
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.10.0-nullsafety.6"
    version: "1.10.0"
  stream_channel:
    dependency: transitive
    description:
      name: stream_channel
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.0-nullsafety.3"
    version: "2.1.0"
  string_scanner:
    dependency: transitive
    description:
      name: string_scanner
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.1.0-nullsafety.3"
    version: "1.1.0"
  synchronized:
    dependency: transitive
    description:
      name: synchronized
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.2.0+2"
    version: "3.0.0"
  term_glyph:
    dependency: transitive
    description:
      name: term_glyph
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.2.0-nullsafety.3"
    version: "1.2.0"
  test_api:
    dependency: transitive
    description:
      name: test_api
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.2.19-nullsafety.6"
    version: "0.3.0"
  tuple:
    dependency: "direct main"
    description:
      name: tuple
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.3"
    version: "2.0.0"
  typed_data:
    dependency: transitive
    description:
      name: typed_data
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.3.0-nullsafety.5"
    version: "1.3.0"
  uni_links:
    dependency: "direct main"
    description:
      name: uni_links
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.5.1"
  uni_links_platform_interface:
    dependency: transitive
    description:
      name: uni_links_platform_interface
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.0"
  uni_links_web:
    dependency: transitive
    description:
      name: uni_links_web
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.4.0"
    version: "0.1.0"
  url_launcher:
    dependency: "direct main"
    description:
      name: url_launcher
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "5.7.10"
    version: "6.0.9"
  url_launcher_linux:
    dependency: transitive
    description:
      name: url_launcher_linux
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+4"
    version: "2.0.0"
  url_launcher_macos:
    dependency: transitive
    description:
      name: url_launcher_macos
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+9"
    version: "2.0.0"
  url_launcher_platform_interface:
    dependency: transitive
    description:
      name: url_launcher_platform_interface
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.0.9"
    version: "2.0.4"
  url_launcher_web:
    dependency: transitive
    description:
      name: url_launcher_web
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.1.5+1"
    version: "2.0.1"
  url_launcher_windows:
    dependency: transitive
    description:
      name: url_launcher_windows
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.0.1+3"
    version: "2.0.0"
  uuid:
    dependency: transitive
    description:
      name: uuid
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.2.2"
    version: "3.0.4"
  vector_math:
    dependency: transitive
    description:
      name: vector_math
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.1.0-nullsafety.5"
    version: "2.1.0"
  webview_flutter:
    dependency: "direct main"
    description:
      name: webview_flutter
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.0.0-nullsafety.3"
    version: "2.0.10"
  win32:
    dependency: transitive
    description:
      name: win32
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.7.4"
    version: "2.0.5"
  xdg_directories:
    dependency: transitive
    description:
      name: xdg_directories
      url: "https://pub.flutter-io.cn"
    source: hosted
    version: "0.1.2"
  xml:
    dependency: transitive
    description:
      name: xml
      url: "https://pub.flutter-io.cn"
      url: "https://pub.dartlang.org"
    source: hosted
    version: "4.5.1"
    version: "0.2.0"
sdks:
  dart: ">=2.12.0-0.0 <3.0.0"
  flutter: ">=1.22.2 <2.0.0"
  dart: ">=2.12.0 <3.0.0"
  flutter: ">=2.0.0"

M pubspec.yaml => pubspec.yaml +22 -22
@@ 15,7 15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.1+6
version: 1.0.2+8

environment:
  sdk: ">=2.7.0 <3.0.0"


@@ 25,28 25,28 @@ dependencies:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  provider: ^4.3.2+3
  tuple: ^1.0.3
  shared_preferences: ^0.5.12+4
  provider: ^5.0.0
  tuple: ^2.0.0
  shared_preferences: ^2.0.6
  intl: ^0.17.0-nullsafety.2
  http: ^0.12.2
  html: ^0.14.0+4
  webview_flutter: ^2.0.0-nullsafety.3
  jaguar: ^2.4.46
  jaguar_flutter_asset: ^2.2.0
  url_launcher: ^5.7.10
  sqflite: ^1.3.2+1
  path: ^1.7.0
  share: '>=0.6.5+4 <2.0.0'
  package_info: '>=0.4.3+1 <2.0.0'
  crypto: ^2.1.5
  responsive_builder: ^0.3.0
  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
  http: ^0.13.3
  html: ^0.15.0
  webview_flutter: ^2.0.10
  jaguar: ^3.0.11
  jaguar_flutter_asset: ^3.0.0
  url_launcher: ^6.0.9
  sqflite: ^2.0.0+3
  path: ^1.8.0
  share: ^2.0.4
  package_info: ^2.0.2
  crypto: ^3.0.1
  responsive_builder: ^0.4.1
  cached_network_image: ^3.0.0
  flutter_cache_manager: ^3.1.2
  lpinyin: ^2.0.3
  uni_links: ^0.5.1
  modal_bottom_sheet: ^2.0.0
  overlay_dialog: ^0.2.0


  # The following adds the Cupertino Icons font to your application.