Refactor app update

This commit is contained in:
problematicconsumer
2023-09-20 22:38:38 +03:30
parent 59365653b8
commit 8958c72fe6
11 changed files with 210 additions and 23 deletions

View File

@@ -195,6 +195,7 @@
"termsAndConditions": "Terms and conditions"
},
"appUpdate": {
"notAvailableMsg": "Already using the latest version",
"dialogTitle": "Update Available",
"updateMsg": "A new version of @:general.appTitle is available. Would you like to update now?",
"currentVersionLbl": "Current Version",

View File

@@ -195,6 +195,7 @@
"termsAndConditions": "شرایط و ضوابط استفاده"
},
"appUpdate": {
"notAvailableMsg": "نسخه جدیدی یافت نشد",
"dialogTitle": "نسخه جدید موجود است",
"updateMsg": "نسخه جدیدی از @:general.appTitle موجود است! الان بروزرسانی شود؟",
"currentVersionLbl": "نسخه فعلی",

View File

@@ -135,6 +135,7 @@ Future<void> initAppServices(
await Future.wait(
[
read(singboxServiceProvider).init(),
read(cronServiceProvider).startScheduler(),
],
);
_logger.debug('initialized app services');

View File

@@ -74,6 +74,23 @@ class EnableAnalytics extends _$EnableAnalytics {
}
}
@Riverpod(keepAlive: true)
class CheckForPreReleaseUpdates extends _$CheckForPreReleaseUpdates {
late final _pref = Pref(
ref.watch(sharedPreferencesProvider),
"check_for_pre_release_updates",
true,
);
@override
bool build() => _pref.getValue();
Future<void> update(bool value) {
state = value;
return _pref.update(value);
}
}
@Riverpod(keepAlive: true)
class DebugModeNotifier extends _$DebugModeNotifier {
late final _pref = Pref(

View File

@@ -22,16 +22,19 @@ class AboutPage extends HookConsumerWidget {
ref.listen(
appUpdateNotifierProvider,
(_, next) async {
if (!context.mounted) return;
switch (next) {
case AsyncData(value: final remoteVersion?):
await NewVersionDialog(
case AppUpdateStateAvailable(:final versionInfo):
return NewVersionDialog(
appInfo.presentVersion,
remoteVersion,
versionInfo,
canIgnore: false,
).show(context);
case AsyncError(:final error):
if (!context.mounted) return;
CustomToast.error(t.printError(error)).show(context);
case AppUpdateStateError(:final error):
return CustomToast.error(t.printError(error)).show(context);
case AppUpdateStateNotAvailable():
return CustomToast.success(t.appUpdate.notAvailableMsg)
.show(context);
}
},
);
@@ -91,15 +94,18 @@ class AboutPage extends HookConsumerWidget {
if (appInfo.release.allowCustomUpdateChecker)
ListTile(
title: Text(t.about.checkForUpdate),
trailing: appUpdate.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(),
)
: const Icon(Icons.update),
onTap: () {
ref.invalidate(appUpdateNotifierProvider);
trailing: switch (appUpdate) {
AppUpdateStateChecking() => const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(),
),
_ => const Icon(Icons.update),
},
onTap: () async {
await ref
.read(appUpdateNotifierProvider.notifier)
.check();
},
),
ListTile(

View File

@@ -1,43 +1,90 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/router/routes/routes.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/app/app.dart';
import 'package:hiddify/features/common/new_version_dialog.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_update_notifier.freezed.dart';
part 'app_update_notifier.g.dart';
@freezed
class AppUpdateState with _$AppUpdateState {
const factory AppUpdateState.initial() = AppUpdateStateInitial;
const factory AppUpdateState.disabled() = AppUpdateStateDisabled;
const factory AppUpdateState.checking() = AppUpdateStateChecking;
const factory AppUpdateState.error(AppFailure error) = AppUpdateStateError;
const factory AppUpdateState.available(RemoteVersionInfo versionInfo) =
AppUpdateStateAvailable;
const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable;
}
@Riverpod(keepAlive: true)
class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
@override
Future<RemoteVersionInfo?> build() async {
AppUpdateState build() {
_schedule();
return const AppUpdateState.initial();
}
Future<AppUpdateState> check() async {
loggy.debug("checking for update");
state = const AppUpdateState.checking();
final appInfo = ref.watch(appInfoProvider);
// TODO use market-specific update checkers
if (!appInfo.release.allowCustomUpdateChecker) {
loggy.debug(
"custom update checkers are not allowed for [${appInfo.release.name}] release",
);
return null;
return state = const AppUpdateState.disabled();
}
final currentVersion = appInfo.version;
return ref
.watch(appRepositoryProvider)
.getLatestVersion(includePreReleases: true)
.getLatestVersion(
includePreReleases: ref.read(checkForPreReleaseUpdatesProvider),
)
.match(
(l) {
loggy.warning("failed to get latest version, $l");
throw l;
(err) {
loggy.warning("failed to get latest version, $err");
return state = AppUpdateState.error(err);
},
(remote) {
if (remote.version.compareTo(currentVersion) > 0) {
loggy.info("new version available: $remote");
return remote;
return state = AppUpdateState.available(remote);
}
loggy.info(
"already using latest version[$currentVersion], remote: $remote",
);
return null;
return state = const AppUpdateState.notAvailable();
},
).run();
}
void _schedule() {
loggy.debug("scheduling app update checker");
ref.watch(cronServiceProvider).schedule(
key: 'app_update',
duration: const Duration(hours: 4),
callback: () async {
await Future.delayed(const Duration(seconds: 5));
final updateState = await check();
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted) {
if (updateState
case AppUpdateStateAvailable(:final versionInfo)) {
await NewVersionDialog(
ref.read(appInfoProvider).presentVersion,
versionInfo,
).show(context);
}
}
},
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:hiddify/features/common/app_update_notifier.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/logs/notifier/notifier.dart';
@@ -20,6 +21,11 @@ void commonControllers(CommonControllersRef ref) {
(previous, next) {},
fireImmediately: true,
);
ref.listen(
appUpdateNotifierProvider,
(previous, next) {},
fireImmediately: true,
);
if (PlatformUtils.isDesktop) {
ref.listen(
windowControllerProvider,

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:neat_periodic_task/neat_periodic_task.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _cronKeyPrefix = "cron_";
typedef Job<T> = (
String key,
Duration duration,
FutureOr<T?> Function() callback,
);
class CronService with InfraLogger {
CronService(this.prefs);
final SharedPreferences prefs;
NeatPeriodicTaskScheduler? _scheduler;
Map<String, Job> jobs = {};
void schedule<T>({
required String key,
required Duration duration,
required FutureOr<T?> Function() callback,
}) {
loggy.debug("scheduling [$key]");
jobs[key] = (key, duration, callback);
_scheduler?.trigger();
}
Future<void> run(Job job) async {
final key = job.$1;
final prefKey = "$_cronKeyPrefix$key";
final previousRunTime = DateTime.tryParse(prefs.getString(prefKey) ?? "");
loggy.debug(
"[$key] > ${previousRunTime == null ? "first run" : "previous run on [$previousRunTime]"}",
);
if (previousRunTime != null &&
previousRunTime.add(job.$2) > DateTime.now()) {
loggy.debug("[$key] > didn't meet criteria");
return;
}
final result = await job.$3();
await prefs.setString(prefKey, DateTime.now().toIso8601String());
return result;
}
Future<void> startScheduler() async {
loggy.debug("starting job scheduler");
await _scheduler?.stop();
int runCount = 0;
_scheduler = NeatPeriodicTaskScheduler(
name: "cron job scheduler",
interval: const Duration(minutes: 5),
timeout: const Duration(seconds: 15),
minCycle: const Duration(minutes: 1),
task: () {
loggy.debug("in run ${runCount++}");
return Future.wait(jobs.values.map(run));
},
);
_scheduler!.start();
}
Future<void> stopScheduler() async {
loggy.debug("stopping job scheduler");
return _scheduler?.stop();
}
}

View File

@@ -1,3 +1,5 @@
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/services/cron_service.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/services/platform_settings.dart';
import 'package:hiddify/services/singbox/singbox_service.dart';
@@ -15,3 +17,10 @@ SingboxService singboxService(SingboxServiceRef ref) => SingboxService();
@riverpod
PlatformSettings platformSettings(PlatformSettingsRef ref) =>
PlatformSettings();
@Riverpod(keepAlive: true)
CronService cronService(CronServiceRef ref) {
final service = CronService(ref.watch(sharedPreferencesProvider));
ref.onDispose(() => service.stopScheduler());
return service;
}

View File

@@ -821,6 +821,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
neat_periodic_task:
dependency: "direct main"
description:
name: neat_periodic_task
sha256: e0dda74c996781e154f6145028dbacbcd9dbef242f5a140fa774e39381c2bf97
url: "https://pub.dev"
source: hosted
version: "2.0.1"
package_config:
dependency: transitive
description:
@@ -1013,6 +1021,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
retry:
dependency: transitive
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
riverpod:
dependency: transitive
description:
@@ -1234,6 +1250,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.12"
slugid:
dependency: transitive
description:
name: slugid
sha256: e0cc54637b666c9c590f0d76df76e5e2bbf6234ae398a182aac82fd70ddd60ab
url: "https://pub.dev"
source: hosted
version: "1.1.2"
source_gen:
dependency: transitive
description:

View File

@@ -71,6 +71,7 @@ dependencies:
uuid: ^3.0.7
tint: ^2.0.1
accessibility_tools: ^1.0.0
neat_periodic_task: ^2.0.1
# widgets
go_router: ^10.1.2