Refactor
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/features/common/side_bar_stats_overview.dart';
|
||||
import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
abstract interface class RootScaffold {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/app/app.dart';
|
||||
import 'package:hiddify/domain/constants.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.freezed.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(localeNotifierProvider).languageCode,
|
||||
),
|
||||
);
|
||||
|
||||
@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.ignored(RemoteVersionInfo versionInfo) =
|
||||
AppUpdateStateIgnored;
|
||||
const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
||||
@override
|
||||
AppUpdateState build() {
|
||||
// _schedule();
|
||||
return const AppUpdateState.initial();
|
||||
}
|
||||
|
||||
Pref<String?, dynamic> get _ignoreReleasePref => Pref(
|
||||
ref.read(sharedPreferencesProvider),
|
||||
'ignored_release_version',
|
||||
null,
|
||||
);
|
||||
|
||||
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 state = const AppUpdateState.disabled();
|
||||
}
|
||||
return ref.watch(appRepositoryProvider).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(
|
||||
AppFailure.unexpected(error, stackTrace),
|
||||
);
|
||||
}
|
||||
},
|
||||
).run();
|
||||
}
|
||||
|
||||
Future<void> ignoreRelease(RemoteVersionInfo versionInfo) async {
|
||||
loggy.debug("ignoring release [${versionInfo.version}]");
|
||||
await _ignoreReleasePref.update(versionInfo.version);
|
||||
state = AppUpdateStateIgnored(versionInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hiddify/core/prefs/general_prefs.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/features/common/window/window_controller.dart';
|
||||
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||
import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart';
|
||||
import 'package:hiddify/features/system_tray/system_tray_controller.dart';
|
||||
import 'package:hiddify/utils/platform_utils.dart';
|
||||
@@ -22,7 +22,7 @@ void commonControllers(CommonControllersRef ref) {
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listen(
|
||||
connectivityControllerProvider,
|
||||
connectionNotifierProvider,
|
||||
(previous, next) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/prefs/service_prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/domain/core_facade.dart';
|
||||
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'connectivity_controller.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ConnectivityController extends _$ConnectivityController with AppLogger {
|
||||
@override
|
||||
Stream<ConnectionStatus> build() {
|
||||
ref.listen(
|
||||
activeProfileProvider.select((value) => value.asData?.value),
|
||||
(previous, next) async {
|
||||
if (previous == null) return;
|
||||
final shouldReconnect = next == null || previous.id != next.id;
|
||||
if (shouldReconnect) {
|
||||
await reconnect(next?.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
return _core.watchConnectionStatus().doOnData((event) {
|
||||
if (event case Disconnected(connectionFailure: final _?)
|
||||
when PlatformUtils.isDesktop) {
|
||||
ref.read(startedByUserProvider.notifier).update(false);
|
||||
}
|
||||
loggy.info("connection status: ${event.format()}");
|
||||
});
|
||||
}
|
||||
|
||||
CoreFacade get _core => ref.watch(coreFacadeProvider);
|
||||
|
||||
Future<void> mayConnect() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value case Disconnected()) return _connect();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleConnection() async {
|
||||
if (state case AsyncError()) {
|
||||
await _connect();
|
||||
} else if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Disconnected():
|
||||
await ref.read(startedByUserProvider.notifier).update(true);
|
||||
await _connect();
|
||||
case Connected():
|
||||
await ref.read(startedByUserProvider.notifier).update(false);
|
||||
await _disconnect();
|
||||
default:
|
||||
loggy.warning("switching status, debounce");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconnect(String? profileId) async {
|
||||
if (state case AsyncData(:final value) when value == const Connected()) {
|
||||
if (profileId == null) {
|
||||
loggy.info("no active profile, disconnecting");
|
||||
return _disconnect();
|
||||
}
|
||||
loggy.info("active profile changed, reconnecting");
|
||||
await ref.read(startedByUserProvider.notifier).update(true);
|
||||
await _core
|
||||
.restart(profileId, ref.read(disableMemoryLimitProvider))
|
||||
.mapLeft((err) {
|
||||
loggy.warning("error reconnecting", err);
|
||||
state = AsyncError(err, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abortConnection() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Connected() || Connecting():
|
||||
loggy.debug("aborting connection");
|
||||
await _disconnect();
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connect() async {
|
||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||
await _core
|
||||
.start(activeProfile!.id, ref.read(disableMemoryLimitProvider))
|
||||
.mapLeft((err) async {
|
||||
loggy.warning("error connecting", err);
|
||||
await ref.read(startedByUserProvider.notifier).update(false);
|
||||
state = AsyncError(err, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await _core.stop().mapLeft((err) {
|
||||
loggy.warning("error disconnecting", err);
|
||||
state = AsyncError(err, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<bool> serviceRunning(ServiceRunningRef ref) => ref
|
||||
.watch(
|
||||
connectivityControllerProvider.selectAsync((data) => data.isConnected),
|
||||
)
|
||||
.onError((error, stackTrace) => false);
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/core/localization/locale_extensions.dart';
|
||||
import 'package:hiddify/core/localization/locale_preferences.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/region.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class LocalePrefTile extends HookConsumerWidget {
|
||||
@@ -12,7 +14,7 @@ class LocalePrefTile extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final locale = ref.watch(localeNotifierProvider);
|
||||
final locale = ref.watch(localePreferencesProvider);
|
||||
|
||||
return ListTile(
|
||||
title: Text(t.settings.general.locale),
|
||||
@@ -39,8 +41,8 @@ class LocalePrefTile extends HookConsumerWidget {
|
||||
);
|
||||
if (selectedLocale != null) {
|
||||
await ref
|
||||
.read(localeNotifierProvider.notifier)
|
||||
.update(selectedLocale);
|
||||
.read(localePreferencesProvider.notifier)
|
||||
.changeLocale(selectedLocale);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/domain/app/app.dart';
|
||||
import 'package:hiddify/features/common/app_update_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
// TODO add release notes
|
||||
class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
NewVersionDialog(
|
||||
this.currentVersion,
|
||||
this.newVersion, {
|
||||
this.canIgnore = true,
|
||||
}) : super(key: _dialogKey);
|
||||
|
||||
final String currentVersion;
|
||||
final RemoteVersionInfo 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/common/stats_provider.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class SideBarStatsOverview extends HookConsumerWidget {
|
||||
const SideBarStatsOverview({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final stats = ref.watch(statsProvider).asData?.value ?? CoreStatus.empty();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_StatCard(
|
||||
title: t.home.stats.traffic,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplink.speed(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlink.speed(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.trafficTotal,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplinkTotal.size(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlinkTotal.size(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends HookConsumerWidget {
|
||||
const _StatCard({
|
||||
required this.title,
|
||||
required this.firstStat,
|
||||
required this.secondStat,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final ({String label, String data, String semanticLabel}) firstStat;
|
||||
final ({String label, String data, String semanticLabel}) secondStat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title),
|
||||
const Gap(4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
firstStat.label,
|
||||
semanticsLabel: firstStat.semanticLabel,
|
||||
style: const TextStyle(color: Colors.green),
|
||||
),
|
||||
Text(
|
||||
firstStat.data,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
secondStat.label,
|
||||
semanticsLabel: secondStat.semanticLabel,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
Text(
|
||||
secondStat.data,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'stats_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class Stats extends _$Stats with AppLogger {
|
||||
@override
|
||||
Stream<CoreStatus> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
yield* ref
|
||||
.watch(coreFacadeProvider)
|
||||
.watchCoreStatus()
|
||||
.map((event) => event.getOrElse((_) => CoreStatus.empty()));
|
||||
} else {
|
||||
yield* Stream.value(CoreStatus.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/prefs/service_prefs.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/core/preferences/service_preferences.dart';
|
||||
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -16,10 +16,10 @@ class WindowController extends _$WindowController
|
||||
Future<bool> build() async {
|
||||
await windowManager.ensureInitialized();
|
||||
const size = Size(868, 668);
|
||||
const minumumSize = Size(368, 568);
|
||||
const minimumSize = Size(368, 568);
|
||||
const windowOptions = WindowOptions(
|
||||
size: size,
|
||||
minimumSize: minumumSize,
|
||||
minimumSize: minimumSize,
|
||||
center: true,
|
||||
);
|
||||
await windowManager.setPreventClose(true);
|
||||
@@ -35,9 +35,7 @@ class WindowController extends _$WindowController
|
||||
() async {
|
||||
if (ref.read(startedByUserProvider)) {
|
||||
loggy.debug("previously started by user, trying to connect");
|
||||
return ref
|
||||
.read(connectivityControllerProvider.notifier)
|
||||
.mayConnect();
|
||||
return ref.read(connectionNotifierProvider.notifier).mayConnect();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user