mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Add follow updates feature. Close #189
This commit is contained in:
@@ -338,7 +338,22 @@
|
|||||||
"Descending": "降序",
|
"Descending": "降序",
|
||||||
"Last Reading: Chapter @ep Page @page": "上次阅读: 第 @ep 章 第 @page 页",
|
"Last Reading: Chapter @ep Page @page": "上次阅读: 第 @ep 章 第 @page 页",
|
||||||
"Last Reading: Page @page": "上次阅读: 第 @page 页",
|
"Last Reading: Page @page": "上次阅读: 第 @page 页",
|
||||||
"Replies": "回复"
|
"Replies": "回复",
|
||||||
|
"Follow Updates": "追更",
|
||||||
|
"Not Configured": "未配置",
|
||||||
|
"Choose a folder to follow updates." : "选择一个文件夹以追更",
|
||||||
|
"Choose Folder": "选择文件夹",
|
||||||
|
"No folders available": "没有可用的文件夹",
|
||||||
|
"Updating comics...": "更新漫画中...",
|
||||||
|
"Automatic update checking enabled." : "已启用自动更新检查",
|
||||||
|
"The app will check for updates at most once a day." : "APP将每天最多检查一次更新",
|
||||||
|
"Change Folder": "更改文件夹",
|
||||||
|
"Check Now": "立即检查",
|
||||||
|
"Updates": "更新",
|
||||||
|
"No updates found": "未找到更新",
|
||||||
|
"All Comics": "全部漫画",
|
||||||
|
"The comic will be marked as no updates as soon as you read it.": "漫画将在您阅读后立即标记为无更新",
|
||||||
|
"Disable": "禁用"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -679,6 +694,21 @@
|
|||||||
"Descending": "降序",
|
"Descending": "降序",
|
||||||
"Last Reading: Chapter @ep Page @page": "上次閱讀: 第 @ep 章 第 @page 頁",
|
"Last Reading: Chapter @ep Page @page": "上次閱讀: 第 @ep 章 第 @page 頁",
|
||||||
"Last Reading: Page @page": "上次閱讀: 第 @page 頁",
|
"Last Reading: Page @page": "上次閱讀: 第 @page 頁",
|
||||||
"Replies": "回覆"
|
"Replies": "回覆",
|
||||||
|
"Follow Updates": "追更",
|
||||||
|
"Not Configured": "未配置",
|
||||||
|
"Choose a folder to follow updates." : "選擇一個文件夾以追更",
|
||||||
|
"Choose Folder": "選擇文件夾",
|
||||||
|
"No folders available": "沒有可用的文件夾",
|
||||||
|
"Updating comics...": "更新漫畫中...",
|
||||||
|
"Automatic update checking enabled." : "已啟用自動更新檢查",
|
||||||
|
"The app will check for updates at most once a day." : "APP將每天最多檢查一次更新",
|
||||||
|
"Change Folder": "更改文件夾",
|
||||||
|
"Check Now": "立即檢查",
|
||||||
|
"Updates": "更新",
|
||||||
|
"No updates found": "未找到更新",
|
||||||
|
"All Comics": "全部漫畫",
|
||||||
|
"The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新",
|
||||||
|
"Disable": "禁用"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -159,6 +159,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
||||||
'preloadImageCount': 4,
|
'preloadImageCount': 4,
|
||||||
|
'followUpdatesFolder': null,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -295,6 +295,41 @@ class ComicDetails with HistoryMixin {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _validateUpdateTime(String time) {
|
||||||
|
time = time.split(" ").first;
|
||||||
|
var segments = time.split("-");
|
||||||
|
if (segments.length != 3) return null;
|
||||||
|
var year = int.tryParse(segments[0]);
|
||||||
|
var month = int.tryParse(segments[1]);
|
||||||
|
var day = int.tryParse(segments[2]);
|
||||||
|
if (year == null || month == null || day == null) return null;
|
||||||
|
if (year < 2000 || year > 3000) return null;
|
||||||
|
if (month < 1 || month > 12) return null;
|
||||||
|
if (day < 1 || day > 31) return null;
|
||||||
|
return "$year-$month-$day";
|
||||||
|
}
|
||||||
|
|
||||||
|
String? findUpdateTime() {
|
||||||
|
if (updateTime != null) {
|
||||||
|
return _validateUpdateTime(updateTime!);
|
||||||
|
}
|
||||||
|
const acceptedNamespaces = [
|
||||||
|
"更新",
|
||||||
|
"最後更新",
|
||||||
|
"最后更新",
|
||||||
|
"update",
|
||||||
|
"last update",
|
||||||
|
];
|
||||||
|
for (var entry in tags.entries) {
|
||||||
|
if (acceptedNamespaces.contains(entry.key.toLowerCase()) &&
|
||||||
|
entry.value.isNotEmpty) {
|
||||||
|
var value = entry.value.first;
|
||||||
|
return _validateUpdateTime(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArchiveInfo {
|
class ArchiveInfo {
|
||||||
|
@@ -6,6 +6,7 @@ import 'package:venera/foundation/appdata.dart';
|
|||||||
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
|
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/pages/follow_updates_page.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@@ -154,6 +155,38 @@ class FavoriteItemWithFolderInfo extends FavoriteItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FavoriteItemWithUpdateInfo extends FavoriteItem {
|
||||||
|
String? updateTime;
|
||||||
|
|
||||||
|
DateTime? lastCheckTime;
|
||||||
|
|
||||||
|
bool hasNewUpdate;
|
||||||
|
|
||||||
|
FavoriteItemWithUpdateInfo(
|
||||||
|
FavoriteItem item,
|
||||||
|
this.updateTime,
|
||||||
|
this.hasNewUpdate,
|
||||||
|
int? lastCheckTime,
|
||||||
|
) : lastCheckTime = lastCheckTime == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(lastCheckTime),
|
||||||
|
super(
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
coverPath: item.coverPath,
|
||||||
|
author: item.author,
|
||||||
|
type: item.type,
|
||||||
|
tags: item.tags,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description {
|
||||||
|
var updateTime = this.updateTime ?? "Unknown";
|
||||||
|
var sourceName = type.comicSource?.name ?? "Unknown";
|
||||||
|
return "$updateTime | $sourceName";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LocalFavoritesManager with ChangeNotifier {
|
class LocalFavoritesManager with ChangeNotifier {
|
||||||
factory LocalFavoritesManager() =>
|
factory LocalFavoritesManager() =>
|
||||||
cache ?? (cache = LocalFavoritesManager._create());
|
cache ?? (cache = LocalFavoritesManager._create());
|
||||||
@@ -599,6 +632,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_modifiedAfterLastCache = true;
|
_modifiedAfterLastCache = true;
|
||||||
|
var followUpdatesFolder = appdata.settings['followUpdatesFolder'];
|
||||||
for (final folder in folderNames) {
|
for (final folder in folderNames) {
|
||||||
var rows = _db.select("""
|
var rows = _db.select("""
|
||||||
select * from "$folder"
|
select * from "$folder"
|
||||||
@@ -627,9 +661,13 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
UPDATE "$folder"
|
UPDATE "$folder"
|
||||||
SET
|
SET
|
||||||
$updateLocationSql
|
$updateLocationSql
|
||||||
|
${followUpdatesFolder == folder ? "has_new_update = 0," : ""}
|
||||||
time = ?
|
time = ?
|
||||||
WHERE id == ?;
|
WHERE id == ? and type == ?;
|
||||||
""", [newTime, id]);
|
""", [newTime, id, type.value]);
|
||||||
|
if (followUpdatesFolder == folder) {
|
||||||
|
updateFollowUpdatesUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -783,6 +821,114 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void prepareTableForFollowUpdates(String table) {
|
||||||
|
// check if the table has the column "last_update_time" "has_new_update" "last_check_time"
|
||||||
|
var columns = _db.select("""
|
||||||
|
pragma table_info("$table");
|
||||||
|
""");
|
||||||
|
if (!columns.any((element) => element["name"] == "last_update_time")) {
|
||||||
|
_db.execute("""
|
||||||
|
alter table "$table"
|
||||||
|
add column last_update_time TEXT;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
if (!columns.any((element) => element["name"] == "has_new_update")) {
|
||||||
|
_db.execute("""
|
||||||
|
alter table "$table"
|
||||||
|
add column has_new_update int;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
_db.execute("""
|
||||||
|
update "$table"
|
||||||
|
set has_new_update = 0;
|
||||||
|
""");
|
||||||
|
if (!columns.any((element) => element["name"] == "last_check_time")) {
|
||||||
|
_db.execute("""
|
||||||
|
alter table "$table"
|
||||||
|
add column last_check_time int;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateUpdateTime(
|
||||||
|
String folder,
|
||||||
|
String id,
|
||||||
|
ComicType type,
|
||||||
|
String updateTime,
|
||||||
|
) {
|
||||||
|
var oldTime = _db.select("""
|
||||||
|
select last_update_time from "$folder"
|
||||||
|
where id == ? and type == ?;
|
||||||
|
""", [id, type.value]).first['last_update_time'];
|
||||||
|
var hasNewUpdate = oldTime != updateTime;
|
||||||
|
_db.execute("""
|
||||||
|
update "$folder"
|
||||||
|
set last_update_time = ?, has_new_update = ?, last_check_time = ?
|
||||||
|
where id == ? and type == ?;
|
||||||
|
""", [
|
||||||
|
updateTime,
|
||||||
|
hasNewUpdate ? 1 : 0,
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
id,
|
||||||
|
type.value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int countUpdates(String folder) {
|
||||||
|
return _db.select("""
|
||||||
|
select count(*) as c from "$folder"
|
||||||
|
where has_new_update == 1;
|
||||||
|
""").first['c'];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FavoriteItemWithUpdateInfo> getUpdates(String folder) {
|
||||||
|
if (!existsFolder(folder)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var res = _db.select("""
|
||||||
|
select * from "$folder"
|
||||||
|
where has_new_update == 1;
|
||||||
|
""");
|
||||||
|
return res
|
||||||
|
.map(
|
||||||
|
(e) => FavoriteItemWithUpdateInfo(
|
||||||
|
FavoriteItem.fromRow(e),
|
||||||
|
e['last_update_time'],
|
||||||
|
e['has_new_update'] == 1,
|
||||||
|
e['last_check_time'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FavoriteItemWithUpdateInfo> getComicsWithUpdatesInfo(String folder) {
|
||||||
|
if (!existsFolder(folder)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var res = _db.select("""
|
||||||
|
select * from "$folder";
|
||||||
|
""");
|
||||||
|
return res
|
||||||
|
.map(
|
||||||
|
(e) => FavoriteItemWithUpdateInfo(
|
||||||
|
FavoriteItem.fromRow(e),
|
||||||
|
e['last_update_time'],
|
||||||
|
e['has_new_update'] == 1,
|
||||||
|
e['last_check_time'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void markAsRead(String folder, String id, ComicType type) {
|
||||||
|
var folder = appdata.settings['followUpdatesFolder'];
|
||||||
|
_db.execute("""
|
||||||
|
update "$folder"
|
||||||
|
set has_new_update = 0
|
||||||
|
where id == ? and type == ?;
|
||||||
|
""", [id, type.value]);
|
||||||
|
}
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
_db.dispose();
|
_db.dispose();
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,9 @@ import 'package:venera/foundation/js_engine.dart';
|
|||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
|
import 'package:venera/pages/follow_updates_page.dart';
|
||||||
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
@@ -55,3 +58,23 @@ Future<void> init() async {
|
|||||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkAppUpdates() async {
|
||||||
|
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
|
||||||
|
var now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (now - lastCheck < 24 * 60 * 60 * 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appdata.implicitData['lastCheckUpdate'] = now;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
ComicSourcePage.checkComicSourceUpdate();
|
||||||
|
if (appdata.settings['checkUpdateOnStart']) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
await checkUpdateUi(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkUpdates() {
|
||||||
|
_checkAppUpdates();
|
||||||
|
FollowUpdatesService.initChecker();
|
||||||
|
}
|
||||||
|
@@ -62,6 +62,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
App.registerForceRebuild(forceRebuild);
|
App.registerForceRebuild(forceRebuild);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
checkUpdates();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
665
lib/pages/follow_updates_page.dart
Normal file
665
lib/pages/follow_updates_page.dart
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
import '../foundation/global_state.dart';
|
||||||
|
|
||||||
|
class FollowUpdatesWidget extends StatefulWidget {
|
||||||
|
const FollowUpdatesWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FollowUpdatesWidget> createState() => _FollowUpdatesWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FollowUpdatesWidgetState
|
||||||
|
extends AutomaticGlobalState<FollowUpdatesWidget> {
|
||||||
|
int _count = 0;
|
||||||
|
|
||||||
|
String? get folder => appdata.settings["followUpdatesFolder"];
|
||||||
|
|
||||||
|
void getCount() {
|
||||||
|
if (folder == null) {
|
||||||
|
_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!LocalFavoritesManager().folderNames.contains(folder)) {
|
||||||
|
_count = 0;
|
||||||
|
appdata.settings["followUpdatesFolder"] = null;
|
||||||
|
Future.microtask(() {
|
||||||
|
appdata.saveData();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_count = LocalFavoritesManager().countUpdates(folder!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCount() {
|
||||||
|
setState(() {
|
||||||
|
getCount();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => FollowUpdatesPage());
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text('Follow Updates'.tl, style: ts.s18),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(Icons.arrow_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
if (_count > 0)
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16, left: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'@c updates'.tlParams({
|
||||||
|
'c': _count,
|
||||||
|
}),
|
||||||
|
style: ts.s16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? get key => 'FollowUpdatesWidget';
|
||||||
|
}
|
||||||
|
|
||||||
|
class FollowUpdatesPage extends StatefulWidget {
|
||||||
|
const FollowUpdatesPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FollowUpdatesPage> createState() => _FollowUpdatesPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
||||||
|
String? get folder => appdata.settings["followUpdatesFolder"];
|
||||||
|
|
||||||
|
var updatedComics = <FavoriteItemWithUpdateInfo>[];
|
||||||
|
var allComics = <FavoriteItemWithUpdateInfo>[];
|
||||||
|
|
||||||
|
/// Sort comics by update time in descending order with nulls at the end.
|
||||||
|
void sortComics() {
|
||||||
|
allComics.sort((a, b) {
|
||||||
|
if (a.updateTime == null && b.updateTime == null) {
|
||||||
|
return 0;
|
||||||
|
} else if (a.updateTime == null) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.updateTime == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return b.updateTime!.compareTo(a.updateTime!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (folder != null) {
|
||||||
|
allComics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder!);
|
||||||
|
sortComics();
|
||||||
|
updatedComics = allComics.where((c) => c.hasNewUpdate).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text('Follow Updates'.tl)),
|
||||||
|
if (folder == null)
|
||||||
|
buildNotConfigured(context)
|
||||||
|
else
|
||||||
|
buildConfigured(context),
|
||||||
|
SliverPadding(padding: const EdgeInsets.only(top: 8)),
|
||||||
|
buildUpdatedComics(),
|
||||||
|
buildAllComics(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNotConfigured(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.info_outline),
|
||||||
|
title: Text("Not Configured".tl),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Choose a folder to follow updates.".tl,
|
||||||
|
style: ts.s16,
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: showSelector,
|
||||||
|
child: Text("Choose Folder".tl),
|
||||||
|
).paddingHorizontal(16).toAlign(Alignment.centerRight),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildConfigured(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.stars_outlined),
|
||||||
|
title: Text(folder!),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Automatic update checking enabled.".tl,
|
||||||
|
style: ts.s14,
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
Text(
|
||||||
|
"The app will check for updates at most once a day.".tl,
|
||||||
|
style: ts.s14,
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: showSelector,
|
||||||
|
child: Text("Change Folder".tl),
|
||||||
|
),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: checkNow,
|
||||||
|
child: Text("Check Now".tl),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildUpdatedComics() {
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.update),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"Updates".tl,
|
||||||
|
style: ts.s18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (updatedComics.isNotEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
"The comic will be marked as no updates as soon as you read it."
|
||||||
|
.tl)
|
||||||
|
.paddingHorizontal(16)
|
||||||
|
.paddingVertical(4),
|
||||||
|
),
|
||||||
|
if (updatedComics.isNotEmpty)
|
||||||
|
SliverGridComics(comics: updatedComics)
|
||||||
|
else
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"No updates found".tl,
|
||||||
|
style: ts.s16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAllComics() {
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.list),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"All Comics".tl,
|
||||||
|
style: ts.s18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverGridComics(comics: allComics),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSelector() {
|
||||||
|
var folders = LocalFavoritesManager().folderNames;
|
||||||
|
if (folders.isEmpty) {
|
||||||
|
context.showMessage(message: "No folders available".tl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String? selectedFolder;
|
||||||
|
showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: "Choose Folder".tl,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text("Folder".tl),
|
||||||
|
trailing: Select(
|
||||||
|
minWidth: 120,
|
||||||
|
current: selectedFolder,
|
||||||
|
values: folders,
|
||||||
|
onTap: (i) {
|
||||||
|
setState(() {
|
||||||
|
selectedFolder = folders[i];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (appdata.settings["followUpdatesFolder"] != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
disable();
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text("Disable".tl),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: selectedFolder == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
context.pop();
|
||||||
|
setFolder(selectedFolder!);
|
||||||
|
},
|
||||||
|
child: Text("Confirm".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
appdata.settings["followUpdatesFolder"] = null;
|
||||||
|
appdata.saveData();
|
||||||
|
updateFollowUpdatesUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFolder(String folder) async {
|
||||||
|
FollowUpdatesService.cancelChecking?.call();
|
||||||
|
LocalFavoritesManager().prepareTableForFollowUpdates(folder);
|
||||||
|
|
||||||
|
var count = LocalFavoritesManager().count(folder);
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
bool isCanceled = false;
|
||||||
|
void onCancel() {
|
||||||
|
isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadingController = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
withProgress: true,
|
||||||
|
cancelButtonText: "Cancel".tl,
|
||||||
|
onCancel: onCancel,
|
||||||
|
message: "Updating comics...".tl,
|
||||||
|
);
|
||||||
|
|
||||||
|
await for (var progress in _updateFolder(folder, true)) {
|
||||||
|
if (isCanceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingController.setProgress(progress.current / progress.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
appdata.settings["followUpdatesFolder"] = folder;
|
||||||
|
updatedComics = [];
|
||||||
|
allComics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||||
|
sortComics();
|
||||||
|
});
|
||||||
|
appdata.saveData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkNow() async {
|
||||||
|
FollowUpdatesService.cancelChecking?.call();
|
||||||
|
|
||||||
|
bool isCanceled = false;
|
||||||
|
void onCancel() {
|
||||||
|
isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadingController = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
withProgress: true,
|
||||||
|
cancelButtonText: "Cancel".tl,
|
||||||
|
onCancel: onCancel,
|
||||||
|
message: "Updating comics...".tl,
|
||||||
|
);
|
||||||
|
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
await for (var progress in _updateFolder(folder!, true)) {
|
||||||
|
if (isCanceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingController.setProgress(progress.current / progress.total);
|
||||||
|
updated = progress.updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingController.close();
|
||||||
|
|
||||||
|
if (updated > 0) {
|
||||||
|
GlobalState.findOrNull<_FollowUpdatesWidgetState>()?.updateCount();
|
||||||
|
updateComics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateComics() {
|
||||||
|
if (folder == null) {
|
||||||
|
setState(() {
|
||||||
|
allComics = [];
|
||||||
|
updatedComics = [];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
allComics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder!);
|
||||||
|
sortComics();
|
||||||
|
updatedComics = allComics.where((c) => c.hasNewUpdate).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? get key => 'FollowUpdatesPage';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpdateProgress {
|
||||||
|
final int total;
|
||||||
|
final int current;
|
||||||
|
final int errors;
|
||||||
|
final int updated;
|
||||||
|
|
||||||
|
_UpdateProgress(this.total, this.current, this.errors, this.updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateFolderBase(
|
||||||
|
String folder,
|
||||||
|
StreamController<_UpdateProgress> stream,
|
||||||
|
bool ignoreCheckTime,
|
||||||
|
) async {
|
||||||
|
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||||
|
int current = 0;
|
||||||
|
int errors = 0;
|
||||||
|
int updated = 0;
|
||||||
|
var futures = <Future>[];
|
||||||
|
const maxConcurrent = 5;
|
||||||
|
|
||||||
|
for (int i = 0; i < comics.length; i++) {
|
||||||
|
if (stream.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ignoreCheckTime) {
|
||||||
|
var lastCheckTime = comics[i].lastCheckTime;
|
||||||
|
if (lastCheckTime != null &&
|
||||||
|
DateTime.now().difference(lastCheckTime).inDays < 1) {
|
||||||
|
current++;
|
||||||
|
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (futures.length >= maxConcurrent) {
|
||||||
|
await Future.any(futures);
|
||||||
|
}
|
||||||
|
|
||||||
|
var future = () async {
|
||||||
|
int retries = 3;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
var c = comics[i];
|
||||||
|
var comicSource = c.type.comicSource;
|
||||||
|
if (comicSource == null) return;
|
||||||
|
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||||
|
|
||||||
|
var newTags = <String>[];
|
||||||
|
for (var entry in newInfo.tags.entries) {
|
||||||
|
const shouldIgnore = ['author', 'artist', 'time'];
|
||||||
|
var namespace = entry.key;
|
||||||
|
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (var tag in entry.value) {
|
||||||
|
newTags.add("$namespace:$tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = FavoriteItem(
|
||||||
|
id: c.id,
|
||||||
|
name: newInfo.title,
|
||||||
|
coverPath: newInfo.cover,
|
||||||
|
author: newInfo.subTitle ??
|
||||||
|
newInfo.tags['author']?.firstOrNull ??
|
||||||
|
c.author,
|
||||||
|
type: c.type,
|
||||||
|
tags: newTags,
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalFavoritesManager().updateInfo(folder, item);
|
||||||
|
|
||||||
|
var updateTime = newInfo.findUpdateTime();
|
||||||
|
if (updateTime != null && updateTime != c.updateTime) {
|
||||||
|
LocalFavoritesManager().updateUpdateTime(
|
||||||
|
folder,
|
||||||
|
c.id,
|
||||||
|
c.type,
|
||||||
|
updateTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updated++;
|
||||||
|
return;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Check Updates", e, s);
|
||||||
|
retries--;
|
||||||
|
if (retries == 0) {
|
||||||
|
errors++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
current++;
|
||||||
|
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
future.then((_) {
|
||||||
|
futures.remove(future);
|
||||||
|
});
|
||||||
|
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait(futures);
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) {
|
||||||
|
var stream = StreamController<_UpdateProgress>();
|
||||||
|
_updateFolderBase(folder, stream, ignoreCheckTime);
|
||||||
|
return stream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Background service for checking updates
|
||||||
|
abstract class FollowUpdatesService {
|
||||||
|
static bool isChecking = false;
|
||||||
|
|
||||||
|
static void Function()? cancelChecking;
|
||||||
|
|
||||||
|
static void check() async {
|
||||||
|
if (isChecking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var folder = appdata.settings["followUpdatesFolder"];
|
||||||
|
if (folder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool isCanceled = false;
|
||||||
|
cancelChecking = () {
|
||||||
|
isCanceled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
isChecking = true;
|
||||||
|
int updated = 0;
|
||||||
|
try {
|
||||||
|
await for (var progress in _updateFolder(folder, false)) {
|
||||||
|
if (isCanceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updated = progress.updated;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cancelChecking = null;
|
||||||
|
isChecking = false;
|
||||||
|
if (updated > 0) {
|
||||||
|
updateFollowUpdatesUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initChecker() {
|
||||||
|
Timer.periodic(const Duration(hours: 1), (timer) {
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFollowUpdatesUI() {
|
||||||
|
GlobalState.findOrNull<_FollowUpdatesWidgetState>()?.updateCount();
|
||||||
|
GlobalState.findOrNull<_FollowUpdatesPageState>()?.updateComics();
|
||||||
|
}
|
@@ -12,6 +12,7 @@ import 'package:venera/foundation/log.dart';
|
|||||||
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
|
import 'package:venera/pages/follow_updates_page.dart';
|
||||||
import 'package:venera/pages/history_page.dart';
|
import 'package:venera/pages/history_page.dart';
|
||||||
import 'package:venera/pages/image_favorites_page/image_favorites_page.dart';
|
import 'package:venera/pages/image_favorites_page/image_favorites_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
@@ -34,6 +35,7 @@ class HomePage extends StatelessWidget {
|
|||||||
const _SyncDataWidget(),
|
const _SyncDataWidget(),
|
||||||
const _History(),
|
const _History(),
|
||||||
const _Local(),
|
const _Local(),
|
||||||
|
const FollowUpdatesWidget(),
|
||||||
const _ComicSourceWidget(),
|
const _ComicSourceWidget(),
|
||||||
const ImageFavorites(),
|
const ImageFavorites(),
|
||||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
|
||||||
import 'package:venera/pages/categories_page.dart';
|
import 'package:venera/pages/categories_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
@@ -7,7 +6,6 @@ import 'package:venera/utils/translations.dart';
|
|||||||
|
|
||||||
import '../components/components.dart';
|
import '../components/components.dart';
|
||||||
import '../foundation/app.dart';
|
import '../foundation/app.dart';
|
||||||
import 'comic_source_page.dart';
|
|
||||||
import 'explore_page.dart';
|
import 'explore_page.dart';
|
||||||
import 'favorites/favorites_page.dart';
|
import 'favorites/favorites_page.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
@@ -36,24 +34,8 @@ class _MainPageState extends State<MainPage> {
|
|||||||
_navigatorKey!.currentContext!.pop();
|
_navigatorKey!.currentContext!.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkUpdates() async {
|
|
||||||
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
|
|
||||||
var now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
if (now - lastCheck < 24 * 60 * 60 * 1000) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appdata.implicitData['lastCheckUpdate'] = now;
|
|
||||||
appdata.writeImplicitData();
|
|
||||||
ComicSourcePage.checkComicSourceUpdate();
|
|
||||||
if (appdata.settings['checkUpdateOnStart']) {
|
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
|
||||||
await checkUpdateUi(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
checkUpdates();
|
|
||||||
_observer = NaviObserver();
|
_observer = NaviObserver();
|
||||||
_navigatorKey = GlobalKey();
|
_navigatorKey = GlobalKey();
|
||||||
App.mainNavigatorKey = _navigatorKey;
|
App.mainNavigatorKey = _navigatorKey;
|
||||||
|
Reference in New Issue
Block a user