Skip to content

Commit 008f565

Browse files
committed
Update UI to present alerts
1 parent 270b46e commit 008f565

3 files changed

Lines changed: 260 additions & 118 deletions

File tree

Core/Sources/HostApp/General.swift

Lines changed: 192 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,196 @@ struct General {
1212
var xpcServiceVersion: String?
1313
var isAccessibilityPermissionGranted: Bool?
1414
var isReloading = false
15+
@Presents var alert: AlertState<Action.Alert>?
1516
}
1617

17-
enum Action: Equatable {
18+
enum Action {
1819
case appear
1920
case setupLaunchAgentIfNeeded
21+
case setupLaunchAgentClicked
22+
case removeLaunchAgentClicked
23+
case reloadLaunchAgentClicked
2024
case openExtensionManager
2125
case reloadStatus
2226
case finishReloading(xpcServiceVersion: String, permissionGranted: Bool)
2327
case failedReloading
28+
case alert(PresentationAction<Alert>)
29+
30+
case setupLaunchAgent
31+
case finishSetupLaunchAgent
32+
case finishRemoveLaunchAgent
33+
case finishReloadLaunchAgent
34+
35+
@CasePathable
36+
enum Alert: Equatable {
37+
case moveToApplications
38+
case moveTo(URL)
39+
case install
40+
}
2441
}
2542

2643
@Dependency(\.toast) var toast
27-
44+
2845
struct ReloadStatusCancellableId: Hashable {}
2946

47+
static var didWarnInstallationPosition: Bool {
48+
get { UserDefaults.standard.bool(forKey: "didWarnInstallationPosition") }
49+
set { UserDefaults.standard.set(newValue, forKey: "didWarnInstallationPosition") }
50+
}
51+
52+
static var bundleIsInApplicationsFolder: Bool {
53+
Bundle.main.bundleURL.path.hasPrefix("/Applications")
54+
}
55+
3056
var body: some ReducerOf<Self> {
3157
Reduce { state, action in
3258
switch action {
3359
case .appear:
34-
return .run { send in
35-
if UserDefaults.shared.value(for: \.doNotInstallLaunchAgentAutomatically) {
36-
return
60+
if Self.bundleIsInApplicationsFolder {
61+
return .run { send in
62+
await send(.setupLaunchAgentIfNeeded)
3763
}
38-
await send(.setupLaunchAgentIfNeeded)
3964
}
4065

66+
if !Self.didWarnInstallationPosition {
67+
Self.didWarnInstallationPosition = true
68+
state.alert = .init {
69+
TextState("Move to Applications Folder?")
70+
} actions: {
71+
ButtonState(action: .moveToApplications) {
72+
TextState("Move")
73+
}
74+
ButtonState(role: .cancel) {
75+
TextState("Not Now")
76+
}
77+
} message: {
78+
TextState(
79+
"To ensure the best experience, please move the app to the Applications folder. If the app is not inside the Applications folder, please set up the launch agent manually by clicking the button."
80+
)
81+
}
82+
}
83+
84+
return .none
85+
4186
case .setupLaunchAgentIfNeeded:
4287
return .run { send in
4388
#if DEBUG
4489
// do not auto install on debug build
4590
#else
46-
Task {
47-
do {
48-
try await LaunchAgentManager()
49-
.setupLaunchAgentForTheFirstTimeIfNeeded()
50-
} catch {
51-
toast(error.localizedDescription, .error)
52-
}
91+
do {
92+
try await LaunchAgentManager()
93+
.setupLaunchAgentForTheFirstTimeIfNeeded()
94+
} catch {
95+
toast(error.localizedDescription, .error)
5396
}
5497
#endif
5598
await send(.reloadStatus)
5699
}
57100

101+
case .setupLaunchAgentClicked:
102+
if Self.bundleIsInApplicationsFolder {
103+
return .run { send in
104+
await send(.setupLaunchAgent)
105+
}
106+
}
107+
108+
state.alert = .init {
109+
TextState("Setup Launch Agent")
110+
} actions: {
111+
ButtonState(action: .install) {
112+
TextState("Setup")
113+
}
114+
115+
ButtonState(action: .moveToApplications) {
116+
TextState("Move to Applications Folder")
117+
}
118+
119+
ButtonState(role: .cancel) {
120+
TextState("Cancel")
121+
}
122+
} message: {
123+
TextState(
124+
"It's recommended to move the app into the Applications folder. But you can still keep it in the current folder and install the launch agent to ~/Library/LaunchAgents."
125+
)
126+
}
127+
128+
return .none
129+
130+
case .removeLaunchAgentClicked:
131+
return .run { send in
132+
do {
133+
try await LaunchAgentManager().removeLaunchAgent()
134+
await send(.finishRemoveLaunchAgent)
135+
} catch {
136+
toast(error.localizedDescription, .error)
137+
}
138+
await send(.reloadStatus)
139+
}
140+
141+
case .reloadLaunchAgentClicked:
142+
return .run { send in
143+
do {
144+
try await LaunchAgentManager().reloadLaunchAgent()
145+
await send(.finishReloadLaunchAgent)
146+
} catch {
147+
toast(error.localizedDescription, .error)
148+
}
149+
await send(.reloadStatus)
150+
}
151+
152+
case .setupLaunchAgent:
153+
return .run { send in
154+
do {
155+
try await LaunchAgentManager().setupLaunchAgent()
156+
await send(.finishSetupLaunchAgent)
157+
} catch {
158+
toast(error.localizedDescription, .error)
159+
}
160+
await send(.reloadStatus)
161+
}
162+
163+
case .finishSetupLaunchAgent:
164+
state.alert = .init {
165+
TextState("Launch Agent Installed")
166+
} actions: {
167+
ButtonState {
168+
TextState("OK")
169+
}
170+
} message: {
171+
TextState(
172+
"The launch agent has been installed. Please restart the app."
173+
)
174+
}
175+
return .none
176+
177+
case .finishRemoveLaunchAgent:
178+
state.alert = .init {
179+
TextState("Launch Agent Removed")
180+
} actions: {
181+
ButtonState {
182+
TextState("OK")
183+
}
184+
} message: {
185+
TextState(
186+
"The launch agent has been removed."
187+
)
188+
}
189+
return .none
190+
191+
case .finishReloadLaunchAgent:
192+
state.alert = .init {
193+
TextState("Launch Agent Reloaded")
194+
} actions: {
195+
ButtonState {
196+
TextState("OK")
197+
}
198+
} message: {
199+
TextState(
200+
"The launch agent has been reloaded."
201+
)
202+
}
203+
return .none
204+
58205
case .openExtensionManager:
59206
return .run { send in
60207
let service = try getService()
@@ -107,6 +254,38 @@ struct General {
107254
case .failedReloading:
108255
state.isReloading = false
109256
return .none
257+
258+
case let .alert(.presented(action)):
259+
switch action {
260+
case .moveToApplications:
261+
return .run { send in
262+
let appURL = URL(fileURLWithPath: "/Applications")
263+
await send(.alert(.presented(.moveTo(appURL))))
264+
}
265+
266+
case let .moveTo(url):
267+
return .run { _ in
268+
do {
269+
try FileManager.default.moveItem(
270+
at: Bundle.main.bundleURL,
271+
to: url.appendingPathComponent(
272+
Bundle.main.bundleURL.lastPathComponent
273+
)
274+
)
275+
await NSApplication.shared.terminate(nil)
276+
} catch {
277+
toast(error.localizedDescription, .error)
278+
}
279+
}
280+
case .install:
281+
return .run { send in
282+
await send(.setupLaunchAgent)
283+
}
284+
}
285+
286+
case .alert(.dismiss):
287+
state.alert = nil
288+
return .none
110289
}
111290
}
112291
}

0 commit comments

Comments
 (0)