feat: make audio retention configurable
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user