Add silent start for desktop

This commit is contained in:
problematicconsumer
2023-07-15 18:00:44 +03:30
parent 292ed96d2a
commit a1209f5453
13 changed files with 188 additions and 67 deletions

View File

@@ -78,7 +78,8 @@
"dark": "dark mode", "dark": "dark mode",
"light": "light mode" "light": "light mode"
}, },
"trueBlack": "true black" "trueBlack": "true black",
"silentStart": "silent start"
}, },
"network": { "network": {
"sectionTitle": "network", "sectionTitle": "network",

View File

@@ -78,7 +78,8 @@
"dark": "تم تیره", "dark": "تم تیره",
"light": "تم روشن" "light": "تم روشن"
}, },
"trueBlack": "کاملا سیاه" "trueBlack": "کاملا سیاه",
"silentStart": "اجرای ساکت"
}, },
"network": { "network": {
"sectionTitle": "شبکه", "sectionTitle": "شبکه",

View File

@@ -3,8 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:hiddify/core/app/app.dart'; import 'package:hiddify/core/app/app.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/system_tray/system_tray.dart'; import 'package:hiddify/features/system_tray/system_tray.dart';
import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/services/deep_link_service.dart';
import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/services/service_providers.dart';
@@ -13,13 +15,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:loggy/loggy.dart'; import 'package:loggy/loggy.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:stack_trace/stack_trace.dart' as stack_trace; import 'package:stack_trace/stack_trace.dart' as stack_trace;
import 'package:window_manager/window_manager.dart';
final _loggy = Loggy('bootstrap'); final _loggy = Loggy('bootstrap');
final _stopWatch = Stopwatch(); final _stopWatch = Stopwatch();
Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async { Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
_stopWatch.start(); _stopWatch.start();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
// temporary solution: https://github.com/rrousselGit/riverpod/issues/1874 // temporary solution: https://github.com/rrousselGit/riverpod/issues/1874
FlutterError.demangleStackTrace = (StackTrace stack) { FlutterError.demangleStackTrace = (StackTrace stack) {
@@ -28,6 +30,9 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
return stack; return stack;
}; };
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await windowManager.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await SharedPreferences.getInstance();
final container = ProviderContainer( final container = ProviderContainer(
overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)], overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)],
@@ -35,6 +40,15 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
Loggy.initLoggy(logPrinter: const PrettyPrinter()); Loggy.initLoggy(logPrinter: const PrettyPrinter());
final silentStart =
container.read(prefsControllerProvider).general.silentStart;
if (silentStart) {
FlutterNativeSplash.remove();
}
if (PlatformUtils.isDesktop) {
await container.read(windowControllerProvider.future);
}
await initAppServices(container.read); await initAppServices(container.read);
await initControllers(container.read); await initControllers(container.read);
@@ -45,7 +59,7 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
), ),
); );
FlutterNativeSplash.remove(); if (!silentStart) FlutterNativeSplash.remove();
_stopWatch.stop(); _stopWatch.stop();
_loggy.debug("bootstrapping took [${_stopWatch.elapsedMilliseconds}]ms"); _loggy.debug("bootstrapping took [${_stopWatch.elapsedMilliseconds}]ms");
} }
@@ -60,7 +74,6 @@ Future<void> initAppServices(
read(clashServiceProvider).init(), read(clashServiceProvider).init(),
read(clashServiceProvider).start(), read(clashServiceProvider).start(),
read(notificationServiceProvider).init(), read(notificationServiceProvider).init(),
if (PlatformUtils.isDesktop) read(windowManagerServiceProvider).init(),
], ],
); );
_loggy.debug('initialized app services'); _loggy.debug('initialized app services');

View File

@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/core/theme/theme.dart'; import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/features/common/common_controllers.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -16,6 +17,8 @@ class AppView extends HookConsumerWidget with PresLogger {
final locale = ref.watch(localeControllerProvider).locale; final locale = ref.watch(localeControllerProvider).locale;
final theme = ref.watch(themeControllerProvider); final theme = ref.watch(themeControllerProvider);
ref.watch(commonControllersProvider);
return MaterialApp.router( return MaterialApp.router(
routerConfig: router, routerConfig: router,
locale: locale, locale: locale,

View File

@@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'general_prefs.freezed.dart';
part 'general_prefs.g.dart';
@freezed
class GeneralPrefs with _$GeneralPrefs {
const GeneralPrefs._();
const factory GeneralPrefs({
// desktop only
@Default(false) bool silentStart,
}) = _GeneralPrefs;
factory GeneralPrefs.fromJson(Map<String, dynamic> json) =>
_$GeneralPrefsFromJson(json);
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/core/prefs/prefs_state.dart'; import 'package:hiddify/core/prefs/prefs_state.dart';
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/clash/clash.dart';
@@ -15,6 +16,7 @@ class PrefsController extends _$PrefsController with AppLogger {
@override @override
PrefsState build() { PrefsState build() {
return PrefsState( return PrefsState(
general: _getGeneralPrefs(),
clash: _getClashPrefs(), clash: _getClashPrefs(),
network: _getNetworkPrefs(), network: _getNetworkPrefs(),
); );
@@ -22,8 +24,15 @@ class PrefsController extends _$PrefsController with AppLogger {
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); SharedPreferences get _prefs => ref.read(sharedPreferencesProvider);
static const _generalKey = "general_prefs";
static const _overridesKey = "clash_overrides"; static const _overridesKey = "clash_overrides";
static const _networkKey = "clash_overrides"; static const _networkKey = "network_prefs";
GeneralPrefs _getGeneralPrefs() {
final persisted = _prefs.getString(_generalKey);
if (persisted == null) return const GeneralPrefs();
return GeneralPrefs.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
}
ClashConfig _getClashPrefs() { ClashConfig _getClashPrefs() {
final persisted = _prefs.getString(_overridesKey); final persisted = _prefs.getString(_overridesKey);
@@ -37,6 +46,14 @@ class PrefsController extends _$PrefsController with AppLogger {
return NetworkPrefs.fromJson(jsonDecode(persisted) as Map<String, dynamic>); return NetworkPrefs.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
} }
Future<void> patchGeneralPrefs({bool? silentStart}) async {
final newPrefs = state.general.copyWith(
silentStart: silentStart ?? state.general.silentStart,
);
await _prefs.setString(_generalKey, jsonEncode(newPrefs.toJson()));
state = state.copyWith(general: newPrefs);
}
Future<void> patchClashOverrides(ClashConfigPatch overrides) async { Future<void> patchClashOverrides(ClashConfigPatch overrides) async {
final newPrefs = state.clash.patch(overrides); final newPrefs = state.clash.patch(overrides);
await _prefs.setString(_overridesKey, jsonEncode(newPrefs.toJson())); await _prefs.setString(_overridesKey, jsonEncode(newPrefs.toJson()));

View File

@@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart';
@@ -9,6 +10,7 @@ class PrefsState with _$PrefsState {
const PrefsState._(); const PrefsState._();
const factory PrefsState({ const factory PrefsState({
@Default(GeneralPrefs()) GeneralPrefs general,
@Default(ClashConfig()) ClashConfig clash, @Default(ClashConfig()) ClashConfig clash,
@Default(NetworkPrefs()) NetworkPrefs network, @Default(NetworkPrefs()) NetworkPrefs network,
}) = _PrefsState; }) = _PrefsState;

View File

@@ -0,0 +1,36 @@
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'common_controllers.g.dart';
// this is a temporary solution to keep providers running even when there are no active listeners
// https://github.com/rrousselGit/riverpod/discussions/2730
@Riverpod(keepAlive: true)
void commonControllers(CommonControllersRef ref) {
ref.listen(
clashControllerProvider,
(previous, next) {},
fireImmediately: true,
);
ref.listen(
connectivityControllerProvider,
(previous, next) {},
fireImmediately: true,
);
if (PlatformUtils.isDesktop) {
ref.listen(
windowControllerProvider,
(previous, next) {},
fireImmediately: true,
);
ref.listen(
systemTrayControllerProvider,
(previous, next) {},
fireImmediately: true,
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:window_manager/window_manager.dart';
part 'window_controller.g.dart';
// TODO improve
@Riverpod(keepAlive: true)
class WindowController extends _$WindowController
with WindowListener, AppLogger {
@override
Future<bool> build() async {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(868, 768),
minimumSize: Size(868, 648),
center: true,
);
await windowManager.setPreventClose(true);
await windowManager.waitUntilReadyToShow(
windowOptions,
() async {
if (ref.read(prefsControllerProvider).general.silentStart) {
loggy.debug("silent start is enabled, hiding window");
await windowManager.hide();
}
},
);
windowManager.addListener(this);
ref.onDispose(() {
loggy.debug("disposing");
windowManager.removeListener(this);
});
return windowManager.isVisible();
}
Future<void> show() async {
await windowManager.show();
state = const AsyncData(true);
}
Future<void> hide() async {
await windowManager.close();
}
@override
Future<void> onWindowClose() async {
await windowManager.hide();
state = const AsyncData(false);
}
}

View File

@@ -3,8 +3,10 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/theme/theme.dart'; import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart'; import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
@@ -15,6 +17,9 @@ class AppearanceSettingTiles extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final general =
ref.watch(prefsControllerProvider.select((value) => value.general));
final locale = ref.watch(localeControllerProvider); final locale = ref.watch(localeControllerProvider);
final theme = ref.watch(themeControllerProvider); final theme = ref.watch(themeControllerProvider);
@@ -89,6 +94,16 @@ class AppearanceSettingTiles extends HookConsumerWidget {
themeController.change(trueBlack: value); themeController.change(trueBlack: value);
}, },
), ),
if (PlatformUtils.isDesktop)
SwitchListTile(
title: Text(t.settings.general.silentStart.titleCase),
value: general.silentStart,
onChanged: (value) {
ref
.read(prefsControllerProvider.notifier)
.patchGeneralPrefs(silentStart: value);
},
),
], ],
); );
} }

View File

@@ -7,48 +7,40 @@ import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/features/common/clash/clash_mode.dart'; import 'package:hiddify/features/common/clash/clash_mode.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/gen/assets.gen.dart'; import 'package:hiddify/gen/assets.gen.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';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
part 'system_tray_controller.g.dart'; part 'system_tray_controller.g.dart';
// TODO: rewrite
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SystemTrayController extends _$SystemTrayController class SystemTrayController extends _$SystemTrayController
with TrayListener, AppLogger { with TrayListener, AppLogger {
@override @override
Future<void> build() async { Future<void> build() async {
await trayManager.setIcon(Assets.images.logoRound); if (!_initialized) {
trayManager.addListener(this); loggy.debug('initializing');
ref.onDispose(() { await trayManager.setIcon(Assets.images.logoRound);
loggy.debug('disposing'); trayManager.addListener(this);
trayManager.removeListener(this); _initialized = true;
}); }
ref.listen(
connectivityControllerProvider, final connection = ref.watch(connectivityControllerProvider);
(_, next) async { final mode =
connection = next; ref.watch(clashModeProvider.select((value) => value.valueOrNull));
await _updateTray();
}, loggy.debug('updating system tray');
fireImmediately: true, await _updateTray(connection, mode);
);
ref.listen(
clashModeProvider.select((value) => value.valueOrNull),
(_, next) async {
mode = next;
await _updateTray();
},
fireImmediately: true,
);
} }
late ConnectionStatus connection; bool _initialized = false;
late TunnelMode? mode;
Future<void> _updateTray() async { Future<void> _updateTray(
ConnectionStatus connection,
TunnelMode? mode,
) async {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final trayMenu = Menu( final trayMenu = Menu(
items: [ items: [
@@ -85,7 +77,7 @@ class SystemTrayController extends _$SystemTrayController
@override @override
Future<void> onTrayIconMouseDown() async { Future<void> onTrayIconMouseDown() async {
await windowManager.show(); await ref.read(windowControllerProvider.notifier).show();
} }
@override @override
@@ -95,8 +87,8 @@ class SystemTrayController extends _$SystemTrayController
} }
Future<void> handleClickShowApp(MenuItem menuItem) async { Future<void> handleClickShowApp(MenuItem menuItem) async {
if (await windowManager.isVisible()) return; if (await ref.read(windowControllerProvider.future)) return;
await windowManager.show(); await ref.read(windowControllerProvider.notifier).show();
} }
Future<void> handleClickModeItem( Future<void> handleClickModeItem(
@@ -112,6 +104,7 @@ class SystemTrayController extends _$SystemTrayController
return ref.read(connectivityControllerProvider.notifier).toggleConnection(); return ref.read(connectivityControllerProvider.notifier).toggleConnection();
} }
// TODO rewrite
Future<void> handleClickExitApp(MenuItem menuItem) async { Future<void> handleClickExitApp(MenuItem menuItem) async {
exit(0); exit(0);
} }

View File

@@ -2,7 +2,6 @@ import 'package:hiddify/services/clash/clash.dart';
import 'package:hiddify/services/connectivity/connectivity.dart'; import 'package:hiddify/services/connectivity/connectivity.dart';
import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/services/notification/notification.dart'; import 'package:hiddify/services/notification/notification.dart';
import 'package:hiddify/services/window_manager_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'service_providers.g.dart'; part 'service_providers.g.dart';
@@ -15,10 +14,6 @@ NotificationService notificationService(NotificationServiceRef ref) =>
FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService filesEditorService(FilesEditorServiceRef ref) =>
FilesEditorService(); FilesEditorService();
@Riverpod(keepAlive: true)
WindowManagerService windowManagerService(WindowManagerServiceRef ref) =>
WindowManagerService();
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
ConnectivityService connectivityService(ConnectivityServiceRef ref) => ConnectivityService connectivityService(ConnectivityServiceRef ref) =>
ConnectivityService( ConnectivityService(

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
// TODO: rewrite
class WindowManagerService with WindowListener {
Future<void> init() async {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(868, 768),
minimumSize: Size(868, 648),
center: true,
);
await windowManager.waitUntilReadyToShow(windowOptions);
await windowManager.setPreventClose(true);
windowManager.addListener(this);
}
@override
Future<void> onWindowClose() async {
await windowManager.hide();
}
void dispose() {
windowManager.removeListener(this);
}
}