Refactor app update
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -195,6 +195,7 @@
|
|||||||
"termsAndConditions": "شرایط و ضوابط استفاده"
|
"termsAndConditions": "شرایط و ضوابط استفاده"
|
||||||
},
|
},
|
||||||
"appUpdate": {
|
"appUpdate": {
|
||||||
|
"notAvailableMsg": "نسخه جدیدی یافت نشد",
|
||||||
"dialogTitle": "نسخه جدید موجود است",
|
"dialogTitle": "نسخه جدید موجود است",
|
||||||
"updateMsg": "نسخه جدیدی از @:general.appTitle موجود است! الان بروزرسانی شود؟",
|
"updateMsg": "نسخه جدیدی از @:general.appTitle موجود است! الان بروزرسانی شود؟",
|
||||||
"currentVersionLbl": "نسخه فعلی",
|
"currentVersionLbl": "نسخه فعلی",
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
74
lib/services/cron_service.dart
Normal file
74
lib/services/cron_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user