This commit is contained in:
problematicconsumer
2023-12-01 12:56:24 +03:30
parent 9c165e178b
commit ed614988a2
181 changed files with 3092 additions and 2341 deletions

View 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));
}

View 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,
);
}
}

View 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,
);
}
}

View 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,
),
};
}
}

View 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}";
}

View 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);
}
}

View 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;
}

View 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),
),
],
);
}
}