6016053a92
- Kalendertermine gehen nativ ueber EventKit statt ueber das Teilen-Blatt:
Apples Kalender meldet sich beim System gar nicht als Teilen-Ziel an, ein
anderes Dateiformat haette also nie geholfen (Abschnitt BS).
- Der Rueckblick stoesst jetzt selbst das Screening an, wenn er Fahrten
angelegt hat - sonst blieb eine importierte Fahrt ohne Ort liegen, solange
das Fahrzeug steht (Abschnitt BT).
- Fahrzeugfotos werden vor dem Upload im Browser auf 2000 px/WebP gebracht.
Gemessen: 3.099.022 -> 325.994 Bytes. Damit ist die Nachrichtengrenze der
WebSocket-Verbindung kein Thema mehr ("connection lost" auf der realen
Instanz). Formate, die der Browser weder umwandeln noch anzeigen kann,
werden mit klarer Meldung abgelehnt statt roh gespeichert.
- Zwei Wechsel des Fahrtsignals in derselben Sekunde kosteten eine ganze
Fahrt: das "on" ueberholte den noch laufenden Beende-Vorgang und fiel durch
beide Zweige. Neue asyncio-Sperre plus Regressionstest, der ohne sie
nachweislich rot ist (Abschnitt BU).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
6.0 KiB
Swift
136 lines
6.0 KiB
Swift
//
|
|
// KalenderTermin.swift
|
|
// DataMetric360
|
|
//
|
|
// Legt einen Termin über EventKit an — der Weg, den das Teilen-Blatt nicht
|
|
// anbietet.
|
|
//
|
|
// WARUM ES DAS BRAUCHT
|
|
// --------------------
|
|
// Bis zum 03.09.2026 ging der Werkstatt- und der Radwechseltermin als
|
|
// .ics-Datei ins iOS-Teilen-Blatt. Dort erscheint nie ein Kalender, und das
|
|
// liegt nicht an der Datei: Apples Kalender-App meldet sich beim System
|
|
// überhaupt nicht als Teilen-Ziel an, für kein Format. Das Blatt kann sie
|
|
// deshalb gar nicht zeigen, egal wie die Datei aussieht.
|
|
//
|
|
// Dass ein per Mail geschickter Anhang funktioniert, führt in die Irre: dort
|
|
// öffnet nicht das Teilen-Blatt, sondern die Dokumentvorschau des Systems
|
|
// (Quick Look). Die erkennt text/calendar und blendet „Alle hinzufügen" ein.
|
|
// Ein anderer Mechanismus, den eine App nicht aus dem Teilen-Blatt heraus
|
|
// erreicht. Gemeldet vom Eigentümer am 02.09.2026.
|
|
//
|
|
// EventKit umgeht beides: der Termin geht direkt an den Kalender.
|
|
//
|
|
// WARUM DER SYSTEM-EDITOR UND NICHT `EKEventStore.save`
|
|
// -----------------------------------------------------
|
|
// `EKEventEditViewController` zeigt den fertig ausgefüllten Termin und lässt
|
|
// ihn den Menschen bestätigen. Das passt zur Vorgabe des Eigentümers, dass
|
|
// der Werkstatttermin keine eigene Verwaltung bekommt (siehe Abschnitt BQ in
|
|
// AGENTS.md): die App liefert „was, wann, welche Werkstatt" und ist danach
|
|
// raus — gespeichert wird im Kalender, nicht bei uns.
|
|
//
|
|
// Der zweite Grund ist die Berechtigung. Seit iOS 17 braucht der System-
|
|
// Editor keine (WWDC23, „Discover Calendar and EventKit"): er läuft außerhalb
|
|
// unseres Zugriffs, wir übergeben nur den Entwurf. Ein direktes `save()`
|
|
// bräuchte dagegen eine Freigabe samt Rückfrage. Deshalb wird hier auch
|
|
// `event.calendar` NICHT gesetzt — das würde `defaultCalendarForNewEvents`
|
|
// lesen und damit doch eine Freigabe verlangen; der Editor sucht den
|
|
// Standardkalender selbst.
|
|
//
|
|
// DIESE DATEI IST VERSIONIERT, ios/ NICHT.
|
|
// `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin
|
|
// verlieren. scripts/ios-teilen-einrichten.mjs kopiert sie bei jedem Bau
|
|
// wieder hinein und hängt sie an die Sources-Phase des App-Ziels.
|
|
//
|
|
|
|
import Capacitor
|
|
import EventKit
|
|
import EventKitUI
|
|
import Foundation
|
|
import UIKit
|
|
|
|
@objc(KalenderTerminPlugin)
|
|
public class KalenderTerminPlugin: CAPPlugin, CAPBridgedPlugin, EKEventEditViewDelegate {
|
|
public let identifier = "KalenderTerminPlugin"
|
|
public let jsName = "KalenderTermin"
|
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
CAPPluginMethod(name: "terminAnlegen", returnType: CAPPluginReturnPromise)
|
|
]
|
|
|
|
/// Muss den Editor überleben — eine lokale Instanz wäre beim Erscheinen
|
|
/// des Blatts schon wieder abgeräumt.
|
|
private let speicher = EKEventStore()
|
|
|
|
/// Der Aufruf wartet, bis der Mensch im Editor entschieden hat.
|
|
private var offenerAufruf: CAPPluginCall?
|
|
|
|
@objc func terminAnlegen(_ call: CAPPluginCall) {
|
|
guard let titel = call.getString("titel"), !titel.isEmpty else {
|
|
return call.reject("Ohne Titel kein Termin.")
|
|
}
|
|
guard let isoTag = call.getString("isoTag"), let tag = tagAus(isoTag) else {
|
|
return call.reject("Kein gültiges Datum (erwartet JJJJ-MM-TT).")
|
|
}
|
|
|
|
let termin = EKEvent(eventStore: speicher)
|
|
termin.title = titel
|
|
termin.isAllDay = true
|
|
// Ganztägig heißt für EventKit: Beginn und Ende am selben Tag. Deckt
|
|
// sich mit dem DTSTART;VALUE=DATE der bisherigen .ics.
|
|
termin.startDate = tag
|
|
termin.endDate = tag
|
|
if let ort = call.getString("ort"), !ort.isEmpty { termin.location = ort }
|
|
if let text = call.getString("beschreibung"), !text.isEmpty { termin.notes = text }
|
|
// Zwei Tage vorher erinnern - dieselbe Vorwarnzeit wie im TRIGGER:-P2D
|
|
// der .ics, die dieser Weg ablöst.
|
|
termin.addAlarm(EKAlarm(relativeOffset: -2 * 24 * 60 * 60))
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { return }
|
|
guard let eltern = self.bridge?.viewController else {
|
|
return call.reject("Kein Fenster zum Anzeigen des Kalender-Editors.")
|
|
}
|
|
|
|
// Ein noch offener Aufruf kann nur von einem Editor stammen, der
|
|
// ohne Rueckmeldung verschwunden ist. Ihn stehen zu lassen hiesse,
|
|
// ein Versprechen nie einzuloesen.
|
|
self.offenerAufruf?.resolve(["status": "abgebrochen"])
|
|
self.offenerAufruf = call
|
|
|
|
let editor = EKEventEditViewController()
|
|
editor.event = termin
|
|
editor.eventStore = self.speicher
|
|
editor.editViewDelegate = self
|
|
// Wegwischen wuerde den Delegaten nicht rufen und den Aufruf oben
|
|
// haengen lassen; so bleiben nur "Hinzufuegen" und "Abbrechen".
|
|
editor.isModalInPresentation = true
|
|
eltern.present(editor, animated: true)
|
|
}
|
|
}
|
|
|
|
public func eventEditViewController(
|
|
_ controller: EKEventEditViewController,
|
|
didCompleteWith action: EKEventEditViewAction
|
|
) {
|
|
controller.dismiss(animated: true)
|
|
let aufruf = offenerAufruf
|
|
offenerAufruf = nil
|
|
// .deleted gibt es hier nicht (der Termin ist neu), zaehlt aber wie ein
|
|
// Abbruch: gespeichert wurde nichts.
|
|
aufruf?.resolve(["status": action == .saved ? "gespeichert" : "abgebrochen"])
|
|
}
|
|
|
|
/// „2027-03-14" -> Mitternacht dieses Tages in der Zeitzone des Geräts.
|
|
private func tagAus(_ isoTag: String) -> Date? {
|
|
let teile = isoTag.split(separator: "-").compactMap { Int($0) }
|
|
guard teile.count == 3 else { return nil }
|
|
var komponenten = DateComponents()
|
|
komponenten.year = teile[0]
|
|
komponenten.month = teile[1]
|
|
komponenten.day = teile[2]
|
|
var kalender = Calendar(identifier: .gregorian)
|
|
kalender.timeZone = TimeZone.current
|
|
return kalender.date(from: komponenten)
|
|
}
|
|
}
|