feat: prepare stable signed updates

This commit is contained in:
2026-07-15 15:14:57 +02:00
parent d8f5c33141
commit c7d4db3dd0
6 changed files with 112 additions and 16 deletions
+5 -5
View File
@@ -276,7 +276,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
DEVELOPMENT_TEAM = ML6HYR5LUR; DEVELOPMENT_TEAM = ML6HYR5LUR;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -290,7 +290,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.1.2; MARKETING_VERSION = 1.1.3;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes; PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
@@ -312,7 +312,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
DEVELOPMENT_TEAM = ML6HYR5LUR; DEVELOPMENT_TEAM = ML6HYR5LUR;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -326,7 +326,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.1.2; MARKETING_VERSION = 1.1.3;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes; PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
@@ -366,7 +366,7 @@
repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 2.7.1; minimumVersion = 2.9.4;
}; };
}; };
CCC33F7F2E236B6F00EDE382 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { CCC33F7F2E236B6F00EDE382 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git", "location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : { "state" : {
"revision" : "df074165274afaa39539c05d57b0832620775b11", "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7",
"version" : "2.7.1" "version" : "2.9.4"
} }
}, },
{ {
+6 -2
View File
@@ -12,9 +12,13 @@
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Meetingnotes needs access to your microphone for transcription.</string> <string>Meetingnotes needs access to your microphone for transcription.</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/superdooper86/meetingnotes/main/appcast.xml</string> <string>https://github.com/superdooper86/meetingnotes/releases/latest/download/appcast.xml</string>
<key>SUPublicEDKey</key> <key>SUPublicEDKey</key>
<string>BVXHOV8ZxPxKZ1swhFndymzew9nyd3si7849JA9cqsg=</string> <string>9ZuN9G9ERB3Qoyyd/4FsF+6LMUv5jzAGP26OXAHBiW0=</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUAutomaticallyUpdate</key>
<true/>
<key>SUEnableInstallerLauncherService</key> <key>SUEnableInstallerLauncherService</key>
<true/> <true/>
</dict> </dict>
@@ -6,6 +6,11 @@ import Foundation
/// Manages local file storage for meetings and app data /// Manages local file storage for meetings and app data
class LocalStorageManager { class LocalStorageManager {
static let shared = LocalStorageManager() static let shared = LocalStorageManager()
struct MeetingImportResult {
let importedCount: Int
let skippedCount: Int
}
private let documentsDirectory: URL private let documentsDirectory: URL
private let meetingsDirectory: URL private let meetingsDirectory: URL
@@ -47,7 +52,11 @@ class LocalStorageManager {
// Write atomically using a temp file then replace // Write atomically using a temp file then replace
let tmpURL = fileURL.appendingPathExtension("tmp") let tmpURL = fileURL.appendingPathExtension("tmp")
try data.write(to: tmpURL, options: .atomic) try data.write(to: tmpURL, options: .atomic)
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL, backupItemName: nil, options: [], resultingItemURL: nil) if FileManager.default.fileExists(atPath: fileURL.path) {
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL)
} else {
try FileManager.default.moveItem(at: tmpURL, to: fileURL)
}
print("✅ Saved meeting: \(meeting.id)") print("✅ Saved meeting: \(meeting.id)")
return true return true
@@ -128,6 +137,51 @@ class LocalStorageManager {
return false return false
} }
} }
/// Imports meeting JSON files from a folder selected by the user.
func importMeetings(from directory: URL) throws -> MeetingImportResult {
let didStartAccess = directory.startAccessingSecurityScopedResource()
defer {
if didStartAccess {
directory.stopAccessingSecurityScopedResource()
}
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let existingIDs = Set(loadMeetings().map(\.id))
var importedIDs = Set<UUID>()
var skippedCount = 0
guard let enumerator = FileManager.default.enumerator(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles, .skipsPackageDescendants]
) else {
throw CocoaError(.fileReadUnknown)
}
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "json" {
do {
let values = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
guard values.isRegularFile == true else { continue }
let data = try Data(contentsOf: fileURL)
let meeting = try decoder.decode(Meeting.self, from: data)
guard meeting.dataVersion <= Meeting.currentDataVersion,
!existingIDs.contains(meeting.id),
!importedIDs.contains(meeting.id),
saveMeeting(meeting) else {
skippedCount += 1
continue
}
importedIDs.insert(meeting.id)
} catch {
skippedCount += 1
}
}
return MeetingImportResult(importedCount: importedIDs.count, skippedCount: skippedCount)
}
// MARK: - Template Management // MARK: - Template Management
@@ -146,7 +200,11 @@ class LocalStorageManager {
// Write atomically using a temp file then replace // Write atomically using a temp file then replace
let tmpURL = fileURL.appendingPathExtension("tmp") let tmpURL = fileURL.appendingPathExtension("tmp")
try data.write(to: tmpURL, options: .atomic) try data.write(to: tmpURL, options: .atomic)
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL, backupItemName: nil, options: [], resultingItemURL: nil) if FileManager.default.fileExists(atPath: fileURL.path) {
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL)
} else {
try FileManager.default.moveItem(at: tmpURL, to: fileURL)
}
print("✅ Saved template: \(template.id)") print("✅ Saved template: \(template.id)")
return true return true
@@ -241,4 +299,4 @@ class LocalStorageManager {
var meetingsDirectoryURL: URL { var meetingsDirectoryURL: URL {
meetingsDirectory meetingsDirectory
} }
} }
+38
View File
@@ -1,10 +1,14 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
struct SettingsView: View { struct SettingsView: View {
@ObservedObject var viewModel: SettingsViewModel @ObservedObject var viewModel: SettingsViewModel
@StateObject private var localAPIServer = LocalAPIServer.shared @StateObject private var localAPIServer = LocalAPIServer.shared
@State private var showingTemplateManager = false @State private var showingTemplateManager = false
@State private var confirmingTokenRegeneration = false @State private var confirmingTokenRegeneration = false
@State private var showingMeetingImporter = false
@State private var meetingImportMessage = ""
@State private var showingMeetingImportResult = false
@Binding var navigationPath: NavigationPath @Binding var navigationPath: NavigationPath
init(viewModel: SettingsViewModel, navigationPath: Binding<NavigationPath> = .constant(NavigationPath())) { init(viewModel: SettingsViewModel, navigationPath: Binding<NavigationPath> = .constant(NavigationPath())) {
@@ -119,6 +123,17 @@ struct SettingsView: View {
} }
} }
} }
VStack(alignment: .leading, spacing: 8) {
Text("Meeting Storage")
.font(.headline)
Button {
showingMeetingImporter = true
} label: {
Label("Import Meetings...", systemImage: "square.and.arrow.down")
}
}
// Note Templates Section: only the Manage Templates button // Note Templates Section: only the Manage Templates button
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -276,6 +291,29 @@ struct SettingsView: View {
} message: { } message: {
Text(viewModel.saveMessage) Text(viewModel.saveMessage)
} }
.fileImporter(
isPresented: $showingMeetingImporter,
allowedContentTypes: [.folder],
allowsMultipleSelection: false
) { result in
do {
guard let directory = try result.get().first else { return }
let importResult = try LocalStorageManager.shared.importMeetings(from: directory)
meetingImportMessage = "Imported \(importResult.importedCount) meeting\(importResult.importedCount == 1 ? "" : "s")."
if importResult.skippedCount > 0 {
meetingImportMessage += " Skipped \(importResult.skippedCount) existing or invalid file\(importResult.skippedCount == 1 ? "" : "s")."
}
NotificationCenter.default.post(name: .meetingSaved, object: nil)
} catch {
meetingImportMessage = "Meeting import failed: \(error.localizedDescription)"
}
showingMeetingImportResult = true
}
.alert("Meeting Import", isPresented: $showingMeetingImportResult) {
Button("OK") { }
} message: {
Text(meetingImportMessage)
}
.confirmationDialog("Regenerate API token?", isPresented: $confirmingTokenRegeneration, titleVisibility: .visible) { .confirmationDialog("Regenerate API token?", isPresented: $confirmingTokenRegeneration, titleVisibility: .visible) {
Button("Regenerate", role: .destructive) { Button("Regenerate", role: .destructive) {
viewModel.regenerateMuteDeckAPIToken() viewModel.regenerateMuteDeckAPIToken()
-4
View File
@@ -4,10 +4,6 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.owen.meetingnotes</string>
</array>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.device.screen-capture</key> <key>com.apple.security.device.screen-capture</key>