Refactor
This commit is contained in:
12
lib/features/app_update/data/app_update_data_providers.dart
Normal file
12
lib/features/app_update/data/app_update_data_providers.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:hiddify/core/http_client/http_client_provider.dart';
|
||||
import 'package:hiddify/features/app_update/data/app_update_repository.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_update_data_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppUpdateRepository appUpdateRepository(
|
||||
AppUpdateRepositoryRef ref,
|
||||
) {
|
||||
return AppUpdateRepositoryImpl(dio: ref.watch(httpClientProvider));
|
||||
}
|
||||
55
lib/features/app_update/data/app_update_repository.dart
Normal file
55
lib/features/app_update/data/app_update_repository.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/model/constants.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
import 'package:hiddify/core/utils/exception_handler.dart';
|
||||
import 'package:hiddify/features/app_update/data/github_release_parser.dart';
|
||||
import 'package:hiddify/features/app_update/model/app_update_failure.dart';
|
||||
import 'package:hiddify/features/app_update/model/remote_version_entity.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
abstract interface class AppUpdateRepository {
|
||||
TaskEither<AppUpdateFailure, RemoteVersionEntity> getLatestVersion({
|
||||
bool includePreReleases = false,
|
||||
Release release = Release.general,
|
||||
});
|
||||
}
|
||||
|
||||
class AppUpdateRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements AppUpdateRepository {
|
||||
AppUpdateRepositoryImpl({required this.dio});
|
||||
|
||||
final Dio dio;
|
||||
|
||||
@override
|
||||
TaskEither<AppUpdateFailure, RemoteVersionEntity> getLatestVersion({
|
||||
bool includePreReleases = false,
|
||||
Release release = Release.general,
|
||||
}) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
if (!release.allowCustomUpdateChecker) {
|
||||
throw Exception("custom update checkers are not supported");
|
||||
}
|
||||
final response = await dio.get<List>(Constants.githubReleasesApiUrl);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
loggy.warning("failed to fetch latest version info");
|
||||
return left(const AppUpdateFailure.unexpected());
|
||||
}
|
||||
|
||||
final releases = response.data!.map(
|
||||
(e) => GithubReleaseParser.parse(e as Map<String, dynamic>),
|
||||
);
|
||||
late RemoteVersionEntity latest;
|
||||
if (includePreReleases) {
|
||||
latest = releases.first;
|
||||
} else {
|
||||
latest = releases.firstWhere((e) => e.preRelease == false);
|
||||
}
|
||||
return right(latest);
|
||||
},
|
||||
AppUpdateFailure.unexpected,
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/features/app_update/data/github_release_parser.dart
Normal file
36
lib/features/app_update/data/github_release_parser.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
import 'package:hiddify/features/app_update/model/remote_version_entity.dart';
|
||||
|
||||
abstract class GithubReleaseParser {
|
||||
static RemoteVersionEntity parse(Map<String, dynamic> json) {
|
||||
final fullTag = json['tag_name'] as String;
|
||||
final fullVersion = fullTag.removePrefix("v").split("-").first.split("+");
|
||||
var version = fullVersion.first;
|
||||
var buildNumber = fullVersion.elementAtOrElse(1, (index) => "");
|
||||
var flavor = Environment.prod;
|
||||
for (final env in Environment.values) {
|
||||
final suffix = ".${env.name}";
|
||||
if (version.endsWith(suffix)) {
|
||||
version = version.removeSuffix(suffix);
|
||||
flavor = env;
|
||||
break;
|
||||
} else if (buildNumber.endsWith(suffix)) {
|
||||
buildNumber = buildNumber.removeSuffix(suffix);
|
||||
flavor = env;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final preRelease = json["prerelease"] as bool;
|
||||
final publishedAt = DateTime.parse(json["published_at"] as String);
|
||||
return RemoteVersionEntity(
|
||||
version: version,
|
||||
buildNumber: buildNumber,
|
||||
releaseTag: fullTag,
|
||||
preRelease: preRelease,
|
||||
url: json["html_url"] as String,
|
||||
publishedAt: publishedAt,
|
||||
flavor: flavor,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/features/app_update/model/app_update_failure.dart
Normal file
26
lib/features/app_update/model/app_update_failure.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
|
||||
part 'app_update_failure.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class AppUpdateFailure with _$AppUpdateFailure, Failure {
|
||||
const AppUpdateFailure._();
|
||||
|
||||
@With<UnexpectedFailure>()
|
||||
const factory AppUpdateFailure.unexpected([
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
]) = AppUpdateUnexpectedFailure;
|
||||
|
||||
@override
|
||||
({String type, String? message}) present(TranslationsEn t) {
|
||||
return switch (this) {
|
||||
AppUpdateUnexpectedFailure() => (
|
||||
type: t.failure.unexpected,
|
||||
message: null,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
22
lib/features/app_update/model/remote_version_entity.dart
Normal file
22
lib/features/app_update/model/remote_version_entity.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
|
||||
part 'remote_version_entity.freezed.dart';
|
||||
|
||||
@Freezed()
|
||||
class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
const RemoteVersionEntity._();
|
||||
|
||||
const factory RemoteVersionEntity({
|
||||
required String version,
|
||||
required String buildNumber,
|
||||
required String releaseTag,
|
||||
required bool preRelease,
|
||||
required String url,
|
||||
required DateTime publishedAt,
|
||||
required Environment flavor,
|
||||
}) = _RemoteVersionEntity;
|
||||
|
||||
String get presentVersion =>
|
||||
flavor == Environment.prod ? version : "$version ${flavor.name}";
|
||||
}
|
||||
87
lib/features/app_update/notifier/app_update_notifier.dart
Normal file
87
lib/features/app_update/notifier/app_update_notifier.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||
import 'package:hiddify/core/localization/locale_preferences.dart';
|
||||
import 'package:hiddify/core/model/constants.dart';
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/features/app_update/data/app_update_data_providers.dart';
|
||||
import 'package:hiddify/features/app_update/model/app_update_failure.dart';
|
||||
import 'package:hiddify/features/app_update/model/remote_version_entity.dart';
|
||||
import 'package:hiddify/features/app_update/notifier/app_update_state.dart';
|
||||
import 'package:hiddify/utils/pref_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:upgrader/upgrader.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
part 'app_update_notifier.g.dart';
|
||||
|
||||
const _debugUpgrader = true;
|
||||
|
||||
@riverpod
|
||||
Upgrader upgrader(UpgraderRef ref) => Upgrader(
|
||||
appcastConfig: AppcastConfiguration(url: Constants.appCastUrl),
|
||||
debugLogging: _debugUpgrader && kDebugMode,
|
||||
durationUntilAlertAgain: const Duration(hours: 12),
|
||||
messages: UpgraderMessages(
|
||||
code: ref.watch(localePreferencesProvider).languageCode,
|
||||
),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
||||
@override
|
||||
AppUpdateState build() => const AppUpdateState.initial();
|
||||
|
||||
Pref<String?, dynamic> get _ignoreReleasePref => Pref(
|
||||
ref.read(sharedPreferencesProvider).requireValue,
|
||||
'ignored_release_version',
|
||||
null,
|
||||
);
|
||||
|
||||
Future<AppUpdateState> check() async {
|
||||
loggy.debug("checking for update");
|
||||
state = const AppUpdateState.checking();
|
||||
final appInfo = ref.watch(appInfoProvider).requireValue;
|
||||
if (!appInfo.release.allowCustomUpdateChecker) {
|
||||
loggy.debug(
|
||||
"custom update checkers are not allowed for [${appInfo.release.name}] release",
|
||||
);
|
||||
return state = const AppUpdateState.disabled();
|
||||
}
|
||||
return ref.watch(appUpdateRepositoryProvider).getLatestVersion().match(
|
||||
(err) {
|
||||
loggy.warning("failed to get latest version", err);
|
||||
return state = AppUpdateState.error(err);
|
||||
},
|
||||
(remote) {
|
||||
try {
|
||||
final latestVersion = Version.parse(remote.version);
|
||||
final currentVersion = Version.parse(appInfo.version);
|
||||
if (latestVersion > currentVersion) {
|
||||
if (remote.version == _ignoreReleasePref.getValue()) {
|
||||
loggy.debug("ignored release [${remote.version}]");
|
||||
return state = AppUpdateStateIgnored(remote);
|
||||
}
|
||||
loggy.debug("new version available: $remote");
|
||||
return state = AppUpdateState.available(remote);
|
||||
}
|
||||
loggy.info(
|
||||
"already using latest version[$currentVersion], remote: [${remote.version}]",
|
||||
);
|
||||
return state = const AppUpdateState.notAvailable();
|
||||
} catch (error, stackTrace) {
|
||||
loggy.warning("error parsing versions", error, stackTrace);
|
||||
return state = AppUpdateState.error(
|
||||
AppUpdateFailure.unexpected(error, stackTrace),
|
||||
);
|
||||
}
|
||||
},
|
||||
).run();
|
||||
}
|
||||
|
||||
Future<void> ignoreRelease(RemoteVersionEntity version) async {
|
||||
loggy.debug("ignoring release [${version.version}]");
|
||||
await _ignoreReleasePref.update(version.version);
|
||||
state = AppUpdateStateIgnored(version);
|
||||
}
|
||||
}
|
||||
19
lib/features/app_update/notifier/app_update_state.dart
Normal file
19
lib/features/app_update/notifier/app_update_state.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/features/app_update/model/app_update_failure.dart';
|
||||
import 'package:hiddify/features/app_update/model/remote_version_entity.dart';
|
||||
|
||||
part 'app_update_state.freezed.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(AppUpdateFailure error) =
|
||||
AppUpdateStateError;
|
||||
const factory AppUpdateState.available(RemoteVersionEntity versionInfo) =
|
||||
AppUpdateStateAvailable;
|
||||
const factory AppUpdateState.ignored(RemoteVersionEntity versionInfo) =
|
||||
AppUpdateStateIgnored;
|
||||
const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable;
|
||||
}
|
||||
102
lib/features/app_update/widget/new_version_dialog.dart
Normal file
102
lib/features/app_update/widget/new_version_dialog.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/features/app_update/model/remote_version_entity.dart';
|
||||
import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
NewVersionDialog(
|
||||
this.currentVersion,
|
||||
this.newVersion, {
|
||||
this.canIgnore = true,
|
||||
}) : super(key: _dialogKey);
|
||||
|
||||
final String currentVersion;
|
||||
final RemoteVersionEntity newVersion;
|
||||
final bool canIgnore;
|
||||
|
||||
static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');
|
||||
|
||||
Future<void> show(BuildContext context) async {
|
||||
if (_dialogKey.currentContext == null) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => this,
|
||||
);
|
||||
} else {
|
||||
loggy.warning("new version dialog is already open");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(t.appUpdate.dialogTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(t.appUpdate.updateMsg),
|
||||
const Gap(8),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${t.appUpdate.currentVersionLbl}: ",
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
TextSpan(
|
||||
text: currentVersion,
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${t.appUpdate.newVersionLbl}: ",
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
TextSpan(
|
||||
text: newVersion.presentVersion,
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (canIgnore)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(appUpdateNotifierProvider.notifier)
|
||||
.ignoreRelease(newVersion);
|
||||
if (context.mounted) context.pop();
|
||||
},
|
||||
child: Text(t.appUpdate.ignoreBtnTxt),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context.pop,
|
||||
child: Text(t.appUpdate.laterBtnTxt),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await UriUtils.tryLaunch(Uri.parse(newVersion.url));
|
||||
},
|
||||
child: Text(t.appUpdate.updateNowBtnTxt),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user