Skip to content

Commit db706fd

Browse files
committed
custom options + better epsilon update checker
1 parent b5545bd commit db706fd

7 files changed

Lines changed: 125 additions & 26 deletions

File tree

NumWorksMac/Core/OnLaunch.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum OnLaunch {
4646
]
4747
)
4848
} catch {
49-
print("[OnLaunch] failed to resolve simulator download URL: \(error)")
49+
print("[EpsilonUpdateChecker] could not get remote URL: \(error)")
5050
}
5151
}
5252
}
@@ -107,8 +107,7 @@ enum OnLaunch {
107107
print("[OnLaunch] epsilon up to date")
108108
}
109109
} catch {
110-
print("[OnLaunch] epsilon update check failed: \(error)")
111-
// ignore
110+
print("[EpsilonUpdateChecker] error: \(error)")
112111
}
113112

114113
guard appNeedsUpdate || epsilonNeedsUpdate else {

NumWorksMac/Core/Preferences.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ final class Preferences: ObservableObject {
1111
static let showPinButtonOnCalculator = "prefs.showPinButtonOnCalculator"
1212
static let showDockIcon = "prefs.showDockIcon"
1313
static let isPinned = "prefs.isPinned"
14+
static let webInjectionDisabled = "prefs.webInjectionDisabled"
15+
static let calculatorImageHidden = "prefs.calculatorImageHidden"
1416
}
1517

1618
// MARK: - Current settings (persisted)
@@ -40,6 +42,14 @@ final class Preferences: ObservableObject {
4042
didSet { d.set(isPinned, forKey: Keys.isPinned) }
4143
}
4244

45+
@Published var webInjectionDisabled: Bool {
46+
didSet { d.set(webInjectionDisabled, forKey: Keys.webInjectionDisabled) }
47+
}
48+
49+
@Published var calculatorImageHidden: Bool {
50+
didSet { d.set(calculatorImageHidden, forKey: Keys.calculatorImageHidden) }
51+
}
52+
4353
// MARK: - Session-only state (not persisted)
4454
// These values describe the current runtime state only
4555
// and are intentionally NOT stored in UserDefaults.
@@ -65,5 +75,7 @@ final class Preferences: ObservableObject {
6575
showPinButtonOnCalculator = d.object(forKey: Keys.showPinButtonOnCalculator) as? Bool ?? true
6676
showDockIcon = d.object(forKey: Keys.showDockIcon) as? Bool ?? true
6777
isPinned = d.object(forKey: Keys.isPinned) as? Bool ?? false
78+
webInjectionDisabled = d.object(forKey: Keys.webInjectionDisabled) as? Bool ?? false
79+
calculatorImageHidden = d.object(forKey: Keys.calculatorImageHidden) as? Bool ?? false
6880
}
6981
}

NumWorksMac/Core/Updaters/EpsilonUpdateChecker.swift

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ struct EpsilonUpdateChecker {
3232
)
3333
}
3434

35-
// Parses NumWorks' official download page, finds all simulator zip URLs, and returns the highest version.
35+
// Parses NumWorks' download page for cdn.numworks.com zip URLs (e.g. numworks-simulator-*.zip or numworks-graphing-emulator-*.zip).
36+
// Picks the URL whose filename contains the highest X.Y.Z version.
3637
static func fetchLatestRemoteURL() async throws -> URL {
3738
let page = URL(string: "https://www.numworks.com/simulator/download/")!
3839
var req = URLRequest(url: page)
@@ -48,24 +49,19 @@ struct EpsilonUpdateChecker {
4849
throw Error.invalidResponse
4950
}
5051

51-
let pattern = #"https:\/\/cdn\.numworks\.com\/[A-Za-z0-9_-]+\/numworks-simulator-(\d+\.\d+\.\d+)\.zip"#
52-
let r = try NSRegularExpression(pattern: pattern)
52+
// Match any cdn.numworks.com ... .zip URL (e.g. .../numworks-graphing-emulator-25.2.2.zip or .../26.1.zip).
53+
let urlPattern = #"https://cdn\.numworks\.com/[^"'\s<>]+\.zip"#
54+
let urlRegex = try NSRegularExpression(pattern: urlPattern)
5355
let range = NSRange(html.startIndex..<html.endIndex, in: html)
54-
let matches = r.matches(in: html, range: range)
55-
guard !matches.isEmpty else {
56-
throw Error.couldNotExtractRemoteURL
57-
}
56+
let urlMatches = urlRegex.matches(in: html, range: range)
5857

5958
var best: (url: URL, ver: SemVer)?
6059

61-
for m in matches {
60+
for m in urlMatches {
6261
guard let urlRange = Range(m.range(at: 0), in: html) else { continue }
6362
let urlString = String(html[urlRange])
6463
guard let url = URL(string: urlString) else { continue }
65-
66-
guard let verRange = Range(m.range(at: 1), in: html) else { continue }
67-
let verString = String(html[verRange])
68-
guard let ver = SemVer(verString) else { continue }
64+
guard let ver = parseVersionFromFilename(url.lastPathComponent) else { continue }
6965

7066
if let currentBest = best {
7167
if ver > currentBest.ver { best = (url, ver) }
@@ -90,13 +86,34 @@ struct EpsilonUpdateChecker {
9086
extractVersionString(from: url.lastPathComponent)
9187
}
9288

89+
/// Extracts a version string (X.Y or X.Y.Z) from a filename and normalizes to X.Y.Z for SemVer.
9390
static func extractVersionString(from filename: String) -> String? {
94-
let pattern = #"(\d+)\.(\d+)\.(\d+)"#
95-
guard let r = try? NSRegularExpression(pattern: pattern) else { return nil }
91+
parseVersionFromFilename(filename)?.string
92+
}
93+
94+
/// Parses X.Y or X.Y.Z from a filename (e.g. "26.1.zip" or "numworks-simulator-25.2.2.zip"). Returns nil if no valid version.
95+
private static func parseVersionFromFilename(_ filename: String) -> SemVer? {
96+
// Prefer X.Y.Z then X.Y (treat as X.Y.0).
97+
let threePart = #"(\d+)\.(\d+)\.(\d+)"#
98+
// X.Y only when not part of X.Y.Z (e.g. 26.1 in "26.1.zip" yes; 26.1 in "26.1.2.zip" no)
99+
let twoPart = #"(\d+)\.(\d+)(?=\.zip|[^.\d]|$)"#
96100
let range = NSRange(filename.startIndex..<filename.endIndex, in: filename)
97-
guard let m = r.firstMatch(in: filename, range: range) else { return nil }
98-
guard let rr = Range(m.range(at: 0), in: filename) else { return nil }
99-
return String(filename[rr])
101+
if let r3 = try? NSRegularExpression(pattern: threePart),
102+
let m3 = r3.firstMatch(in: filename, range: range),
103+
let rr = Range(m3.range(at: 0), in: filename) {
104+
let s = String(filename[rr])
105+
return SemVer(s)
106+
}
107+
if let r2 = try? NSRegularExpression(pattern: twoPart),
108+
let m2 = r2.firstMatch(in: filename, range: range),
109+
let r0 = Range(m2.range(at: 0), in: filename),
110+
let r1 = Range(m2.range(at: 1), in: filename),
111+
let r2g = Range(m2.range(at: 2), in: filename) {
112+
let major = Int(filename[r1]) ?? 0
113+
let minor = Int(filename[r2g]) ?? 0
114+
return SemVer("\(major).\(minor).0")
115+
}
116+
return nil
100117
}
101118
}
102119

NumWorksMac/Core/Updaters/SimulatorUpdater.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ final class SimulatorUpdater: NSObject, ObservableObject, URLSessionDownloadDele
390390
}
391391

392392
private func restartApp() {
393+
Self.relaunchApplication()
394+
}
395+
396+
/// Use when a settings change requires an app restart (e.g. disable web injection, hide calculator image).
397+
static func relaunchApplication() {
393398
let appURL = Bundle.main.bundleURL
394399
let appPath = appURL.path
395400
let pid = ProcessInfo.processInfo.processIdentifier

NumWorksMac/UI/Calculator/CalculatorView.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ struct CalculatorView: View {
4444
var body: some View {
4545
ZStack(alignment: .topTrailing) {
4646
GeometryReader { geo in
47-
Image("CalculatorImage")
48-
.resizable()
49-
.scaledToFit()
50-
.frame(width: geo.size.width, height: geo.size.height)
47+
if !prefs.calculatorImageHidden {
48+
Image("CalculatorImage")
49+
.resizable()
50+
.scaledToFit()
51+
.frame(width: geo.size.width, height: geo.size.height)
52+
}
5153

5254
if OnLaunch.hasInstalledSimulator() {
5355
CalculatorWebView(

NumWorksMac/UI/Calculator/CalculatorWebView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ struct CalculatorWebView: NSViewRepresentable {
1111

1212
func makeNSView(context: Context) -> WKWebView {
1313
let config = WKWebViewConfiguration()
14-
WebInjection.scripts().forEach { config.userContentController.addUserScript($0) }
14+
if !Preferences.shared.webInjectionDisabled {
15+
WebInjection.scripts().forEach { config.userContentController.addUserScript($0) }
16+
}
1517
config.userContentController.add(context.coordinator, name: "nwSize")
1618

1719
let webView = WKWebView(frame: .zero, configuration: config)

NumWorksMac/UI/Settings/SettingsView.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,24 @@ private struct AppUpdateSettingsPane: View {
252252
}
253253
}
254254

255+
private enum RelaunchSetting {
256+
case webInjection
257+
case calculatorImage
258+
}
259+
255260
private struct EpsilonUpdateSettingsPane: View {
261+
@ObservedObject private var prefs = Preferences.shared
256262
@State private var currentSimulatorVersion: String = ""
257263
@State private var isChecking = false
258264
@State private var showNoUpdatesAlert = false
259265

266+
@State private var webInjectionDisabledLocal: Bool = false
267+
@State private var calculatorImageHiddenLocal: Bool = false
268+
@State private var showRelaunchAlert = false
269+
@State private var pendingRelaunchSetting: RelaunchSetting?
270+
@State private var isReverting = false
271+
@State private var isSyncingFromPrefs = false
272+
260273
var body: some View {
261274
VStack(alignment: .leading, spacing: 12) {
262275
Text("Epsilon Update")
@@ -283,6 +296,24 @@ private struct EpsilonUpdateSettingsPane: View {
283296
}
284297
.disabled(isChecking)
285298

299+
Text("Simulator")
300+
.fontWeight(.bold)
301+
.padding(.top, 8)
302+
303+
Toggle("Disable web injection", isOn: $webInjectionDisabledLocal)
304+
.onChange(of: webInjectionDisabledLocal) { _, _ in
305+
guard !isReverting, !isSyncingFromPrefs else { return }
306+
pendingRelaunchSetting = .webInjection
307+
showRelaunchAlert = true
308+
}
309+
310+
Toggle("Disable calculator image", isOn: $calculatorImageHiddenLocal)
311+
.onChange(of: calculatorImageHiddenLocal) { _, _ in
312+
guard !isReverting, !isSyncingFromPrefs else { return }
313+
pendingRelaunchSetting = .calculatorImage
314+
showRelaunchAlert = true
315+
}
316+
286317
Spacer()
287318
}
288319
.frame(maxWidth: .infinity, alignment: .leading)
@@ -298,6 +329,37 @@ private struct EpsilonUpdateSettingsPane: View {
298329
}
299330
.onAppear {
300331
currentSimulatorVersion = simulatorVersionString()
332+
isSyncingFromPrefs = true
333+
webInjectionDisabledLocal = prefs.webInjectionDisabled
334+
calculatorImageHiddenLocal = prefs.calculatorImageHidden
335+
DispatchQueue.main.async {
336+
isSyncingFromPrefs = false
337+
}
338+
}
339+
.alert("The app needs to relaunch", isPresented: $showRelaunchAlert) {
340+
Button("Undo") {
341+
isReverting = true
342+
if let pending = pendingRelaunchSetting {
343+
switch pending {
344+
case .webInjection: webInjectionDisabledLocal = prefs.webInjectionDisabled
345+
case .calculatorImage: calculatorImageHiddenLocal = prefs.calculatorImageHidden
346+
}
347+
}
348+
pendingRelaunchSetting = nil
349+
isReverting = false
350+
}
351+
Button("Relaunch") {
352+
if let pending = pendingRelaunchSetting {
353+
switch pending {
354+
case .webInjection: prefs.webInjectionDisabled = webInjectionDisabledLocal
355+
case .calculatorImage: prefs.calculatorImageHidden = calculatorImageHiddenLocal
356+
}
357+
}
358+
pendingRelaunchSetting = nil
359+
SimulatorUpdater.relaunchApplication()
360+
}
361+
} message: {
362+
Text("Your change will take effect after the app restarts.")
301363
}
302364
.alert("No updates available", isPresented: $showNoUpdatesAlert) {
303365
Button("OK") {}
@@ -336,7 +398,7 @@ private struct EpsilonUpdateSettingsPane: View {
336398

337399
currentSimulatorVersion = simulatorVersionString()
338400
} catch {
339-
print("[Settings] epsilon update check failed: \(error)")
401+
print("[EpsilonUpdateChecker] error: \(error)")
340402
}
341403
}
342404
}

0 commit comments

Comments
 (0)