M README.md => README.md +80 -40
@@ 2,77 2,117 @@
<img width="120" height="120" src="https://github.com/yang991178/fluent-reader/raw/master/build/icon.png">
</p>
<h3 align="center">Fluent Reader Lite</h3>
-<p align="center">A simplistic mobile RSS client</p>
+<p align="center">简洁的移动端 RSS 阅读器(个人修改版)</p>
<hr />
-## Download
+本仓库为 [yang991178/fluent-reader-lite](https://github.com/yang991178/fluent-reader-lite) 的个人修改版本,托管于 [git.cytrogen.icu](https://git.cytrogen.icu) 上的自部署 sourcehut 实例。
-### iOS
+## 从源码构建 (Build from Source)
-- [Download from App Store](https://apps.apple.com/app/id1549611796) ($1.99. This will support development and help cover the $99 annual fee.)
-- [Download from TestFlight](https://testflight.apple.com/join/9fwRtH8C) (Free. Inactive testers may be removed due to TestFlight restrictions.)
+本仓库 **不提供** 预构建的 iOS 或 Android 应用,需要自行从源码构建。
-### Android
+前置条件:[Flutter SDK](https://flutter.dev/docs/get-started/install)(stable channel)
-- [Download from Google Play](https://play.google.com/store/apps/details?id=me.hyliu.fluent_reader_lite) ($1.99)
-- [Download APK from GitHub Releases](https://github.com/yang991178/fluent-reader-lite/releases) (Free)
+```bash
+git clone https://git.cytrogen.icu/~cytrogen/fluent-reader-mobile
+cd fluent-reader-mobile
+flutter pub get
+```
-### Desktop App
+Android:
-The repo of the full-featured desktop app [can be found here](https://github.com/yang991178/fluent-reader).
+```bash
+flutter build apk --release
+```
-## Features
+iOS:
+
+```bash
+flutter build ios --release
+```
+
+## 功能特性
<p align="center">
<img src="https://github.com/yang991178/fluent-reader-lite/raw/master/assets/demo/demo.png">
</p>
-Fluent Reader Lite is a simplistic, cross-platform, and open-source RSS client.
+Fluent Reader Lite 是一个简洁、跨平台、开源的 RSS 阅读器。
-The following self-hosted and commercial RSS services are supported.
+支持以下自托管及商业 RSS 服务:
-- Fever API (TT-RSS Fever plugin, Miniflux, etc.)
-- Google Reader API (Bazqux Reader, FreshRSS, The Old Reader, etc.)
+- Fever API(TT-RSS Fever 插件、Miniflux 等)
+- Google Reader API(Bazqux Reader、FreshRSS、The Old Reader 等)
- Inoreader
-- Feedbin (official or self-hosted)
+- Feedbin(官方或自托管)
+
+### 本 fork 新增功能
+
+- **自定义字体**:支持上传自定义字体文件(TTF/OTF/WOFF/WOFF2),文章 WebView 中通过 `@font-face` 注入渲染
+- **字体预览与管理**:字体选择器中显示预览文字,支持删除已上传的自定义字体
+- **内置字体修复**:修复原版中内置字体(OpenSans/Roboto/SourceSerif)在 WebView 中不渲染的问题
+- **字体大小校验**:上传字体文件大小限制为 1KB–50MB
+- **多语言补全**:为所有 9 种语言(中文、德语、西班牙语、法语、克罗地亚语、葡萄牙语、土耳其语、乌克兰语)补充了字体相关翻译
+- **订阅源搜索**:支持按名称搜索订阅源(不区分大小写)
+- **订阅源排序**:最近更新 / 名称 A→Z / 名称 Z→A / 未读数量,偏好持久化保存
+
+### 其他主要功能
-Other key features include:
+- 界面与阅读的深色模式
+- 可配置订阅源默认加载全文或网页
+- 按最新更新排列的专用订阅标签页,显示文章标题
+- 搜索本地文章或按已读状态筛选
+- 使用分组管理订阅源
+- iPad 和 Android 平板支持双栏视图与多任务
-- Dark mode for UI and reading.
-- Configure sources to load full content or webpage by default.
-- A dedicated subscriptions tab organized by latest updates with article titles.
-- Search for local articles or filter by read status.
-- Organize subscriptions with groups.
-- Support for two-pane view and multitasking on iPad and Android tablets.
+以下桌面版功能**不包含**在移动版中:
-The following features from the desktop app are **NOT** present:
+- 本地 RSS 支持及订阅源/分组管理
+- 导入导出 OPML 文件,完整应用数据备份与恢复
+- 正则表达式规则自动标记文章
+- 后台抓取文章并推送通知
+- 键盘快捷键
-- Local RSS support and source / group management.
-- Importing or exporting OPML files, full application data backup & restoration.
-- Regular expression rules that mark articles as they arrive.
-- Fetch articles in the background and send push notifications.
-- Keyboard shortcuts.
+桌面端完整版请参阅 [fluent-reader](https://github.com/yang991178/fluent-reader)。
-## Development
+## 变更日志 (Changelog)
-### Contribute
+### 2026-02-22 — 订阅源搜索与排序
-Help make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader-lite/issues).
+- 支持按名称搜索订阅源(不区分大小写)
+- 排序选项:最近更新 / 名称 A→Z / 名称 Z→A / 未读数量
+- 排序偏好保存至 SharedPreferences,重启后保留
+- 为全部 9 种语言添加排序相关翻译
-You can also help internationalize the app by providing [translations into additional languages](https://github.com/yang991178/fluent-reader-lite/tree/master/lib/l10n).
-You can read more about ARB files [here](https://localizely.com/flutter-arb) or [here](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification).
+### 2026-02-18 · `34469ca` — 自定义字体增强
-If you enjoy using this app, consider supporting its development by donating through [Paypal](https://www.paypal.me/yang991178) or [Alipay](https://hyliu.me/fluent-reader/imgs/alipay.jpg).
+- 支持上传自定义字体文件(TTF/OTF/WOFF/WOFF2),文章 WebView 中通过 `@font-face` 注入渲染
+- 修复原版中内置字体(OpenSans/Roboto/SourceSerif)在 WebView 中不渲染的问题
+- 字体选择器中显示预览文字,支持删除已上传的自定义字体
+- 字体文件大小限制为 1KB–50MB
+- 为全部 9 种语言补充字体相关翻译
-### Build from source
+### 2026-02-18 — 基础设施升级
-See [Flutter documentation](https://flutter.dev/docs).
+Flutter SDK 及依赖升级、Android 构建配置更新(Gradle / AGP / Kotlin)、Dart null safety 迁移。
-### Developed with
+## 支持原作者
+
+如果你想要直接下载安装,或希望支持原作者的开发工作,可以购买付费版本:
+
+- **iOS**:[App Store](https://apps.apple.com/app/id1549611796)($1.99)
+- **Android**:[Google Play](https://play.google.com/store/apps/details?id=me.hyliu.fluent_reader_lite)($1.99)
+- **Android(免费)**:[GitHub Releases 下载 APK](https://github.com/yang991178/fluent-reader-lite/releases)
+
+## 开发相关
- [Flutter](https://github.com/flutter/flutter)
- [Mercury Parser](https://github.com/postlight/mercury-parser)
-### License
+## 许可证
+
+BSD 3-Clause — 详见 [LICENSE](LICENSE)
+
+## 原始 README
-BSD
+原项目 README 请参阅 [GitHub 上的原始版本](https://github.com/yang991178/fluent-reader-lite/blob/master/README.md)。
M android/app/build.gradle => android/app/build.gradle +15 -13
@@ 1,5 1,7 @@
plugins {
id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
@@ 10,11 12,6 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ 32,7 29,8 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdk 33
+ compileSdk 36
+ namespace "icu.cytrogen.fluent_reader"
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ 42,11 40,19 @@ android {
disable 'InvalidPackage'
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "me.hyliu.fluent_reader_lite"
+ applicationId "icu.cytrogen.fluent_reader"
minSdk 24
- targetSdk 33
+ targetSdk 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ 72,7 78,3 @@ android {
flutter {
source '../..'
}
-
-dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-}>
\ No newline at end of file
M android/app/src/main/AndroidManifest.xml => android/app/src/main/AndroidManifest.xml +1 -1
@@ 1,5 1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="me.hyliu.fluent_reader_lite">
+ package="icu.cytrogen.fluent_reader">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
R android/app/src/main/kotlin/com/example/fluent_reader_lite/MainActivity.kt => android/app/src/main/kotlin/icu/cytrogen/fluent_reader/MainActivity.kt +1 -1
@@ 1,4 1,4 @@
-package com.example.fluent_reader_lite
+package icu.cytrogen.fluent_reader
import io.flutter.embedding.android.FlutterActivity
M android/build.gradle => android/build.gradle +4 -21
@@ 1,35 1,18 @@
-plugins {
- id "dev.flutter.flutter-gradle-plugin"
-}
-
-buildscript {
- ext.kotlin_version = '1.7.0'
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:7.0.2'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
-rootProject.buildDir = '../build'
+rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
- project.evaluationDependsOn(':app')
+ project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.layout.buildDirectory
-}>
\ No newline at end of file
+}
M android/gradle.properties => android/gradle.properties +2 -2
@@ 1,3 1,3 @@
-org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=128m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
-android.enableJetifier=true
+android.enableJetifier=false
M android/gradle/wrapper/gradle-wrapper.properties => android/gradle/wrapper/gradle-wrapper.properties +1 -1
@@ 2,4 2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
M android/settings.gradle => android/settings.gradle +15 -1
@@ 22,4 22,18 @@ plugins {
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
-include ":app">
\ No newline at end of file
+include ":app"
+
+gradle.allprojects { project ->
+ project.afterEvaluate {
+ if (it.hasProperty('android') && it.android.namespace == null) {
+ def manifest = file("${it.projectDir}/src/main/AndroidManifest.xml")
+ if (manifest.exists()) {
+ def pkg = new XmlParser().parse(manifest).attribute('package')
+ if (pkg) {
+ it.android.namespace = pkg
+ }
+ }
+ }
+ }
+}<
\ No newline at end of file
A assets/fonts/OpenSans-Bold.ttf => assets/fonts/OpenSans-Bold.ttf +0 -0
A assets/fonts/OpenSans-Regular.ttf => assets/fonts/OpenSans-Regular.ttf +0 -0
A assets/fonts/Roboto-Bold.ttf => assets/fonts/Roboto-Bold.ttf +0 -0
A assets/fonts/Roboto-Regular.ttf => assets/fonts/Roboto-Regular.ttf +0 -0
A assets/fonts/SourceSerif-Bold.ttf => assets/fonts/SourceSerif-Bold.ttf +0 -0
A assets/fonts/SourceSerif-Regular.ttf => assets/fonts/SourceSerif-Regular.ttf +0 -0
M lib/components/article_item.dart => lib/components/article_item.dart +13 -14
@@ 11,7 11,7 @@ import 'package:fluent_reader_lite/utils/global.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:share/share.dart';
+import 'package:share_plus/share_plus.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
@@ 20,7 20,7 @@ class ArticleItem extends StatefulWidget {
final RSSSource source;
final Function openActionSheet;
- ArticleItem(this.item, this.source, this.openActionSheet, {Key key}) : super(key: key);
+ ArticleItem(this.item, this.source, this.openActionSheet, {Key? key}) : super(key: key);
@override
_ArticleItemState createState() => _ArticleItemState();
@@ 36,12 36,12 @@ class _ArticleItemState extends State<ArticleItem> {
if (!widget.item.hasRead) {
Global.itemsModel.updateItem(widget.item.id, read: true);
}
- if (widget.source.openTarget == SourceOpenTarget.External) {
- launch(widget.item.link, forceSafariVC: false, forceWebView: false);
+ if (Global.resolveOpenTarget(widget.source) == SourceOpenTarget.External) {
+ launchUrl(Uri.parse(widget.item.link), mode: LaunchMode.externalApplication);
} else {
var isSource = Navigator.of(context).canPop();
if (ArticlePage.state.currentWidget != null) {
- ArticlePage.state.currentState.loadNewItem(
+ (ArticlePage.state.currentState as ArticlePageState?)?.loadNewItem(
widget.item.id,
isSource: isSource,
);
@@ 49,7 49,7 @@ class _ArticleItemState extends State<ArticleItem> {
var navigator = Global.responsiveNavigator(context);
while (navigator.canPop()) navigator.pop();
navigator.pushNamed(
- "/article",
+ "/article",
arguments: Tuple2(widget.item.id, isSource)
);
}
@@ 101,7 101,6 @@ class _ArticleItemState extends State<ArticleItem> {
case ItemSwipeOption.OpenExternal:
return CupertinoIcons.square_arrow_right;
}
- return null;
}
void _performSwipeAction(ItemSwipeOption option) async {
@@ 124,7 123,7 @@ class _ArticleItemState extends State<ArticleItem> {
if (!widget.item.hasRead) {
Global.itemsModel.updateItem(widget.item.id, read: true);
}
- launch(widget.item.link, forceSafariVC: false, forceWebView: false);
+ launchUrl(Uri.parse(widget.item.link), mode: LaunchMode.externalApplication);
break;
}
}
@@ 183,10 182,10 @@ class _ArticleItemState extends State<ArticleItem> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- widget.item.title,
+ widget.item.title,
style: _titleStyle,
),
- if (Global.feedsModel.showSnippet && widget.item.snippet.length > 0) Text(
+ if (Global.feedsModel.showSnippet && widget.item.snippet.isNotEmpty) Text(
widget.item.snippet,
style: _snippetStyle,
overflow: TextOverflow.ellipsis,
@@ 201,8 200,8 @@ class _ArticleItemState extends State<ArticleItem> {
onLongPress: _openActionSheet,
onTap: _openArticle,
child: Container(
- color: pressed
- ? CupertinoColors.systemGrey4.resolveFrom(context)
+ color: pressed
+ ? CupertinoColors.systemGrey4.resolveFrom(context)
: CupertinoColors.systemBackground.resolveFrom(context),
child: Column(children: [
Padding(
@@ 228,7 227,7 @@ class _ArticleItemState extends State<ArticleItem> {
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
- imageUrl: widget.item.thumb,
+ imageUrl: widget.item.thumb!,
width: 64, height: 64, fit: BoxFit.cover,
placeholder: _imagePlaceholderBuilder,
),
@@ 265,4 264,4 @@ class _ArticleItemState extends State<ArticleItem> {
child: body,
);
}
-}>
\ No newline at end of file
+}
M lib/components/badge.dart => lib/components/badge.dart +2 -2
@@ 1,7 1,7 @@
import 'package:flutter/cupertino.dart';
class Badge extends StatelessWidget {
- Badge(int count, {this.color : CupertinoColors.systemRed, Key key}) :
+ Badge(int count, {this.color = CupertinoColors.systemRed, Key? key}) :
label = count >= 1000 ? "999+" : count.toString(),
super(key: key);
@@ 26,4 26,4 @@ class Badge extends StatelessWidget {
),
)
);
-}>
\ No newline at end of file
+}
M lib/components/cupertino_toolbar.dart => lib/components/cupertino_toolbar.dart +10 -68
@@ 8,58 8,17 @@ import 'package:fluent_reader_lite/utils/colors.dart';
import 'package:fluent_reader_lite/utils/global.dart';
import 'package:flutter/cupertino.dart';
-/// Display a persistent bottom iOS styled toolbar for Cupertino theme
-///
class CupertinoToolbar extends StatelessWidget {
- /// Creates a persistent bottom iOS styled toolbar for Cupertino
- /// themed app,
- ///
- /// Typically used as the [child] attribute of a [CupertinoPageScaffold].
- ///
- /// {@tool sample}
- ///
- /// A sample code implementing a typical iOS page with bottom toolbar.
- ///
- /// ```dart
- /// CupertinoPageScaffold(
- /// navigationBar: CupertinoNavigationBar(
- /// middle: Text('Cupertino Toolbar')
- /// ),
- /// child: CupertinoToolbar(
- /// items: <CupertinoToolbarItem>[
- /// CupertinoToolbarItem(
- /// icon: CupertinoIcons.delete,
- /// onPressed: () {}
- /// ),
- /// CupertinoToolbarItem(
- /// icon: CupertinoIcons.settings,
- /// onPressed: () {}
- /// )
- /// ],
- /// body: Center(
- /// child: Text('Hello World')
- /// )
- /// )
- /// )
- /// ```
- /// {@end-tool}
- ///
CupertinoToolbar({
- Key key,
- @required this.items,
- @required this.body
- }) : assert(items != null),
- assert(
- items.every((CupertinoToolbarItem item) => (item.icon != null)) == true,
- 'Every item must have an icon and onPressed defined',
+ Key? key,
+ required this.items,
+ required this.body
+ }) : assert(
+ items.every((CupertinoToolbarItem item) => true) == true,
),
- assert(body != null),
super(key: key);
- /// The interactive items laid out within the toolbar where each item has an icon.
final List<CupertinoToolbarItem> items;
-
- /// The body displayed above the toolbar.
final Widget body;
@override
@@ 97,7 56,6 @@ class CupertinoToolbar extends StatelessWidget {
padding: EdgeInsets.zero,
child: Icon(
items[i].icon,
- // color: CupertinoColors.systemBlue,
semanticLabel: items[i].semanticLabel,
),
onPressed: items[i].onPressed
@@ 107,30 65,14 @@ class CupertinoToolbar extends StatelessWidget {
}
}
-/// An interactive button within iOS themed [CupertinoToolbar]
class CupertinoToolbarItem {
- /// Creates an item that is used with [CupertinoToolbar.items].
- ///
- /// The argument [icon] should not be null.
const CupertinoToolbarItem({
- @required this.icon,
+ required this.icon,
this.onPressed,
this.semanticLabel
- }) : assert(icon != null);
+ });
- /// The icon of the item.
- ///
- /// This attribute must not be null.
final IconData icon;
-
- /// The callback that is called when the item is tapped.
- ///
- /// This attribute must not be null.
- final VoidCallback onPressed;
-
- /// Semantic label for the icon.
- ///
- /// Announced in accessibility modes (e.g TalkBack/VoiceOver).
- /// This label does not show in the UI.
- final String semanticLabel;
-}>
\ No newline at end of file
+ final VoidCallback? onPressed;
+ final String? semanticLabel;
+}
M lib/components/dismissible_background.dart => lib/components/dismissible_background.dart +2 -2
@@ 4,7 4,7 @@ class DismissibleBackground extends StatelessWidget {
final IconData icon;
final bool isToRight;
- DismissibleBackground(this.icon, this.isToRight, {Key key})
+ DismissibleBackground(this.icon, this.isToRight, {Key? key})
: super(key: key);
@override
@@ 22,4 22,4 @@ class DismissibleBackground extends StatelessWidget {
)],
),
);
-}>
\ No newline at end of file
+}
M lib/components/favicon.dart => lib/components/favicon.dart +6 -6
@@ 6,7 6,7 @@ class Favicon extends StatelessWidget {
final RSSSource source;
final double size;
- const Favicon(this.source, {this.size: 16, Key key}) : super(key: key);
+ const Favicon(this.source, {this.size = 16, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ 14,10 14,10 @@ class Favicon extends StatelessWidget {
fontSize: size - 5,
color: CupertinoColors.systemGrey6,
);
-
- if (source.iconUrl != null && source.iconUrl.length > 0) {
+
+ if (source.iconUrl != null && source.iconUrl!.isNotEmpty) {
return CachedNetworkImage(
- imageUrl: source.iconUrl,
+ imageUrl: source.iconUrl!,
width: size,
height: size,
);
@@ 27,10 27,10 @@ class Favicon extends StatelessWidget {
height: size,
color: CupertinoColors.systemGrey.resolveFrom(context),
child: Center(child: Text(
- source.name.length > 0 ? source.name[0] : "?",
+ source.name.isNotEmpty ? source.name[0] : "?",
style: _textStyle,
)),
);
}
}
-}>
\ No newline at end of file
+}
M lib/components/list_tile_group.dart => lib/components/list_tile_group.dart +8 -8
@@ 5,18 5,18 @@ import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
class ListTileGroup extends StatelessWidget {
- ListTileGroup(this.children, {this.title, Key key}) : super(key: key);
+ ListTileGroup(this.children, {this.title, Key? key}) : super(key: key);
ListTileGroup.fromOptions(
- List<Tuple2<String, dynamic>> options,
- dynamic selected,
+ List<Tuple2<String, dynamic>> options,
+ dynamic selected,
Function onSelected,
- {this.title, Key key}) :
+ {this.title, Key? key}) :
children = options.map((t) => MyListTile(
title: Text(t.item1),
trailing: t.item2 == selected
? Icon(Icons.done)
- : Icon(null),
+ : null,
trailingChevron: false,
onTap: () { onSelected(t.item2); },
withDivider: t.item2 != options.last.item2,
@@ 24,7 24,7 @@ class ListTileGroup extends StatelessWidget {
super(key: key);
final Iterable<Widget> children;
- final String title;
+ final String? title;
static const _titleStyle = TextStyle(
fontSize: 12,
@@ 37,7 37,7 @@ class ListTileGroup extends StatelessWidget {
children: [
if (title != null) Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- child: Text(title, style: _titleStyle),
+ child: Text(title!, style: _titleStyle),
),
Container(
color: MyColors.tileBackground.resolveFrom(context),
@@ 57,4 57,4 @@ class ListTileGroup extends StatelessWidget {
),
),
],);
-}>
\ No newline at end of file
+}
M lib/components/mark_all_action_sheet.dart => lib/components/mark_all_action_sheet.dart +6 -6
@@ 6,13 6,13 @@ import 'package:flutter/cupertino.dart';
class MarkAllActionSheet extends StatelessWidget {
final Set<String> sids;
- MarkAllActionSheet(this.sids, {Key key}) : super(key: key);
+ MarkAllActionSheet(this.sids, {Key? key}) : super(key: key);
DateTime _offset(int days) {
return DateTime.now().subtract(Duration(days: days));
}
- void _markAll(BuildContext context, {DateTime date}) {
+ void _markAll(BuildContext context, {DateTime? date}) {
Navigator.of(context, rootNavigator: true).pop();
Global.itemsModel.markAllRead(sids, date: date);
}
@@ 42,11 42,11 @@ class MarkAllActionSheet extends StatelessWidget {
],
cancelButton: CupertinoActionSheetAction(
child: Text(S.of(context).cancel),
- onPressed: () {
+ onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
- );
- return ResponsiveActionSheet(sheet);
+ );
+ return ResponsiveActionSheet(sheet);
}
-}>
\ No newline at end of file
+}
M lib/components/my_list_tile.dart => lib/components/my_list_tile.dart +11 -11
@@ 3,23 3,23 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class MyListTile extends StatefulWidget {
- final Widget leading;
+ final Widget? leading;
final Widget title;
- final Widget trailing;
+ final Widget? trailing;
final bool trailingChevron;
final bool withDivider;
- final Function onTap;
+ final Function? onTap;
final CupertinoDynamicColor background;
MyListTile({
this.leading,
- @required this.title,
+ required this.title,
this.trailing,
- this.trailingChevron : true,
- this.withDivider : true,
+ this.trailingChevron = true,
+ this.withDivider = true,
this.onTap,
- this.background : MyColors.tileBackground,
- Key key,
+ this.background = MyColors.tileBackground,
+ Key? key,
}) : super(key: key);
@override
@@ 30,7 30,7 @@ class _MyListTileState extends State<MyListTile> {
bool pressed = false;
void _onTap() {
- if (widget.onTap != null) widget.onTap();
+ if (widget.onTap != null) widget.onTap!();
}
@override
@@ 60,7 60,7 @@ class _MyListTileState extends State<MyListTile> {
final rightPart = Row(
children: [
if (widget.trailing != null) DefaultTextStyle(
- child: widget.trailing,
+ child: widget.trailing!,
style: _labelStyle,
),
if (widget.trailingChevron) Icon(
@@ 77,7 77,7 @@ class _MyListTileState extends State<MyListTile> {
child: Column(children: [
Container(
color: (pressed && widget.onTap != null)
- ? CupertinoColors.systemGrey4.resolveFrom(context)
+ ? CupertinoColors.systemGrey4.resolveFrom(context)
: widget.background.resolveFrom(context),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6),
constraints: BoxConstraints(minHeight: 48),
M lib/components/subscription_item.dart => lib/components/subscription_item.dart +1 -1
@@ 13,7 13,7 @@ import 'badge.dart';
class SubscriptionItem extends StatefulWidget {
final RSSSource source;
- SubscriptionItem(this.source, {Key key}) : super(key: key);
+ SubscriptionItem(this.source, {Key? key}) : super(key: key);
@override
_SubscriptionItemState createState() => _SubscriptionItemState();
M lib/components/sync_control.dart => lib/components/sync_control.dart +2 -3
@@ 11,13 11,12 @@ class SyncControl extends StatefulWidget {
class _SyncControlState extends State<SyncControl> {
Future<void> _onRefresh() {
var completer = Completer();
- Function listener;
- listener = () {
+ void listener() {
if (!Global.syncModel.syncing) {
completer.complete();
Global.syncModel.removeListener(listener);
}
- };
+ }
Global.syncModel.addListener(listener);
Global.syncModel.syncWithService();
return completer.future;
M lib/components/time_text.dart => lib/components/time_text.dart +7 -7
@@ 4,17 4,17 @@ import 'package:flutter/cupertino.dart';
class TimeText extends StatefulWidget {
final DateTime date;
- final TextStyle style;
+ final TextStyle? style;
- TimeText(this.date, {this.style, Key key}) : super(key: key);
+ TimeText(this.date, {this.style, Key? key}) : super(key: key);
@override
_TimeTextState createState() => _TimeTextState();
}
class _TimeTextState extends State<TimeText> {
- Timer _timer;
- Duration _duration;
+ Timer? _timer;
+ Duration? _duration;
int diffMinutes() {
final now = DateTime.now();
@@ 37,9 37,9 @@ class _TimeTextState extends State<TimeText> {
} else {
duration = Duration(minutes: (60 * 24) - diff % (60 * 24));
}
- if (_duration == null || duration.compareTo(_duration) != 0) {
+ if (_duration == null || duration.compareTo(_duration!) != 0) {
_duration = duration;
- if (_timer != null) _timer.cancel();
+ _timer?.cancel();
_timer = Timer.periodic(duration, (_) {
setState(() {});
updateTimer();
@@ 49,7 49,7 @@ class _TimeTextState extends State<TimeText> {
@override
void dispose() {
- if (_timer != null) _timer.cancel();
+ _timer?.cancel();
super.dispose();
}
M lib/l10n/intl_de.arb => lib/l10n/intl_de.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Langdruck auf den Titel dieser Seite zum Wechsel der Ansicht zwischen aller oder ungelesender Abonnements.",
"uncategorized": "Ohne Kategorie",
"showUncategorized": "Zeige ohne Kategorie",
- "serviceExists": "Ein Service ist schon eingestellt. Bitte vor dem Import abmelden."
+ "serviceExists": "Ein Service ist schon eingestellt. Bitte vor dem Import abmelden.",
+ "inheritDefault": "Vererben",
+ "sortBy": "Sortieren nach",
+ "sortByLatest": "Neueste Aktualisierung",
+ "sortByNameAsc": "Name A→Z",
+ "sortByNameDesc": "Name Z→A",
+ "sortByUnread": "Ungelesene Anzahl"
}=
\ No newline at end of file
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "You can long press on the title of this page to toggle between all and unread subscriptions.",
"uncategorized": "Uncategorized",
"showUncategorized": "Show uncategorized",
- "serviceExists": "A service already exists. Please log out before importing."
+ "serviceExists": "A service already exists. Please log out before importing.",
+ "inheritDefault": "Inherit",
+ "sortBy": "Sort by",
+ "sortByLatest": "Latest update",
+ "sortByNameAsc": "Name A→Z",
+ "sortByNameDesc": "Name Z→A",
+ "sortByUnread": "Unread count"
}=
\ No newline at end of file
M lib/l10n/intl_es.arb => lib/l10n/intl_es.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Puede hacer una pulsación larga en el título de esta página para alternar entre todas las suscripciones y las no leídas.",
"uncategorized": "Sin categorizar",
"showUncategorized": "Mostrar sin categorizar",
- "serviceExists": "Ya existe este servicio. Por favor, cierre la sesión antes de importar."
+ "serviceExists": "Ya existe este servicio. Por favor, cierre la sesión antes de importar.",
+ "inheritDefault": "Heredar",
+ "sortBy": "Ordenar por",
+ "sortByLatest": "Última actualización",
+ "sortByNameAsc": "Nombre A→Z",
+ "sortByNameDesc": "Nombre Z→A",
+ "sortByUnread": "Cantidad no leídos"
}
M lib/l10n/intl_fr.arb => lib/l10n/intl_fr.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Vous pouvez appuyer longuement sur le titre de cette page pour basculer entre tous les abonnements et les abonnements non lus",
"uncategorized": "Non classé(s)",
"showUncategorized": "Voir les non classé(s)",
- "serviceExists": "Un service existe déjà. Veuillez vous déconnecter avant d'importer."
+ "serviceExists": "Un service existe déjà. Veuillez vous déconnecter avant d'importer.",
+ "inheritDefault": "Hériter",
+ "sortBy": "Trier par",
+ "sortByLatest": "Dernière mise à jour",
+ "sortByNameAsc": "Nom A→Z",
+ "sortByNameDesc": "Nom Z→A",
+ "sortByUnread": "Nombre de non lus"
}
M lib/l10n/intl_hr.arb => lib/l10n/intl_hr.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Možete dugo pritisnuti naslov ove stranice za prebacivanje između svih i nepročitanih pretplata.",
"uncategorized": "Nekategorizirano",
"showUncategorized": "Prikaži nekategorizirano",
- "serviceExists": "Servis već postoji. Odjavite se prije uvoza."
+ "serviceExists": "Servis već postoji. Odjavite se prije uvoza.",
+ "inheritDefault": "Naslijedi",
+ "sortBy": "Sortiraj po",
+ "sortByLatest": "Posljednje ažuriranje",
+ "sortByNameAsc": "Naziv A→Z",
+ "sortByNameDesc": "Naziv Z→A",
+ "sortByUnread": "Broj nepročitanih"
}
M lib/l10n/intl_pt.arb => lib/l10n/intl_pt.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Você pode pressionar de forma longa o título dessa página para alternar entre todas as inscrições e as não lidas.",
"uncategorized": "Não categorizado",
"showUncategorized": "Mostrar não categorizado",
- "serviceExists": "Um serviço já existe. Por favor saia antes de importar."
+ "serviceExists": "Um serviço já existe. Por favor saia antes de importar.",
+ "inheritDefault": "Herdar",
+ "sortBy": "Ordenar por",
+ "sortByLatest": "Última atualização",
+ "sortByNameAsc": "Nome A→Z",
+ "sortByNameDesc": "Nome Z→A",
+ "sortByUnread": "Quantidade não lidos"
}
M lib/l10n/intl_tr.arb => lib/l10n/intl_tr.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "üm ve okunmamış abonelikler arasında geçiş yapmak için bu sayfanın başlığına uzun süre basabilirsiniz.",
"uncategorized": "Kategorize edilmemiş",
"showUncategorized": "Kategorize edilmemişleri göster",
- "serviceExists": "Bir hizmet zaten var. Lütfen içe aktarmadan önce oturumu kapatın."
+ "serviceExists": "Bir hizmet zaten var. Lütfen içe aktarmadan önce oturumu kapatın.",
+ "inheritDefault": "Devral",
+ "sortBy": "Sırala",
+ "sortByLatest": "Son güncelleme",
+ "sortByNameAsc": "Ad A→Z",
+ "sortByNameDesc": "Ad Z→A",
+ "sortByUnread": "Okunmamış sayısı"
}=
\ No newline at end of file
M lib/l10n/intl_uk.arb => lib/l10n/intl_uk.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "Ви можете довго натискати на заголовок цієї сторінки, щоб перемикатися між усіма та непрочитаними підписками.",
"uncategorized": "Без категорії",
"showUncategorized": "Показати без категорії",
- "serviceExists": "Служба вже існує. Будь ласка, вийдіть з системи перед імпортом."
+ "serviceExists": "Служба вже існує. Будь ласка, вийдіть з системи перед імпортом.",
+ "inheritDefault": "Успадкувати",
+ "sortBy": "Сортувати за",
+ "sortByLatest": "Останнє оновлення",
+ "sortByNameAsc": "Назва А→Я",
+ "sortByNameDesc": "Назва Я→А",
+ "sortByUnread": "Кількість непрочитаних"
}=
\ No newline at end of file
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +7 -1
@@ 104,5 104,11 @@
"unreadSourceTip": "您可以长按此页面的标题来切换全部订阅源或仅未读订阅源。",
"uncategorized": "未分组",
"showUncategorized": "显示“未分组”",
- "serviceExists": "已登录至一个服务,请在导入前登出。"
+ "serviceExists": "已登录至一个服务,请在导入前登出。",
+ "inheritDefault": "继承",
+ "sortBy": "排序方式",
+ "sortByLatest": "最近更新",
+ "sortByNameAsc": "名称 A→Z",
+ "sortByNameDesc": "名称 Z→A",
+ "sortByUnread": "未读数量"
}=
\ No newline at end of file
M lib/main.dart => lib/main.dart +9 -16
@@ 24,7 24,6 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
-import 'package:webview_flutter/webview_flutter.dart';
import 'generated/l10n.dart';
import 'models/global_model.dart';
@@ 36,12 35,11 @@ void main() async {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
));
- WebView.platform = SurfaceAndroidWebView();
}
runApp(MyApp());
- SystemChannels.lifecycle.setMessageHandler((msg) {
+ SystemChannels.lifecycle.setMessageHandler((msg) async {
if (msg == AppLifecycleState.resumed.toString()) {
- if (Global.server != null) Global.server.restart();
+ if (Global.server != null) Global.server!.restart();
if (Global.globalModel.syncOnStart &&
DateTime.now().difference(Global.syncModel.lastSynced).inMinutes >=
10) {
@@ 79,15 77,12 @@ class MyApp extends StatelessWidget {
return FeedbinPage();
case SyncService.GReader:
return GReaderPage();
- break;
case SyncService.Inoreader:
return InoreaderPage();
- break;
}
return AboutPage();
}
};
- // This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
@@ 104,7 99,6 @@ class MyApp extends StatelessWidget {
title: "Fluent Reader",
debugShowCheckedModeBanner: false,
localizationsDelegates: [
- // ... app-specific localization delegate[s] here
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
@@ 122,12 116,12 @@ class MyApp extends StatelessWidget {
const Locale("pt"),
const Locale("tr"),
],
- localeResolutionCallback: (_locale, supportedLocales) {
- _locale = Locale(_locale.languageCode);
+ localeResolutionCallback: (locale, supportedLocales) {
+ final resolvedLocale = Locale(locale?.languageCode ?? "en");
if (globalModel.locale != null)
return globalModel.locale;
- else if (supportedLocales.contains(_locale))
- return _locale;
+ else if (supportedLocales.contains(resolvedLocale))
+ return resolvedLocale;
else
return Locale("en");
},
@@ 138,7 132,6 @@ class MyApp extends StatelessWidget {
routes: {
"/": (context) => CupertinoScaffold(
body: CupertinoTheme(
- // For fixing the bug with modal_bottom_sheet overriding primary color
data: CupertinoThemeData(
primaryColor: CupertinoColors.activeBlue),
child: HomePage())),
@@ 146,11 139,11 @@ class MyApp extends StatelessWidget {
},
builder: (context, child) {
final mediaQueryData = MediaQuery.of(context);
- if (Global.globalModel.textScale == null) return child;
+ if (Global.globalModel.textScale == null) return child!;
return MediaQuery(
data: mediaQueryData.copyWith(
- textScaleFactor: Global.globalModel.textScale),
- child: child);
+ textScaler: TextScaler.linear(Global.globalModel.textScale!)),
+ child: child!);
},
),
),
M lib/models/feed.dart => lib/models/feed.dart +12 -8
@@ 19,19 19,22 @@ class RSSFeed {
FilterType filterType;
String search = "";
- RSSFeed({this.sids}) {
- if (sids == null) sids = Set();
- filterType = FilterType.values[Store.sp.getInt(_filterKey) ?? 0];
- }
+ RSSFeed({Set<String>? sids})
+ : sids = sids ?? Set(),
+ filterType = FilterType.values[Store.sp.getInt(
+ (sids == null || sids.isEmpty)
+ ? StoreKeys.FEED_FILTER_ALL
+ : StoreKeys.FEED_FILTER_SOURCE
+ ) ?? 0];
- String get _filterKey => sids.length == 0
+ String get _filterKey => sids.isEmpty
? StoreKeys.FEED_FILTER_ALL
: StoreKeys.FEED_FILTER_SOURCE;
Tuple2<String, List<String>> _getPredicates() {
List<String> where = ["1 = 1"];
List<String> whereArgs = [];
- if (sids.length > 0) {
+ if (sids.isNotEmpty) {
var placeholders = List.filled(sids.length, "?").join(" , ");
where.add("source IN ($placeholders)");
whereArgs.addAll(sids);
@@ 51,7 54,7 @@ class RSSFeed {
}
bool testItem(RSSItem item) {
- if (sids.length > 0 && !sids.contains(item.source)) return false;
+ if (sids.isNotEmpty && !sids.contains(item.source)) return false;
if (filterType == FilterType.Unread && item.hasRead) return false;
if (filterType == FilterType.Starred && !item.starred) return false;
if (search != "") {
@@ 88,7 91,8 @@ class RSSFeed {
var predicates = _getPredicates();
var offset = iids
.map((iid) => Global.itemsModel.getItem(iid))
- .fold(0, (c, i) => c + (testItem(i) ? 1 : 0));
+ .where((i) => i != null)
+ .fold(0, (c, i) => c + (testItem(i!) ? 1 : 0));
var items = (await Global.db.query(
"items",
orderBy: "date DESC",
M lib/models/feeds_model.dart => lib/models/feeds_model.dart +9 -6
@@ 12,7 12,7 @@ enum ItemSwipeOption {
class FeedsModel with ChangeNotifier {
RSSFeed all = RSSFeed();
- RSSFeed source;
+ RSSFeed? source;
bool _showThumb = Store.sp.getBool(StoreKeys.SHOW_THUMB) ?? true;
bool get showThumb => _showThumb;
@@ 59,20 59,23 @@ class FeedsModel with ChangeNotifier {
Future<void> initSourcesFeed(Iterable<String> sids) async {
Set<String> sidSet = Set.from(sids);
source = RSSFeed(sids: sidSet);
- await source.init();
+ await source!.init();
}
void addFetchedItems(Iterable<RSSItem> items) {
for (var feed in [all, source]) {
if (feed == null) continue;
- var lastDate = feed.iids.length > 0
- ? Global.itemsModel.getItem(feed.iids.last).date
+ var lastDate = feed.iids.isNotEmpty
+ ? Global.itemsModel.getItem(feed.iids.last)?.date
: null;
for (var item in items) {
if (!feed.testItem(item)) continue;
if (lastDate != null && item.date.isBefore(lastDate)) continue;
var idx = Utils.binarySearch(feed.iids, item.id, (a, b) {
- return Global.itemsModel.getItem(b).date.compareTo(Global.itemsModel.getItem(a).date);
+ final dateA = Global.itemsModel.getItem(a)?.date;
+ final dateB = Global.itemsModel.getItem(b)?.date;
+ if (dateA == null || dateB == null) return 0;
+ return dateB.compareTo(dateA);
});
feed.iids.insert(idx, item.id);
}
@@ 86,4 89,4 @@ class FeedsModel with ChangeNotifier {
feed.init();
}
}
-}>
\ No newline at end of file
+}
M lib/models/global_model.dart => lib/models/global_model.dart +19 -8
@@ 1,5 1,6 @@
import 'dart:io';
+import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/utils/store.dart';
import 'package:flutter/material.dart';
@@ 9,12 10,13 @@ enum ThemeSetting {
class GlobalModel with ChangeNotifier {
ThemeSetting _theme = Store.getTheme();
- Locale _locale = Store.getLocale();
+ Locale? _locale = Store.getLocale();
int _keepItemsDays = Store.sp.getInt(StoreKeys.KEEP_ITEMS_DAYS) ?? 21;
bool _syncOnStart = Store.sp.getBool(StoreKeys.SYNC_ON_START) ?? true;
bool _inAppBrowser = Store.sp.getBool(StoreKeys.IN_APP_BROWSER) ?? Platform.isIOS;
- double _textScale = Store.sp.getDouble(StoreKeys.TEXT_SCALE);
+ double? _textScale = Store.sp.getDouble(StoreKeys.TEXT_SCALE);
String _fontFamily = Store.getFontFamily();
+ SourceOpenTarget _globalOpenTarget = Store.getGlobalOpenTarget();
ThemeSetting get theme => _theme;
set theme(ThemeSetting value) {
@@ 24,13 26,13 @@ class GlobalModel with ChangeNotifier {
Store.setTheme(value);
}
}
- Brightness getBrightness() {
+ Brightness? getBrightness() {
if (_theme == ThemeSetting.Default) return null;
else return _theme == ThemeSetting.Light ? Brightness.light : Brightness.dark;
}
- Locale get locale => _locale;
- set locale(Locale value) {
+ Locale? get locale => _locale;
+ set locale(Locale? value) {
if (value != _locale) {
_locale = value;
notifyListeners();
@@ 56,8 58,8 @@ class GlobalModel with ChangeNotifier {
Store.sp.setBool(StoreKeys.IN_APP_BROWSER, value);
}
- double get textScale => _textScale;
- set textScale(double value) {
+ double? get textScale => _textScale;
+ set textScale(double? value) {
if (_textScale != value) {
_textScale = value;
notifyListeners();
@@ 77,4 79,13 @@ class GlobalModel with ChangeNotifier {
Store.setFontFamily(value);
}
}
-}>
\ No newline at end of file
+
+ SourceOpenTarget get globalOpenTarget => _globalOpenTarget;
+ set globalOpenTarget(SourceOpenTarget value) {
+ if (value != _globalOpenTarget) {
+ _globalOpenTarget = value;
+ notifyListeners();
+ Store.setGlobalOpenTarget(value);
+ }
+ }
+}
M lib/models/groups_model.dart => lib/models/groups_model.dart +28 -3
@@ 1,10 1,12 @@
+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:flutter/cupertino.dart';
class GroupsModel with ChangeNotifier {
Map<String, List<String>> _groups = Store.getGroups();
- List<String> uncategorized = Store.getUncategorized();
+ List<String>? uncategorized = Store.getUncategorized();
+ Map<String, int> _groupOpenTargets = Store.getGroupOpenTargets();
Map<String, List<String>> get groups => _groups;
set groups(Map<String, List<String>> groups) {
@@ 14,7 16,7 @@ class GroupsModel with ChangeNotifier {
Store.setGroups(groups);
}
- void updateUncategorized({force: false}) {
+ void updateUncategorized({bool force = false}) {
if (uncategorized != null || force) {
final sids = Set<String>.from(
Global.sourcesModel.getSources().map<String>((s) => s.id)
@@ 41,4 43,27 @@ class GroupsModel with ChangeNotifier {
notifyListeners();
}
}
-}>
\ No newline at end of file
+
+ SourceOpenTarget getGroupOpenTarget(String groupName) {
+ var idx = _groupOpenTargets[groupName];
+ if (idx == null || idx >= SourceOpenTarget.values.length) {
+ return SourceOpenTarget.Inherit;
+ }
+ return SourceOpenTarget.values[idx];
+ }
+
+ void setGroupOpenTarget(String groupName, SourceOpenTarget target) {
+ _groupOpenTargets[groupName] = target.index;
+ Store.setGroupOpenTargets(_groupOpenTargets);
+ notifyListeners();
+ }
+
+ String? findGroupForSource(String sourceId) {
+ for (var entry in _groups.entries) {
+ if (entry.value.contains(sourceId)) {
+ return entry.key;
+ }
+ }
+ return null;
+ }
+}
M lib/models/item.dart => lib/models/item.dart +19 -18
@@ 8,13 8,15 @@ class RSSItem {
String snippet;
bool hasRead;
bool starred;
- String creator; // Optional
- String thumb; // Optional
+ String creator;
+ String? thumb;
RSSItem({
- this.id, this.source, this.title, this.link, this.date,
- this.content, this.snippet, this.hasRead, this.starred,
- this.creator, this.thumb
+ required this.id, required this.source, required this.title,
+ required this.link, required this.date,
+ required this.content, required this.snippet,
+ required this.hasRead, required this.starred,
+ this.creator = "", this.thumb
});
RSSItem clone() {
@@ 41,17 43,16 @@ class RSSItem {
};
}
- RSSItem.fromMap(Map<String, dynamic> map) {
- id = map["iid"];
- source = map["source"];
- title = map["title"];
- link = map["link"];
- date = DateTime.fromMillisecondsSinceEpoch(map["date"]);
- content = map["content"];
- snippet = map["snippet"];
- hasRead = map["hasRead"] != 0;
- starred = map["starred"] != 0;
- creator = map["creator"];
- thumb = map["thumb"];
- }
+ RSSItem.fromMap(Map<String, dynamic> map)
+ : id = map["iid"],
+ source = map["source"],
+ title = map["title"],
+ link = map["link"],
+ date = DateTime.fromMillisecondsSinceEpoch(map["date"]),
+ content = map["content"],
+ snippet = map["snippet"],
+ hasRead = map["hasRead"] != 0,
+ starred = map["starred"] != 0,
+ creator = map["creator"] ?? "",
+ thumb = map["thumb"];
}
M lib/models/items_model.dart => lib/models/items_model.dart +16 -17
@@ 8,7 8,7 @@ class ItemsModel with ChangeNotifier {
bool has(String id) => _items.containsKey(id);
- RSSItem getItem(String id) => _items[id];
+ RSSItem? getItem(String id) => _items[id];
Iterable<RSSItem> getItems() => _items.values;
void loadItems(Iterable<RSSItem> items) {
@@ 17,24 17,24 @@ class ItemsModel with ChangeNotifier {
}
}
- Future<void> updateItem(String iid,
- {Batch batch, bool read, bool starred, local: false}) async {
+ Future<void> updateItem(String iid,
+ {Batch? batch, bool? read, bool? starred, bool local = false}) async {
Map<String, dynamic> updateMap = Map();
if (_items.containsKey(iid)) {
- final item = _items[iid].clone();
+ final item = _items[iid]!.clone();
if (read != null) {
item.hasRead = read;
if (!local) {
- if (read) Global.service.markRead(item);
- else Global.service.markUnread(item);
+ if (read) Global.service?.markRead(item);
+ else Global.service?.markUnread(item);
}
Global.sourcesModel.updateUnreadCount(item.source, read ? -1 : 1);
}
if (starred != null) {
item.starred = starred;
if (!local) {
- if (starred) Global.service.star(item);
- else Global.service.unstar(item);
+ if (starred) Global.service?.star(item);
+ else Global.service?.unstar(item);
}
}
_items[iid] = item;
@@ 49,10 49,10 @@ class ItemsModel with ChangeNotifier {
}
}
- Future<void> markAllRead(Set<String> sids, {DateTime date, before = true}) async {
- Global.service.markAllRead(sids, date, before);
+ Future<void> markAllRead(Set<String> sids, {DateTime? date, bool before = true}) async {
+ Global.service?.markAllRead(sids, date, before);
List<String> predicates = ["hasRead = 0"];
- if (sids.length > 0) {
+ if (sids.isNotEmpty) {
predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})");
}
if (date != null) {
@@ 65,8 65,8 @@ class ItemsModel with ChangeNotifier {
whereArgs: sids.toList(),
);
for (var item in _items.values.toList()) {
- if (sids.length > 0 && !sids.contains(item.source)) continue;
- if (date != null &&
+ if (sids.isNotEmpty && !sids.contains(item.source)) continue;
+ if (date != null &&
(before ? item.date.compareTo(date) > 0 : item.date.compareTo(date) < 0))
continue;
item.hasRead = true;
@@ 76,7 76,7 @@ class ItemsModel with ChangeNotifier {
}
Future<void> fetchItems() async {
- final items = await Global.service.fetchItems();
+ final items = await Global.service!.fetchItems();
final batch = Global.db.batch();
for (var item in items) {
if (!Global.sourcesModel.has(item.source)) continue;
@@ 88,13 88,12 @@ class ItemsModel with ChangeNotifier {
);
}
await batch.commit(noResult: true);
- // notifyListeners();
Global.sourcesModel.updateWithFetchedItems(items);
Global.feedsModel.addFetchedItems(items);
}
Future<void> syncItems() async {
- final tuple = await Global.service.syncItems();
+ final tuple = await Global.service!.syncItems();
final unreadIds = tuple.item1;
final starredIds = tuple.item2;
final rows = await Global.db.query(
@@ 104,7 103,7 @@ class ItemsModel with ChangeNotifier {
);
final batch = Global.db.batch();
for (var row in rows) {
- final id = row["iid"];
+ final id = row["iid"] as String;
if (row["hasRead"] == 0 && !unreadIds.remove(id)) {
await updateItem(id, read: true, batch: batch, local: true);
}
M lib/models/service.dart => lib/models/service.dart +1 -1
@@ 19,7 19,7 @@ abstract class ServiceHandler {
Future<Tuple2<List<RSSSource>, Map<String, List<String>>>> getSources();
Future<List<RSSItem>> fetchItems();
Future<Tuple2<Set<String>, Set<String>>> syncItems();
- Future<void> markAllRead(Set<String> sids, DateTime date, bool before);
+ Future<void> markAllRead(Set<String> sids, DateTime? date, bool before);
Future<void> markRead(RSSItem item);
Future<void> markUnread(RSSItem item);
Future<void> star(RSSItem item);
M lib/models/services/feedbin.dart => lib/models/services/feedbin.dart +29 -30
@@ 13,25 13,24 @@ import '../item.dart';
import '../source.dart';
class FeedbinServiceHandler extends ServiceHandler {
- String endpoint;
- String username;
- String password;
- int fetchLimit;
+ late String endpoint;
+ late String username;
+ late String password;
+ late int fetchLimit;
int _lastId;
- Tuple2<Set<String>, Set<String>> _lastSynced;
+ Tuple2<Set<String>, Set<String>>? _lastSynced;
- FeedbinServiceHandler() {
- endpoint = Store.sp.getString(StoreKeys.ENDPOINT);
- username = Store.sp.getString(StoreKeys.USERNAME);
- password = Store.sp.getString(StoreKeys.PASSWORD);
- fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT);
- _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
+ FeedbinServiceHandler()
+ : _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0 {
+ endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "";
+ username = Store.sp.getString(StoreKeys.USERNAME) ?? "";
+ password = Store.sp.getString(StoreKeys.PASSWORD) ?? "";
+ fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 500;
}
FeedbinServiceHandler.fromValues(
- this.endpoint, this.username, this.password, this.fetchLimit) {
- _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
- }
+ this.endpoint, this.username, this.password, this.fetchLimit)
+ : _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
void persist() {
Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Feedbin.index);
@@ 72,9 71,9 @@ class FeedbinServiceHandler extends ServiceHandler {
final promises = List<Future>.empty(growable: true);
final client = http.Client();
try {
- while (refs.length > 0) {
+ while (refs.isNotEmpty) {
final batch = List<int>.empty(growable: true);
- while (batch.length < 1000 && refs.length > 0) {
+ while (batch.length < 1000 && refs.isNotEmpty) {
batch.add(int.parse(refs.removeLast()));
}
final bodyObject = {
@@ 125,7 124,7 @@ class FeedbinServiceHandler extends ServiceHandler {
for (var tag in tags) {
final name = tag["name"].trim();
groupsMap.putIfAbsent(name, () => []);
- groupsMap[name].add(tag["feed_id"].toString());
+ groupsMap[name]!.add(tag["feed_id"].toString());
}
final sources = subscriptions.map<RSSSource>((s) {
return RSSSource(s["feed_id"].toString(), s["feed_url"], s["title"]);
@@ 138,7 137,7 @@ class FeedbinServiceHandler extends ServiceHandler {
var page = 1;
var minId = Utils.syncMaxId;
var items = [];
- List lastFetched;
+ List? lastFetched;
do {
try {
final response = await _fetchAPI(
@@ 146,8 145,8 @@ class FeedbinServiceHandler extends ServiceHandler {
assert(response.statusCode == 200);
lastFetched = jsonDecode(response.body);
items.addAll(
- lastFetched.where((i) => i["id"] > lastId && i["id"] < minId));
- minId = lastFetched.fold(minId, (m, n) => min(m, n["id"]));
+ lastFetched!.where((i) => i["id"] > lastId && i["id"] < minId));
+ minId = lastFetched.fold(minId, (m, n) => min(m as int, n["id"] as int));
page += 1;
} catch (exp) {
break;
@@ 156,10 155,10 @@ class FeedbinServiceHandler extends ServiceHandler {
lastFetched != null &&
lastFetched.length >= 125 &&
items.length < fetchLimit);
- lastId = items.fold(lastId, (m, n) => max(m, n["id"]));
+ lastId = items.fold(lastId, (m, n) => max(m as int, n["id"] as int));
final parsedItems = List<RSSItem>.empty(growable: true);
- final unread = _lastSynced.item1;
- final starred = _lastSynced.item2;
+ final unread = _lastSynced!.item1;
+ final starred = _lastSynced!.item2;
for (var i in items) {
if (i["content"] == null) continue;
final dom = parse(i["content"]);
@@ 171,8 170,8 @@ class FeedbinServiceHandler extends ServiceHandler {
link: i["url"],
date: DateTime.parse(i["published"]),
content: i["content"],
- snippet: dom.documentElement.text.trim(),
- creator: i["author"],
+ snippet: dom.documentElement?.text.trim() ?? "",
+ creator: i["author"] ?? "",
hasRead: !unread.contains(iid),
starred: starred.contains(iid),
);
@@ 181,7 180,7 @@ class FeedbinServiceHandler extends ServiceHandler {
} else {
var img = dom.querySelector("img");
if (img != null && img.attributes["src"] != null) {
- var thumb = img.attributes["src"];
+ var thumb = img.attributes["src"]!;
if (thumb.startsWith("http")) {
item.thumb = thumb;
}
@@ 207,13 206,13 @@ class FeedbinServiceHandler extends ServiceHandler {
Set.from(unread.map((i) => i.toString())),
Set.from(starred.map((i) => i.toString())),
);
- return _lastSynced;
+ return _lastSynced!;
}
@override
- Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
+ Future<void> markAllRead(Set<String> sids, DateTime? date, bool before) async {
List<String> predicates = ["hasRead = 0"];
- if (sids.length > 0) {
+ if (sids.isNotEmpty) {
predicates
.add("source IN (${List.filled(sids.length, "?").join(" , ")})");
}
@@ 227,7 226,7 @@ class FeedbinServiceHandler extends ServiceHandler {
where: predicates.join(" AND "),
whereArgs: sids.toList(),
);
- final iids = rows.map((r) => r["iid"]);
+ final iids = rows.map((r) => r["iid"] as String);
await _markItems("unread", "DELETE", List.from(iids));
}
M lib/models/services/fever.dart => lib/models/services/fever.dart +25 -28
@@ 13,28 13,26 @@ import 'package:tuple/tuple.dart';
import '../service.dart';
class FeverServiceHandler extends ServiceHandler {
- String endpoint;
- String apiKey;
+ late String endpoint;
+ late String apiKey;
int _lastId;
- int fetchLimit;
+ late int fetchLimit;
bool _useInt32;
- FeverServiceHandler() {
- endpoint = Store.sp.getString(StoreKeys.ENDPOINT);
- apiKey = Store.sp.getString(StoreKeys.API_KEY);
- _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
- fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT);
- _useInt32 = Store.sp.getBool(StoreKeys.FEVER_INT_32) ?? false;
+ FeverServiceHandler()
+ : _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0,
+ _useInt32 = Store.sp.getBool(StoreKeys.FEVER_INT_32) ?? false {
+ endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "";
+ apiKey = Store.sp.getString(StoreKeys.API_KEY) ?? "";
+ fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 500;
}
FeverServiceHandler.fromValues(
this.endpoint,
this.apiKey,
this.fetchLimit,
- ) {
- _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0;
- _useInt32 = Store.sp.getBool(StoreKeys.FEVER_INT_32) ?? false;
- }
+ ) : _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0,
+ _useInt32 = Store.sp.getBool(StoreKeys.FEVER_INT_32) ?? false;
void persist(String username, String password) {
Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Fever.index);
@@ 61,7 59,7 @@ class FeverServiceHandler extends ServiceHandler {
Global.service = null;
}
- Future<Map<String, dynamic>> _fetchAPI({params: "", postparams: ""}) async {
+ Future<Map<String, dynamic>> _fetchAPI({String params = "", String postparams = ""}) async {
var uri = Uri.parse(endpoint + "?api" + params);
final response = await http.post(
uri,
@@ 111,9 109,10 @@ class FeverServiceHandler extends ServiceHandler {
}
for (var group in feedGroups) {
var name = groupsIdMap[group["group_id"]];
+ if (name == null) continue;
for (var fid in group["feed_ids"].split(",")) {
groupsMap.putIfAbsent(name, () => []);
- groupsMap[name].add(fid);
+ groupsMap[name]!.add(fid);
}
}
return Tuple2(sources, groupsMap);
@@ 122,7 121,7 @@ class FeverServiceHandler extends ServiceHandler {
@override
Future<List<RSSItem>> fetchItems() async {
var minId = useInt32 ? 2147483647 : Utils.syncMaxId;
- List<dynamic> response;
+ List<dynamic>? response;
List<dynamic> items = [];
do {
response = (await _fetchAPI(params: "&items&max_id=$minId"))["items"];
@@ 131,12 130,12 @@ class FeverServiceHandler extends ServiceHandler {
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) {
+ if (response.isEmpty && minId == Utils.syncMaxId) {
useInt32 = true;
minId = 2147483647;
response = null;
} else {
- minId = response.fold(minId, (m, n) => min<int>(m, n["id"]));
+ minId = response!.fold(minId, (m, n) => min<int>(m, n["id"]));
}
} while (minId > lastId &&
(response == null || response.length >= 50) &&
@@ 150,20 149,18 @@ class FeverServiceHandler extends ServiceHandler {
link: i["url"],
date: DateTime.fromMillisecondsSinceEpoch(i["created_on_time"] * 1000),
content: i["html"],
- snippet: dom.documentElement.text.trim(),
- creator: i["author"],
+ snippet: dom.documentElement?.text.trim() ?? "",
+ creator: i["author"] ?? "",
hasRead: i["is_read"] == 1,
starred: i["is_saved"] == 1,
);
- // Try to get the thumbnail of the item
var img = dom.querySelector("img");
if (img != null && img.attributes["src"] != null) {
- var thumb = img.attributes["src"];
+ var thumb = img.attributes["src"]!;
if (thumb.startsWith("http")) {
item.thumb = thumb;
}
} 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") &&
@@ 181,8 178,8 @@ class FeverServiceHandler extends ServiceHandler {
_fetchAPI(params: "&unread_item_ids"),
_fetchAPI(params: "&saved_item_ids"),
]);
- final unreadIds = responses[0]["unread_item_ids"];
- final starredIds = responses[1]["saved_item_ids"];
+ final unreadIds = responses[0]["unread_item_ids"] as String;
+ final starredIds = responses[1]["saved_item_ids"] as String;
return Tuple2(
Set.from(unreadIds.split(",")), Set.from(starredIds.split(",")));
}
@@ 196,10 193,10 @@ class FeverServiceHandler extends ServiceHandler {
}
@override
- Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
+ 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)) &&
+ (sids.isEmpty || sids.contains(i.source)) &&
i.date.compareTo(date) >= 0);
await Future.wait(items.map((i) => markRead(i)));
} else {
@@ 210,7 207,7 @@ class FeverServiceHandler extends ServiceHandler {
try {
await Future.wait(Global.sourcesModel
.getSources()
- .where((s) => sids.length == 0 || sids.contains(s.id))
+ .where((s) => sids.isEmpty || sids.contains(s.id))
.map((s) => _fetchAPI(
postparams:
"&mark=feed&as=read&id=${s.id}&before=$timestamp")));
M lib/models/services/greader.dart => lib/models/services/greader.dart +55 -57
@@ 15,27 15,27 @@ class GReaderServiceHandler extends ServiceHandler {
static const _READ_TAG = "user/-/state/com.google/read";
static const _STAR_TAG = "user/-/state/com.google/starred";
- String endpoint;
- String username;
- String password;
- int fetchLimit;
- int _lastFetched;
- String _lastId;
- String _auth;
- bool useInt64;
- String inoreaderId;
- String inoreaderKey;
- bool removeInoreaderAd;
+ late String endpoint;
+ late String username;
+ late String password;
+ late int fetchLimit;
+ int? _lastFetched;
+ String? _lastId;
+ String? _auth;
+ late bool useInt64;
+ String? inoreaderId;
+ String? inoreaderKey;
+ bool? removeInoreaderAd;
GReaderServiceHandler() {
- endpoint = Store.sp.getString(StoreKeys.ENDPOINT);
- username = Store.sp.getString(StoreKeys.USERNAME);
- password = Store.sp.getString(StoreKeys.PASSWORD);
- fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT);
+ endpoint = Store.sp.getString(StoreKeys.ENDPOINT) ?? "";
+ username = Store.sp.getString(StoreKeys.USERNAME) ?? "";
+ password = Store.sp.getString(StoreKeys.PASSWORD) ?? "";
+ fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT) ?? 500;
_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);
+ useInt64 = Store.sp.getBool(StoreKeys.USE_INT_64) ?? true;
inoreaderId = Store.sp.getString(StoreKeys.API_ID);
inoreaderKey = Store.sp.getString(StoreKeys.API_KEY);
removeInoreaderAd = Store.sp.getBool(StoreKeys.INOREADER_REMOVE_AD);
@@ 69,9 69,9 @@ class GReaderServiceHandler extends ServiceHandler {
Store.sp.setInt(StoreKeys.FETCH_LIMIT, fetchLimit);
Store.sp.setBool(StoreKeys.USE_INT_64, useInt64);
if (inoreaderId != null) {
- Store.sp.setString(StoreKeys.API_ID, inoreaderId);
- Store.sp.setString(StoreKeys.API_KEY, inoreaderKey);
- Store.sp.setBool(StoreKeys.INOREADER_REMOVE_AD, removeInoreaderAd);
+ Store.sp.setString(StoreKeys.API_ID, inoreaderId!);
+ Store.sp.setString(StoreKeys.API_KEY, inoreaderKey!);
+ Store.sp.setBool(StoreKeys.INOREADER_REMOVE_AD, removeInoreaderAd ?? false);
}
Global.service = this;
}
@@ 93,30 93,30 @@ class GReaderServiceHandler extends ServiceHandler {
Global.service = null;
}
- int get lastFetched => _lastFetched;
- set lastFetched(int value) {
+ int? get lastFetched => _lastFetched;
+ set lastFetched(int? value) {
_lastFetched = value;
- Store.sp.setInt(StoreKeys.LAST_FETCHED, value);
+ if (value != null) Store.sp.setInt(StoreKeys.LAST_FETCHED, value);
}
- String get lastId => _lastId;
- set lastId(String value) {
+ String? get lastId => _lastId;
+ set lastId(String? value) {
_lastId = value;
- Store.sp.setString(StoreKeys.LAST_ID, value);
+ if (value != null) Store.sp.setString(StoreKeys.LAST_ID, value);
}
- String get auth => _auth;
- set auth(String value) {
+ String? get auth => _auth;
+ set auth(String? value) {
_auth = value;
- Store.sp.setString(StoreKeys.AUTH, value);
+ if (value != null) Store.sp.setString(StoreKeys.AUTH, value);
}
Future<http.Response> _fetchAPI(String params, {dynamic body}) async {
final headers = Map<String, String>();
- if (auth != null) headers["Authorization"] = auth;
+ if (auth != null) headers["Authorization"] = auth!;
if (inoreaderId != null) {
- headers["AppId"] = inoreaderId;
- headers["AppKey"] = inoreaderKey;
+ headers["AppId"] = inoreaderId!;
+ headers["AppKey"] = inoreaderKey!;
}
var uri = Uri.parse(endpoint + params);
if (body == null) {
@@ 129,8 129,8 @@ class GReaderServiceHandler extends ServiceHandler {
Future<Set<String>> _fetchAll(String params) async {
final results = List<String>.empty(growable: true);
- List fetched;
- String continuation;
+ List? fetched;
+ String? continuation;
do {
var p = params;
if (continuation != null) p += "&c=$continuation";
@@ 138,17 138,17 @@ class GReaderServiceHandler extends ServiceHandler {
assert(response.statusCode == 200);
final parsed = jsonDecode(response.body);
fetched = parsed["itemRefs"];
- if (fetched != null && fetched.length > 0) {
+ if (fetched != null && fetched.isNotEmpty) {
for (var i in fetched) {
results.add(i["id"]);
}
}
continuation = parsed["continuation"];
} while (continuation != null && fetched != null && fetched.length >= 1000);
- return new Set.from(results);
+ return Set.from(results);
}
- Future<http.Response> _editTag(String ref, String tag, {add: true}) async {
+ Future<http.Response> _editTag(String ref, String tag, {bool add = true}) async {
final body = "i=$ref&${add ? "a" : "r"}=$tag";
return await _fetchAPI("/reader/api/0/edit-tag", body: body);
}
@@ 199,7 199,7 @@ class GReaderServiceHandler extends ServiceHandler {
if (categories != null) {
for (var c in categories) {
groupsMap.putIfAbsent(c["label"], () => []);
- groupsMap[c["label"]].add(s["id"]);
+ groupsMap[c["label"]]!.add(s["id"]);
}
}
}
@@ 212,8 212,8 @@ class GReaderServiceHandler extends ServiceHandler {
@override
Future<List<RSSItem>> fetchItems() async {
List items = [];
- List fetchedItems;
- String continuation;
+ List? fetchedItems;
+ String? continuation;
do {
try {
final limit = min(fetchLimit - items.length, 1000);
@@ 224,7 224,7 @@ class GReaderServiceHandler extends ServiceHandler {
assert(response.statusCode == 200);
final fetched = jsonDecode(response.body);
fetchedItems = fetched["items"];
- for (var i in fetchedItems) {
+ for (var i in fetchedItems!) {
i["id"] = _compactId(i["id"]);
if (i["id"] == lastId || items.length >= fetchLimit) {
break;
@@ 237,15 237,15 @@ class GReaderServiceHandler extends ServiceHandler {
break;
}
} while (continuation != null && items.length < fetchLimit);
- if (items.length > 0) {
+ if (items.isNotEmpty) {
lastId = items[0]["id"];
lastFetched = int.parse(items[0]["crawlTimeMsec"]) ~/ 1000;
}
final parsedItems = items.map<RSSItem>((i) {
final dom = parse(i["summary"]["content"]);
if (removeInoreaderAd == true) {
- if (dom.documentElement.text.trim().startsWith("Ads from Inoreader")) {
- dom.body.firstChild.remove();
+ if (dom.documentElement?.text.trim().startsWith("Ads from Inoreader") ?? false) {
+ dom.body?.firstChild?.remove();
}
}
final item = RSSItem(
@@ 254,19 254,19 @@ class GReaderServiceHandler extends ServiceHandler {
title: i["title"],
link: i["canonical"][0]["href"],
date: DateTime.fromMillisecondsSinceEpoch(i["published"] * 1000),
- content: dom.body.innerHtml,
- snippet: dom.documentElement.text.trim(),
- creator: i["author"],
+ content: dom.body?.innerHtml ?? "",
+ snippet: dom.documentElement?.text.trim() ?? "",
+ creator: i["author"] ?? "",
hasRead: false,
starred: false,
);
if (inoreaderId != null) {
final titleDom = parse(item.title);
- item.title = titleDom.documentElement.text;
+ item.title = titleDom.documentElement?.text ?? item.title;
}
var img = dom.querySelector("img");
if (img != null && img.attributes["src"] != null) {
- var thumb = img.attributes["src"];
+ var thumb = img.attributes["src"]!;
if (thumb.startsWith("http")) {
item.thumb = thumb;
}
@@ 304,24 304,22 @@ class GReaderServiceHandler extends ServiceHandler {
}
@override
- Future<void> markAllRead(Set<String> sids, DateTime date, bool before) async {
+ Future<void> markAllRead(Set<String> sids, DateTime? date, bool before) async {
if (date != null) {
List<String> predicates = ["hasRead = 0"];
- if (sids.length > 0) {
+ if (sids.isNotEmpty) {
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",
columns: ["iid"],
where: predicates.join(" AND "),
whereArgs: sids.toList(),
);
- final iids = rows.map((r) => r["iid"]).iterator;
+ final iids = rows.map((r) => r["iid"] as String).iterator;
List<String> refs = [];
while (iids.moveNext()) {
refs.add(iids.current);
@@ 330,9 328,9 @@ class GReaderServiceHandler extends ServiceHandler {
refs = [];
}
}
- if (refs.length > 0) _editTag(refs.join("&i="), _READ_TAG);
+ if (refs.isNotEmpty) _editTag(refs.join("&i="), _READ_TAG);
} else {
- if (sids.length == 0)
+ if (sids.isEmpty)
sids = Set.from(Global.sourcesModel.getSources().map((s) => s.id));
for (var sid in sids) {
final body = {"s": sid};
M lib/models/services/service_import.dart => lib/models/services/service_import.dart +5 -5
@@ 1,9 1,9 @@
class ServiceImport {
- String endpoint;
- String username;
- String password;
- String apiId;
- String apiKey;
+ String? endpoint;
+ String? username;
+ String? password;
+ String? apiId;
+ String? apiKey;
static const typeMap = {
"f": "/settings/service/fever",
M lib/models/source.dart => lib/models/source.dart +28 -18
@@ 1,23 1,32 @@
enum SourceOpenTarget {
- Local, FullContent, Webpage, External
+ Local, FullContent, Webpage, External, Inherit
+}
+
+SourceOpenTarget resolveOpenTargetCascade(
+ SourceOpenTarget sourceTarget,
+ SourceOpenTarget groupTarget,
+ SourceOpenTarget globalTarget,
+) {
+ if (sourceTarget != SourceOpenTarget.Inherit) return sourceTarget;
+ if (groupTarget != SourceOpenTarget.Inherit) return groupTarget;
+ return globalTarget;
}
class RSSSource {
String id;
String url;
- String iconUrl;
+ String? iconUrl;
String name;
SourceOpenTarget openTarget;
int unreadCount;
DateTime latest;
String lastTitle;
- RSSSource(this.id, this.url, this.name) {
- openTarget = SourceOpenTarget.Local;
- latest = DateTime.now();
- unreadCount = 0;
- lastTitle = "";
- }
+ RSSSource(this.id, this.url, this.name)
+ : openTarget = SourceOpenTarget.Inherit,
+ latest = DateTime.now(),
+ unreadCount = 0,
+ lastTitle = "";
RSSSource._privateConstructor(
this.id, this.url, this.iconUrl, this.name, this.openTarget,
@@ 43,14 52,15 @@ class RSSSource {
};
}
- RSSSource.fromMap(Map<String, dynamic> map) {
- id = map["sid"];
- url = map["url"];
- iconUrl = map["iconUrl"];
- name = map["name"];
- openTarget = SourceOpenTarget.values[map["openTarget"]];
- latest = DateTime.fromMillisecondsSinceEpoch(map["latest"]);
- lastTitle = map["lastTitle"];
- unreadCount = 0;
- }
+ RSSSource.fromMap(Map<String, dynamic> map)
+ : id = map["sid"],
+ url = map["url"],
+ iconUrl = map["iconUrl"],
+ name = map["name"],
+ openTarget = (map["openTarget"] as int) < SourceOpenTarget.values.length
+ ? SourceOpenTarget.values[map["openTarget"]]
+ : SourceOpenTarget.Local,
+ latest = DateTime.fromMillisecondsSinceEpoch(map["latest"]),
+ lastTitle = map["lastTitle"],
+ unreadCount = 0;
}
M lib/models/sources_model.dart => lib/models/sources_model.dart +17 -15
@@ 24,7 24,7 @@ class SourcesModel with ChangeNotifier {
bool has(String id) => _sources.containsKey(id);
- RSSSource getSource(String id) => _sources[id] ?? _deleted[id];
+ RSSSource? getSource(String id) => _sources[id] ?? _deleted[id];
Iterable<RSSSource> getSources() => _sources.values;
@@ 47,13 47,13 @@ class SourcesModel with ChangeNotifier {
cloned.unreadCount = 0;
}
for (var row in rows) {
- _sources[row["source"]].unreadCount = row["COUNT(iid)"];
+ _sources[row["source"]]?.unreadCount = row["COUNT(iid)"] as int;
}
notifyListeners();
}
void updateUnreadCount(String sid, int diff) {
- _sources[sid].unreadCount += diff;
+ _sources[sid]?.unreadCount += diff;
notifyListeners();
}
@@ 61,19 61,21 @@ class SourcesModel with ChangeNotifier {
Set<String> changed = Set();
for (var item in items) {
var source = _sources[item.source];
+ if (source == null) continue;
if (!item.hasRead) source.unreadCount += 1;
if (item.date.compareTo(source.latest) > 0 ||
- source.lastTitle.length == 0) {
+ source.lastTitle.isEmpty) {
source.latest = item.date;
source.lastTitle = item.title;
changed.add(source.id);
}
}
notifyListeners();
- if (changed.length > 0) {
+ if (changed.isNotEmpty) {
var batch = Global.db.batch();
for (var sid in changed) {
var source = _sources[sid];
+ if (source == null) continue;
batch.update(
"sources",
{
@@ 88,7 90,7 @@ class SourcesModel with ChangeNotifier {
}
}
- Future<void> put(RSSSource source, {force: false}) async {
+ Future<void> put(RSSSource source, {bool force = false}) async {
if (_deleted.containsKey(source.id) && !force) return;
_sources[source.id] = source;
notifyListeners();
@@ 99,7 101,7 @@ class SourcesModel with ChangeNotifier {
);
}
- Future<void> putAll(Iterable<RSSSource> sources, {force: false}) async {
+ Future<void> putAll(Iterable<RSSSource> sources, {bool force = false}) async {
Batch batch = Global.db.batch();
for (var source in sources) {
if (_deleted.containsKey(source.id) && !force) continue;
@@ 115,7 117,7 @@ class SourcesModel with ChangeNotifier {
}
Future<void> updateSources() async {
- final tuple = await Global.service.getSources();
+ final tuple = await Global.service!.getSources();
final sources = tuple.item1;
var curr = Set<String>.from(_sources.keys);
List<RSSSource> newSources = [];
@@ 136,7 138,7 @@ class SourcesModel with ChangeNotifier {
final batch = Global.db.batch();
for (var id in ids) {
if (!_sources.containsKey(id)) continue;
- var source = _sources[id];
+ var source = _sources[id]!;
batch.delete(
"items",
where: "source = ?",
@@ 157,18 159,18 @@ class SourcesModel with ChangeNotifier {
Future<void> fetchFavicons() async {
for (var key in _sources.keys) {
- if (_sources[key].iconUrl == null) {
- _fetchFavicon(_sources[key].url).then((url) {
+ if (_sources[key]!.iconUrl == null) {
+ _fetchFavicon(_sources[key]!.url).then((url) {
if (!_sources.containsKey(key)) return;
- var source = _sources[key].clone();
- source.iconUrl = url == null ? "" : url;
+ var source = _sources[key]!.clone();
+ source.iconUrl = url ?? "";
put(source);
});
}
}
}
- Future<String> _fetchFavicon(String url) async {
+ Future<String?> _fetchFavicon(String url) async {
try {
url = url.split("/").getRange(0, 3).join("/");
var uri = Uri.parse(url);
@@ 181,7 183,7 @@ class SourcesModel with ChangeNotifier {
var rel = link.attributes["rel"];
if ((rel == "icon" || rel == "shortcut icon") &&
link.attributes.containsKey("href")) {
- var href = link.attributes["href"];
+ var href = link.attributes["href"]!;
var parsedUrl = Uri.parse(url);
if (href.startsWith("//"))
return parsedUrl.scheme + ":" + href;
M lib/models/sync_model.dart => lib/models/sync_model.dart +3 -3
@@ 26,7 26,7 @@ class SyncModel with ChangeNotifier {
.map((s) => s.id)
.toList();
await Global.sourcesModel.removeSources(sids);
- Global.service.remove();
+ Global.service!.remove();
hasService = false;
syncing = false;
notifyListeners();
@@ 49,7 49,7 @@ class SyncModel with ChangeNotifier {
syncing = true;
notifyListeners();
try {
- await Global.service.reauthenticate();
+ await Global.service!.reauthenticate();
await Global.sourcesModel.updateSources();
await Global.itemsModel.syncItems();
await Global.itemsModel.fetchItems();
@@ 63,4 63,4 @@ class SyncModel with ChangeNotifier {
syncing = false;
notifyListeners();
}
-}>
\ No newline at end of file
+}
M lib/pages/article_page.dart => lib/pages/article_page.dart +59 -46
@@ 17,7 17,7 @@ import 'package:intl/intl.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
-import 'package:share/share.dart';
+import 'package:share_plus/share_plus.dart';
import 'package:fluent_reader_lite/components/cupertino_toolbar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ 34,16 34,27 @@ class ArticlePage extends StatefulWidget {
enum _ArticleLoadState { Loading, Success, Failure }
class ArticlePageState extends State<ArticlePage> {
- WebViewController _controller;
+ WebViewController? _controller;
int requestId = 0;
_ArticleLoadState loaded = _ArticleLoadState.Loading;
bool navigated = false;
- SourceOpenTarget _target;
- String iid;
- bool isSourceFeed;
+ SourceOpenTarget? _target;
+ String? iid;
+ bool? isSourceFeed;
- void loadNewItem(String id, {bool isSource}) {
- if (!Global.itemsModel.getItem(id).hasRead) {
+ WebViewController _createController() {
+ final controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(NavigationDelegate(
+ onNavigationRequest: _onNavigate,
+ onPageStarted: _onPageReady,
+ onPageFinished: _onWebpageReady,
+ ));
+ return controller;
+ }
+
+ void loadNewItem(String id, {bool? isSource}) {
+ if (!Global.itemsModel.getItem(id)!.hasRead) {
Global.itemsModel.updateItem(id, read: true);
}
setState(() {
@@ 55,18 66,20 @@ class ArticlePageState extends State<ArticlePage> {
});
}
- Future<NavigationDecision> _onNavigate(NavigationRequest request) async {
- if (navigated && request.isForMainFrame) {
+ NavigationDecision _onNavigate(NavigationRequest request) {
+ if (navigated && request.isMainFrame) {
final internal = Global.globalModel.inAppBrowser;
- await launch(request.url,
- forceSafariVC: internal, forceWebView: internal);
+ launchUrl(
+ Uri.parse(request.url),
+ mode: internal ? LaunchMode.inAppWebView : LaunchMode.externalApplication,
+ );
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
}
- void _loadHtml(RSSItem item, RSSSource source, {loadFull: false}) async {
+ void _loadHtml(RSSItem item, RSSSource source, {bool loadFull = false}) async {
var localUrl = "http://127.0.0.1:9000/article/article.html";
var currId = requestId;
String a;
@@ 88,7 101,7 @@ class ArticlePageState extends State<ArticlePage> {
}
if (!mounted || currId != requestId) return;
var h =
- '<p id="source">${source.name}${(item.creator != null && item.creator.length > 0) ? ' / ' + item.creator : ''}</p>';
+ '<p id="source">${source.name}${item.creator.isNotEmpty ? ' / ' + 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>';
@@ 108,10 121,10 @@ class ArticlePageState extends State<ArticlePage> {
var brightness = Global.currentBrightness(context);
localUrl += "&t=${brightness.index}";
}
- _controller.loadUrl(localUrl);
+ _controller?.loadRequest(Uri.parse(localUrl));
}
- void _onPageReady(_) async {
+ void _onPageReady(String url) async {
if (Platform.isAndroid || Global.globalModel.getBrightness() != null) {
await Future.delayed(Duration(milliseconds: 300));
}
@@ 124,14 137,13 @@ class ArticlePageState extends State<ArticlePage> {
}
}
- void _onWebpageReady(_) {
+ void _onWebpageReady(String url) {
if (loaded == _ArticleLoadState.Success) navigated = true;
}
- void _setOpenTarget(RSSSource source, {SourceOpenTarget target}) {
- setState(() {
- _target = target ?? source.openTarget;
- });
+ void _setOpenTarget(RSSItem item, RSSSource source, {SourceOpenTarget? target}) {
+ _target = target ?? Global.resolveOpenTarget(source);
+ _loadOpenTarget(item, source);
}
void _loadOpenTarget(RSSItem item, RSSSource source) {
@@ 140,7 152,7 @@ class ArticlePageState extends State<ArticlePage> {
loaded = _ArticleLoadState.Loading;
navigated = false;
});
- switch (_target) {
+ switch (_target!) {
case SourceOpenTarget.Local:
_loadHtml(item, source);
break;
@@ 149,7 161,10 @@ class ArticlePageState extends State<ArticlePage> {
break;
case SourceOpenTarget.Webpage:
case SourceOpenTarget.External:
- _controller.loadUrl(item.link);
+ _controller?.loadRequest(Uri.parse(item.link));
+ break;
+ case SourceOpenTarget.Inherit:
+ _loadHtml(item, source);
break;
}
}
@@ 157,9 172,9 @@ class ArticlePageState extends State<ArticlePage> {
@override
Widget build(BuildContext context) {
final Tuple2<String, bool> arguments =
- ModalRoute.of(context).settings.arguments;
- if (iid == null) iid = arguments.item1;
- if (isSourceFeed == null) isSourceFeed = arguments.item2;
+ ModalRoute.of(context)!.settings.arguments as Tuple2<String, bool>;
+ iid ??= arguments.item1;
+ isSourceFeed ??= arguments.item2;
final resolvedDarkGrey = MyColors.dynamicDarkGrey.resolveFrom(context);
final viewOptions = {
0: Padding(
@@ 183,29 198,26 @@ class ArticlePageState extends State<ArticlePage> {
};
return Selector2<ItemsModel, SourcesModel, Tuple2<RSSItem, RSSSource>>(
selector: (context, itemsModel, sourcesModel) {
- var item = itemsModel.getItem(iid);
- var source = sourcesModel.getSource(item.source);
+ var item = itemsModel.getItem(iid!)!;
+ var source = sourcesModel.getSource(item.source)!;
return Tuple2(item, source);
},
builder: (context, tuple, child) {
var item = tuple.item1;
var source = tuple.item2;
- if (_target == null) _target = source.openTarget;
+ _target ??= Global.resolveOpenTarget(source);
+ if (_controller == null) {
+ _controller = _createController();
+ Future.microtask(() => _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,
+ WebViewWidget(
+ key: Key("a-$iid-${_target!.index}"),
+ controller: _controller!,
),
Center(
child: Column(
@@ 232,19 244,19 @@ class ArticlePageState extends State<ArticlePage> {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
backgroundColor: CupertinoColors.systemBackground,
- middle: CupertinoSlidingSegmentedControl(
+ middle: CupertinoSlidingSegmentedControl<int>(
children: viewOptions,
onValueChanged: (v) {
- _setOpenTarget(source, target: SourceOpenTarget.values[v]);
+ _setOpenTarget(item, source, target: SourceOpenTarget.values[v!]);
},
- groupValue: _target.index,
+ groupValue: _target!.index > 2 ? 2 : _target!.index,
),
),
child: Consumer<FeedsModel>(
child: body,
builder: (context, feedsModel, child) {
- final feed = isSourceFeed ? feedsModel.source : feedsModel.all;
- var idx = feed.iids.indexOf(iid);
+ final feed = isSourceFeed! ? feedsModel.source! : feedsModel.all;
+ var idx = feed.iids.indexOf(iid!);
return CupertinoToolbar(
items: [
CupertinoToolbarItem(
@@ 281,7 293,8 @@ class ArticlePageState extends State<ArticlePage> {
Share.share(item.link,
sharePositionOrigin: Rect.fromLTWH(
media.size.width -
- ArticlePage.state.currentContext.size.width /
+ (ArticlePage.state.currentContext?.size?.width ??
+ 0) /
2,
media.size.height - media.padding.bottom - 54,
0,
@@ 307,14 320,14 @@ class ArticlePageState extends State<ArticlePage> {
if (idx == feed.iids.length - 1) {
await feed.loadMore();
}
- idx = feed.iids.indexOf(iid);
+ idx = feed.iids.indexOf(iid!);
if (idx != feed.iids.length - 1) {
loadNewItem(feed.iids[idx + 1]);
}
},
),
],
- body: child,
+ body: child!,
);
},
),
M lib/pages/group_list_page.dart => lib/pages/group_list_page.dart +72 -20
@@ 1,16 1,19 @@
import 'package:fluent_reader_lite/components/badge.dart';
import 'package:fluent_reader_lite/components/dismissible_background.dart';
+import 'package:fluent_reader_lite/components/list_tile_group.dart';
import 'package:fluent_reader_lite/components/mark_all_action_sheet.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/groups_model.dart';
import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/models/sources_model.dart';
+import 'package:fluent_reader_lite/utils/colors.dart';
import 'package:fluent_reader_lite/utils/global.dart';
import 'package:fluent_reader_lite/utils/utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
+import 'package:tuple/tuple.dart';
class GroupListPage extends StatefulWidget {
@override
@@ 18,12 21,19 @@ class GroupListPage extends StatefulWidget {
}
class _GroupListPageState extends State<GroupListPage> {
- static const List<String> _uncategorizedIndicator = [null, null];
+ static const List<String> _uncategorizedIndicator = ["_", "_"];
- int _unreadCount(Iterable<RSSSource> sources) {
+ int _unreadCount(Iterable<RSSSource?> sources) {
return sources.fold(0, (c, s) => c + (s != null ? s.unreadCount : 0));
}
+ void _showGroupSettings(BuildContext context, String groupName) {
+ HapticFeedback.mediumImpact();
+ Navigator.of(context).push(CupertinoPageRoute(
+ builder: (context) => _GroupSettingsPage(groupName: groupName),
+ ));
+ }
+
static const _dismissThresholds = {
DismissDirection.startToEnd: 0.25,
};
@@ 55,35 65,40 @@ class _GroupListPageState extends State<GroupListPage> {
final dismissBg = DismissibleBackground(CupertinoIcons.checkmark_circle, true);
final groupList = Consumer2<GroupsModel, SourcesModel>(
builder: (context, groupsModel, sourcesModel, child) {
- final groupNames = groupsModel.groups.keys.toList();
- groupNames.sort(Utils.localStringCompare);
- if (groupsModel.uncategorized != null) {
- groupNames.insert(0, null);
- }
+ final sortedKeys = groupsModel.groups.keys.toList();
+ sortedKeys.sort(Utils.localStringCompare);
+ final hasUncategorized = groupsModel.uncategorized != null;
+ final totalCount = sortedKeys.length + (hasUncategorized ? 1 : 0);
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
String groupName;
List<String> group;
- final isUncategorized = groupsModel.showUncategorized && index == 0;
+ final isUncategorized = groupsModel.showUncategorized && hasUncategorized && index == 0;
if (isUncategorized) {
groupName = S.of(context).uncategorized;
- group = groupsModel.uncategorized;
+ group = groupsModel.uncategorized!;
} else {
- groupName = groupNames[index];
- group = groupsModel.groups[groupName];
+ final adjustedIndex = hasUncategorized ? index - 1 : index;
+ groupName = sortedKeys[adjustedIndex];
+ group = groupsModel.groups[groupName]!;
}
final count = _unreadCount(
group.map((sid) => sourcesModel.getSource(sid))
);
- final tile = MyListTile(
- title: Flexible(child: Text(groupName, overflow: TextOverflow.ellipsis)),
- trailing: count > 0 ? Badge(count) : null,
- onTap: () {
- Navigator.of(context).pop(
- isUncategorized ? _uncategorizedIndicator : [groupName]
- );
+ final tile = GestureDetector(
+ onLongPress: isUncategorized ? null : () {
+ _showGroupSettings(context, groupName);
},
- background: CupertinoColors.systemBackground,
+ child: MyListTile(
+ title: Flexible(child: Text(groupName, overflow: TextOverflow.ellipsis)),
+ trailing: count > 0 ? Badge(count) : null,
+ onTap: () {
+ Navigator.of(context).pop(
+ isUncategorized ? _uncategorizedIndicator : [groupName]
+ );
+ },
+ background: CupertinoColors.systemBackground,
+ ),
);
return Dismissible(
key: Key("$groupName$index"),
@@ 101,7 116,7 @@ class _GroupListPageState extends State<GroupListPage> {
return false;
},
);
- }, childCount: groupNames.length),
+ }, childCount: totalCount),
);
},
);
@@ 121,3 136,40 @@ class _GroupListPageState extends State<GroupListPage> {
);
}
}
+
+class _GroupSettingsPage extends StatelessWidget {
+ final String groupName;
+
+ const _GroupSettingsPage({required this.groupName});
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer<GroupsModel>(
+ builder: (context, groupsModel, child) {
+ final openTarget = ListTileGroup.fromOptions(
+ [
+ Tuple2(S.of(context).inheritDefault, SourceOpenTarget.Inherit),
+ Tuple2(S.of(context).rssText, SourceOpenTarget.Local),
+ Tuple2(S.of(context).loadFull, SourceOpenTarget.FullContent),
+ Tuple2(S.of(context).loadWebpage, SourceOpenTarget.Webpage),
+ Tuple2(S.of(context).openExternal, SourceOpenTarget.External),
+ ],
+ groupsModel.getGroupOpenTarget(groupName),
+ (v) {
+ groupsModel.setGroupOpenTarget(groupName, v);
+ },
+ title: S.of(context).openTarget,
+ );
+ return CupertinoPageScaffold(
+ backgroundColor: MyColors.background,
+ navigationBar: CupertinoNavigationBar(
+ middle: Text(groupName, overflow: TextOverflow.ellipsis),
+ ),
+ child: ListView(children: [
+ openTarget,
+ ]),
+ );
+ },
+ );
+ }
+}
M lib/pages/home_page.dart => lib/pages/home_page.dart +9 -8
@@ 12,7 12,7 @@ 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 'package:app_links/app_links.dart';
import 'item_list_page.dart';
@@ 40,9 40,9 @@ class _HomePageState extends State<HomePage> {
GlobalKey(),
GlobalKey(),
];
- StreamSubscription _uriSub;
+ StreamSubscription? _uriSub;
- void _uriStreamListener(Uri uri) {
+ void _uriStreamListener(Uri? uri) {
if (uri == null) return;
if (uri.host == "import") {
if (Global.syncModel.hasService) {
@@ 75,10 75,11 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
- _uriSub = uriLinkStream.listen(_uriStreamListener);
+ final appLinks = AppLinks();
+ _uriSub = appLinks.uriLinkStream.listen(_uriStreamListener);
Future.delayed(Duration.zero, () async {
try {
- final uri = await getInitialUri();
+ final uri = await appLinks.getInitialLink();
if (uri != null) {
_uriStreamListener(uri);
}
@@ 90,7 91,7 @@ class _HomePageState extends State<HomePage> {
@override
dispose() {
- _uriSub.cancel();
+ _uriSub?.cancel();
super.dispose();
}
@@ 107,7 108,7 @@ class _HomePageState extends State<HomePage> {
);
}
- Widget buildLeft(BuildContext context, {isMobile: true}) {
+ Widget buildLeft(BuildContext context, {bool isMobile = true}) {
final leftTabs = CupertinoTabScaffold(
controller: _controller,
backgroundColor: CupertinoColors.systemBackground,
@@ 147,7 148,7 @@ class _HomePageState extends State<HomePage> {
child: leftTabs,
onWillPop: () async {
return !(await _tabNavigatorKeys[_controller.index]
- .currentState
+ .currentState!
.maybePop());
},
);
M lib/pages/item_list_page.dart => lib/pages/item_list_page.dart +11 -11
@@ 15,7 15,7 @@ import 'package:fluent_reader_lite/utils/global.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' hide Badge;
import 'package:provider/provider.dart';
-import 'package:share/share.dart';
+import 'package:share_plus/share_plus.dart';
import 'package:tuple/tuple.dart';
import 'home_page.dart';
@@ 23,14 23,14 @@ import 'home_page.dart';
class ItemListPage extends StatefulWidget {
final ScrollTopNotifier scrollTopNotifier;
- ItemListPage(this.scrollTopNotifier, {Key key}) : super(key: key);
+ ItemListPage(this.scrollTopNotifier, {Key? key}) : super(key: key);
@override
_ItemListPageState createState() => _ItemListPageState();
}
class _ItemListPageState extends State<ItemListPage> {
- DateTime lastLoadedMore;
+ DateTime? lastLoadedMore;
void _onScrollTop() {
var expectedCanPop = widget.scrollTopNotifier.index == 1;
@@ 56,14 56,14 @@ class _ItemListPageState extends State<ItemListPage> {
}
RSSFeed getFeed() {
- return ModalRoute.of(context).settings.arguments != null
- ? Global.feedsModel.source
+ return ModalRoute.of(context)!.settings.arguments != null
+ ? Global.feedsModel.source!
: Global.feedsModel.all;
}
bool _onScroll(ScrollNotification scrollInfo) {
var feed = getFeed();
- if (!ModalRoute.of(context).isCurrent ||
+ if (!ModalRoute.of(context)!.isCurrent ||
!feed.initialized ||
feed.loading ||
feed.allLoaded) {
@@ 72,7 72,7 @@ class _ItemListPageState extends State<ItemListPage> {
if (scrollInfo.metrics.extentAfter == 0.0 &&
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent * 0.8 &&
(lastLoadedMore == null ||
- DateTime.now().difference(lastLoadedMore).inSeconds > 1)) {
+ DateTime.now().difference(lastLoadedMore!).inSeconds > 1)) {
lastLoadedMore = DateTime.now();
feed.loadMore();
}
@@ 175,7 175,7 @@ class _ItemListPageState extends State<ItemListPage> {
}
void _editSearchKeyword() async {
- String keyword = await Navigator.of(context).push(CupertinoPageRoute(
+ String? keyword = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
S.of(context).editKeyword,
(v) => v.trim().length > 0,
@@ 296,7 296,7 @@ class _ItemListPageState extends State<ItemListPage> {
@override
Widget build(BuildContext context) {
- final String title = ModalRoute.of(context).settings.arguments;
+ final String? title = ModalRoute.of(context)!.settings.arguments as String?;
final titleWidget = Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ 374,8 374,8 @@ class _ItemListPageState extends State<ItemListPage> {
return Selector2<ItemsModel, SourcesModel,
Tuple2<RSSItem, RSSSource>>(
selector: (context, itemsModel, sourcesModel) {
- var item = itemsModel.getItem(feed.iids[index]);
- var source = sourcesModel.getSource(item.source);
+ var item = itemsModel.getItem(feed.iids[index])!;
+ var source = sourcesModel.getSource(item.source)!;
return Tuple2(item, source);
},
builder: (context, tuple, child) =>
M lib/pages/settings/about_page.dart => lib/pages/settings/about_page.dart +1 -1
@@ 8,7 8,7 @@ import 'package:flutter/cupertino.dart';
class AboutPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final String version = ModalRoute.of(context).settings.arguments ?? "1.0.0";
+ final String version = (ModalRoute.of(context)!.settings.arguments as String?) ?? "1.0.0";
final nameStyle = TextStyle(
color: CupertinoColors.label.resolveFrom(context),
fontSize: 18,
M lib/pages/settings/feed_page.dart => lib/pages/settings/feed_page.dart +2 -2
@@ 106,12 106,12 @@ class FeedPage extends StatelessWidget {
ListTileGroup([
MyListTile(
title: Text(S.of(context).swipeRight),
- trailing: Text(swipeOptons[feedsModel.swipeR]),
+ trailing: Text(swipeOptons[feedsModel.swipeR]!),
onTap: () { _openGestureOptions(context, true); },
),
MyListTile(
title: Text(S.of(context).swipeLeft),
- trailing: Text(swipeOptons[feedsModel.swipeL]),
+ trailing: Text(swipeOptons[feedsModel.swipeL]!),
onTap: () { _openGestureOptions(context, false); },
withDivider: false,
),
M lib/pages/settings/general_page.dart => lib/pages/settings/general_page.dart +17 -2
@@ 2,6 2,7 @@ import 'package:fluent_reader_lite/components/list_tile_group.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/models/global_model.dart';
+import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/utils/colors.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
@@ 15,7 16,7 @@ class GeneralPage extends StatefulWidget {
class _GeneralPageState extends State<GeneralPage> {
bool _clearingCache = false;
- double textScale;
+ double? textScale;
void _clearCache() async {
setState(() {
@@ 57,7 58,7 @@ class _GeneralPageState extends State<GeneralPage> {
min: 0.5,
max: 1.5,
divisions: 8,
- value: textScale ?? globalModel.textScale,
+ value: textScale ?? globalModel.textScale ?? 1.0,
onChanged: (v) {
setState(() {
textScale = v;
@@ 139,6 140,19 @@ class _GeneralPageState extends State<GeneralPage> {
},
title: S.of(context).theme,
);
+ final openTargetItems = ListTileGroup.fromOptions(
+ [
+ Tuple2(S.of(context).rssText, SourceOpenTarget.Local),
+ Tuple2(S.of(context).loadFull, SourceOpenTarget.FullContent),
+ Tuple2(S.of(context).loadWebpage, SourceOpenTarget.Webpage),
+ Tuple2(S.of(context).openExternal, SourceOpenTarget.External),
+ ],
+ globalModel.globalOpenTarget,
+ (v) {
+ globalModel.globalOpenTarget = v;
+ },
+ title: S.of(context).openTarget,
+ );
final localeItems = ListTileGroup.fromOptions(
[
Tuple2(S.of(context).followSystem, null),
@@ 161,6 175,7 @@ class _GeneralPageState extends State<GeneralPage> {
return ListView(
children: [
syncItems,
+ openTargetItems,
textScaleItems,
storageItems,
themeItems,
M lib/pages/settings/services/feedbin_page.dart => lib/pages/settings/services/feedbin_page.dart +12 -12
@@ 32,16 32,16 @@ class _FeedbinPageState extends State<FeedbinPage> {
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
- ServiceImport import = ModalRoute.of(context).settings.arguments;
+ final import = ModalRoute.of(context)!.settings.arguments as ServiceImport?;
if (import == null) return;
if (Utils.testUrl(import.endpoint)) {
- setState(() { _endpoint = import.endpoint; });
+ setState(() { _endpoint = import.endpoint!; });
}
if (Utils.notEmpty(import.username)) {
- setState(() { _username = import.username; });
+ setState(() { _username = import.username!; });
}
if (Utils.notEmpty(import.password)) {
- final bytes = base64.decode(import.password);
+ final bytes = base64.decode(import.password!);
final password = utf8.decode(bytes);
setState(() { _password = password; });
}
@@ 49,9 49,9 @@ class _FeedbinPageState extends State<FeedbinPage> {
}
void _editEndpoint() async {
- final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).endpoint,
+ S.of(context).endpoint,
Utils.testUrl,
initialValue: _endpoint,
inputType: TextInputType.url,
@@ 66,9 66,9 @@ class _FeedbinPageState extends State<FeedbinPage> {
}
void _editUsername() async {
- final String username = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? username = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).username,
+ S.of(context).username,
Utils.notEmpty,
initialValue: _username,
),
@@ 78,9 78,9 @@ class _FeedbinPageState extends State<FeedbinPage> {
}
void _editPassword() async {
- final String password = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? password = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).password,
+ S.of(context).password,
Utils.notEmpty,
inputType: TextInputType.visiblePassword,
),
@@ 123,7 123,7 @@ class _FeedbinPageState extends State<FeedbinPage> {
}
void _logOut() async {
- final bool confirmed = await showCupertinoDialog(
+ final bool? confirmed = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(S.of(context).logOutWarning),
@@ 145,7 145,7 @@ class _FeedbinPageState extends State<FeedbinPage> {
],
),
);
- if (confirmed != null) {
+ if (confirmed == true) {
setState(() { _validating = true; });
DialogHelper().show(
context,
M lib/pages/settings/services/fever_page.dart => lib/pages/settings/services/fever_page.dart +12 -12
@@ 25,7 25,7 @@ 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? _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;
@@ 34,13 34,13 @@ class _FeverPageState extends State<FeverPage> {
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
- ServiceImport import = ModalRoute.of(context).settings.arguments;
+ final import = ModalRoute.of(context)!.settings.arguments as ServiceImport?;
if (import == null) return;
if (Utils.testUrl(import.endpoint)) {
- setState(() { _endpoint = import.endpoint; });
+ setState(() { _endpoint = import.endpoint!; });
}
if (Utils.notEmpty(import.username)) {
- setState(() { _username = import.username; });
+ setState(() { _username = import.username!; });
}
if (Utils.notEmpty(import.apiKey)) {
setState(() { _apiKey = import.apiKey; });
@@ 49,9 49,9 @@ class _FeverPageState extends State<FeverPage> {
}
void _editEndpoint() async {
- final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).endpoint,
+ S.of(context).endpoint,
Utils.testUrl,
initialValue: _endpoint,
inputType: TextInputType.url,
@@ 62,9 62,9 @@ class _FeverPageState extends State<FeverPage> {
}
void _editUsername() async {
- final String username = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? username = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).username,
+ S.of(context).username,
Utils.notEmpty,
initialValue: _username,
),
@@ 77,9 77,9 @@ class _FeverPageState extends State<FeverPage> {
}
void _editPassword() async {
- final String password = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? password = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).password,
+ S.of(context).password,
Utils.notEmpty,
inputType: TextInputType.visiblePassword,
),
@@ 127,7 127,7 @@ class _FeverPageState extends State<FeverPage> {
}
void _logOut() async {
- final bool confirmed = await showCupertinoDialog(
+ final bool? confirmed = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(S.of(context).logOutWarning),
@@ 149,7 149,7 @@ class _FeverPageState extends State<FeverPage> {
],
),
);
- if (confirmed != null) {
+ if (confirmed == true) {
setState(() { _validating = true; });
DialogHelper().show(
context,
M lib/pages/settings/services/greader_page.dart => lib/pages/settings/services/greader_page.dart +12 -12
@@ 33,16 33,16 @@ class _GReaderPageState extends State<GReaderPage> {
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
- ServiceImport import = ModalRoute.of(context).settings.arguments;
+ final import = ModalRoute.of(context)!.settings.arguments as ServiceImport?;
if (import == null) return;
if (Utils.testUrl(import.endpoint)) {
- setState(() { _endpoint = import.endpoint; });
+ setState(() { _endpoint = import.endpoint!; });
}
if (Utils.notEmpty(import.username)) {
- setState(() { _username = import.username; });
+ setState(() { _username = import.username!; });
}
if (Utils.notEmpty(import.password)) {
- final bytes = base64.decode(import.password);
+ final bytes = base64.decode(import.password!);
final password = utf8.decode(bytes);
setState(() { _password = password; });
}
@@ 50,9 50,9 @@ class _GReaderPageState extends State<GReaderPage> {
}
void _editEndpoint() async {
- final String endpoint = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? endpoint = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).endpoint,
+ S.of(context).endpoint,
Utils.testUrl,
initialValue: _endpoint,
inputType: TextInputType.url,
@@ 67,9 67,9 @@ class _GReaderPageState extends State<GReaderPage> {
}
void _editUsername() async {
- final String username = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? username = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).username,
+ S.of(context).username,
Utils.notEmpty,
initialValue: _username,
),
@@ 79,9 79,9 @@ class _GReaderPageState extends State<GReaderPage> {
}
void _editPassword() async {
- final String password = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? password = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).password,
+ S.of(context).password,
Utils.notEmpty,
inputType: TextInputType.visiblePassword,
),
@@ 127,7 127,7 @@ class _GReaderPageState extends State<GReaderPage> {
}
void _logOut() async {
- final bool confirmed = await showCupertinoDialog(
+ final bool? confirmed = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(S.of(context).logOutWarning),
@@ 149,7 149,7 @@ class _GReaderPageState extends State<GReaderPage> {
],
),
);
- if (confirmed != null) {
+ if (confirmed == true) {
setState(() { _validating = true; });
DialogHelper().show(
context,
M lib/pages/settings/services/inoreader_page.dart => lib/pages/settings/services/inoreader_page.dart +20 -17
@@ 44,32 44,32 @@ class _InoreaderPageState extends State<InoreaderPage> {
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
- ServiceImport import = ModalRoute.of(context).settings.arguments;
+ final import = ModalRoute.of(context)!.settings.arguments as ServiceImport?;
if (import == null) return;
if (_endpointOptions.contains(import.endpoint)) {
- setState(() { _endpoint = import.endpoint; });
+ setState(() { _endpoint = import.endpoint!; });
}
if (Utils.notEmpty(import.username)) {
- setState(() { _username = import.username; });
+ setState(() { _username = import.username!; });
}
if (Utils.notEmpty(import.password)) {
- final bytes = base64.decode(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; });
+ setState(() { _apiId = import.apiId!; });
}
if (Utils.notEmpty(import.apiKey)) {
- setState(() { _apiKey = import.apiKey; });
+ setState(() { _apiKey = import.apiKey!; });
}
});
}
void _editUsername() async {
- final String username = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? username = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).username,
+ S.of(context).username,
Utils.notEmpty,
initialValue: _username,
),
@@ 79,9 79,9 @@ class _InoreaderPageState extends State<InoreaderPage> {
}
void _editPassword() async {
- final String password = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? password = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- S.of(context).password,
+ S.of(context).password,
Utils.notEmpty,
inputType: TextInputType.visiblePassword,
),
@@ 91,9 91,9 @@ class _InoreaderPageState extends State<InoreaderPage> {
}
void _editAPIId() async {
- final String apiId = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? apiId = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- "API ID",
+ "API ID",
Utils.notEmpty,
initialValue: _apiId,
inputType: TextInputType.number,
@@ 104,9 104,9 @@ class _InoreaderPageState extends State<InoreaderPage> {
}
void _editAPIKey() async {
- final String apiKey = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? apiKey = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
- "API Key",
+ "API Key",
Utils.notEmpty,
initialValue: _apiKey,
),
@@ 156,7 156,7 @@ class _InoreaderPageState extends State<InoreaderPage> {
}
void _logOut() async {
- final bool confirmed = await showCupertinoDialog(
+ final bool? confirmed = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(S.of(context).logOutWarning),
@@ 178,7 178,7 @@ class _InoreaderPageState extends State<InoreaderPage> {
],
),
);
- if (confirmed != null) {
+ if (confirmed == true) {
setState(() { _validating = true; });
DialogHelper().show(
context,
@@ 193,7 193,10 @@ class _InoreaderPageState extends State<InoreaderPage> {
}
void _getKey() {
- launch(_endpoint + "/all_articles#preferences-developer", forceSafariVC: false, forceWebView: false);
+ launchUrl(
+ Uri.parse(_endpoint + "/all_articles#preferences-developer"),
+ mode: LaunchMode.externalApplication,
+ );
}
@override
M lib/pages/settings/source_edit_page.dart => lib/pages/settings/source_edit_page.dart +6 -5
@@ 14,7 14,7 @@ import 'package:tuple/tuple.dart';
class SourceEditPage extends StatelessWidget {
void _editName(BuildContext context, RSSSource source) async {
- final String name = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? name = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
S.of(context).name,
(v) => v.trim().length > 0,
@@ 28,7 28,7 @@ class SourceEditPage extends StatelessWidget {
}
void _editIcon(BuildContext context, RSSSource source) async {
- final String iconUrl = await Navigator.of(context).push(CupertinoPageRoute(
+ final String? iconUrl = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
S.of(context).icon,
(v) async {
@@ 36,7 36,7 @@ class SourceEditPage extends StatelessWidget {
if (trimmed.length == 0) return false;
return await Utils.validateFavicon(trimmed);
},
- initialValue: source.iconUrl,
+ initialValue: source.iconUrl ?? "",
),
));
if (iconUrl == null || iconUrl == source.iconUrl) return;
@@ 47,9 47,9 @@ class SourceEditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final String sid = ModalRoute.of(context).settings.arguments;
+ final String sid = ModalRoute.of(context)!.settings.arguments as String;
return Selector<SourcesModel, RSSSource>(
- selector: (context, sourcesModel) => sourcesModel.getSource(sid),
+ selector: (context, sourcesModel) => sourcesModel.getSource(sid)!,
builder: (context, source, child) {
final urlStyle = TextStyle(
color: CupertinoColors.secondaryLabel.resolveFrom(context),
@@ 79,6 79,7 @@ class SourceEditPage extends StatelessWidget {
], title: S.of(context).edit);
final openTarget = ListTileGroup.fromOptions(
[
+ Tuple2(S.of(context).inheritDefault, SourceOpenTarget.Inherit),
Tuple2(S.of(context).rssText, SourceOpenTarget.Local),
Tuple2(S.of(context).loadFull, SourceOpenTarget.FullContent),
Tuple2(S.of(context).loadWebpage, SourceOpenTarget.Webpage),
M lib/pages/settings/text_editor_page.dart => lib/pages/settings/text_editor_page.dart +10 -10
@@ 8,13 8,13 @@ import 'package:flutter/cupertino.dart';
class TextEditorPage extends StatefulWidget {
final String title;
- final String saveText;
+ final String? saveText;
final String initialValue;
- final Color navigationBarColor;
+ final Color? navigationBarColor;
final FutureOr<bool> Function(String) validate;
- final TextInputType inputType;
+ final TextInputType? inputType;
final bool autocorrect;
- final List<String> suggestions;
+ final List<String>? suggestions;
TextEditorPage(
this.title,
@@ 22,11 22,11 @@ class TextEditorPage extends StatefulWidget {
{
this.navigationBarColor,
this.saveText,
- this.initialValue: "",
+ this.initialValue = "",
this.inputType,
- this.autocorrect: false,
+ this.autocorrect = false,
this.suggestions,
- Key key,
+ Key? key,
})
: super(key: key);
@@ 35,7 35,7 @@ class TextEditorPage extends StatefulWidget {
}
class _TextEditorPage extends State<TextEditorPage> {
- TextEditingController _controller;
+ late TextEditingController _controller;
bool _validating = false;
@override
@@ 68,7 68,7 @@ class _TextEditorPage extends State<TextEditorPage> {
);
}
}
-
+
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
@@ 100,7 100,7 @@ class _TextEditorPage extends State<TextEditorPage> {
enableSuggestions: widget.autocorrect,
),
]),
- if (widget.suggestions != null) ...widget.suggestions.map((s) {
+ if (widget.suggestions != null) ...widget.suggestions!.map((s) {
return MyListTile(
title: Flexible(child: Text(
s,
M lib/pages/subscription_list_page.dart => lib/pages/subscription_list_page.dart +183 -16
@@ 1,6 1,7 @@
import 'package:fluent_reader_lite/components/badge.dart';
import 'package:fluent_reader_lite/components/mark_all_action_sheet.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
+import 'package:fluent_reader_lite/components/responsive_action_sheet.dart';
import 'package:fluent_reader_lite/components/subscription_item.dart';
import 'package:fluent_reader_lite/components/sync_control.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
@@ 9,6 10,7 @@ import 'package:fluent_reader_lite/models/sources_model.dart';
import 'package:fluent_reader_lite/models/sync_model.dart';
import 'package:fluent_reader_lite/pages/group_list_page.dart';
import 'package:fluent_reader_lite/pages/home_page.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';
@@ 19,10 21,17 @@ import 'package:intl/intl.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:provider/provider.dart';
+enum SubscriptionSortType {
+ ByLatest,
+ ByNameAsc,
+ ByNameDesc,
+ ByUnread,
+}
+
class SubscriptionListPage extends StatefulWidget {
final ScrollTopNotifier scrollTopNotifier;
- SubscriptionListPage(this.scrollTopNotifier, {Key key}) : super(key: key);
+ SubscriptionListPage(this.scrollTopNotifier, {Key? key}) : super(key: key);
@override
_SubscriptionListPageState createState() {
@@ 31,10 40,13 @@ class SubscriptionListPage extends StatefulWidget {
}
class _SubscriptionListPageState extends State<SubscriptionListPage> {
- List<String> sids;
- String title;
+ List<String>? sids;
+ String? title;
bool transitioning = false;
bool unreadOnly = Store.sp.getBool(StoreKeys.UNREAD_SUBS_ONLY) ?? false;
+ String _search = "";
+ SubscriptionSortType _sortType = SubscriptionSortType.values[
+ Store.sp.getInt(StoreKeys.SUBSCRIPTION_SORT) ?? 0];
void _onScrollTop() {
if (widget.scrollTopNotifier.index == 1 &&
@@ 60,7 72,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
}
void _openGroups() async {
- List<String> result;
+ List<String>? result;
if (Global.isTablet) {
result = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => GroupListPage(),
@@ 90,7 102,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
});
} else {
setState(() {
- title = result[0];
+ title = result![0];
sids = Global.groupsModel.groups[title];
});
}
@@ 105,7 117,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
showCupertinoModalPopup(
context: context,
builder: (context) =>
- MarkAllActionSheet(sids == null ? {} : Set.from(sids)),
+ MarkAllActionSheet(sids == null ? {} : Set.from(sids!)),
);
}
@@ 136,6 148,129 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
}
}
+ static const _iconPadding = Padding(padding: EdgeInsets.only(left: 24));
+
+ void _setSortType(SubscriptionSortType type) {
+ setState(() {
+ _sortType = type;
+ });
+ _onScrollTop();
+ Store.sp.setInt(StoreKeys.SUBSCRIPTION_SORT, type.index);
+ }
+
+ void _editSearchKeyword() async {
+ String? keyword = await Navigator.of(context).push(CupertinoPageRoute(
+ builder: (context) => TextEditorPage(
+ S.of(context).editKeyword,
+ (v) => v.trim().length > 0,
+ saveText: S.of(context).search,
+ initialValue: _search,
+ navigationBarColor: CupertinoColors.systemBackground,
+ autocorrect: true,
+ ),
+ ));
+ if (keyword == null) return;
+ setState(() {
+ _search = keyword;
+ });
+ _onScrollTop();
+ }
+
+ void _openFilterModal() {
+ showCupertinoModalPopup(
+ context: context,
+ builder: (context) {
+ final sheet = CupertinoActionSheet(
+ title: Text(S.of(context).sortBy),
+ actions: [
+ CupertinoActionSheetAction(
+ child: Row(children: [
+ Icon(CupertinoIcons.time),
+ Text(S.of(context).sortByLatest),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ _setSortType(SubscriptionSortType.ByLatest);
+ },
+ ),
+ CupertinoActionSheetAction(
+ child: Row(children: [
+ Icon(CupertinoIcons.sort_up),
+ Text(S.of(context).sortByNameAsc),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ _setSortType(SubscriptionSortType.ByNameAsc);
+ },
+ ),
+ CupertinoActionSheetAction(
+ child: Row(children: [
+ Icon(CupertinoIcons.sort_down),
+ Text(S.of(context).sortByNameDesc),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ _setSortType(SubscriptionSortType.ByNameDesc);
+ },
+ ),
+ CupertinoActionSheetAction(
+ child: Row(children: [
+ Icon(Icons.radio_button_checked),
+ Text(S.of(context).sortByUnread),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ _setSortType(SubscriptionSortType.ByUnread);
+ },
+ ),
+ CupertinoActionSheetAction(
+ isDestructiveAction: true,
+ child: Row(children: [
+ Icon(CupertinoIcons.search,
+ color: CupertinoColors.destructiveRed),
+ Text(_search.length > 0
+ ? S.of(context).editKeyword
+ : S.of(context).search),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ _editSearchKeyword();
+ },
+ ),
+ if (_search.length > 0)
+ CupertinoActionSheetAction(
+ isDestructiveAction: true,
+ child: Row(children: [
+ Icon(CupertinoIcons.clear_fill,
+ color: CupertinoColors.destructiveRed),
+ Text(S.of(context).clearSearch),
+ _iconPadding,
+ ], mainAxisAlignment: MainAxisAlignment.spaceBetween),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ setState(() {
+ _search = "";
+ });
+ _onScrollTop();
+ },
+ ),
+ ],
+ cancelButton: CupertinoActionSheetAction(
+ child: Text(S.of(context).cancel),
+ onPressed: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ },
+ ),
+ );
+ return ResponsiveActionSheet(sheet);
+ });
+ }
+
Widget _buildUnreadTip() {
return SliverToBoxAdapter(
child: Container(
@@ 202,6 337,11 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
padding: EdgeInsets.only(left: 4),
child: Icon(Icons.radio_button_checked, size: 18),
),
+ if (_search.length > 0)
+ Padding(
+ padding: EdgeInsets.only(left: 4),
+ child: Icon(CupertinoIcons.search, size: 18),
+ ),
],
),
);
@@ 227,6 367,18 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(
+ (_sortType != SubscriptionSortType.ByLatest ||
+ _search.length > 0)
+ ? CupertinoIcons
+ .line_horizontal_3_decrease_circle_fill
+ : CupertinoIcons.line_horizontal_3_decrease_circle,
+ semanticLabel: S.of(context).sortBy,
+ ),
+ onPressed: _openFilterModal,
+ ),
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ child: Icon(
CupertinoIcons.checkmark_circle,
semanticLabel: S.of(context).markAll,
),
@@ 253,17 405,32 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
}
} else {
sources = [];
- for (var sid in sids) {
+ for (var sid in sids!) {
final source = Global.sourcesModel.getSource(sid);
- if (!unreadOnly || source.unreadCount > 0) {
+ if (source != null && (!unreadOnly || source.unreadCount > 0)) {
sources.add(source);
}
}
}
- // Latest sources first
- sources.sort((a, b) {
- return b.latest.compareTo(a.latest);
- });
+ if (_search.length > 0) {
+ final keyword = _search.toUpperCase();
+ sources =
+ sources.where((s) => s.name.toUpperCase().contains(keyword)).toList();
+ }
+ switch (_sortType) {
+ case SubscriptionSortType.ByLatest:
+ sources.sort((a, b) => b.latest.compareTo(a.latest));
+ break;
+ case SubscriptionSortType.ByNameAsc:
+ sources.sort((a, b) => a.name.compareTo(b.name));
+ break;
+ case SubscriptionSortType.ByNameDesc:
+ sources.sort((a, b) => b.name.compareTo(a.name));
+ break;
+ case SubscriptionSortType.ByUnread:
+ sources.sort((a, b) => b.unreadCount.compareTo(a.unreadCount));
+ break;
+ }
return SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
var source = sources[index];
@@ 309,19 476,19 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
navigationBar,
SyncControl(),
if (Global.sourcesModel.showUnreadTip) _buildUnreadTip(),
- if (sids != null && sids.length > 0)
+ if (sids != null && sids!.length > 0)
Consumer<SourcesModel>(
builder: (context, sourcesModel, child) {
- var count = sids
+ var count = sids!
.map((sid) => sourcesModel.getSource(sid))
- .fold(0, (c, s) => c + s.unreadCount);
+ .fold(0, (c, s) => c + (s?.unreadCount ?? 0));
return SliverToBoxAdapter(
child: MyListTile(
title: Text(S.of(context).allArticles),
trailing: count > 0 ? Badge(count) : null,
trailingChevron: false,
onTap: () async {
- await Global.feedsModel.initSourcesFeed(sids.toList());
+ await Global.feedsModel.initSourcesFeed(sids!.toList());
Navigator.of(context).pushNamed("/feed", arguments: title);
},
background: CupertinoColors.systemBackground,
M lib/utils/db.dart => lib/utils/db.dart +4 -4
@@ 7,13 7,13 @@ abstract class DatabaseHelper {
static final _dbName = "frlite.db";
static final _dbVersion = 1;
- static Database _database;
+ static Database? _database;
static Future<Database> getDatabase() async {
- if (_database != null) return _database;
+ if (_database != null) return _database!;
String path = join(await getDatabasesPath(), _dbName);
_database = await openDatabase(path, version:_dbVersion, onCreate: _onCreate);
- return _database;
+ return _database!;
}
static Future<void> _onCreate(Database db, int version) async {
@@ 45,4 45,4 @@ abstract class DatabaseHelper {
''');
await db.execute("CREATE INDEX itemsDate ON items (date DESC);");
}
-}>
\ No newline at end of file
+}
M lib/utils/global.dart => lib/utils/global.dart +27 -14
@@ 5,6 5,7 @@ import 'package:fluent_reader_lite/models/global_model.dart';
import 'package:fluent_reader_lite/models/groups_model.dart';
import 'package:fluent_reader_lite/models/items_model.dart';
import 'package:fluent_reader_lite/models/service.dart';
+import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/models/services/feedbin.dart';
import 'package:fluent_reader_lite/models/services/fever.dart';
import 'package:fluent_reader_lite/models/services/greader.dart';
@@ 20,15 21,15 @@ import 'package:sqflite/sqflite.dart';
abstract class Global {
static bool _initialized = false;
- static GlobalModel globalModel;
- static SourcesModel sourcesModel;
- static ItemsModel itemsModel;
- static FeedsModel feedsModel;
- static GroupsModel groupsModel;
- static SyncModel syncModel;
- static ServiceHandler service;
- static Database db;
- static Jaguar server;
+ static late GlobalModel globalModel;
+ static late SourcesModel sourcesModel;
+ static late ItemsModel itemsModel;
+ static late FeedsModel feedsModel;
+ static late GroupsModel groupsModel;
+ static late SyncModel syncModel;
+ static ServiceHandler? service;
+ static late Database db;
+ static Jaguar? server;
static final GlobalKey<NavigatorState> tabletPanel = GlobalKey();
static void init() {
@@ 70,11 71,11 @@ abstract class Global {
],
);
server = Jaguar(address: "127.0.0.1",port: 9000);
- server.addRoute(serveFlutterAssets());
+ server!.addRoute(serveFlutterAssets());
// Serve custom font files from app documents directory
final fontsDir = await FontManager.getFontsDirectory();
- server.get('/custom-fonts/:filename*', (ctx) async {
+ server!.get('/custom-fonts/:filename*', (ctx) async {
final filename = ctx.pathParams['filename'];
if (filename == null || filename.isEmpty) {
return Response(statusCode: 404);
@@ 97,7 98,7 @@ abstract class Global {
return ByteResponse(body: bytes, mimeType: mimeType);
});
- await server.serve();
+ await server!.serve();
await sourcesModel.init();
await feedsModel.all.init();
if (globalModel.syncOnStart) await syncModel.syncWithService();
@@ 107,11 108,23 @@ abstract class Global {
return globalModel.getBrightness() ?? MediaQuery.of(context).platformBrightness;
}
+ static SourceOpenTarget resolveOpenTarget(RSSSource source) {
+ var groupName = groupsModel.findGroupForSource(source.id);
+ var groupTarget = groupName != null
+ ? groupsModel.getGroupOpenTarget(groupName)
+ : SourceOpenTarget.Inherit;
+ return resolveOpenTargetCascade(
+ source.openTarget,
+ groupTarget,
+ globalModel.globalOpenTarget,
+ );
+ }
+
static bool get isTablet => tabletPanel.currentWidget != null;
static NavigatorState responsiveNavigator(BuildContext context) {
return tabletPanel.currentWidget != null
- ? Global.tabletPanel.currentState
+ ? Global.tabletPanel.currentState!
: Navigator.of(context, rootNavigator: true);
}
-}>
\ No newline at end of file
+}
M lib/utils/store.dart => lib/utils/store.dart +46 -12
@@ 1,6 1,7 @@
import 'dart:convert';
import 'package:fluent_reader_lite/models/global_model.dart';
+import 'package:fluent_reader_lite/models/source.dart';
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ 9,7 10,8 @@ abstract class StoreKeys {
static const ERROR_LOG = "errorLog";
static const UNCATEGORIZED = "uncategorized";
static const UNREAD_SUBS_ONLY = "unreadSubsOnly";
-
+ static const SUBSCRIPTION_SORT = "subSort";
+
// General
static const THEME = "theme";
static const LOCALE = "locale";
@@ 49,15 51,20 @@ abstract class StoreKeys {
static const AUTH = "auth";
static const USE_INT_64 = "useInt64";
static const INOREADER_REMOVE_AD = "inoRemoveAd";
+
+ // Default open target
+ static const GLOBAL_OPEN_TARGET = "globalOpenTarget";
+ static const GROUP_OPEN_TARGETS = "groupOpenTargets";
}
class Store {
// Initialized in main.dart
- static SharedPreferences sp;
+ static late SharedPreferences sp;
- static Locale getLocale() {
+ static Locale? getLocale() {
if (!sp.containsKey(StoreKeys.LOCALE)) return null;
var localeString = sp.getString(StoreKeys.LOCALE);
+ if (localeString == null) return null;
var splitted = localeString.split('_');
if (splitted.length > 1) {
return Locale(splitted[0], splitted[1]);
@@ 66,14 73,14 @@ class Store {
}
}
- static void setLocale(Locale locale) {
+ static void setLocale(Locale? locale) {
if (locale == null) sp.remove(StoreKeys.LOCALE);
else sp.setString(StoreKeys.LOCALE, locale.toString());
}
static ThemeSetting getTheme() {
- return sp.containsKey(StoreKeys.THEME)
- ? ThemeSetting.values[sp.getInt(StoreKeys.THEME)]
+ return sp.containsKey(StoreKeys.THEME)
+ ? ThemeSetting.values[sp.getInt(StoreKeys.THEME)!]
: ThemeSetting.Default;
}
@@ 96,14 103,14 @@ class Store {
sp.setString(StoreKeys.GROUPS, jsonEncode(groups));
}
- static List<String> getUncategorized() {
+ static List<String>? getUncategorized() {
final stored = sp.getString(StoreKeys.UNCATEGORIZED);
if (stored == null) return null;
final parsed = jsonDecode(stored);
return List.castFrom(parsed);
}
- static void setUncategorized(List<String> value) {
+ static void setUncategorized(List<String>? value) {
if (value == null) {
sp.remove(StoreKeys.UNCATEGORIZED);
} else {
@@ 114,7 121,7 @@ class Store {
static int getArticleFontSize() {
return sp.getInt(StoreKeys.ARTICLE_FONT_SIZE) ?? 16;
}
-
+
static void setArticleFontSize(int value) {
sp.setInt(StoreKeys.ARTICLE_FONT_SIZE, value);
}
@@ 135,15 142,42 @@ class Store {
sp.setString(StoreKeys.FONT_FAMILY, value);
}
- static String getCustomFontPath() {
+ static String? getCustomFontPath() {
return sp.getString(StoreKeys.CUSTOM_FONT_PATH);
}
- static void setCustomFontPath(String value) {
+ static void setCustomFontPath(String? value) {
if (value == null) {
sp.remove(StoreKeys.CUSTOM_FONT_PATH);
} else {
sp.setString(StoreKeys.CUSTOM_FONT_PATH, value);
}
}
-}>
\ No newline at end of file
+
+ static SourceOpenTarget getGlobalOpenTarget() {
+ var idx = sp.getInt(StoreKeys.GLOBAL_OPEN_TARGET);
+ if (idx == null || idx >= SourceOpenTarget.values.length) {
+ return SourceOpenTarget.Local;
+ }
+ return SourceOpenTarget.values[idx];
+ }
+
+ static void setGlobalOpenTarget(SourceOpenTarget target) {
+ sp.setInt(StoreKeys.GLOBAL_OPEN_TARGET, target.index);
+ }
+
+ static Map<String, int> getGroupOpenTargets() {
+ var stored = sp.getString(StoreKeys.GROUP_OPEN_TARGETS);
+ if (stored == null) return {};
+ Map<String, int> result = {};
+ var parsed = jsonDecode(stored);
+ for (var key in parsed.keys) {
+ result[key] = parsed[key] as int;
+ }
+ return result;
+ }
+
+ static void setGroupOpenTargets(Map<String, int> targets) {
+ sp.setString(StoreKeys.GROUP_OPEN_TARGETS, jsonEncode(targets));
+ }
+}
M lib/utils/utils.dart => lib/utils/utils.dart +3 -3
@@ 8,7 8,7 @@ abstract class Utils {
static const syncMaxId = 9007199254740991;
static void openExternal(String url) {
- launch(url, forceSafariVC: false, forceWebView: false);
+ launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}
static int binarySearch<T>(
@@ 48,10 48,10 @@ 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) =>
+ static bool testUrl(String? url) =>
url != null && _urlRegex.hasMatch(url.trim());
- static bool notEmpty(String text) => text != null && 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 +284 -172
@@ 1,14 1,46 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ app_links:
+ dependency: "direct main"
+ description:
+ name: app_links
+ sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.1"
+ app_links_linux:
+ dependency: transitive
+ description:
+ name: app_links_linux
+ sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
+ app_links_platform_interface:
+ dependency: transitive
+ description:
+ name: app_links_platform_interface
+ sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ app_links_web:
+ dependency: transitive
+ description:
+ name: app_links_web
+ sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
async:
dependency: transitive
description:
name: async
- sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
+ sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
- version: "2.10.0"
+ version: "2.13.0"
auth_header:
dependency: transitive
description:
@@ 21,42 53,42 @@ packages:
dependency: transitive
description:
name: boolean_selector
- sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "2.1.2"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
- sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
+ sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
url: "https://pub.dev"
source: hosted
- version: "3.2.3"
+ version: "3.3.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
- sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
+ sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "4.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
- sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
+ sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "1.2.0"
characters:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
clock:
dependency: transitive
description:
@@ 65,6 97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
+ code_assets:
+ dependency: transitive
+ description:
+ name: code_assets
+ sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
collection:
dependency: transitive
description:
@@ 73,30 113,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.5+2"
crypto:
dependency: "direct main"
description:
name: crypto
- sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+ sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
- version: "3.0.3"
+ version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
- sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
+ sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
- version: "1.0.0"
+ version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
- sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
- version: "1.0.6"
+ version: "1.0.8"
fake_async:
dependency: transitive
description:
@@ 109,39 157,39 @@ packages:
dependency: transitive
description:
name: ffi
- sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
+ sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
- version: "2.0.2"
+ version: "2.2.0"
file:
dependency: transitive
description:
name: file
- sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
- version: "6.1.4"
+ version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
- sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf"
+ sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
- version: "5.3.1"
+ version: "8.3.7"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
- flutter_blurhash:
- dependency: transitive
- description:
- name: flutter_blurhash
- sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
- url: "https://pub.dev"
- source: hosted
- version: "0.7.0"
flutter_cache_manager:
dependency: "direct main"
description:
@@ 159,10 207,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
- sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
+ sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
- version: "2.0.30"
+ version: "2.0.33"
flutter_test:
dependency: "direct dev"
description: flutter
@@ 173,14 221,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
+ gtk:
+ dependency: transitive
+ description:
+ name: gtk
+ sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ hooks:
+ dependency: transitive
+ description:
+ name: hooks
+ sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
html:
dependency: "direct main"
description:
name: html
- sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
+ sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
- version: "0.15.4"
+ version: "0.15.6"
http:
dependency: "direct main"
description:
@@ 193,10 265,10 @@ packages:
dependency: transitive
description:
name: http_parser
- sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
- version: "4.0.2"
+ version: "4.1.2"
http_server:
dependency: transitive
description:
@@ 217,10 289,10 @@ packages:
dependency: "direct main"
description:
name: jaguar
- sha256: "1614ea947a81f2160fd6f962c5bff0b3f11eb6e5417d47140d447f4758a7f164"
+ sha256: "07d8203fb1432c4228e434e64a2b6b7e03eb4c585ef024de05e3ddf517f29471"
url: "https://pub.dev"
source: hosted
- version: "3.1.3"
+ version: "3.1.4"
jaguar_common:
dependency: transitive
description:
@@ 265,10 337,10 @@ packages:
dependency: transitive
description:
name: logging
- sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+ sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.3.0"
lpinyin:
dependency: "direct main"
description:
@@ 281,42 353,50 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
- sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
+ sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
- version: "1.0.4"
+ version: "1.0.6"
modal_bottom_sheet:
dependency: "direct main"
description:
name: modal_bottom_sheet
- sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f"
+ sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0"
+ native_toolchain_c:
+ dependency: transitive
+ description:
+ name: native_toolchain_c
+ sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
- version: "3.0.0-pre"
+ version: "0.17.4"
nested:
dependency: transitive
description:
@@ 325,14 405,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
+ objective_c:
+ dependency: transitive
+ description:
+ name: objective_c
+ sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.3.0"
octo_image:
dependency: transitive
description:
name: octo_image
- sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
+ sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "2.1.0"
overlay_dialog:
dependency: "direct main"
description:
@@ 345,10 433,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
- sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a"
+ sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
url: "https://pub.dev"
source: hosted
- version: "4.1.0"
+ version: "4.2.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ 369,26 457,26 @@ packages:
dependency: "direct main"
description:
name: path_provider
- sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
+ sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
- sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
+ sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
- sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
+ sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ 401,18 489,18 @@ packages:
dependency: transitive
description:
name: path_provider_platform_interface
- sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
- sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.3.0"
path_tree:
dependency: transitive
description:
@@ 425,26 513,34 @@ packages:
dependency: transitive
description:
name: platform
- sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
+ sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
- version: "3.1.2"
+ version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
- sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
- version: "2.1.6"
+ version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
- sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
+ sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.5+1"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
- version: "6.0.5"
+ version: "2.2.0"
responsive_builder:
dependency: "direct main"
description:
@@ 461,70 557,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.27.7"
- share:
+ share_plus:
dependency: "direct main"
description:
- name: share
- sha256: "97e6403f564ed1051a01534c2fc919cb6e40ea55e60a18ec23cee6e0ce19f4be"
+ name: share_plus
+ sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
url: "https://pub.dev"
source: hosted
- version: "2.0.4"
+ version: "7.2.2"
+ share_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: share_plus_platform_interface
+ sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.4.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
- sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
+ sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
- sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
+ sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.4.20"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
- sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
+ sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
- version: "2.3.4"
+ version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
- sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
+ sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
- sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
+ sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
- sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
+ sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
- sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
+ sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@@ 534,34 638,50 @@ packages:
dependency: transitive
description:
name: source_span
- sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
- url: "https://pub.dev"
- source: hosted
- version: "1.9.1"
- sprintf:
- dependency: transitive
- description:
- name: sprintf
- sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
- version: "7.0.0"
+ version: "1.10.2"
sqflite:
dependency: "direct main"
description:
name: sqflite
- sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
+ sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
- version: "2.2.8+4"
+ version: "2.4.2"
+ sqflite_android:
+ dependency: transitive
+ description:
+ name: sqflite_android
+ sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2+2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
- sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f"
+ sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.6"
+ sqflite_darwin:
+ dependency: transitive
+ description:
+ name: sqflite_darwin
+ sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
- version: "2.4.5+1"
+ version: "2.4.2"
+ sqflite_platform_interface:
+ dependency: transitive
+ description:
+ name: sqflite_platform_interface
+ sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.0"
stack_trace:
dependency: transitive
description:
@@ 582,34 702,34 @@ packages:
dependency: transitive
description:
name: string_scanner
- sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
- sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
+ sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
- version: "3.1.0"
+ version: "3.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
- sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
- version: "1.2.1"
+ version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
- version: "0.7.6"
+ version: "0.7.9"
tuple:
dependency: "direct main"
description:
@@ 622,114 742,90 @@ packages:
dependency: transitive
description:
name: typed_data
- sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
- url: "https://pub.dev"
- source: hosted
- version: "1.3.2"
- uni_links:
- dependency: "direct main"
- description:
- name: uni_links
- sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e"
- url: "https://pub.dev"
- source: hosted
- version: "0.5.1"
- uni_links_platform_interface:
- dependency: transitive
- description:
- name: uni_links_platform_interface
- sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507"
- url: "https://pub.dev"
- source: hosted
- version: "1.0.0"
- uni_links_web:
- dependency: transitive
- description:
- name: uni_links_web
- sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df"
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
- version: "0.1.0"
+ version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
- sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d"
+ sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.3.1"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
- sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
- version: "6.1.11"
+ version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
- sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
+ sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
- version: "6.1.0"
+ version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
- sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
+ sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
- version: "6.1.5"
+ version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
- sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
- version: "3.0.6"
+ version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
- sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
- version: "3.0.7"
+ version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
- sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
- version: "2.1.5"
+ version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
- sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2
+ sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
- version: "2.0.19"
+ version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
- sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
- version: "3.0.8"
+ version: "3.1.5"
uuid:
dependency: transitive
description:
name: uuid
- sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134
+ sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
- version: "4.0.0"
+ version: "4.5.2"
vector_math:
dependency: transitive
description:
@@ 746,54 842,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
- sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88"
+ sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9
url: "https://pub.dev"
source: hosted
- version: "3.0.4"
+ version: "4.13.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
- sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd"
+ sha256: eeeb3fcd5f0ff9f8446c9f4bbc18a99b809e40297528a3395597d03aafb9f510
url: "https://pub.dev"
source: hosted
- version: "2.10.4"
+ version: "4.10.11"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
- sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf"
+ sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
url: "https://pub.dev"
source: hosted
- version: "1.9.5"
+ version: "2.14.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
- sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0
+ sha256: "0412b657a2828fb301e73509909e6ec02b77cd2b441ae9f77125d482b3ddf0e7"
url: "https://pub.dev"
source: hosted
- version: "2.9.5"
+ version: "3.23.6"
win32:
dependency: transitive
description:
name: win32
- sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
+ sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
- version: "4.1.4"
+ version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
- sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
- version: "1.0.3"
+ version: "1.1.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
- dart: ">=3.8.0-0 <4.0.0"
- flutter: ">=3.29.0"
+ dart: ">=3.10.3 <4.0.0"
+ flutter: ">=3.38.4"
M pubspec.yaml => pubspec.yaml +5 -5
@@ 31,23 31,23 @@ dependencies:
intl: 0.20.2
http: ^0.13.4
html: ^0.15.0
- webview_flutter: ^3.0.4
+ webview_flutter: ^4.0.0
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
+ share_plus: ^7.0.0
package_info_plus: ^4.1.0
crypto: ^3.0.1
responsive_builder: ^0.4.1
- cached_network_image: ^3.2.1
+ cached_network_image: ^3.3.1
flutter_cache_manager: ^3.3.0
lpinyin: ^2.0.3
- uni_links: ^0.5.1
+ app_links: ^6.0.0
modal_bottom_sheet: ^3.0.0-pre
overlay_dialog: ^0.2.0
- file_picker: ^5.0.0
+ file_picker: ^8.0.4
path_provider: ^2.0.0