Teilen-Weg repariert, Zwischenablage nativ, Sprung in den Tankvorgang (2026.9.2.24/.25)
BEFUND. Die Uebergabe der Teilen-Erweiterung an die App konnte nie
funktionieren: sie legte den Beleg im Gruppen-Container ab, die App suchte ihn
ueber @capacitor/preferences - und dessen iOS-Code liest IMMER
UserDefaults.standard der App, configure({group}) setzt nur ein
Schluessel-Praefix. Falscher Behaelter, falscher Schluessel, und weil ein
fehlender Eintrag der Normalfall ist, meldete nichts einen Fehler.
UMBAU. Die Erweiterung schreibt das PDF als Datei in den Gruppen-Container und
gibt den Pfad im Aufruf mit; die App liest ihn ueber @capacitor/filesystem
(volle file://-Pfade) und loescht ihn danach. Abgeholt ueber getLaunchUrl()
und appUrlOpen - iOS benutzt beide. Nebenbei entfallen die
Base64-Aufblaehung und die 4-MB-Grenze der UserDefaults.
ZWEI WEITERE FEHLER AUF DEMSELBEN WEG. Ein Beleg zu einem bestehenden
Tankvorgang lief in die 20-Sekunden-Frist, obwohl der Server laengst fertig
war. Und beim Duplikat verriet das Backend nicht, welcher Vorgang gemeint ist
- jetzt kommt vorhandener_tank_id mit, und beide Oberflaechen springen
dorthin. Dazu: nach dem Speichern oeffnet sich der neue Vorgang, und waehrend
gelesen wird, steht "Tankbeleg wird verarbeitet ..." mit Zapfsaeule und
Kreisel im Formular.
ZWISCHENABLAGE. navigator.clipboard.read() gibt nur Text, HTML und Bilder
heraus - ein kopiertes PDF ist dort nie zu bekommen. UIPasteboard kennt die
Grenze nicht: ein kleines eigenes Plugin holt es, auch aus Mail (Anhang als
Verweis, ueber pasteboard.urls mit Sicherheitsbereich).
Der native Teil braucht einen Mac-Lauf; ios-teilen-einrichten.mjs spielt beide
Swift-Dateien bei jedem Bau ein.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// BelegZwischenablage.swift
|
||||
// DataMetric360
|
||||
//
|
||||
// Holt ein PDF aus der System-Zwischenablage — der Weg, den die Web-API
|
||||
// nicht anbietet.
|
||||
//
|
||||
// WARUM ES DAS BRAUCHT
|
||||
// --------------------
|
||||
// `navigator.clipboard.read()` gibt aus Datenschutzgründen nur Text, HTML
|
||||
// und Bilder heraus; ein kopiertes PDF taucht dort NIE auf. Der Knopf „Aus
|
||||
// Zwischenablage einfügen" endete deshalb auf dem Telefon zwangsläufig mit
|
||||
// „In der Zwischenablage liegt kein PDF" (gemeldet vom Eigentümer,
|
||||
// 02.09.2026). Das ist keine Lücke in unserem Code, sondern die Grenze der
|
||||
// Schnittstelle.
|
||||
//
|
||||
// `UIPasteboard` kennt diese Grenze nicht: es gibt beliebige Typen heraus,
|
||||
// auch `com.adobe.pdf`. Der Preis ist derselbe wie im Web — iOS zeigt beim
|
||||
// Zugriff seine eigene Einfügen-Rückfrage, und das ist richtig so.
|
||||
//
|
||||
// ZWEI FORMEN, IN DENEN EIN PDF IN DER ZWISCHENABLAGE LIEGT
|
||||
// ---------------------------------------------------------
|
||||
// 1. Als Daten unter `com.adobe.pdf` — so legen es die meisten Apps ab.
|
||||
// 2. Als Datei-URL — Mail kopiert einen Anhang oft nur als Verweis auf eine
|
||||
// temporäre Kopie. Ohne den zweiten Weg bliebe genau der Fall übrig, für
|
||||
// den der Knopf gedacht ist.
|
||||
//
|
||||
// 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 Foundation
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@objc(BelegZwischenablagePlugin)
|
||||
public class BelegZwischenablagePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "BelegZwischenablagePlugin"
|
||||
public let jsName = "BelegZwischenablage"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "pdfHolen", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
/// Liefert `{ base64, name }`, wenn ein PDF in der Zwischenablage liegt —
|
||||
/// sonst ein leeres Ergebnis. Bewusst kein Fehler: „nichts kopiert" ist
|
||||
/// kein Fehlschlag, sondern eine Antwort, und die Oberfläche sagt dazu
|
||||
/// ohnehin das Passende (zwischenablage.ts).
|
||||
@objc func pdfHolen(_ call: CAPPluginCall) {
|
||||
let ablage = UIPasteboard.general
|
||||
|
||||
if let daten = ablage.data(forPasteboardType: UTType.pdf.identifier) {
|
||||
return call.resolve(["base64": daten.base64EncodedString(), "name": "beleg.pdf"])
|
||||
}
|
||||
|
||||
// Mail legt den Anhang oft nur als Verweis ab.
|
||||
for url in ablage.urls ?? [] where url.pathExtension.lowercased() == "pdf" {
|
||||
// Sicherheitsbereich anfordern: die URL zeigt auf eine Datei
|
||||
// außerhalb des eigenen Sandkastens, sonst schlägt das Lesen fehl.
|
||||
let erlaubt = url.startAccessingSecurityScopedResource()
|
||||
defer { if erlaubt { url.stopAccessingSecurityScopedResource() } }
|
||||
if let daten = try? Data(contentsOf: url) {
|
||||
return call.resolve([
|
||||
"base64": daten.base64EncodedString(),
|
||||
"name": url.lastPathComponent,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve([:])
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,28 @@
|
||||
// Sie kann der App nichts direkt geben; der einzige gemeinsame Boden ist
|
||||
// eine App-Gruppe. Der Weg hier:
|
||||
//
|
||||
// 1. Das PDF wird gelesen und Base64-kodiert.
|
||||
// 2. Es landet zusammen mit seinem Namen in den geteilten
|
||||
// UserDefaults der Gruppe `group.app.datametric360`.
|
||||
// 3. Die Erweiterung öffnet die App über ihr eigenes URL-Schema.
|
||||
// 4. Die App liest den Eintrag beim Start bzw. beim Zurückkehren aus,
|
||||
// löscht ihn und lädt den Beleg hoch (daten/geteilterBeleg.ts).
|
||||
// 1. Das PDF wird in den Container der App-Gruppe geschrieben.
|
||||
// 2. Die Erweiterung öffnet die App über ihr URL-Schema und gibt den
|
||||
// Pfad mit: `datametric360://beleg?pfad=<file://…>`.
|
||||
// 3. Die App liest die Datei über @capacitor/filesystem (das volle
|
||||
// file://-Pfade ausdrücklich unterstützt) und löscht sie danach.
|
||||
//
|
||||
// Base64 in UserDefaults statt einer Datei im Gruppen-Container: die App
|
||||
// liest UserDefaults ohnehin schon über @capacitor/preferences, das genau
|
||||
// diese Gruppe kennt. Eine Datei im Container bräuchte dagegen eigenen
|
||||
// nativen Code auf der App-Seite — ein zusätzliches Plugin für einen Weg,
|
||||
// den es sonst nicht gäbe.
|
||||
// DER ERSTE ENTWURF GING ÜBER USERDEFAULTS UND HAT NIE FUNKTIONIERT.
|
||||
// Er legte das PDF base64-kodiert unter `dm360_geteilter_beleg` in
|
||||
// `UserDefaults(suiteName: gruppe)` ab, weil die App UserDefaults über
|
||||
// @capacitor/preferences ohnehin liest. Nur liest dieses Plugin (8.0.1,
|
||||
// ios/Sources/PreferencesPlugin/Preferences.swift) IMMER
|
||||
// `UserDefaults.standard` der App — `configure({ group })` wechselt nicht
|
||||
// den Container, sondern setzt nur ein Schlüssel-Präfix. Die App suchte
|
||||
// also `group.app.datametric360.dm360_geteilter_beleg` in ihrem eigenen
|
||||
// Bereich, während hier der plain Schlüssel im Gruppen-Container lag:
|
||||
// falscher Behälter und falscher Schlüssel. Gemeldet am 02.09.2026
|
||||
// („es wird kein Tankvorgang angelegt beim Teilen mit der App“).
|
||||
//
|
||||
// Der Preis ist eine Größengrenze: UserDefaults ist für Einstellungen
|
||||
// gedacht, nicht für Anhänge. GRENZE_BYTES hält das bei 4 MB. Ein
|
||||
// Tankbeleg liegt bei einigen Dutzend Kilobyte, ein gescanntes Mehrseiten-
|
||||
// PDF kann darüber liegen — dann sagt die Erweiterung das und der Weg über
|
||||
// „Datei auswählen" in der App bleibt.
|
||||
// Die Datei hat zwei weitere Vorteile: keine Base64-Aufblähung um ein
|
||||
// Drittel, und keine 4-MB-Grenze — UserDefaults ist für Einstellungen
|
||||
// gedacht, nicht für Anhänge. Geblieben ist eine Obergrenze, aber eine
|
||||
// großzügige (GRENZE_BYTES).
|
||||
//
|
||||
// DIESE DATEI IST VERSIONIERT, ios/ NICHT.
|
||||
// `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin
|
||||
@@ -45,13 +49,13 @@ class ShareViewController: UIViewController {
|
||||
/// Muss zur App-Gruppe in beiden Entitlements-Dateien passen.
|
||||
private let gruppe = "group.app.datametric360"
|
||||
|
||||
/// Schlüssel, unter dem die App den Beleg abholt (geteilterBeleg.ts).
|
||||
private let schluessel = "dm360_geteilter_beleg"
|
||||
/// Unterordner im Gruppen-Container, in dem geteilte Belege liegen.
|
||||
private let ordner = "geteilte-belege"
|
||||
|
||||
/// 4 MB. UserDefaults ist kein Dateispeicher — darüber wird abgelehnt,
|
||||
/// statt das System mit einem Anhang zu belasten, für den es nicht
|
||||
/// gedacht ist.
|
||||
private let grenzeBytes = 4 * 1024 * 1024
|
||||
/// 24 MB. Kein technisches Limit mehr, sondern eine Vernunftgrenze:
|
||||
/// ein Tankbeleg liegt bei einigen Dutzend Kilobyte, alles jenseits
|
||||
/// davon ist kein Beleg, sondern ein Versehen.
|
||||
private let grenzeBytes = 24 * 1024 * 1024
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@@ -102,26 +106,30 @@ class ShareViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func ablegen(daten: Data, name: String) {
|
||||
guard let ablage = UserDefaults(suiteName: gruppe) else {
|
||||
guard
|
||||
let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: gruppe)
|
||||
else {
|
||||
return melden("Die App-Gruppe ist nicht eingerichtet.")
|
||||
}
|
||||
// Form wie in geteilterBeleg.ts erwartet. @capacitor/preferences legt
|
||||
// Werte als String ab, deshalb JSON und kein Dictionary.
|
||||
let inhalt: [String: Any] = [
|
||||
"name": name,
|
||||
"base64": daten.base64EncodedString(),
|
||||
"zeit": Int(Date().timeIntervalSince1970 * 1000),
|
||||
]
|
||||
guard
|
||||
let json = try? JSONSerialization.data(withJSONObject: inhalt),
|
||||
let text = String(data: json, encoding: .utf8)
|
||||
else {
|
||||
|
||||
let ziel = container.appendingPathComponent(ordner, isDirectory: true)
|
||||
// Ein Zeitstempel im Namen: zwei rasch nacheinander geteilte Belege
|
||||
// sollen sich nicht gegenseitig überschreiben, und der Name bleibt
|
||||
// für den Nutzer lesbar, falls er den Container je zu sehen bekommt.
|
||||
let datei = ziel.appendingPathComponent(
|
||||
"\(Int(Date().timeIntervalSince1970))-\(name)")
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: ziel, withIntermediateDirectories: true)
|
||||
try daten.write(to: datei, options: .atomic)
|
||||
} catch {
|
||||
return melden("Der Beleg ließ sich nicht übergeben.")
|
||||
}
|
||||
ablage.set(text, forKey: schluessel)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.appOeffnen()
|
||||
self?.appOeffnen(pfad: datei)
|
||||
self?.abschliessen()
|
||||
}
|
||||
}
|
||||
@@ -129,8 +137,15 @@ class ShareViewController: UIViewController {
|
||||
/// Öffnet die Host-App. Eine Erweiterung darf `UIApplication.shared` nicht
|
||||
/// benutzen, deshalb der Umweg über die Responder-Kette — der einzige von
|
||||
/// Apple vorgesehene Weg aus einer Erweiterung heraus.
|
||||
private func appOeffnen() {
|
||||
guard let ziel = URL(string: "datametric360://beleg") else { return }
|
||||
private func appOeffnen(pfad: URL) {
|
||||
// Der Pfad wandert als Abfrageparameter mit. Er zeigt in den
|
||||
// Gruppen-Container, den die App mit derselben Berechtigung lesen
|
||||
// darf - kein zusätzliches Plugin nötig.
|
||||
var bau = URLComponents()
|
||||
bau.scheme = "datametric360"
|
||||
bau.host = "beleg"
|
||||
bau.queryItems = [URLQueryItem(name: "pfad", value: pfad.absoluteString)]
|
||||
guard let ziel = bau.url else { return }
|
||||
var antwort: UIResponder? = self
|
||||
while let r = antwort {
|
||||
if let app = r as? UIApplication {
|
||||
|
||||
Reference in New Issue
Block a user