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

View File

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

View File

@@ -135,6 +135,7 @@ Future<void> initAppServices(
await Future.wait( await Future.wait(
[ [
read(singboxServiceProvider).init(), read(singboxServiceProvider).init(),
read(cronServiceProvider).startScheduler(),
], ],
); );
_logger.debug('initialized app services'); _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) @Riverpod(keepAlive: true)
class DebugModeNotifier extends _$DebugModeNotifier { class DebugModeNotifier extends _$DebugModeNotifier {
late final _pref = Pref( late final _pref = Pref(

View File

@@ -22,16 +22,19 @@ class AboutPage extends HookConsumerWidget {
ref.listen( ref.listen(
appUpdateNotifierProvider, appUpdateNotifierProvider,
(_, next) async { (_, next) async {
if (!context.mounted) return;
switch (next) { switch (next) {
case AsyncData(value: final remoteVersion?): case AppUpdateStateAvailable(:final versionInfo):
await NewVersionDialog( return NewVersionDialog(
appInfo.presentVersion, appInfo.presentVersion,
remoteVersion, versionInfo,
canIgnore: false, canIgnore: false,
).show(context); ).show(context);
case AsyncError(:final error): case AppUpdateStateError(:final error):
if (!context.mounted) return; return CustomToast.error(t.printError(error)).show(context);
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) if (appInfo.release.allowCustomUpdateChecker)
ListTile( ListTile(
title: Text(t.about.checkForUpdate), title: Text(t.about.checkForUpdate),
trailing: appUpdate.isLoading trailing: switch (appUpdate) {
? const SizedBox( AppUpdateStateChecking() => const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) ),
: const Icon(Icons.update), _ => const Icon(Icons.update),
onTap: () { },
ref.invalidate(appUpdateNotifierProvider); onTap: () async {
await ref
.read(appUpdateNotifierProvider.notifier)
.check();
}, },
), ),
ListTile( 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/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/data/data_providers.dart';
import 'package:hiddify/domain/app/app.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:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_update_notifier.freezed.dart';
part 'app_update_notifier.g.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) @Riverpod(keepAlive: true)
class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
@override @override
Future<RemoteVersionInfo?> build() async { AppUpdateState build() {
_schedule();
return const AppUpdateState.initial();
}
Future<AppUpdateState> check() async {
loggy.debug("checking for update"); loggy.debug("checking for update");
state = const AppUpdateState.checking();
final appInfo = ref.watch(appInfoProvider); final appInfo = ref.watch(appInfoProvider);
// TODO use market-specific update checkers // TODO use market-specific update checkers
if (!appInfo.release.allowCustomUpdateChecker) { if (!appInfo.release.allowCustomUpdateChecker) {
loggy.debug( loggy.debug(
"custom update checkers are not allowed for [${appInfo.release.name}] release", "custom update checkers are not allowed for [${appInfo.release.name}] release",
); );
return null; return state = const AppUpdateState.disabled();
} }
final currentVersion = appInfo.version; final currentVersion = appInfo.version;
return ref return ref
.watch(appRepositoryProvider) .watch(appRepositoryProvider)
.getLatestVersion(includePreReleases: true) .getLatestVersion(
includePreReleases: ref.read(checkForPreReleaseUpdatesProvider),
)
.match( .match(
(l) { (err) {
loggy.warning("failed to get latest version, $l"); loggy.warning("failed to get latest version, $err");
throw l; return state = AppUpdateState.error(err);
}, },
(remote) { (remote) {
if (remote.version.compareTo(currentVersion) > 0) { if (remote.version.compareTo(currentVersion) > 0) {
loggy.info("new version available: $remote"); loggy.info("new version available: $remote");
return remote; return state = AppUpdateState.available(remote);
} }
loggy.info( loggy.info(
"already using latest version[$currentVersion], remote: $remote", "already using latest version[$currentVersion], remote: $remote",
); );
return null; return state = const AppUpdateState.notAvailable();
}, },
).run(); ).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/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/logs/notifier/notifier.dart'; import 'package:hiddify/features/logs/notifier/notifier.dart';
@@ -20,6 +21,11 @@ void commonControllers(CommonControllersRef ref) {
(previous, next) {}, (previous, next) {},
fireImmediately: true, fireImmediately: true,
); );
ref.listen(
appUpdateNotifierProvider,
(previous, next) {},
fireImmediately: true,
);
if (PlatformUtils.isDesktop) { if (PlatformUtils.isDesktop) {
ref.listen( ref.listen(
windowControllerProvider, 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/files_editor_service.dart';
import 'package:hiddify/services/platform_settings.dart'; import 'package:hiddify/services/platform_settings.dart';
import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart';
@@ -15,3 +17,10 @@ SingboxService singboxService(SingboxServiceRef ref) => SingboxService();
@riverpod @riverpod
PlatformSettings platformSettings(PlatformSettingsRef ref) => PlatformSettings platformSettings(PlatformSettingsRef ref) =>
PlatformSettings(); 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" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" 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: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -1013,6 +1021,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
retry:
dependency: transitive
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -1234,6 +1250,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.12" version: "0.2.12"
slugid:
dependency: transitive
description:
name: slugid
sha256: e0cc54637b666c9c590f0d76df76e5e2bbf6234ae398a182aac82fd70ddd60ab
url: "https://pub.dev"
source: hosted
version: "1.1.2"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:

View File

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