From 0f45f2d066d5badabefd525decababbcbded26f8 Mon Sep 17 00:00:00 2001 From: superdooper86 Date: Wed, 22 Jul 2026 16:31:01 +0200 Subject: [PATCH] feat: make audio retention configurable --- Meetingnotes.xcodeproj/project.pbxproj | 8 ++++---- meetingnotes/Managers/AudioManager.swift | 6 ++++-- meetingnotes/Managers/LocalStorageManager.swift | 4 ++-- meetingnotes/Managers/UserDefaultsManager.swift | 9 +++++++++ meetingnotes/Models/Settings.swift | 5 +++++ meetingnotes/ViewModels/SettingsViewModel.swift | 1 + meetingnotes/Views/SettingsView.swift | 17 +++++++++++++++++ 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index c32433a..cdd6a81 100644 --- a/Meetingnotes.xcodeproj/project.pbxproj +++ b/Meetingnotes.xcodeproj/project.pbxproj @@ -276,7 +276,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -290,7 +290,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.14; + MARKETING_VERSION = 1.1.15; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; @@ -312,7 +312,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -326,7 +326,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.14; + MARKETING_VERSION = 1.1.15; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index 31687fc..0b189c4 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -102,12 +102,14 @@ final class AudioManager: NSObject, ObservableObject { let audioFolder = preserveAudioFiles(completedFiles, meetingID: completedMeetingID) lastRecoveryAudioFolderName = audioFolder?.lastPathComponent if !failures.isEmpty { + let retentionDays = UserDefaultsManager.shared.audioRetentionDays + let retentionUnit = retentionDays == 1 ? "day" : "days" let recoveryMessage = audioFolder == nil ? " The audio remains in the app's temporary folder." - : " Audio was kept for three days. Use Show Audio Folder in the Meetingnotes menu to find it." + : " Audio was kept for \(retentionDays) \(retentionUnit). Use Show Audio Folder in the Meetingnotes menu to find it." errorMessage = "Transcription failed for " + failures.joined(separator: "; ") + recoveryMessage } else if audioFolder == nil, !completedFiles.isEmpty { - errorMessage = "The transcript completed, but Meetingnotes could not move the audio into its three-day storage folder." + errorMessage = "The transcript completed, but Meetingnotes could not move the audio into its retention folder." } return updated } diff --git a/meetingnotes/Managers/LocalStorageManager.swift b/meetingnotes/Managers/LocalStorageManager.swift index a813bca..0c81c13 100644 --- a/meetingnotes/Managers/LocalStorageManager.swift +++ b/meetingnotes/Managers/LocalStorageManager.swift @@ -17,7 +17,6 @@ class LocalStorageManager { private let meetingsDirectory: URL private let templatesDirectory: URL private let recoveryDirectory: URL - private let audioRetentionInterval: TimeInterval = 3 * 24 * 60 * 60 private init() { // Get the app's documents directory @@ -189,7 +188,8 @@ class LocalStorageManager { options: [.skipsHiddenFiles] ) else { return } - let expirationDate = now.addingTimeInterval(-audioRetentionInterval) + let retentionInterval = TimeInterval(UserDefaultsManager.shared.audioRetentionDays) * 24 * 60 * 60 + let expirationDate = now.addingTimeInterval(-retentionInterval) for folder in folders { guard (try? folder.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true else { continue } let audioFiles = recoveryAudioFiles(in: folder) diff --git a/meetingnotes/Managers/UserDefaultsManager.swift b/meetingnotes/Managers/UserDefaultsManager.swift index aeaadf0..40dbfad 100644 --- a/meetingnotes/Managers/UserDefaultsManager.swift +++ b/meetingnotes/Managers/UserDefaultsManager.swift @@ -23,6 +23,7 @@ class UserDefaultsManager { static let transcriptionModel = "transcriptionModel" static let muteDeckAPIEnabled = "muteDeckAPIEnabled" static let muteDeckAPIPort = "muteDeckAPIPort" + static let audioRetentionDays = "audioRetentionDays" } // MARK: - User Blurb @@ -94,4 +95,12 @@ class UserDefaultsManager { } set { userDefaults.set(newValue, forKey: Keys.muteDeckAPIPort) } } + + var audioRetentionDays: Int { + get { + guard userDefaults.object(forKey: Keys.audioRetentionDays) != nil else { return 3 } + return min(max(userDefaults.integer(forKey: Keys.audioRetentionDays), 1), 365) + } + set { userDefaults.set(min(max(newValue, 1), 365), forKey: Keys.audioRetentionDays) } + } } diff --git a/meetingnotes/Models/Settings.swift b/meetingnotes/Models/Settings.swift index 07054d0..32f77a3 100644 --- a/meetingnotes/Models/Settings.swift +++ b/meetingnotes/Models/Settings.swift @@ -54,6 +54,11 @@ struct Settings: Codable { set { UserDefaultsManager.shared.muteDeckAPIPort = newValue } } + var audioRetentionDays: Int { + get { UserDefaultsManager.shared.audioRetentionDays } + set { UserDefaultsManager.shared.audioRetentionDays = newValue } + } + // System prompt default loading static func defaultSystemPrompt() -> String { guard let path = Bundle.main.path(forResource: "DefaultSystemPrompt", ofType: "txt"), diff --git a/meetingnotes/ViewModels/SettingsViewModel.swift b/meetingnotes/ViewModels/SettingsViewModel.swift index eb04b78..03d5329 100644 --- a/meetingnotes/ViewModels/SettingsViewModel.swift +++ b/meetingnotes/ViewModels/SettingsViewModel.swift @@ -71,6 +71,7 @@ class SettingsViewModel: ObservableObject { // via computed properties when they're modified let coderSaved = KeychainHelper.shared.saveCoderAPIKey(settings.coderAPIKey) LocalAPIServer.shared.applyConfiguration() + LocalStorageManager.shared.purgeExpiredAudioFolders() if showMessage { if coderSaved { diff --git a/meetingnotes/Views/SettingsView.swift b/meetingnotes/Views/SettingsView.swift index 10068f4..e95f71f 100644 --- a/meetingnotes/Views/SettingsView.swift +++ b/meetingnotes/Views/SettingsView.swift @@ -128,6 +128,23 @@ struct SettingsView: View { Text("Meeting Storage") .font(.headline) + LabeledContent("Audio retention") { + Stepper( + value: $viewModel.settings.audioRetentionDays, + in: 1...365 + ) { + Text("\(viewModel.settings.audioRetentionDays) \(viewModel.settings.audioRetentionDays == 1 ? "day" : "days")") + .monospacedDigit() + .frame(minWidth: 70, alignment: .trailing) + } + } + + Button { + LocalStorageManager.shared.showAudioFolderInFinder() + } label: { + Label("Show Audio Folder", systemImage: "folder") + } + Button { showingMeetingImporter = true } label: {