Guter Zwischenstand
This commit is contained in:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -187,7 +187,7 @@ struct AddMomentView: View {
|
|||||||
|
|
||||||
let moment = Moment(text: trimmed, type: selectedType, person: person)
|
let moment = Moment(text: trimmed, type: selectedType, person: person)
|
||||||
modelContext.insert(moment)
|
modelContext.insert(moment)
|
||||||
person.moments.append(moment)
|
person.moments?.append(moment)
|
||||||
|
|
||||||
guard addToCalendar else {
|
guard addToCalendar else {
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -201,7 +201,7 @@ struct AddMomentView: View {
|
|||||||
person: person
|
person: person
|
||||||
)
|
)
|
||||||
modelContext.insert(calEntry)
|
modelContext.insert(calEntry)
|
||||||
person.logEntries.append(calEntry)
|
person.logEntries?.append(calEntry)
|
||||||
|
|
||||||
// Kein async/await — Callback-API vermeidet "unsafeForcedSync"
|
// Kein async/await — Callback-API vermeidet "unsafeForcedSync"
|
||||||
createCalendarEvent(notes: trimmed)
|
createCalendarEvent(notes: trimmed)
|
||||||
|
|||||||
@@ -2,21 +2,75 @@ import Foundation
|
|||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
/// Gemeinsame App-Group-Konfiguration für Hauptapp und Share Extension.
|
/// Gemeinsame App-Group-Konfiguration für Hauptapp und Share Extension.
|
||||||
/// Die App-Group-ID muss in beiden Targets als Capability eingetragen sein.
|
|
||||||
enum AppGroup {
|
enum AppGroup {
|
||||||
static let identifier = "group.nahbar.shared"
|
static let identifier = "group.nahbar.shared"
|
||||||
|
|
||||||
/// URL des geteilten Containers. Fällt auf das Documents-Verzeichnis zurück,
|
/// Shared UserDefaults für die Kommunikation zwischen Hauptapp und Extension.
|
||||||
/// falls die App Group noch nicht eingerichtet ist (z.B. in frühen Dev-Builds).
|
static var userDefaults: UserDefaults {
|
||||||
static var containerURL: URL {
|
UserDefaults(suiteName: identifier) ?? .standard
|
||||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier)
|
|
||||||
?? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeModelContainer() throws -> ModelContainer {
|
// MARK: - Hauptapp: Standard-Store (Daten bleiben erhalten)
|
||||||
|
|
||||||
|
/// ModelContainer für die Hauptapp – nutzt Standard-Speicherort (Daten-kompatibel).
|
||||||
|
/// Versucht CloudKit; fällt bei Bedarf auf lokalen Store zurück.
|
||||||
|
static let icloudSyncKey = "icloudSyncEnabled"
|
||||||
|
|
||||||
|
static func makeMainContainer() -> ModelContainer {
|
||||||
let schema = Schema([Person.self, Moment.self, LogEntry.self])
|
let schema = Schema([Person.self, Moment.self, LogEntry.self])
|
||||||
let storeURL = containerURL.appendingPathComponent("nahbar.store")
|
let icloudEnabled = UserDefaults.standard.bool(forKey: icloudSyncKey)
|
||||||
let config = ModelConfiguration(schema: schema, url: storeURL)
|
let cloudKit: ModelConfiguration.CloudKitDatabase = icloudEnabled ? .automatic : .none
|
||||||
return try ModelContainer(for: schema, configurations: [config])
|
let config = ModelConfiguration(schema: schema, cloudKitDatabase: cloudKit)
|
||||||
|
if let container = try? ModelContainer(for: schema, configurations: [config]) {
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
// Fallback: lokal ohne CloudKit
|
||||||
|
let localConfig = ModelConfiguration(schema: schema, cloudKitDatabase: .none)
|
||||||
|
if let container = try? ModelContainer(for: schema, configurations: [localConfig]) {
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
return try! ModelContainer(for: schema, configurations: [ModelConfiguration(isStoredInMemoryOnly: true)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Share Extension: App-Group-Store (nur für Extension)
|
||||||
|
|
||||||
|
/// Speichert eine Nachricht als ausstehenden Moment in der App Group.
|
||||||
|
/// Die Hauptapp importiert diese beim nächsten Start.
|
||||||
|
static func enqueueMoment(personName: String, text: String, type: String) {
|
||||||
|
var queue = pendingMoments
|
||||||
|
queue.append(["personName": personName, "text": text, "type": type,
|
||||||
|
"createdAt": ISO8601DateFormatter().string(from: Date())])
|
||||||
|
if let data = try? JSONSerialization.data(withJSONObject: queue) {
|
||||||
|
userDefaults.set(data, forKey: "pendingMoments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var pendingMoments: [[String: String]] {
|
||||||
|
guard let data = userDefaults.data(forKey: "pendingMoments"),
|
||||||
|
let array = try? JSONSerialization.jsonObject(with: data) as? [[String: String]]
|
||||||
|
else { return [] }
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clearPendingMoments() {
|
||||||
|
userDefaults.removeObject(forKey: "pendingMoments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Personenliste für Extension
|
||||||
|
|
||||||
|
/// Hauptapp schreibt beim Start die Personenliste in UserDefaults,
|
||||||
|
/// damit die Extension sie ohne Store-Zugriff lesen kann.
|
||||||
|
static func savePeopleList(_ people: [Person]) {
|
||||||
|
let list = people.map { ["id": $0.id.uuidString, "name": $0.name, "tag": $0.tagRaw] }
|
||||||
|
if let data = try? JSONSerialization.data(withJSONObject: list) {
|
||||||
|
userDefaults.set(data, forKey: "cachedPeople")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var cachedPeople: [[String: String]] {
|
||||||
|
guard let data = userDefaults.data(forKey: "cachedPeople"),
|
||||||
|
let array = try? JSONSerialization.jsonObject(with: data) as? [[String: String]]
|
||||||
|
else { return [] }
|
||||||
|
return array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,13 @@ struct ContentView: View {
|
|||||||
suggestionDateStr = ISO8601DateFormatter().string(from: Date())
|
suggestionDateStr = ISO8601DateFormatter().string(from: Date())
|
||||||
let entry = LogEntry(type: .call, title: "Anruf mit \(person.firstName)", person: person)
|
let entry = LogEntry(type: .call, title: "Anruf mit \(person.firstName)", person: person)
|
||||||
modelContext.insert(entry)
|
modelContext.insert(entry)
|
||||||
person.logEntries.append(entry)
|
person.logEntries?.append(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
syncPeopleCache()
|
||||||
|
importPendingMoments()
|
||||||
if !onboardingDone {
|
if !onboardingDone {
|
||||||
showingOnboarding = true
|
showingOnboarding = true
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +68,14 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { _, phase in
|
.onChange(of: scenePhase) { _, phase in
|
||||||
if phase == .active { checkCallWindow() }
|
if phase == .active {
|
||||||
|
syncPeopleCache()
|
||||||
|
importPendingMoments()
|
||||||
|
checkCallWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: persons) { _, _ in
|
||||||
|
syncPeopleCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +92,32 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schreibt die aktuelle Personenliste in den App-Group-Cache für die Share Extension.
|
||||||
|
private func syncPeopleCache() {
|
||||||
|
AppGroup.savePeopleList(persons)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Importiert Momente, die über die Share Extension eingereiht wurden.
|
||||||
|
private func importPendingMoments() {
|
||||||
|
let pending = AppGroup.pendingMoments
|
||||||
|
guard !pending.isEmpty else { return }
|
||||||
|
for entry in pending {
|
||||||
|
guard let name = entry["personName"],
|
||||||
|
let text = entry["text"],
|
||||||
|
let typeRaw = entry["type"] else { continue }
|
||||||
|
let type_ = MomentType(rawValue: typeRaw) ?? .conversation
|
||||||
|
if let person = persons.first(where: { $0.name == name }) {
|
||||||
|
let moment = Moment(text: text, type: type_, person: person)
|
||||||
|
modelContext.insert(moment)
|
||||||
|
person.moments?.append(moment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pending.isEmpty {
|
||||||
|
try? modelContext.save()
|
||||||
|
AppGroup.clearPendingMoments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var suggestionShownToday: Bool {
|
private var suggestionShownToday: Bool {
|
||||||
guard !suggestionDateStr.isEmpty,
|
guard !suggestionDateStr.isEmpty,
|
||||||
let date = ISO8601DateFormatter().date(from: suggestionDateStr) else { return false }
|
let date = ISO8601DateFormatter().date(from: suggestionDateStr) else { return false }
|
||||||
|
|||||||
+19
-20
@@ -59,23 +59,23 @@ enum MomentType: String, CaseIterable, Codable {
|
|||||||
|
|
||||||
@Model
|
@Model
|
||||||
class Person {
|
class Person {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var name: String
|
var name: String = ""
|
||||||
var tagRaw: String
|
var tagRaw: String = PersonTag.other.rawValue
|
||||||
var birthday: Date?
|
var birthday: Date?
|
||||||
var occupation: String?
|
var occupation: String?
|
||||||
var location: String?
|
var location: String?
|
||||||
var interests: String?
|
var interests: String?
|
||||||
var generalNotes: String?
|
var generalNotes: String?
|
||||||
var nudgeFrequencyRaw: String
|
var nudgeFrequencyRaw: String = NudgeFrequency.monthly.rawValue
|
||||||
var photoData: Data?
|
var photoData: Data?
|
||||||
var nextStep: String?
|
var nextStep: String?
|
||||||
var nextStepCompleted: Bool
|
var nextStepCompleted: Bool = false
|
||||||
var nextStepReminderDate: Date?
|
var nextStepReminderDate: Date?
|
||||||
var lastSuggestedForCall: Date?
|
var lastSuggestedForCall: Date?
|
||||||
var createdAt: Date
|
var createdAt: Date = Date()
|
||||||
@Relationship(deleteRule: .cascade) var moments: [Moment]
|
@Relationship(deleteRule: .cascade) var moments: [Moment]? = []
|
||||||
@Relationship(deleteRule: .cascade) var logEntries: [LogEntry]
|
@Relationship(deleteRule: .cascade) var logEntries: [LogEntry]? = []
|
||||||
|
|
||||||
init(
|
init(
|
||||||
name: String,
|
name: String,
|
||||||
@@ -117,7 +117,7 @@ class Person {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lastMomentDate: Date? {
|
var lastMomentDate: Date? {
|
||||||
moments.sorted { $0.createdAt > $1.createdAt }.first?.createdAt
|
(moments ?? []).sorted { $0.createdAt > $1.createdAt }.first?.createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
var needsAttention: Bool {
|
var needsAttention: Bool {
|
||||||
@@ -125,7 +125,6 @@ class Person {
|
|||||||
if let last = lastMomentDate {
|
if let last = lastMomentDate {
|
||||||
return Date().timeIntervalSince(last) > Double(days * 86400)
|
return Date().timeIntervalSince(last) > Double(days * 86400)
|
||||||
}
|
}
|
||||||
// Never had a moment – show if added more than `days` ago
|
|
||||||
return Date().timeIntervalSince(createdAt) > Double(days * 86400)
|
return Date().timeIntervalSince(createdAt) > Double(days * 86400)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +155,11 @@ class Person {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sortedMoments: [Moment] {
|
var sortedMoments: [Moment] {
|
||||||
moments.sorted { $0.createdAt > $1.createdAt }
|
(moments ?? []).sorted { $0.createdAt > $1.createdAt }
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortedLogEntries: [LogEntry] {
|
var sortedLogEntries: [LogEntry] {
|
||||||
logEntries.sorted { $0.loggedAt > $1.loggedAt }
|
(logEntries ?? []).sorted { $0.loggedAt > $1.loggedAt }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +191,10 @@ enum LogEntryType: String, Codable {
|
|||||||
|
|
||||||
@Model
|
@Model
|
||||||
class LogEntry {
|
class LogEntry {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var typeRaw: String
|
var typeRaw: String = LogEntryType.nextStep.rawValue
|
||||||
var title: String
|
var title: String = ""
|
||||||
var loggedAt: Date
|
var loggedAt: Date = Date()
|
||||||
var person: Person?
|
var person: Person?
|
||||||
|
|
||||||
init(type: LogEntryType, title: String, person: Person? = nil) {
|
init(type: LogEntryType, title: String, person: Person? = nil) {
|
||||||
@@ -216,10 +215,10 @@ class LogEntry {
|
|||||||
|
|
||||||
@Model
|
@Model
|
||||||
class Moment {
|
class Moment {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var text: String
|
var text: String = ""
|
||||||
var typeRaw: String
|
var typeRaw: String = MomentType.conversation.rawValue
|
||||||
var createdAt: Date
|
var createdAt: Date = Date()
|
||||||
var person: Person?
|
var person: Person?
|
||||||
|
|
||||||
init(text: String, type: MomentType = .conversation, person: Person? = nil) {
|
init(text: String, type: MomentType = .conversation, person: Person? = nil) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ struct NahbarApp: App {
|
|||||||
.onAppear { applyTabBarAppearance(activeTheme) }
|
.onAppear { applyTabBarAppearance(activeTheme) }
|
||||||
.onChange(of: activeThemeIDRaw) { _, _ in applyTabBarAppearance(activeTheme) }
|
.onChange(of: activeThemeIDRaw) { _, _ in applyTabBarAppearance(activeTheme) }
|
||||||
}
|
}
|
||||||
.modelContainer(try! AppGroup.makeModelContainer())
|
.modelContainer(AppGroup.makeMainContainer())
|
||||||
.onChange(of: scenePhase) { _, phase in
|
.onChange(of: scenePhase) { _, phase in
|
||||||
if phase == .background {
|
if phase == .background {
|
||||||
appLockManager.lockIfEnabled()
|
appLockManager.lockIfEnabled()
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ struct PersonDetailView: View {
|
|||||||
if let step = person.nextStep {
|
if let step = person.nextStep {
|
||||||
let entry = LogEntry(type: .nextStep, title: step, person: person)
|
let entry = LogEntry(type: .nextStep, title: step, person: person)
|
||||||
modelContext.insert(entry)
|
modelContext.insert(entry)
|
||||||
person.logEntries.append(entry)
|
person.logEntries?.append(entry)
|
||||||
}
|
}
|
||||||
person.nextStepCompleted = true
|
person.nextStepCompleted = true
|
||||||
cancelReminder(for: person)
|
cancelReminder(for: person)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ struct SettingsView: View {
|
|||||||
@EnvironmentObject private var appLockManager: AppLockManager
|
@EnvironmentObject private var appLockManager: AppLockManager
|
||||||
|
|
||||||
@AppStorage("upcomingDaysAhead") private var daysAhead: Int = 7
|
@AppStorage("upcomingDaysAhead") private var daysAhead: Int = 7
|
||||||
|
@AppStorage("icloudSyncEnabled") private var icloudSyncEnabled: Bool = false
|
||||||
@AppStorage("aiBaseURL") private var aiBaseURL: String = AIConfig.fallback.baseURL
|
@AppStorage("aiBaseURL") private var aiBaseURL: String = AIConfig.fallback.baseURL
|
||||||
@AppStorage("aiAPIKey") private var aiAPIKey: String = AIConfig.fallback.apiKey
|
@AppStorage("aiAPIKey") private var aiAPIKey: String = AIConfig.fallback.apiKey
|
||||||
@AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model
|
@AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model
|
||||||
@@ -289,6 +290,47 @@ struct SettingsView: View {
|
|||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// iCloud
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
SectionHeader(title: "iCloud", icon: "icloud")
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("iCloud-Backup")
|
||||||
|
.font(.system(size: 15))
|
||||||
|
.foregroundStyle(theme.contentPrimary)
|
||||||
|
Text(icloudSyncEnabled
|
||||||
|
? "Daten werden mit iCloud synchronisiert"
|
||||||
|
: "Daten werden nur lokal gespeichert")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(theme.contentTertiary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Toggle("", isOn: $icloudSyncEnabled)
|
||||||
|
.tint(theme.accent)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
|
||||||
|
RowDivider()
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "info.circle")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(theme.contentTertiary)
|
||||||
|
Text("Änderung wird beim nächsten App-Start übernommen.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(theme.contentTertiary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
.background(theme.surfaceCard)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
|
||||||
// About
|
// About
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
SectionHeader(title: "Über nahbar", icon: "info.circle")
|
SectionHeader(title: "Über nahbar", icon: "info.circle")
|
||||||
@@ -297,8 +339,6 @@ struct SettingsView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsInfoRow(label: "Version", value: "1.0 Draft")
|
SettingsInfoRow(label: "Version", value: "1.0 Draft")
|
||||||
RowDivider()
|
RowDivider()
|
||||||
SettingsInfoRow(label: "Daten", value: "Lokal + iCloud")
|
|
||||||
RowDivider()
|
|
||||||
SettingsInfoRow(label: "Datenschutz", value: "Deine Daten verlassen nicht dein Gerät")
|
SettingsInfoRow(label: "Datenschutz", value: "Deine Daten verlassen nicht dein Gerät")
|
||||||
}
|
}
|
||||||
.background(theme.surfaceCard)
|
.background(theme.surfaceCard)
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>iCloud.Team.nahbar</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.icloud-services</key>
|
||||||
|
<array>
|
||||||
|
<string>CloudKit</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.nahbar.shared</string>
|
<string>group.nahbar.shared</string>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
|
||||||
|
/// Einfache Personen-Darstellung für die Share Extension (aus UserDefaults-Cache).
|
||||||
|
struct CachedPerson: Identifiable, Equatable {
|
||||||
|
let id: UUID
|
||||||
|
let name: String
|
||||||
|
let tag: String
|
||||||
|
}
|
||||||
|
|
||||||
struct ShareExtensionView: View {
|
struct ShareExtensionView: View {
|
||||||
let sharedText: String
|
let sharedText: String
|
||||||
@@ -7,8 +13,8 @@ struct ShareExtensionView: View {
|
|||||||
|
|
||||||
@State private var text: String
|
@State private var text: String
|
||||||
@State private var momentType: MomentType = .conversation
|
@State private var momentType: MomentType = .conversation
|
||||||
@State private var people: [Person] = []
|
@State private var people: [CachedPerson] = []
|
||||||
@State private var selectedPerson: Person?
|
@State private var selectedPerson: CachedPerson?
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@State private var isSaving = false
|
@State private var isSaving = false
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@@ -19,7 +25,7 @@ struct ShareExtensionView: View {
|
|||||||
self._text = State(initialValue: sharedText)
|
self._text = State(initialValue: sharedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filteredPeople: [Person] {
|
private var filteredPeople: [CachedPerson] {
|
||||||
searchText.isEmpty ? people : people.filter {
|
searchText.isEmpty ? people : people.filter {
|
||||||
$0.name.localizedCaseInsensitiveContains(searchText)
|
$0.name.localizedCaseInsensitiveContains(searchText)
|
||||||
}
|
}
|
||||||
@@ -46,14 +52,15 @@ struct ShareExtensionView: View {
|
|||||||
|
|
||||||
Section("Kontakt") {
|
Section("Kontakt") {
|
||||||
if people.isEmpty {
|
if people.isEmpty {
|
||||||
Text("Keine Kontakte gefunden")
|
Text("Keine Kontakte gefunden. Öffne nahbar einmal, damit die Kontakte hier erscheinen.")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
} else {
|
} else {
|
||||||
TextField("Suchen…", text: $searchText)
|
TextField("Suchen…", text: $searchText)
|
||||||
ForEach(filteredPeople) { person in
|
ForEach(filteredPeople) { person in
|
||||||
PersonPickerRow(
|
PersonPickerRow(
|
||||||
person: person,
|
name: person.name,
|
||||||
|
tag: person.tag,
|
||||||
isSelected: selectedPerson?.id == person.id
|
isSelected: selectedPerson?.id == person.id
|
||||||
) {
|
) {
|
||||||
selectedPerson = (selectedPerson?.id == person.id) ? nil : person
|
selectedPerson = (selectedPerson?.id == person.id) ? nil : person
|
||||||
@@ -88,7 +95,8 @@ struct ShareExtensionView: View {
|
|||||||
// MARK: - Subviews
|
// MARK: - Subviews
|
||||||
|
|
||||||
struct PersonPickerRow: View {
|
struct PersonPickerRow: View {
|
||||||
let person: Person
|
let name: String
|
||||||
|
let tag: String
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
|
||||||
@@ -96,18 +104,12 @@ struct ShareExtensionView: View {
|
|||||||
Button(action: onTap) {
|
Button(action: onTap) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(person.name)
|
Text(name).foregroundStyle(.primary)
|
||||||
.foregroundStyle(.primary)
|
Text(tag).font(.caption).foregroundStyle(.secondary)
|
||||||
if let tag = PersonTag(rawValue: person.tagRaw) {
|
|
||||||
Text(tag.rawValue)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if isSelected {
|
if isSelected {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark").foregroundStyle(Color.accentColor)
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,41 +120,19 @@ struct ShareExtensionView: View {
|
|||||||
// MARK: - Data
|
// MARK: - Data
|
||||||
|
|
||||||
private func loadPeople() {
|
private func loadPeople() {
|
||||||
do {
|
people = AppGroup.cachedPeople.compactMap { dict in
|
||||||
let container = try AppGroup.makeModelContainer()
|
guard let idString = dict["id"], let id = UUID(uuidString: idString),
|
||||||
let context = ModelContext(container)
|
let name = dict["name"], let tag = dict["tag"] else { return nil }
|
||||||
let descriptor = FetchDescriptor<Person>(sortBy: [SortDescriptor(\.name)])
|
return CachedPerson(id: id, name: name, tag: tag)
|
||||||
people = (try? context.fetch(descriptor)) ?? []
|
|
||||||
} catch {
|
|
||||||
errorMessage = "Kontakte konnten nicht geladen werden."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
guard let person = selectedPerson else { return }
|
guard let person = selectedPerson else { return }
|
||||||
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
isSaving = true
|
isSaving = true
|
||||||
errorMessage = nil
|
AppGroup.enqueueMoment(personName: person.name, text: trimmed, type: momentType.rawValue)
|
||||||
do {
|
|
||||||
let container = try AppGroup.makeModelContainer()
|
|
||||||
let context = ModelContext(container)
|
|
||||||
let personID = person.id
|
|
||||||
let descriptor = FetchDescriptor<Person>(predicate: #Predicate { $0.id == personID })
|
|
||||||
guard let target = try context.fetch(descriptor).first else {
|
|
||||||
errorMessage = "Kontakt nicht gefunden."
|
|
||||||
isSaving = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let moment = Moment(
|
|
||||||
text: text.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
||||||
type: momentType,
|
|
||||||
person: target
|
|
||||||
)
|
|
||||||
context.insert(moment)
|
|
||||||
try context.save()
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
} catch {
|
|
||||||
errorMessage = "Speichern fehlgeschlagen: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
isSaving = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user