Skip to content

Commit 9239fce

Browse files
authored
Merge pull request #1 from EllandeVED/v1.6.1
V1.6.1
2 parents 16d6554 + 2eea787 commit 9239fce

6 files changed

Lines changed: 217 additions & 116 deletions

File tree

NumWorksWebView.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
PRODUCT_BUNDLE_IDENTIFIER = com.example.NumWorksWebView;
187187
PRODUCT_NAME = "$(TARGET_NAME)";
188188
SDKROOT = macosx;
189+
STRIPFLAGS = "";
189190
SWIFT_EMIT_LOC_STRINGS = NO;
190191
SWIFT_VERSION = 5.0;
191192
};
@@ -213,6 +214,7 @@
213214
PRODUCT_BUNDLE_IDENTIFIER = com.example.NumWorksWebView;
214215
PRODUCT_NAME = "$(TARGET_NAME)";
215216
SDKROOT = macosx;
217+
STRIPFLAGS = "";
216218
SWIFT_EMIT_LOC_STRINGS = NO;
217219
SWIFT_VERSION = 5.0;
218220
};

NumWorksWebView.xcodeproj/xcshareddata/xcschemes/NumWorksWebView.xcscheme

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
ReferencedContainer = "container:NumWorksWebView.xcodeproj">
5151
</BuildableReference>
5252
</BuildableProductRunnable>
53+
<CommandLineArguments>
54+
<CommandLineArgument
55+
argument = "--force-update-check-outside-applications"
56+
isEnabled = "YES">
57+
</CommandLineArgument>
58+
</CommandLineArguments>
5359
</LaunchAction>
5460
<ProfileAction
5561
buildConfiguration = "Release"

NumWorksWebView/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundleVersion</key>
2020
<string>10</string>
2121
<key>CFBundleShortVersionString</key>
22-
<string>1.6</string>
22+
<string>1.6.1</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.utilities</string>
2525
<key>LSMinimumSystemVersion</key>

NumWorksWebView/NWUpdateChecker.swift

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ final class NWUpdateChecker {
9595

9696
if nwIsRemote(remoteTag, newerThan: current) {
9797
lastAlertedVersion = remoteTag
98+
// Signal the status bar to show an “update available” badge
99+
NotificationCenter.default.post(name: .showUpdateBadge, object: nil)
98100
let notes = remote.body?.trimmingCharacters(in: .whitespacesAndNewlines)
99101

100102
// Prefer a .zip asset if available
@@ -114,6 +116,7 @@ final class NWUpdateChecker {
114116
)
115117
} else if userInitiated {
116118
nwShowInfoAlert(title: "You’re Up To Date", text: "You have the latest version (\(current)).")
119+
NotificationCenter.default.post(name: .hideUpdateBadge, object: nil)
117120
}
118121
}
119122

@@ -179,16 +182,33 @@ final class NWUpdateChecker {
179182
return formatter.string(fromByteCount: bytes)
180183
}
181184

185+
@objc private func progressNextClicked(_ sender: Any?) {
186+
// When the user clicks Next, show the final instructions and turn the button into "Finish (Go to Finder)"
187+
if let label = progressLabel {
188+
label.stringValue = "File has been downloaded. Open the zip and launch the new app."
189+
}
190+
if let button = finderButton {
191+
button.title = "Finish (Go to Finder)"
192+
button.target = self
193+
button.action = #selector(progressGoToFinderClicked(_:))
194+
}
195+
}
196+
182197
@objc private func progressGoToFinderClicked(_ sender: Any?) {
183198
if let url = downloadedFileURL {
184199
NSWorkspace.shared.activateFileViewerSelecting([url])
185200
}
201+
202+
// Clean up the progress window UI
186203
progressWindow?.close()
187204
progressWindow = nil
188205
progressBar = nil
189206
progressLabel = nil
190207
finderButton = nil
191208
downloadedFileURL = nil
209+
210+
// Quit the app so the user can open the new app without conflicts
211+
NSApp.terminate(nil)
192212
}
193213

194214
// MARK: - Alerts
@@ -332,17 +352,20 @@ final class NWUpdateChecker {
332352
label.alignment = .center
333353
content.addSubview(label)
334354

335-
// “Go to Finder” button centered under the label, initially hidden until download completes
336-
// “Go to Finder” button centered under the label, initially hidden until download completes
337-
let finderWidth: CGFloat = 120
338-
let finderX = (windowWidth - finderWidth) / 2
339-
let finder = NSButton(title: "Go to Finder", target: self, action: #selector(progressGoToFinderClicked(_:)))
340-
// Slightly lower Y to increase spacing between label and button
341-
finder.frame = NSRect(x: finderX, y: 14, width: finderWidth, height: 30)
355+
// “Next” button near the bottom-right, initially hidden until download completes.
356+
// After the user clicks it, it will turn into “Finish (Go to Finder)”.
357+
let finderWidth: CGFloat = 150 // about one-quarter wider than the original button
358+
let finderHeight: CGFloat = 32
359+
let finderPadding: CGFloat = 16
360+
let finderX = windowWidth - finderPadding - finderWidth
361+
let finderY: CGFloat = 8
362+
let finder = NSButton(title: "Next", target: self, action: #selector(progressNextClicked(_:)))
363+
finder.frame = NSRect(x: finderX, y: finderY, width: finderWidth, height: finderHeight)
342364
finder.isHidden = true
343365
finder.alphaValue = 0 // start hidden for fade-in
344366
finder.bezelStyle = .rounded
345-
finder.contentTintColor = .systemBlue // blue button
367+
finder.bezelColor = .systemBlue
368+
finder.contentTintColor = .white // white text on blue background
346369
content.addSubview(finder)
347370

348371
window.contentView = content
@@ -408,28 +431,14 @@ final class NWUpdateChecker {
408431
context.duration = 0.25
409432
bar.animator().doubleValue = 1.0
410433
}, completionHandler: {
411-
// 2) Fade in the completion text
412-
if let label = self.progressLabel {
413-
label.stringValue = "File has been downloaded. Open the zip and launch the new app."
414-
label.alphaValue = 0.0
415-
NSAnimationContext.runAnimationGroup({ context in
416-
context.duration = 0.25
417-
label.animator().alphaValue = 1.0
418-
}, completionHandler: {
419-
// 3) Fade in the “Go to Finder” button
420-
if let button = self.finderButton {
421-
button.isHidden = false
422-
button.alphaValue = 0.0
423-
NSAnimationContext.runAnimationGroup { context in
424-
context.duration = 0.25
425-
button.animator().alphaValue = 1.0
426-
}
427-
}
428-
})
429-
} else if let button = self.finderButton {
430-
// No label; just show the button if label is missing
434+
// After the bar is full, fade in the "Next" button.
435+
if let button = self.finderButton {
431436
button.isHidden = false
432-
button.alphaValue = 1.0
437+
button.alphaValue = 0.0
438+
NSAnimationContext.runAnimationGroup { context in
439+
context.duration = 0.25
440+
button.animator().alphaValue = 1.0
441+
}
433442
}
434443
})
435444
}
@@ -450,7 +459,6 @@ final class NWUpdateChecker {
450459
DockProgress.style = .squircle(color: .blue)
451460
DockProgress.progressInstance = task.progress
452461

453-
// Observe progress to update the small window's bar
454462
// Observe progress to update the small window's bar
455463
activeDownloadObservation = task.progress.observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progressObj, _ in
456464
guard let self else { return }
@@ -468,7 +476,11 @@ final class NWUpdateChecker {
468476
if let label = self.progressLabel {
469477
let clamped = max(0, min(1, fraction))
470478
let percent = Int((clamped * 100).rounded())
471-
label.stringValue = "Downloading… \(percent)%"
479+
if clamped >= 1.0 {
480+
label.stringValue = "Finished downloading (\(percent)%)"
481+
} else {
482+
label.stringValue = "Downloading… \(percent)%"
483+
}
472484
}
473485
}
474486
}

NumWorksWebView/NumWorksWebViewApp.swift

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ extension Notification.Name {
1111
static let calculatorDidLoad = Notification.Name("CalculatorDidLoad")
1212
static let reloadCalculatorNow = Notification.Name("ReloadCalculatorNow")
1313
static let openSettingsRequest = Notification.Name("OpenSettingsRequest")
14+
static let showUpdateBadge = Notification.Name("ShowUpdateBadge")
15+
static let hideUpdateBadge = Notification.Name("HideUpdateBadge")
1416
}
1517

1618
private struct SettingsOpenerView: View {
@@ -76,6 +78,14 @@ extension KeyboardShortcuts.Name {
7678
return path.hasPrefix("/Applications/") || path.hasPrefix(NSHomeDirectory() + "/Applications/")
7779
}
7880

81+
/// When this launch argument is present, always run the auto-updater
82+
/// even if the app bundle is not in an Applications folder.
83+
/// Usage (in Xcode scheme or command line):
84+
/// --force-update-check-outside-applications
85+
private var forceUpdateCheckOutsideApplications: Bool {
86+
return CommandLine.arguments.contains("--force-update-check-outside-applications")
87+
}
88+
7989
private let status = StatusBarController()
8090

8191

@@ -163,9 +173,9 @@ extension KeyboardShortcuts.Name {
163173
guard let self else { return }
164174
self.maybePromptMoveToApplications()
165175

166-
// Only schedule the update check when we are already in an Applications folder.
167-
// This avoids showing an update alert on the same run where the user might decide to move the app.
168-
if self.isInApplicationsFolder {
176+
// Normally only schedule the update check when we are already in an Applications folder.
177+
// If the special launch flag is present, force the updater even outside Applications.
178+
if self.isInApplicationsFolder || self.forceUpdateCheckOutsideApplications {
169179
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
170180
NWUpdateChecker.shared.nwCheckOnLaunch()
171181
}
@@ -191,7 +201,15 @@ extension KeyboardShortcuts.Name {
191201
}
192202
}
193203
}
194-
204+
205+
// Show / hide update badge in the menu bar icon
206+
NotificationCenter.default.addObserver(forName: .showUpdateBadge, object: nil, queue: .main) { [weak self] _ in
207+
self?.status.setUpdateBadgeVisible(true)
208+
}
209+
NotificationCenter.default.addObserver(forName: .hideUpdateBadge, object: nil, queue: .main) { [weak self] _ in
210+
self?.status.setUpdateBadgeVisible(false)
211+
}
212+
195213
// +++ Start monitoring connectivity
196214
startNetworkMonitoring()
197215
showWaitingScreen = true
@@ -439,6 +457,37 @@ extension KeyboardShortcuts.Name {
439457

440458
// (Settings window management is now handled by SwiftUI Windows.)
441459

460+
/// Attempts to move the app bundle into /Applications via Finder / AppleScript.
461+
/// This path will trigger the standard macOS authorization dialog if needed
462+
/// (for example when writing into the system Applications folder).
463+
private func moveToApplicationsViaFinder(bundleURL: URL, destinationDir: URL) throws {
464+
// We ask Finder to move the app bundle into the Applications folder.
465+
// Using Finder ensures macOS shows the normal permission/auth dialog
466+
// instead of failing silently with a "no permission" error.
467+
let fromPath = bundleURL.path.replacingOccurrences(of: "\"", with: "\\\"")
468+
let destPath = destinationDir.path.replacingOccurrences(of: "\"", with: "\\\"")
469+
470+
let scriptSource = """
471+
tell application "Finder"
472+
move POSIX file "\(fromPath)" to POSIX file "\(destPath)"
473+
end tell
474+
"""
475+
476+
var errorDict: NSDictionary?
477+
if let appleScript = NSAppleScript(source: scriptSource) {
478+
_ = appleScript.executeAndReturnError(&errorDict)
479+
if let errorDict,
480+
let message = errorDict[NSAppleScript.errorMessage] as? String {
481+
throw NSError(domain: "AppleScriptMoveError",
482+
code: 1,
483+
userInfo: [NSLocalizedDescriptionKey: message])
484+
}
485+
} else {
486+
throw NSError(domain: "AppleScriptMoveError",
487+
code: 0,
488+
userInfo: [NSLocalizedDescriptionKey: "Could not create AppleScript for move operation."])
489+
}
490+
}
442491
// Moves the app bundle to /Applications if not already there.
443492
func moveToApplicationsIfNeeded(userInitiated: Bool) {
444493
if isInApplicationsFolder {
@@ -464,15 +513,30 @@ extension KeyboardShortcuts.Name {
464513
NSLog("Move to Applications failed: \(error.localizedDescription)")
465514

466515
let nsError = error as NSError
467-
// If the failure is due to a permissions issue, explain this to the user
516+
517+
// If the failure is due to a permissions issue, try again via Finder / AppleScript.
468518
if nsError.domain == NSCocoaErrorDomain,
469-
nsError.code == NSFileWriteNoPermissionError || nsError.code == NSFileWriteVolumeReadOnlyError {
470-
let alert = NSAlert()
471-
alert.alertStyle = .warning
472-
alert.messageText = "Can’t Move to Applications"
473-
alert.informativeText = "NumWorksWebView doesn’t have permission to move itself to the Applications folder.\n\nYou may need to enter your macOS password or move the app manually by dragging it into the Applications folder in Finder."
474-
alert.addButton(withTitle: "OK")
475-
alert.runModal()
519+
(nsError.code == NSFileWriteNoPermissionError || nsError.code == NSFileWriteVolumeReadOnlyError) {
520+
do {
521+
try moveToApplicationsViaFinder(bundleURL: bundleURL, destinationDir: destinationDir)
522+
// If Finder successfully moved the app, relaunch from /Applications.
523+
relaunchFromApplications(at: destinationURL)
524+
} catch {
525+
// Finder/AppleScript path also failed: explain this to the user.
526+
let finalError = error as NSError
527+
let alert = NSAlert()
528+
alert.alertStyle = .warning
529+
alert.messageText = "Can’t Move to Applications"
530+
alert.informativeText = """
531+
NumWorksWebView tried to move itself to the Applications folder, but macOS did not allow it.
532+
533+
Please move the app manually by dragging it into the Applications folder in Finder.
534+
535+
Error: \(finalError.localizedDescription)
536+
"""
537+
alert.addButton(withTitle: "OK")
538+
alert.runModal()
539+
}
476540
} else if userInitiated {
477541
// For other errors, show a generic failure alert if the user explicitly requested the move
478542
let alert = NSAlert()

0 commit comments

Comments
 (0)