Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions NotchIA.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1289,15 +1289,15 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20905;
CURRENT_PROJECT_VERSION = 20906;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NotchIAXPCHelper/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 2.9.5;
MARKETING_VERSION = 2.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -1315,15 +1315,15 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20905;
CURRENT_PROJECT_VERSION = 20906;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NotchIAXPCHelper/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 2.9.5;
MARKETING_VERSION = 2.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -1474,7 +1474,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20905;
CURRENT_PROJECT_VERSION = 20906;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\"";
DEVELOPMENT_TEAM = "";
Expand Down Expand Up @@ -1502,7 +1502,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 2.9.5;
MARKETING_VERSION = 2.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -1530,7 +1530,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = YES;
CURRENT_PROJECT_VERSION = 20905;
CURRENT_PROJECT_VERSION = 20906;
DEAD_CODE_STRIPPING = YES;
DEPLOYMENT_POSTPROCESSING = YES;
DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\"";
Expand Down Expand Up @@ -1558,7 +1558,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 2.9.5;
MARKETING_VERSION = 2.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
12 changes: 11 additions & 1 deletion NotchIA/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1955,9 +1955,19 @@ final class PomodoroManager: ObservableObject {
private func handleTick(_ now: Date) {
guard isRunning else { return }

let elapsed = max(1, Int(now.timeIntervalSince(lastTickDate).rounded(.down)))
let rawElapsed = now.timeIntervalSince(lastTickDate)
lastTickDate = now

// A gap over a minute means the Mac was asleep (the runloop timer
// doesn't fire during sleep and delivers ONE tick on wake). Replaying
// it second-by-second froze the main thread for the whole catch-up
// (two Defaults writes per simulated second — 1800 iterations for a
// 30-min nap) and chain-completed phases with a burst of beeps and
// sneak peeks. The user wasn't working while the Mac slept — treat
// sleep as a pause and resume from where the timer stood.
guard rawElapsed < 60 else { return }

let elapsed = max(1, Int(rawElapsed.rounded(.down)))
for _ in 0..<elapsed {
tickOneSecond()
}
Expand Down
20 changes: 10 additions & 10 deletions NotchIA/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -15060,30 +15060,30 @@
}
}
},
"Dernière vérification : %@. Re-vérifié automatiquement toutes les 24 h." : {
"Dernière vérification : %@. Re-vérifié automatiquement tous les 7 jours." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zuletzt geprüft: %@. Wird alle 24 Std. automatisch erneut geprüft."
"value" : "Zuletzt geprüft: %@. Wird alle 7 Tage automatisch erneut geprüft."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Last checked: %@. Re-checked automatically every 24 h."
"value" : "Last checked: %@. Re-checked automatically every 7 days."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Última verificación: %@. Se vuelve a verificar automáticamente cada 24 h."
"value" : "Última verificación: %@. Se vuelve a verificar automáticamente cada 7 días."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dernière vérification : %@. Re-vérifié automatiquement toutes les 24 h."
"value" : "Dernière vérification : %@. Re-vérifié automatiquement tous les 7 jours."
}
}
}
Expand Down Expand Up @@ -23636,30 +23636,30 @@
}
}
},
"La licence est revérifiée toutes les 24 h auprès du serveur." : {
"La licence est revérifiée tous les 7 jours auprès du serveur." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Die Lizenz wird alle 24 Std. erneut beim Server überprüft."
"value" : "Die Lizenz wird alle 7 Tage erneut beim Server überprüft."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The license is rechecked with the server every 24 h."
"value" : "The license is rechecked with the server every 7 days."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "La licencia se vuelve a verificar con el servidor cada 24 h."
"value" : "La licencia se vuelve a verificar con el servidor cada 7 días."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "La licence est revérifiée toutes les 24 h auprès du serveur."
"value" : "La licence est revérifiée tous les 7 jours auprès du serveur."
}
}
}
Expand Down
63 changes: 47 additions & 16 deletions NotchIA/MediaControllers/NowPlayingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import OSLog

private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "NotchIA", category: "NowPlayingController")

// @MainActor like every other media controller: `handleAdapterUpdate` mutates
// the @Published `playbackState` from the adapter stream — without the
// annotation those writes landed on a background executor (data race +
// "Publishing changes from background threads" violations in SwiftUI).
@MainActor
final class NowPlayingController: ObservableObject, MediaControllerProtocol {
func updatePlaybackInfo() async {
await fetchFavoriteStateIfSupported()
Expand Down Expand Up @@ -191,16 +196,22 @@ final class NowPlayingController: ObservableObject, MediaControllerProtocol {

deinit {
streamTask?.cancel()

if let pipeHandler = self.pipeHandler {
Task { await pipeHandler.close()
}
}

if let process = self.process {
if process.isRunning {
process.terminate()
process.waitUntilExit()
// Reap off-thread: waitUntilExit() here ran on whichever
// thread dropped the controller (the main actor when the user
// switches media source in Settings) and froze the UI for the
// several hundred ms the perl adapter takes to shut down.
Task.detached(priority: .background) {
process.waitUntilExit()
}
}
}

Expand Down Expand Up @@ -445,7 +456,15 @@ actor JSONLinesPipeHandler {
private let pipe: Pipe
private let fileHandle: FileHandle
private var buffer = ""

/// In-flight `readData()` continuation. Resumed either by the
/// readabilityHandler (next chunk, hopped back into the actor) or by
/// `close()` with empty Data so the `processLines` loop exits cleanly.
/// Without this handoff, `close()` nil'ed the readabilityHandler while
/// `readData()` was suspended — the continuation leaked and the stream
/// task hung forever (fired on every media-controller switch in Settings).
private var pendingRead: CheckedContinuation<Data, Error>?
private var isClosed = false

init() {
self.pipe = Pipe()
self.fileHandle = pipe.fileHandleForReading
Expand Down Expand Up @@ -497,24 +516,36 @@ actor JSONLinesPipeHandler {
}
}

private func resumePendingRead(with data: Data) {
pendingRead?.resume(returning: data)
pendingRead = nil
}

private func readData() async throws -> Data {
// After close(), the file handle is gone — a readabilityHandler
// installed on it would never fire and the continuation would hang.
guard !isClosed else { return Data() }
return try await withCheckedThrowingContinuation { continuation in

fileHandle.readabilityHandler = { handle in
pendingRead = continuation
// The handler runs on an arbitrary GCD queue — never touch actor
// state from it directly; hop back into the actor to resume.
fileHandle.readabilityHandler = { [weak self] handle in
let data = handle.availableData
handle.readabilityHandler = nil
continuation.resume(returning: data)
guard let self else { return }
Task { await self.resumePendingRead(with: data) }
}
}
}

func close() async {
do {
fileHandle.readabilityHandler = nil
try fileHandle.close()
try pipe.fileHandleForWriting.close()
} catch {
log.error("Error closing pipe handler: \(error.localizedDescription)")
}

func close() {
isClosed = true
fileHandle.readabilityHandler = nil
// Unblock a suspended readData(): empty Data makes processLines'
// `guard !data.isEmpty else { break }` end the loop, so the stream
// task finishes instead of leaking its continuation.
resumePendingRead(with: Data())
try? fileHandle.close()
try? pipe.fileHandleForWriting.close()
}
}
15 changes: 9 additions & 6 deletions NotchIA/components/Calendar/NotchIACalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -461,13 +461,16 @@ struct CalendarView: View {

/// Toggle calendar visibility for filter menu.
private func toggleCalendarSelection(_ id: String) {
if calendarManager.selectedCalendarIDs.contains(id) {
calendarManager.selectedCalendarIDs.remove(id)
} else {
calendarManager.selectedCalendarIDs.insert(id)
}
guard let calendar = calendarManager.allCalendars.first(where: { $0.id == id }) else { return }
let isSelected = calendarManager.selectedCalendarIDs.contains(id)
Task {
await calendarManager.updateEvents()
// Route through setCalendarSelected: it persists to Defaults and
// rebuilds the private `selectedCalendars` array that
// updateEvents()/updateMonthEvents() actually filter on. Mutating
// selectedCalendarIDs directly only moved the checkmark — the
// event list never changed and the toggle was discarded on the
// next notch open (updateSelectedCalendars re-reads Defaults).
await calendarManager.setCalendarSelected(calendar, isSelected: !isSelected)
}
}

Expand Down
6 changes: 4 additions & 2 deletions NotchIA/components/Settings/LicenseSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,12 @@ struct LicenseSettings: View {
}

private var footerHint: String {
// 7 jours = LicenseManager.revocationInterval. L'UI annonçait « 24 h »
// alors que le check serveur est hebdomadaire.
if let last = license.lastVerifiedAt {
return String(localized: "Dernière vérification : \(formattedDateTime(last)). Re-vérifié automatiquement toutes les 24 h.")
return String(localized: "Dernière vérification : \(formattedDateTime(last)). Re-vérifié automatiquement tous les 7 jours.")
}
return String(localized: "La licence est revérifiée toutes les 24 h auprès du serveur.")
return String(localized: "La licence est revérifiée tous les 7 jours auprès du serveur.")
}

private func activate() {
Expand Down
8 changes: 8 additions & 0 deletions NotchIA/components/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,12 @@ struct Shelf: View {
if !license.state.isPro {
proRequiredBanner()
}
// Shelf est Pro : les réglages restent visibles mais inertes en
// compte free (le banner et son bouton « Voir Pro » restent
// actifs, hors du Group). Sans ce gate un user free pouvait
// écrire les Defaults Shelf et lancer l'installation des outils
// yt-dlp/ffmpeg depuis la section téléchargement vidéo.
Group {
Section {
Defaults.Toggle(key: .notchiaShelf) {
Text("Activer Shelf")
Expand Down Expand Up @@ -1275,6 +1281,8 @@ struct Shelf: View {
}

videoDownloadSection
}
.disabled(!license.state.isPro)
}
.accentColor(.effectiveAccent)
.navigationTitle("Shelf")
Expand Down
9 changes: 4 additions & 5 deletions NotchIA/components/Settings/SettingsWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,10 @@ class SettingsWindowController: NSWindowController {
}
}

override func close() {
super.close()
relinquishFocus()
}

// NOTE: no `close()` override. `super.close()` already triggers
// `windowWillClose`, which calls `relinquishFocus()` — overriding here
// ran orderOut + setActivationPolicy(.accessory) twice per close.

private func relinquishFocus() {
window?.orderOut(nil)

Expand Down
Loading
Loading