mirror of
https://github.com/localsend/localsend.git
synced 2026-05-24 16:56:32 -04:00
feat(macos): add sandboxed launch at login for App Store version (#1793)
This commit is contained in:
@@ -105,7 +105,11 @@ Future<RefenaContainer> preInit(List<String> args) async {
|
||||
if (args.contains(startHiddenFlag)) {
|
||||
// keep this app hidden
|
||||
startHidden = true;
|
||||
} else if (defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
startHidden = await isLaunchedAsLoginItem() && await getLaunchAtLoginMinimized();
|
||||
}
|
||||
|
||||
if (startHidden) {
|
||||
unawaited(hideToTray());
|
||||
} else {
|
||||
await WindowManager.instance.show();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:common/constants.dart';
|
||||
import 'package:common/model/device.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -30,8 +28,6 @@ import 'package:refena_flutter/refena_flutter.dart';
|
||||
import 'package:routerino/routerino.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final _isMacOSSandboxed = defaultTargetPlatform == TargetPlatform.macOS && Platform.environment['APP_SANDBOX_CONTAINER_ID'] != null;
|
||||
|
||||
class SettingsTab extends StatelessWidget {
|
||||
const SettingsTab();
|
||||
|
||||
@@ -106,7 +102,7 @@ class SettingsTab extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
if (checkPlatformIsDesktop() && !_isMacOSSandboxed) ...[
|
||||
if (checkPlatformIsDesktop()) ...[
|
||||
_BooleanEntry(
|
||||
label: t.settingsTab.general.launchAtStartup,
|
||||
value: vm.autoStart,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:localsend_app/util/native/macos_channel.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
@@ -30,31 +31,8 @@ Terminal=false
|
||||
file.writeAsStringSync(contents);
|
||||
return true;
|
||||
case TargetPlatform.macOS:
|
||||
final file = File(_getMacOSFilePath(packageInfo.packageName));
|
||||
file.writeAsStringSync('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${packageInfo.packageName}</string>
|
||||
<key>AssociatedBundleIdentifiers</key>
|
||||
<string>${packageInfo.packageName}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>${Platform.executable}</string>
|
||||
${startHidden ? '<string>$startHiddenFlag</string>' : ''}
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>ProcessType</key>
|
||||
<string>Interactive</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/dev/null</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/dev/null</string>
|
||||
</dict>
|
||||
</plist>
|
||||
''');
|
||||
await setLaunchAtLogin(true);
|
||||
await setLaunchAtLoginMinimized(startHidden);
|
||||
return true;
|
||||
case TargetPlatform.windows:
|
||||
_getWindowsRegistryKey().createValue(RegistryValue(
|
||||
@@ -80,7 +58,7 @@ Future<bool> disableAutoStart() async {
|
||||
File(_getLinuxFilePath(packageInfo.packageName)).deleteSync();
|
||||
break;
|
||||
case TargetPlatform.macOS:
|
||||
File(_getMacOSFilePath(packageInfo.packageName)).deleteSync();
|
||||
await setLaunchAtLogin(false);
|
||||
break;
|
||||
case TargetPlatform.windows:
|
||||
_getWindowsRegistryKey().deleteValue(_windowsRegistryKeyValue);
|
||||
@@ -101,7 +79,7 @@ Future<bool> isAutoStartEnabled() async {
|
||||
case TargetPlatform.linux:
|
||||
return File(_getLinuxFilePath(packageInfo.packageName)).existsSync();
|
||||
case TargetPlatform.macOS:
|
||||
return File(_getMacOSFilePath(packageInfo.packageName)).existsSync();
|
||||
return await getLaunchAtLogin();
|
||||
case TargetPlatform.windows:
|
||||
return _getWindowsRegistryKey().getValueAsString(_windowsRegistryKeyValue)?.contains(Platform.resolvedExecutable) ?? false;
|
||||
default:
|
||||
@@ -119,11 +97,7 @@ Future<bool> isAutoStartHidden() async {
|
||||
}
|
||||
return file.readAsStringSync().contains(startHiddenFlag);
|
||||
case TargetPlatform.macOS:
|
||||
final file = File(_getMacOSFilePath(packageInfo.packageName));
|
||||
if (!file.existsSync()) {
|
||||
return false;
|
||||
}
|
||||
return file.readAsStringSync().contains(startHiddenFlag);
|
||||
return await getLaunchAtLoginMinimized();
|
||||
case TargetPlatform.windows:
|
||||
return _getWindowsRegistryKey().getValueAsString(_windowsRegistryKeyValue)?.contains(startHiddenFlag) ?? false;
|
||||
default:
|
||||
@@ -144,7 +118,3 @@ RegistryKey _getWindowsRegistryKey() {
|
||||
String _getLinuxFilePath(String appName) {
|
||||
return '${Platform.environment['HOME']}/.config/autostart/$appName.desktop';
|
||||
}
|
||||
|
||||
String _getMacOSFilePath(String appName) {
|
||||
return '${Platform.environment['HOME']}/Library/LaunchAgents/$appName.plist';
|
||||
}
|
||||
|
||||
@@ -27,6 +27,46 @@ Future<void> updateDockProgress(double progress) async {
|
||||
await _methodChannel.invokeMethod('updateDockProgress', progress);
|
||||
}
|
||||
|
||||
Future<void> setLaunchAtLogin(bool value) async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _methodChannel.invokeMethod('setLaunchAtLogin', value);
|
||||
}
|
||||
|
||||
Future<bool> getLaunchAtLogin() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _methodChannel.invokeMethod('getLaunchAtLogin');
|
||||
}
|
||||
|
||||
Future<void> setLaunchAtLoginMinimized(bool value) async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _methodChannel.invokeMethod('setLaunchAtLoginMinimized', value);
|
||||
}
|
||||
|
||||
Future<bool> getLaunchAtLoginMinimized() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _methodChannel.invokeMethod('getLaunchAtLoginMinimized');
|
||||
}
|
||||
|
||||
Future<bool> isLaunchedAsLoginItem() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _methodChannel.invokeMethod('isLaunchedAsLoginItem');
|
||||
}
|
||||
|
||||
Future<void> setDockIcon(TaskbarIcon icon) async {
|
||||
if (defaultTargetPlatform != TargetPlatform.macOS) {
|
||||
return;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
0543E87B2C8A93A9004AA8B7 /* ContentDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0543E87A2C8A93A9004AA8B7 /* ContentDropView.swift */; };
|
||||
05BAA8D02C8CE248003CFCF1 /* SecurityScopedResourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BAA8CF2C8CE248003CFCF1 /* SecurityScopedResourceManager.swift */; };
|
||||
05CD1E642C98F2220044F24D /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = 05CD1E632C98F2220044F24D /* DockProgress */; };
|
||||
05F86B372C9B80BD000EB4BA /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 05F86B362C9B80BD000EB4BA /* LaunchAtLogin */; };
|
||||
0F663CE30FCD07225E44A808 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F8CCFF0FAB783405F067709 /* Pods_Runner.framework */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
@@ -123,6 +124,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
05F86B372C9B80BD000EB4BA /* LaunchAtLogin in Frameworks */,
|
||||
05CD1E642C98F2220044F24D /* DockProgress in Frameworks */,
|
||||
0F663CE30FCD07225E44A808 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
@@ -261,6 +263,7 @@
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
05F86B382C9B8118000EB4BA /* Copy “Launch at Login Helper” */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
2DA7D291A0AEEC0A0C7CE5C0 /* [CP] Embed Pods Frameworks */,
|
||||
@@ -275,6 +278,7 @@
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
05CD1E632C98F2220044F24D /* DockProgress */,
|
||||
05F86B362C9B80BD000EB4BA /* LaunchAtLogin */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* LocalSend.app */;
|
||||
@@ -321,6 +325,7 @@
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
packageReferences = (
|
||||
05CD1E622C98F2220044F24D /* XCRemoteSwiftPackageReference "DockProgress" */,
|
||||
05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */,
|
||||
);
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -354,6 +359,25 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
05F86B382C9B8118000EB4BA /* Copy “Launch at Login Helper” */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy “Launch at Login Helper”";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n";
|
||||
};
|
||||
2DA7D291A0AEEC0A0C7CE5C0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -935,6 +959,14 @@
|
||||
minimumVersion = 4.3.1;
|
||||
};
|
||||
};
|
||||
05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Legacy";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.0.2;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -943,6 +975,11 @@
|
||||
package = 05CD1E622C98F2220044F24D /* XCRemoteSwiftPackageReference "DockProgress" */;
|
||||
productName = DockProgress;
|
||||
};
|
||||
05F86B362C9B80BD000EB4BA /* LaunchAtLogin */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */;
|
||||
productName = LaunchAtLogin;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "3e0046e1b0518dc180bd8a7c98fa476d9af079586f505a34aa5b924f90460bda",
|
||||
"originHash" : "72ae1b64a4d55b124e5d523ea3b7e7432749772550fc8b66a0f17a2caa5d4dc1",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dockprogress",
|
||||
@@ -9,6 +9,15 @@
|
||||
"revision" : "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
|
||||
"version" : "4.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "launchatlogin-legacy",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin-Legacy",
|
||||
"state" : {
|
||||
"revision" : "9a894d799269cb591037f9f9cb0961510d4dca81",
|
||||
"version" : "5.0.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
||||
@@ -2,6 +2,7 @@ import Cocoa
|
||||
import FlutterMacOS
|
||||
import Defaults
|
||||
import DockProgress
|
||||
import LaunchAtLogin
|
||||
|
||||
enum DockIcon: CaseIterable {
|
||||
case regular
|
||||
@@ -15,6 +16,7 @@ class AppDelegate: FlutterAppDelegate {
|
||||
private var channel: FlutterMethodChannel?
|
||||
private var pendingFilesObservation: Defaults.Observation?
|
||||
private var pendingStringsObservation: Defaults.Observation?
|
||||
private var isLaunchedAsLoginItem: Bool?
|
||||
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
// LocalSend handles the close event manually
|
||||
@@ -30,6 +32,12 @@ class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
let localsendBrandColor = NSColor(red: 0, green: 0.392, blue: 0.353, alpha: 0.8) // #00645a
|
||||
DockProgress.style = .squircle(color: localsendBrandColor)
|
||||
|
||||
// source: https://stackoverflow.com/a/19890943
|
||||
// NOTE: this check must be in applicationDidFinishLaunching, otherwise the `NSAppleEventManager.shared().currentAppleEvent` will be nil
|
||||
if let event = NSAppleEventManager.shared().currentAppleEvent {
|
||||
isLaunchedAsLoginItem = (event.eventID == kAEOpenApplication) && (event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem)
|
||||
}
|
||||
}
|
||||
|
||||
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
@@ -155,6 +163,26 @@ class AppDelegate: FlutterAppDelegate {
|
||||
let newIconIndex = call.arguments as! Int
|
||||
let newIcon = DockIcon.allCases[newIconIndex]
|
||||
setDockIcon(icon: newIcon)
|
||||
case "getLaunchAtLogin":
|
||||
result(LaunchAtLogin.isEnabled)
|
||||
case "setLaunchAtLogin":
|
||||
if let launchAtLogin = call.arguments as? Bool {
|
||||
LaunchAtLogin.isEnabled = launchAtLogin
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a boolean value", details: nil))
|
||||
}
|
||||
case "getLaunchAtLoginMinimized":
|
||||
result(UserDefaults.standard.bool(forKey: "launchAtLoginMinimized"))
|
||||
case "setLaunchAtLoginMinimized":
|
||||
if let launchAtLoginMinimized = call.arguments as? Bool {
|
||||
UserDefaults.standard.set(launchAtLoginMinimized, forKey: "launchAtLoginMinimized")
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a boolean value", details: nil))
|
||||
}
|
||||
case "isLaunchedAsLoginItem":
|
||||
result(isLaunchedAsLoginItem)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user