feat: prepare stable signed updates
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user