From b8068e6f5d708e1b2fced7cb6efa7d657cd034fd Mon Sep 17 00:00:00 2001 From: Tien Do Nam <38380847+Tienisto@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:43:59 +0200 Subject: [PATCH] feat: remove msix, improve auto start (#1467) --- README.md | 8 + app/assets/CHANGELOG.md | 2 + app/lib/init.dart | 11 +- app/lib/model/state/settings_state.dart | 4 - .../model/state/settings_state.mapper.dart | 16 -- app/lib/pages/tabs/settings_tab.dart | 55 +------ .../pages/tabs/settings_tab_controller.dart | 60 +++++++- app/lib/pages/tabs/settings_tab_vm.dart | 8 + .../pages/tabs/settings_tab_vm.mapper.dart | 33 +++++ app/lib/provider/persistence_provider.dart | 9 -- app/lib/provider/settings_provider.dart | 16 -- app/lib/util/native/autostart_helper.dart | 138 ++++++++++++------ app/pubspec.lock | 50 +------ app/pubspec.yaml | 20 +-- app/test/mocks.mocks.dart | 20 --- 15 files changed, 214 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index 4fe243e7..048cd7b1 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,14 @@ Create a file named `settings.json` located in the same directory as the executa This file can be empty. The app will use this file to store settings instead of the default location. +**Start hidden** + +(Updated in v1.15.0) + +To start the app hidden (only in tray), use the `--hidden` flag (example: `localsend_app.exe --hidden`). + +On v1.14.0 and earlier, the app starts hidden if `autostart` flag is set, and the hidden setting is enabled. + ## How It Works LocalSend uses a secure communication protocol that allows devices to communicate with each other using a REST API. All data is sent securely over HTTPS, and the TLS/SSL certificate is generated on the fly on each device, ensuring maximum security. diff --git a/app/assets/CHANGELOG.md b/app/assets/CHANGELOG.md index ed5c9e44..9bb0832e 100644 --- a/app/assets/CHANGELOG.md +++ b/app/assets/CHANGELOG.md @@ -2,6 +2,8 @@ - feat: add clear button in the send tab (@Caesarovich) - feat: add URL view when sharing via link that shows the URL in bigger font (@harriseldon) +- feat(windows): toggle "start hidden" in-app instead of referring to the system settings (@Tienisto) +- feat(desktop): make auto start + start hidden more stable, now listens to `--hidden` parameter instead of `autostart` (@Tienisto) - feat(desktop): show progress in the taskbar (@NightFeather0615) - feat: add discovery timeout setting for advanced users (@o2e) - fix: sanitize file names with invalid characters (@Caesarovich) diff --git a/app/lib/init.dart b/app/lib/init.dart index 96511430..77922034 100644 --- a/app/lib/init.dart +++ b/app/lib/init.dart @@ -25,6 +25,7 @@ import 'package:localsend_app/theme.dart'; import 'package:localsend_app/util/api_route_builder.dart'; import 'package:localsend_app/util/i18n.dart'; import 'package:localsend_app/util/logger.dart'; +import 'package:localsend_app/util/native/autostart_helper.dart'; import 'package:localsend_app/util/native/cache_helper.dart'; import 'package:localsend_app/util/native/cross_file_converters.dart'; import 'package:localsend_app/util/native/device_info_helper.dart'; @@ -37,8 +38,6 @@ import 'package:refena_flutter/refena_flutter.dart'; import 'package:share_handler/share_handler.dart'; import 'package:window_manager/window_manager.dart'; -const launchAtStartupArg = 'autostart'; - final _logger = Logger('Init'); /// Will be called before the MaterialApp started @@ -86,13 +85,11 @@ Future preInit(List args) async { // initialize size and position await WindowManager.instance.ensureInitialized(); await WindowDimensionsController(persistenceService).initDimensionsConfiguration(); - if (!args.contains(launchAtStartupArg) || !persistenceService.isAutoStartLaunchMinimized()) { - // We show this app, when (1) app started manually, (2) app should not start minimized - // In other words: only start minimized when launched on startup and "launchMinimized" is configured - await WindowManager.instance.show(); - } else { + if (args.contains(startHiddenFlag)) { // keep this app hidden startHidden = true; + } else { + await WindowManager.instance.show(); } } diff --git a/app/lib/model/state/settings_state.dart b/app/lib/model/state/settings_state.dart index 9e256844..f1e33d7c 100644 --- a/app/lib/model/state/settings_state.dart +++ b/app/lib/model/state/settings_state.dart @@ -22,8 +22,6 @@ class SettingsState with SettingsStateMappable { final bool quickSave; // automatically accept file requests final bool autoFinish; // automatically finish sessions final bool minimizeToTray; // minimize to tray instead of exiting the app - final bool launchAtStartup; // Tracks if the option is enabled on Linux - final bool autoStartLaunchMinimized; // start hidden in tray (only available when launchAtStartup is true) final bool https; final SendMode sendMode; final bool saveWindowPlacement; @@ -47,8 +45,6 @@ class SettingsState with SettingsStateMappable { required this.quickSave, required this.autoFinish, required this.minimizeToTray, - required this.launchAtStartup, - required this.autoStartLaunchMinimized, required this.https, required this.sendMode, required this.saveWindowPlacement, diff --git a/app/lib/model/state/settings_state.mapper.dart b/app/lib/model/state/settings_state.mapper.dart index a5d6932c..8041d235 100644 --- a/app/lib/model/state/settings_state.mapper.dart +++ b/app/lib/model/state/settings_state.mapper.dart @@ -47,10 +47,6 @@ class SettingsStateMapper extends ClassMapperBase { static const Field _f$autoFinish = Field('autoFinish', _$autoFinish); static bool _$minimizeToTray(SettingsState v) => v.minimizeToTray; static const Field _f$minimizeToTray = Field('minimizeToTray', _$minimizeToTray); - static bool _$launchAtStartup(SettingsState v) => v.launchAtStartup; - static const Field _f$launchAtStartup = Field('launchAtStartup', _$launchAtStartup); - static bool _$autoStartLaunchMinimized(SettingsState v) => v.autoStartLaunchMinimized; - static const Field _f$autoStartLaunchMinimized = Field('autoStartLaunchMinimized', _$autoStartLaunchMinimized); static bool _$https(SettingsState v) => v.https; static const Field _f$https = Field('https', _$https); static SendMode _$sendMode(SettingsState v) => v.sendMode; @@ -83,8 +79,6 @@ class SettingsStateMapper extends ClassMapperBase { #quickSave: _f$quickSave, #autoFinish: _f$autoFinish, #minimizeToTray: _f$minimizeToTray, - #launchAtStartup: _f$launchAtStartup, - #autoStartLaunchMinimized: _f$autoStartLaunchMinimized, #https: _f$https, #sendMode: _f$sendMode, #saveWindowPlacement: _f$saveWindowPlacement, @@ -110,8 +104,6 @@ class SettingsStateMapper extends ClassMapperBase { quickSave: data.dec(_f$quickSave), autoFinish: data.dec(_f$autoFinish), minimizeToTray: data.dec(_f$minimizeToTray), - launchAtStartup: data.dec(_f$launchAtStartup), - autoStartLaunchMinimized: data.dec(_f$autoStartLaunchMinimized), https: data.dec(_f$https), sendMode: data.dec(_f$sendMode), saveWindowPlacement: data.dec(_f$saveWindowPlacement), @@ -180,8 +172,6 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out> implem bool? quickSave, bool? autoFinish, bool? minimizeToTray, - bool? launchAtStartup, - bool? autoStartLaunchMinimized, bool? https, SendMode? sendMode, bool? saveWindowPlacement, @@ -214,8 +204,6 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting bool? quickSave, bool? autoFinish, bool? minimizeToTray, - bool? launchAtStartup, - bool? autoStartLaunchMinimized, bool? https, SendMode? sendMode, bool? saveWindowPlacement, @@ -238,8 +226,6 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting if (quickSave != null) #quickSave: quickSave, if (autoFinish != null) #autoFinish: autoFinish, if (minimizeToTray != null) #minimizeToTray: minimizeToTray, - if (launchAtStartup != null) #launchAtStartup: launchAtStartup, - if (autoStartLaunchMinimized != null) #autoStartLaunchMinimized: autoStartLaunchMinimized, if (https != null) #https: https, if (sendMode != null) #sendMode: sendMode, if (saveWindowPlacement != null) #saveWindowPlacement: saveWindowPlacement, @@ -264,8 +250,6 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting quickSave: data.get(#quickSave, or: $value.quickSave), autoFinish: data.get(#autoFinish, or: $value.autoFinish), minimizeToTray: data.get(#minimizeToTray, or: $value.minimizeToTray), - launchAtStartup: data.get(#launchAtStartup, or: $value.launchAtStartup), - autoStartLaunchMinimized: data.get(#autoStartLaunchMinimized, or: $value.autoStartLaunchMinimized), https: data.get(#https, or: $value.https), sendMode: data.get(#sendMode, or: $value.sendMode), saveWindowPlacement: data.get(#saveWindowPlacement, or: $value.saveWindowPlacement), diff --git a/app/lib/pages/tabs/settings_tab.dart b/app/lib/pages/tabs/settings_tab.dart index bb71ee37..11888b94 100644 --- a/app/lib/pages/tabs/settings_tab.dart +++ b/app/lib/pages/tabs/settings_tab.dart @@ -11,7 +11,6 @@ import 'package:localsend_app/provider/settings_provider.dart'; import 'package:localsend_app/provider/version_provider.dart'; import 'package:localsend_app/theme.dart'; import 'package:localsend_app/util/device_type_ext.dart'; -import 'package:localsend_app/util/native/autostart_helper.dart'; import 'package:localsend_app/util/native/pick_directory_path.dart'; import 'package:localsend_app/util/native/platform_check.dart'; import 'package:localsend_app/widget/custom_dropdown_button.dart'; @@ -25,9 +24,6 @@ import 'package:refena_flutter/refena_flutter.dart'; import 'package:routerino/routerino.dart'; import 'package:url_launcher/url_launcher.dart'; -final _isLinux = checkPlatform([TargetPlatform.linux]); -final _isWindows = checkPlatform([TargetPlatform.windows]); - class SettingsTab extends StatelessWidget { const SettingsTab(); @@ -100,62 +96,27 @@ class SettingsTab extends StatelessWidget { }, ), ], - // Linux autostart is simpler, so a boolean entry is used - if (_isLinux) + if (checkPlatformIsDesktop()) ...[ _BooleanEntry( label: t.settingsTab.general.launchAtStartup, - value: vm.settings.launchAtStartup, - onChanged: (b) async { - late bool result; - if (await isLinuxLaunchAtStartEnabled()) { - result = await initDisableAutoStart(vm.settings); - } else { - result = await initEnableAutoStartAndOpenSettings(vm.settings); - } - if (result) { - await ref.notifier(settingsProvider).setLaunchAtStartup(b); - } - }, + value: vm.autoStart, + onChanged: (_) => vm.onToggleAutoStart(context), ), - // Windows requires a manual action, so this settings entry is required - if (_isWindows) - _SettingsEntry( - label: t.settingsTab.general.launchAtStartup, - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).inputDecorationTheme.fillColor, - shape: RoundedRectangleBorder(borderRadius: Theme.of(context).inputDecorationTheme.borderRadius), - foregroundColor: Theme.of(context).colorScheme.onSurface, - ), - onPressed: () async { - await initDisableAutoStart(vm.settings); - await initEnableAutoStartAndOpenSettings(vm.settings, _isWindows); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Text(t.general.settings, style: Theme.of(context).textTheme.titleMedium), - ), - ), - ), - if (_isWindows || _isLinux) Visibility( - visible: vm.settings.launchAtStartup || _isWindows, + visible: vm.autoStart, maintainAnimation: true, maintainState: true, child: AnimatedOpacity( - opacity: vm.settings.launchAtStartup || _isWindows ? 1.0 : 0.0, + opacity: vm.autoStart ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: _BooleanEntry( label: t.settingsTab.general.launchMinimized, - value: vm.settings.autoStartLaunchMinimized, - onChanged: (b) async { - await initDisableAutoStart(vm.settings); - await ref.notifier(settingsProvider).setAutoStartLaunchMinimized(b); - await initEnableAutoStartAndOpenSettings(vm.settings, _isWindows); - }, + value: vm.autoStartLaunchHidden, + onChanged: (_) => vm.onToggleAutoStartLaunchHidden(context), ), ), ), + ], ], _BooleanEntry( label: t.settingsTab.general.animations, diff --git a/app/lib/pages/tabs/settings_tab_controller.dart b/app/lib/pages/tabs/settings_tab_controller.dart index 48ceaa38..0b27a199 100644 --- a/app/lib/pages/tabs/settings_tab_controller.dart +++ b/app/lib/pages/tabs/settings_tab_controller.dart @@ -6,6 +6,7 @@ import 'package:localsend_app/provider/device_info_provider.dart'; import 'package:localsend_app/provider/network/server/server_provider.dart'; import 'package:localsend_app/provider/settings_provider.dart'; import 'package:localsend_app/theme.dart'; +import 'package:localsend_app/util/native/autostart_helper.dart'; import 'package:localsend_app/util/native/device_info_helper.dart'; import 'package:localsend_app/util/sleep.dart'; import 'package:localsend_app/util/ui/dynamic_colors.dart'; @@ -56,6 +57,8 @@ class SettingsTabController extends ReduxNotifier { serverState: _serverService.state, deviceInfo: _initialDeviceInfo, colorModes: _supportsDynamicColors ? ColorMode.values : ColorMode.values.where((e) => e != ColorMode.system).toList(), + autoStart: false, + autoStartLaunchHidden: false, onChangeTheme: (context, theme) async { await _settingsService.setTheme(theme); await sleepAsync(500); // workaround: brightness takes some time to be updated @@ -73,6 +76,26 @@ class SettingsTabController extends ReduxNotifier { onTapLanguage: (context) async { await context.push(() => const LanguagePage()); }, + onToggleAutoStart: (context) async { + final bool success; + if (state.autoStart) { + success = await disableAutoStart(); + } else { + success = await enableAutoStart(startHidden: state.autoStartLaunchHidden); + } + + if (success) { + redux.dispatch(_SetAutoStartAction(!state.autoStart)); + } + }, + onToggleAutoStartLaunchHidden: (context) async { + if (state.autoStart) { + final success = await enableAutoStart(startHidden: !state.autoStartLaunchHidden); + if (success) { + redux.dispatch(_SetAutoStartLaunchHiddenAction(!state.autoStartLaunchHidden)); + } + } + }, onTapRestartServer: (context) async { try { final newServerState = await _serverService.restartServer( @@ -107,7 +130,7 @@ class SettingsTabController extends ReduxNotifier { } @override - get initialAction => _SettingsTabWatchAction(); + get initialAction => _SettingsTabInitAction(); @override void dispose() { @@ -120,6 +143,19 @@ class SettingsTabController extends ReduxNotifier { } } +class _SettingsTabInitAction extends AsyncReduxAction { + @override + Future reduce() async { + dispatch(_SettingsTabWatchAction()); + final autoStartEnabled = await isAutoStartEnabled(); + final autoStartHidden = await isAutoStartHidden(); + return state.copyWith( + autoStart: autoStartEnabled, + autoStartLaunchHidden: autoStartHidden, + ); + } +} + class _SettingsTabWatchAction extends WatchAction { @override SettingsTabVm reduce() { @@ -141,3 +177,25 @@ class SetAdvancedAction extends ReduxAction { + final bool enabled; + + _SetAutoStartAction(this.enabled); + + @override + SettingsTabVm reduce() { + return state.copyWith(autoStart: enabled); + } +} + +class _SetAutoStartLaunchHiddenAction extends ReduxAction { + final bool enabled; + + _SetAutoStartLaunchHiddenAction(this.enabled); + + @override + SettingsTabVm reduce() { + return state.copyWith(autoStartLaunchHidden: enabled); + } +} diff --git a/app/lib/pages/tabs/settings_tab_vm.dart b/app/lib/pages/tabs/settings_tab_vm.dart index 31a25bc4..f9b87937 100644 --- a/app/lib/pages/tabs/settings_tab_vm.dart +++ b/app/lib/pages/tabs/settings_tab_vm.dart @@ -21,9 +21,13 @@ class SettingsTabVm with SettingsTabVmMappable { final DeviceInfoResult deviceInfo; final List themeModes = ThemeMode.values; final List colorModes; + final bool autoStart; + final bool autoStartLaunchHidden; final void Function(BuildContext context, ThemeMode mode) onChangeTheme; final void Function(ColorMode mode) onChangeColorMode; final void Function(BuildContext context) onTapLanguage; + final void Function(BuildContext context) onToggleAutoStart; + final void Function(BuildContext context) onToggleAutoStartLaunchHidden; final void Function(BuildContext context) onTapRestartServer; final void Function(BuildContext context) onTapStartServer; final void Function() onTapStopServer; @@ -40,9 +44,13 @@ class SettingsTabVm with SettingsTabVmMappable { required this.serverState, required this.deviceInfo, required this.colorModes, + required this.autoStart, + required this.autoStartLaunchHidden, required this.onChangeTheme, required this.onChangeColorMode, required this.onTapLanguage, + required this.onToggleAutoStart, + required this.onToggleAutoStartLaunchHidden, required this.onTapRestartServer, required this.onTapStartServer, required this.onTapStopServer, diff --git a/app/lib/pages/tabs/settings_tab_vm.mapper.dart b/app/lib/pages/tabs/settings_tab_vm.mapper.dart index 76706b70..58f734e6 100644 --- a/app/lib/pages/tabs/settings_tab_vm.mapper.dart +++ b/app/lib/pages/tabs/settings_tab_vm.mapper.dart @@ -42,12 +42,21 @@ class SettingsTabVmMapper extends ClassMapperBase { static const Field _f$deviceInfo = Field('deviceInfo', _$deviceInfo); static List _$colorModes(SettingsTabVm v) => v.colorModes; static const Field> _f$colorModes = Field('colorModes', _$colorModes); + static bool _$autoStart(SettingsTabVm v) => v.autoStart; + static const Field _f$autoStart = Field('autoStart', _$autoStart); + static bool _$autoStartLaunchHidden(SettingsTabVm v) => v.autoStartLaunchHidden; + static const Field _f$autoStartLaunchHidden = Field('autoStartLaunchHidden', _$autoStartLaunchHidden); static void Function(BuildContext, ThemeMode) _$onChangeTheme(SettingsTabVm v) => v.onChangeTheme; static const Field _f$onChangeTheme = Field('onChangeTheme', _$onChangeTheme); static void Function(ColorMode) _$onChangeColorMode(SettingsTabVm v) => v.onChangeColorMode; static const Field _f$onChangeColorMode = Field('onChangeColorMode', _$onChangeColorMode); static void Function(BuildContext) _$onTapLanguage(SettingsTabVm v) => v.onTapLanguage; static const Field _f$onTapLanguage = Field('onTapLanguage', _$onTapLanguage); + static void Function(BuildContext) _$onToggleAutoStart(SettingsTabVm v) => v.onToggleAutoStart; + static const Field _f$onToggleAutoStart = Field('onToggleAutoStart', _$onToggleAutoStart); + static void Function(BuildContext) _$onToggleAutoStartLaunchHidden(SettingsTabVm v) => v.onToggleAutoStartLaunchHidden; + static const Field _f$onToggleAutoStartLaunchHidden = + Field('onToggleAutoStartLaunchHidden', _$onToggleAutoStartLaunchHidden); static void Function(BuildContext) _$onTapRestartServer(SettingsTabVm v) => v.onTapRestartServer; static const Field _f$onTapRestartServer = Field('onTapRestartServer', _$onTapRestartServer); static void Function(BuildContext) _$onTapStartServer(SettingsTabVm v) => v.onTapStartServer; @@ -71,9 +80,13 @@ class SettingsTabVmMapper extends ClassMapperBase { #serverState: _f$serverState, #deviceInfo: _f$deviceInfo, #colorModes: _f$colorModes, + #autoStart: _f$autoStart, + #autoStartLaunchHidden: _f$autoStartLaunchHidden, #onChangeTheme: _f$onChangeTheme, #onChangeColorMode: _f$onChangeColorMode, #onTapLanguage: _f$onTapLanguage, + #onToggleAutoStart: _f$onToggleAutoStart, + #onToggleAutoStartLaunchHidden: _f$onToggleAutoStartLaunchHidden, #onTapRestartServer: _f$onTapRestartServer, #onTapStartServer: _f$onTapStartServer, #onTapStopServer: _f$onTapStopServer, @@ -93,9 +106,13 @@ class SettingsTabVmMapper extends ClassMapperBase { serverState: data.dec(_f$serverState), deviceInfo: data.dec(_f$deviceInfo), colorModes: data.dec(_f$colorModes), + autoStart: data.dec(_f$autoStart), + autoStartLaunchHidden: data.dec(_f$autoStartLaunchHidden), onChangeTheme: data.dec(_f$onChangeTheme), onChangeColorMode: data.dec(_f$onChangeColorMode), onTapLanguage: data.dec(_f$onTapLanguage), + onToggleAutoStart: data.dec(_f$onToggleAutoStart), + onToggleAutoStartLaunchHidden: data.dec(_f$onToggleAutoStartLaunchHidden), onTapRestartServer: data.dec(_f$onTapRestartServer), onTapStartServer: data.dec(_f$onTapStartServer), onTapStopServer: data.dec(_f$onTapStopServer), @@ -160,9 +177,13 @@ abstract class SettingsTabVmCopyWith<$R, $In extends SettingsTabVm, $Out> implem ServerState? serverState, DeviceInfoResult? deviceInfo, List? colorModes, + bool? autoStart, + bool? autoStartLaunchHidden, void Function(BuildContext, ThemeMode)? onChangeTheme, void Function(ColorMode)? onChangeColorMode, void Function(BuildContext)? onTapLanguage, + void Function(BuildContext)? onToggleAutoStart, + void Function(BuildContext)? onToggleAutoStartLaunchHidden, void Function(BuildContext)? onTapRestartServer, void Function(BuildContext)? onTapStartServer, void Function()? onTapStopServer, @@ -195,9 +216,13 @@ class _SettingsTabVmCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting Object? serverState = $none, DeviceInfoResult? deviceInfo, List? colorModes, + bool? autoStart, + bool? autoStartLaunchHidden, void Function(BuildContext, ThemeMode)? onChangeTheme, void Function(ColorMode)? onChangeColorMode, void Function(BuildContext)? onTapLanguage, + void Function(BuildContext)? onToggleAutoStart, + void Function(BuildContext)? onToggleAutoStartLaunchHidden, void Function(BuildContext)? onTapRestartServer, void Function(BuildContext)? onTapStartServer, void Function()? onTapStopServer, @@ -213,9 +238,13 @@ class _SettingsTabVmCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting if (serverState != $none) #serverState: serverState, if (deviceInfo != null) #deviceInfo: deviceInfo, if (colorModes != null) #colorModes: colorModes, + if (autoStart != null) #autoStart: autoStart, + if (autoStartLaunchHidden != null) #autoStartLaunchHidden: autoStartLaunchHidden, if (onChangeTheme != null) #onChangeTheme: onChangeTheme, if (onChangeColorMode != null) #onChangeColorMode: onChangeColorMode, if (onTapLanguage != null) #onTapLanguage: onTapLanguage, + if (onToggleAutoStart != null) #onToggleAutoStart: onToggleAutoStart, + if (onToggleAutoStartLaunchHidden != null) #onToggleAutoStartLaunchHidden: onToggleAutoStartLaunchHidden, if (onTapRestartServer != null) #onTapRestartServer: onTapRestartServer, if (onTapStartServer != null) #onTapStartServer: onTapStartServer, if (onTapStopServer != null) #onTapStopServer: onTapStopServer, @@ -233,9 +262,13 @@ class _SettingsTabVmCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting serverState: data.get(#serverState, or: $value.serverState), deviceInfo: data.get(#deviceInfo, or: $value.deviceInfo), colorModes: data.get(#colorModes, or: $value.colorModes), + autoStart: data.get(#autoStart, or: $value.autoStart), + autoStartLaunchHidden: data.get(#autoStartLaunchHidden, or: $value.autoStartLaunchHidden), onChangeTheme: data.get(#onChangeTheme, or: $value.onChangeTheme), onChangeColorMode: data.get(#onChangeColorMode, or: $value.onChangeColorMode), onTapLanguage: data.get(#onTapLanguage, or: $value.onTapLanguage), + onToggleAutoStart: data.get(#onToggleAutoStart, or: $value.onToggleAutoStart), + onToggleAutoStartLaunchHidden: data.get(#onToggleAutoStartLaunchHidden, or: $value.onToggleAutoStartLaunchHidden), onTapRestartServer: data.get(#onTapRestartServer, or: $value.onTapRestartServer), onTapStartServer: data.get(#onTapStartServer, or: $value.onTapStartServer), onTapStopServer: data.get(#onTapStopServer, or: $value.onTapStopServer), diff --git a/app/lib/provider/persistence_provider.dart b/app/lib/provider/persistence_provider.dart index 4786da71..f527b6aa 100644 --- a/app/lib/provider/persistence_provider.dart +++ b/app/lib/provider/persistence_provider.dart @@ -60,7 +60,6 @@ const _quickSave = 'ls_quick_save'; const _autoFinish = 'ls_auto_finish'; const _minimizeToTray = 'ls_minimize_to_tray'; const _launchAtStartup = 'ls_launch_at_startup'; -const _autoStartLaunchMinimized = 'ls_auto_start_launch_minimized'; const _https = 'ls_https'; const _sendMode = 'ls_send_mode'; const _enableAnimations = 'ls_enable_animations'; @@ -317,14 +316,6 @@ class PersistenceService { await _prefs.setBool(_launchAtStartup, launchAtStartup); } - bool isAutoStartLaunchMinimized() { - return _prefs.getBool(_autoStartLaunchMinimized) ?? true; - } - - Future setAutoStartLaunchMinimized(bool launchMinimized) async { - await _prefs.setBool(_autoStartLaunchMinimized, launchMinimized); - } - bool isHttps() { return _prefs.getBool(_https) ?? true; } diff --git a/app/lib/provider/settings_provider.dart b/app/lib/provider/settings_provider.dart index 269b05b0..83bc4850 100644 --- a/app/lib/provider/settings_provider.dart +++ b/app/lib/provider/settings_provider.dart @@ -31,8 +31,6 @@ class SettingsService extends PureNotifier { quickSave: _persistence.isQuickSave(), autoFinish: _persistence.isAutoFinish(), minimizeToTray: _persistence.isMinimizeToTray(), - launchAtStartup: _persistence.isLaunchAtStartup(), - autoStartLaunchMinimized: _persistence.isAutoStartLaunchMinimized(), https: _persistence.isHttps(), sendMode: _persistence.getSendMode(), saveWindowPlacement: _persistence.getSaveWindowPlacement(), @@ -134,20 +132,6 @@ class SettingsService extends PureNotifier { ); } - Future setLaunchAtStartup(bool launchAtStartup) async { - await _persistence.setLaunchAtStartup(launchAtStartup); - state = state.copyWith( - launchAtStartup: launchAtStartup, - ); - } - - Future setAutoStartLaunchMinimized(bool launchMinimized) async { - await _persistence.setAutoStartLaunchMinimized(launchMinimized); - state = state.copyWith( - autoStartLaunchMinimized: launchMinimized, - ); - } - Future setHttps(bool https) async { await _persistence.setHttps(https); state = state.copyWith( diff --git a/app/lib/util/native/autostart_helper.dart b/app/lib/util/native/autostart_helper.dart index a676f06d..b5207c71 100644 --- a/app/lib/util/native/autostart_helper.dart +++ b/app/lib/util/native/autostart_helper.dart @@ -1,73 +1,115 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; -import 'package:localsend_app/init.dart'; -import 'package:localsend_app/model/state/settings_state.dart'; +import 'package:localsend_app/util/native/platform_check.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:win32_registry/win32_registry.dart'; + +const startHiddenFlag = '--hidden'; final _logger = Logger('AutoStartHelper'); -/// Currently, only works for windows -Future initEnableAutoStartAndOpenSettings(SettingsState settings, [bool? isWindows]) async { +Future enableAutoStart({required bool startHidden}) async { try { - // In case somebody don't use msix - final packageInfo = await PackageInfo.fromPlatform(); - launchAtStartup.setup( - appName: packageInfo.appName, - appPath: Platform.resolvedExecutable, - args: [if (!settings.autoStartLaunchMinimized) launchAtStartupArg], - ); - - // We just add this entry so we have the same behaviour like in msix - if (!(await launchAtStartup.isEnabled())) { - final result = await launchAtStartup.enable(); - return result; + if (checkPlatform(const [TargetPlatform.linux, TargetPlatform.macOS])) { + final packageInfo = await PackageInfo.fromPlatform(); + launchAtStartup.setup( + appName: packageInfo.appName, + appPath: Platform.resolvedExecutable, + args: [ + if (startHidden) startHiddenFlag, + ], + ); + await launchAtStartup.enable(); + } else { + // launch_at_startup does not add quotes around the executable path + final packageInfo = await PackageInfo.fromPlatform(); + _getWindowsRegistryKey().createValue(RegistryValue( + packageInfo.appName, + RegistryValueType.string, + '"${Platform.resolvedExecutable}"${startHidden ? ' $startHiddenFlag' : ''}', + )); } + return true; } catch (e) { - _logger.warning('Could not init auto start', e); + _logger.warning('Could enable auto start', e); return false; } - - // Can be linux on startup flag change - if (isWindows ?? false) { - try { - // Ideally, we should configure it programmatically - // The launch_at_startup package does not support this currently - // See: https://learn.microsoft.com/en-us/uwp/api/Windows.ApplicationModel.StartupTask?view=winrt-22621 - await launchUrl(Uri.parse('ms-settings:startupapps')); - } catch (e) { - _logger.warning('Could not open startup settings', e); - } - } - return false; } -Future initDisableAutoStart(SettingsState settings) async { +Future disableAutoStart() async { try { - // In case somebody don't use msix final packageInfo = await PackageInfo.fromPlatform(); - launchAtStartup.setup( - appName: packageInfo.appName, - appPath: Platform.resolvedExecutable, - args: [if (settings.autoStartLaunchMinimized) launchAtStartupArg], - ); - - // We just add this entry so we have the same behaviour like in msix - if (await launchAtStartup.isEnabled()) { - final result = await launchAtStartup.disable(); - return result; + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + File(_getLinuxFilePath(packageInfo.appName)).deleteSync(); + break; + case TargetPlatform.macOS: + File(_getMacOSFilePath(packageInfo.appName)).deleteSync(); + break; + case TargetPlatform.windows: + _getWindowsRegistryKey().deleteValue(packageInfo.appName); + break; + default: + break; } + return true; } catch (e) { - _logger.warning('Could not init auto start', e); + _logger.warning('Could disable auto start', e); return false; } - return false; } -Future isLinuxLaunchAtStartEnabled() async { +Future isAutoStartEnabled() async { final packageInfo = await PackageInfo.fromPlatform(); - File desktopFile = File('${Platform.environment['HOME']}/.config/autostart/${packageInfo.appName}.desktop'); - return desktopFile.existsSync(); + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + return File(_getLinuxFilePath(packageInfo.appName)).existsSync(); + case TargetPlatform.macOS: + return File(_getMacOSFilePath(packageInfo.appName)).existsSync(); + case TargetPlatform.windows: + return _getWindowsRegistryKey().getValueAsString(packageInfo.appName)?.contains(Platform.resolvedExecutable) ?? false; + default: + return false; + } +} + +Future isAutoStartHidden() async { + final packageInfo = await PackageInfo.fromPlatform(); + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + final file = File(_getLinuxFilePath(packageInfo.appName)); + if (!file.existsSync()) { + return false; + } + return file.readAsStringSync().contains(startHiddenFlag); + case TargetPlatform.macOS: + final file = File(_getMacOSFilePath(packageInfo.appName)); + if (!file.existsSync()) { + return false; + } + return file.readAsStringSync().contains(startHiddenFlag); + case TargetPlatform.windows: + return _getWindowsRegistryKey().getValueAsString(packageInfo.appName)?.contains(startHiddenFlag) ?? false; + default: + return false; + } +} + +RegistryKey _getWindowsRegistryKey() { + return Registry.openPath( + RegistryHive.currentUser, + path: r'Software\Microsoft\Windows\CurrentVersion\Run', + desiredAccessRights: AccessRights.allAccess, + ); +} + +String _getLinuxFilePath(String appName) { + return '${Platform.environment['HOME']}/.config/autostart/$appName.desktop'; +} + +String _getMacOSFilePath(String appName) { + return '${Platform.environment['HOME']}/Library/LaunchAgents/$appName.plist'; } diff --git a/app/pubspec.lock b/app/pubspec.lock index 6b300e06..138562f8 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -33,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" args: dependency: transitive description: @@ -153,14 +145,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 - url: "https://pub.dev" - source: hosted - version: "0.4.1" clock: dependency: transitive description: @@ -216,14 +200,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.4" - console: - dependency: transitive - description: - name: console - sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a - url: "https://pub.dev" - source: hosted - version: "4.1.0" convert: dependency: transitive description: @@ -572,14 +548,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - get_it: - dependency: transitive - description: - name: get_it - sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 - url: "https://pub.dev" - source: hosted - version: "7.6.7" glob: dependency: transitive description: @@ -660,14 +628,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" - url: "https://pub.dev" - source: hosted - version: "4.2.0" image_picker: dependency: "direct main" description: @@ -900,14 +860,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" - msix: - dependency: "direct dev" - description: - name: msix - sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" - url: "https://pub.dev" - source: hosted - version: "3.16.7" nested: dependency: transitive description: @@ -1771,7 +1723,7 @@ packages: source: hosted version: "5.1.1" win32_registry: - dependency: transitive + dependency: "direct main" description: name: win32_registry sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 6042412c..ecf3982b 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -68,6 +68,7 @@ dependencies: uuid: 3.0.7 wakelock_plus: 1.1.4 wechat_assets_picker: 8.8.1+1 + win32_registry: 1.1.2 window_manager: 0.3.9 windows_taskbar: 1.1.2 yaru: 1.2.2 @@ -78,7 +79,6 @@ dev_dependencies: flutter_gen_runner: 5.6.0 flutter_lints: 3.0.1 mockito: 5.4.4 - msix: 3.16.7 refena_inspector: 2.0.0 slang_build_runner: 3.31.0 slang_gpt: 0.10.2 @@ -102,21 +102,3 @@ flutter: flutter_gen: line_length: 150 - -msix_config: - display_name: LocalSend - publisher_display_name: Tien Do Nam - # Using third-party CA for now (see: https://github.com/localsend/localsend/issues/220) - # publisher: CN=0A8E9755-183F-4F0B-823F-1B8C991D7B97 - identity_name: 11157TienDoNam.LocalSend - logo_path: assets\img\logo-512.png - architecture: x64 - languages: en, ar, bn, cs, da, de, el, es-ES, eu, fa, fr, he, hu, in, it, ja, ko, ne, nl, pl, pt-BR, ru, sv, th, tr, uk, vi, zh-CN, zh-HK, zh-TW - - # https://github.com/localsend/localsend/issues/398 - os_min_version: 10.0.19041.0 - - startup_task: - task_id: localsend - enabled: false - parameters: autostart diff --git a/app/test/mocks.mocks.dart b/app/test/mocks.mocks.dart index 607cd530..72e2a500 100644 --- a/app/test/mocks.mocks.dart +++ b/app/test/mocks.mocks.dart @@ -443,26 +443,6 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - bool isAutoStartLaunchMinimized() => (super.noSuchMethod( - Invocation.method( - #isAutoStartLaunchMinimized, - [], - ), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); - - @override - _i4.Future setAutoStartLaunchMinimized(bool? launchMinimized) => (super.noSuchMethod( - Invocation.method( - #setAutoStartLaunchMinimized, - [launchMinimized], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override bool isHttps() => (super.noSuchMethod( Invocation.method(