Teilen-Blatt, HIG-Blaetter, geteilte Ansichtseinstellungen (2026.8.31.1)
Share-Erweiterung: ein PDF aus Mail landet ueber Teilen -> DataMetric360 als Tankbeleg. Alles Native liegt versioniert unter companion-app/native/, weil ios/ gitignored ist und npx cap add ios es sonst wieder frisst; scripts/ios-teilen-einrichten.mjs haengt es bei jedem Bau ins Xcode-Projekt und ist in ios-signieren.sh eingehaengt. Der pbxproj-Teil ist auf keinem Mac erprobt - er bricht vor dem Schreiben ab, und docs/SHARE_EXTENSION.md beschreibt dieselben Handgriffe fuer Xcode. "In den Kalender uebernehmen" ging in der App nie: WKWebView ignoriert das download-Attribut, der Klick lief ins Leere. Auf dem Geraet jetzt @capacitor/filesystem plus @capacitor/share. useTheme/useBoden legten je Aufrufer eigenen Zustand an - fuenf Kopien. Der Schalter aenderte nur seine eigene, data-theme in App.tsx blieb stehen, die Tag/Nacht-Umschaltung wirkte erst nach einem Neustart. Jetzt ein Speicher je Einstellung ueber useSyncExternalStore. Zwei Regressionstests, gegen den alten Stand als fehlschlagend nachgewiesen. Alle Popups nach Apple HIG: neue Bausteine Sheet und ActionSheet, blickdicht und mit abgedunkeltem Schleier, in beiden Codebasen. Popup portalierte nach document.body - ausserhalb von [data-theme] - und war deshalb im Tagmodus fast unsichtbar; Portal zielt jetzt auf .ads-root. ImagePlaceholder merkte sich "fehlgeschlagen" ohne die Adresse: nach einem leeren Galerieplatz zeigten auch die mit Foto nur noch den Platzhalter. Vier Regressionstests, ebenfalls gegen den alten Stand geprueft. Bildadressen tragen jetzt einen Cache-Brecher - HA liefert /local/ mit 31 Tagen Cache-Vorgabe aus, im Panel eingefuegte Radfotos erschienen in der App deshalb nicht. uebersichtsbild: Panel speichert den Namen, die App verglich gegen den Dateinamen - der Vergleich traf nie zu. Kanonisch ist der Name. Weiter: Navigationspfeil statt gleichschenkligem Dreieck auf der Streckenlinie (die Kerbe unterscheidet Kopf und Ende), CI-Symbol tour-s als Fahrten-Icon, Datumsfelder zeigen ihren Wert ohne erstes Antippen, feste Beispielnamen im Setup, Kopf-Gegengewicht fuer mittige Titel, Standort-Blatt mit festem Fussabstand, NSLocationWhenInUseUsageDescription, watchPosition fuer die Live-Ortung, Art-Pille wieder als Knopf, Zwischenablage fuer Tankbelege. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,49 @@ if [ ! -d ios ]; then
|
||||
fi
|
||||
npx cap sync ios
|
||||
|
||||
# Standort-Berechtigung ins Info.plist eintragen.
|
||||
#
|
||||
# Ohne diesen Schluessel zeigt iOS beim Aufruf von navigator.geolocation in
|
||||
# der WebView (src/screens/Standort.tsx) ueberhaupt keine Rueckfrage: der
|
||||
# Aufruf scheitert sofort, die App meldet "GPS-Freigabe fehlt", und unter
|
||||
# Einstellungen > DataMetric360 > Ort fehlt die Auswahl "Beim Verwenden der
|
||||
# App" - es bleiben nur "Nie" und "Beim Teilen". Nachgewiesen am 2026-08-31
|
||||
# an der ausgelieferten App.ipa: kein einziger Location-Schluessel darin.
|
||||
#
|
||||
# Warum hier und nicht in Xcode: ios/ ist gitignored und wird von
|
||||
# `npx cap add ios` aus Capacitors Vorlage neu erzeugt - die kennt keine
|
||||
# Standort-Schluessel. Ein Eintrag von Hand in Xcode waere beim naechsten
|
||||
# Erzeugen wieder verschwunden, genau wie es der Team-Kennung oben ergangen
|
||||
# waere. Deshalb steht er versioniert hier und wird nach jedem `cap sync`
|
||||
# neu gesetzt.
|
||||
#
|
||||
# Nur "WhenInUse": berechnet wird der Abstand zum geparkten Fahrzeug, waehrend
|
||||
# jemand die Standort-Seite ansieht. Hintergrundortung braucht die App nicht,
|
||||
# und was sie nicht braucht, soll sie auch nicht erfragen.
|
||||
# Share-Erweiterung ins Xcode-Projekt haengen.
|
||||
#
|
||||
# Aus demselben Grund wie die Team-Kennung und der Standort-Schluessel oben:
|
||||
# ios/ ist gitignored, also verschwindet jedes von Hand angelegte Ziel beim
|
||||
# naechsten `npx cap add ios`. Die Quellen liegen versioniert unter native/,
|
||||
# das Skript spielt sie bei jedem Bau wieder ein. Es ist idempotent - ein
|
||||
# zweiter Lauf ueber ein fertiges Projekt aendert nichts.
|
||||
#
|
||||
# Schlaegt es fehl, bricht der ganze Bau ab: eine App ohne den Eintrag im
|
||||
# Teilen-Blatt sieht fertig aus und ist es nicht, und der Unterschied faellt
|
||||
# erst auf dem Geraet auf.
|
||||
echo "==> Share-Erweiterung einrichten"
|
||||
node scripts/ios-teilen-einrichten.mjs
|
||||
|
||||
echo "==> Standort-Berechtigung eintragen"
|
||||
PLIST="ios/App/App/Info.plist"
|
||||
GRUND="Zeigt auf der Standort-Karte, wie weit du vom geparkten Fahrzeug entfernt bist."
|
||||
/usr/libexec/PlistBuddy -c "Set :NSLocationWhenInUseUsageDescription $GRUND" "$PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :NSLocationWhenInUseUsageDescription string $GRUND" "$PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Print :NSLocationWhenInUseUsageDescription" "$PLIST" >/dev/null || {
|
||||
echo "FEHLER: NSLocationWhenInUseUsageDescription liess sich nicht setzen." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "==> Archiv bauen und signieren (Team $TEAM)"
|
||||
rm -rf "$AUSGABE"
|
||||
mkdir -p "$AUSGABE"
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Hängt die Share-Erweiterung ins erzeugte Xcode-Projekt.
|
||||
*
|
||||
* WARUM ES DIESES SKRIPT GIBT
|
||||
* ---------------------------
|
||||
* `companion-app/ios/` ist absichtlich gitignored — `npx cap add ios` baut den
|
||||
* Ordner aus dem Webbündel jederzeit neu. Damit verschwindet aber auch jedes
|
||||
* Ziel, das man in Xcode von Hand anlegt. Dieselbe Falle, die schon die
|
||||
* Team-Kennung und den Standort-Schlüssel in `ios-signieren.sh` getrieben hat:
|
||||
* was den nächsten `cap add` überleben soll, muss versioniert sein und bei
|
||||
* jedem Bau neu eingespielt werden.
|
||||
*
|
||||
* Die Quellen liegen deshalb unter `companion-app/native/`; dieses Skript
|
||||
* kopiert sie nach `ios/App/` und trägt das Erweiterungsziel in die
|
||||
* `project.pbxproj` ein.
|
||||
*
|
||||
* WAS ES ANLEGT
|
||||
* -------------
|
||||
* - Ziel `DataMetric360Share` (App-Erweiterung, Bundle-ID
|
||||
* `app.datametric360.teilen`)
|
||||
* - dessen Quelldateien, Info.plist und Entitlements
|
||||
* - die App-Gruppe `group.app.datametric360` auf BEIDEN Zielen
|
||||
* - die Einbettung der Erweiterung in die App (Build-Phase
|
||||
* „Embed App Extensions")
|
||||
* - das URL-Schema `datametric360://`, über das die Erweiterung die App holt
|
||||
*
|
||||
* IDEMPOTENT
|
||||
* ----------
|
||||
* Ein zweiter Lauf über ein bereits eingerichtetes Projekt ändert nichts und
|
||||
* meldet das. Wichtig, weil `ios-signieren.sh` bei jedem Bau durchläuft und
|
||||
* `ios/` nicht immer neu erzeugt wird.
|
||||
*
|
||||
* NICHT GETESTET AUF EINEM MAC
|
||||
* ----------------------------
|
||||
* Geschrieben ohne Zugriff auf macOS/Xcode. Die pbxproj-Bearbeitung folgt der
|
||||
* API des npm-Pakets `xcode`; scheitert sie, bricht das Skript mit einer
|
||||
* klaren Meldung ab und `docs/SHARE_EXTENSION.md` beschreibt die Handgriffe,
|
||||
* die dasselbe in Xcode erledigen. Lieber ein lauter Abbruch mit Anleitung
|
||||
* als ein halb eingerichtetes Projekt.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "node:fs"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import xcode from "xcode"
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url))
|
||||
const APP = dirname(HIER)
|
||||
|
||||
const ZIEL_NAME = "DataMetric360Share"
|
||||
const BUNDLE_ID = "app.datametric360.teilen"
|
||||
const GRUPPE = "group.app.datametric360"
|
||||
const SCHEMA = "datametric360"
|
||||
|
||||
const iosApp = join(APP, "ios", "App")
|
||||
const pbxPfad = join(iosApp, "App.xcodeproj", "project.pbxproj")
|
||||
const quelleErw = join(APP, "native", "teilen")
|
||||
const zielErw = join(iosApp, ZIEL_NAME)
|
||||
|
||||
function melden(text) {
|
||||
console.log(" " + text)
|
||||
}
|
||||
|
||||
function abbruch(text) {
|
||||
console.error("")
|
||||
console.error("FEHLER: " + text)
|
||||
console.error("")
|
||||
console.error("Die Share-Erweiterung ist damit NICHT eingerichtet. Die App selbst")
|
||||
console.error("baut und läuft weiterhin - nur der Eintrag im Teilen-Blatt fehlt.")
|
||||
console.error("Von Hand nachziehen: companion-app/docs/SHARE_EXTENSION.md")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!existsSync(pbxPfad)) {
|
||||
abbruch(`${pbxPfad} nicht gefunden - erst 'npx cap sync ios' laufen lassen.`)
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Dateien */
|
||||
|
||||
mkdirSync(zielErw, { recursive: true })
|
||||
for (const datei of ["ShareViewController.swift", "Info.plist", `${ZIEL_NAME}.entitlements`]) {
|
||||
copyFileSync(join(quelleErw, datei), join(zielErw, datei))
|
||||
}
|
||||
copyFileSync(join(APP, "native", "App.entitlements"), join(iosApp, "App", "App.entitlements"))
|
||||
melden(`Quellen nach ios/App/${ZIEL_NAME}/ kopiert`)
|
||||
|
||||
/* ------------------------------------------------------------- URL-Schema */
|
||||
|
||||
/* Ohne dieses Schema kann die Erweiterung die App nicht holen - sie legt den
|
||||
Beleg zwar ab, aber niemand merkt es, bis die App das nächste Mal von Hand
|
||||
geöffnet wird. */
|
||||
const appPlistPfad = join(iosApp, "App", "Info.plist")
|
||||
let appPlist = readFileSync(appPlistPfad, "utf8")
|
||||
if (!appPlist.includes("CFBundleURLTypes")) {
|
||||
const eintrag = [
|
||||
"\t<key>CFBundleURLTypes</key>",
|
||||
"\t<array>",
|
||||
"\t\t<dict>",
|
||||
"\t\t\t<key>CFBundleURLName</key>",
|
||||
`\t\t\t<string>app.datametric360</string>`,
|
||||
"\t\t\t<key>CFBundleURLSchemes</key>",
|
||||
"\t\t\t<array>",
|
||||
`\t\t\t\t<string>${SCHEMA}</string>`,
|
||||
"\t\t\t</array>",
|
||||
"\t\t</dict>",
|
||||
"\t</array>",
|
||||
"</dict>",
|
||||
].join("\n")
|
||||
const letztes = appPlist.lastIndexOf("</dict>")
|
||||
if (letztes < 0) abbruch("ios/App/App/Info.plist hat keine schließende <dict>-Marke.")
|
||||
appPlist = appPlist.slice(0, letztes) + eintrag + appPlist.slice(letztes + "</dict>".length)
|
||||
writeFileSync(appPlistPfad, appPlist, "utf8")
|
||||
melden(`URL-Schema ${SCHEMA}:// eingetragen`)
|
||||
} else {
|
||||
melden("URL-Schema war bereits eingetragen")
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Projekt */
|
||||
|
||||
const projekt = xcode.project(pbxPfad)
|
||||
projekt.parseSync()
|
||||
|
||||
const vorhanden = Object.values(projekt.pbxNativeTargetSection()).some(
|
||||
(z) => z && typeof z === "object" && z.name === ZIEL_NAME,
|
||||
)
|
||||
if (vorhanden) {
|
||||
melden(`Ziel ${ZIEL_NAME} war bereits im Projekt - nichts geändert`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let ziel
|
||||
try {
|
||||
ziel = projekt.addTarget(ZIEL_NAME, "app_extension", ZIEL_NAME, BUNDLE_ID)
|
||||
|
||||
// Eigene Build-Phasen: ohne sie hat das Ziel keine Quelldateien und keine
|
||||
// Ressourcen, und Xcode baut ein leeres Bündel.
|
||||
projekt.addBuildPhase([], "PBXSourcesBuildPhase", "Sources", ziel.uuid)
|
||||
projekt.addBuildPhase([], "PBXResourcesBuildPhase", "Resources", ziel.uuid)
|
||||
projekt.addBuildPhase([], "PBXFrameworksBuildPhase", "Frameworks", ziel.uuid)
|
||||
|
||||
const gruppe = projekt.addPbxGroup(
|
||||
["ShareViewController.swift", "Info.plist", `${ZIEL_NAME}.entitlements`],
|
||||
ZIEL_NAME,
|
||||
ZIEL_NAME,
|
||||
)
|
||||
// In die Projektwurzel einhängen, sonst taucht die Gruppe in Xcode nicht auf.
|
||||
const wurzel = Object.keys(projekt.hash.project.objects.PBXGroup).find(
|
||||
(k) => projekt.hash.project.objects.PBXGroup[k].name === "CustomTemplate",
|
||||
)
|
||||
if (wurzel) projekt.addToPbxGroup(gruppe.uuid, wurzel)
|
||||
|
||||
projekt.addSourceFile("ShareViewController.swift", { target: ziel.uuid }, gruppe.uuid)
|
||||
|
||||
// Einstellungen, die `addTarget` nicht selbst setzt.
|
||||
const konfigs = projekt.pbxXCBuildConfigurationSection()
|
||||
for (const schluessel of Object.keys(konfigs)) {
|
||||
const eintrag = konfigs[schluessel]
|
||||
if (!eintrag || typeof eintrag !== "object" || !eintrag.buildSettings) continue
|
||||
const s = eintrag.buildSettings
|
||||
if (s.PRODUCT_BUNDLE_IDENTIFIER === `"${BUNDLE_ID}"` || s.PRODUCT_NAME === `"${ZIEL_NAME}"`) {
|
||||
s.INFOPLIST_FILE = `"${ZIEL_NAME}/Info.plist"`
|
||||
s.CODE_SIGN_ENTITLEMENTS = `"${ZIEL_NAME}/${ZIEL_NAME}.entitlements"`
|
||||
s.IPHONEOS_DEPLOYMENT_TARGET = "14.0"
|
||||
s.SWIFT_VERSION = "5.0"
|
||||
s.TARGETED_DEVICE_FAMILY = `"1,2"`
|
||||
s.SKIP_INSTALL = "YES"
|
||||
}
|
||||
// Die App selbst braucht ihre neue Entitlements-Datei.
|
||||
if (s.PRODUCT_BUNDLE_IDENTIFIER === `"app.datametric360"`) {
|
||||
s.CODE_SIGN_ENTITLEMENTS = `"App/App.entitlements"`
|
||||
}
|
||||
}
|
||||
|
||||
// Erweiterung in die App einbetten - ohne diese Phase liegt sie zwar im
|
||||
// Projekt, wandert aber nicht mit ins fertige .ipa.
|
||||
projekt.addBuildPhase(
|
||||
[`${ZIEL_NAME}.appex`],
|
||||
"PBXCopyFilesBuildPhase",
|
||||
"Embed App Extensions",
|
||||
projekt.getFirstTarget().uuid,
|
||||
"app_extension",
|
||||
)
|
||||
|
||||
writeFileSync(pbxPfad, projekt.writeSync(), "utf8")
|
||||
} catch (fehler) {
|
||||
abbruch(`Das Xcode-Projekt ließ sich nicht bearbeiten: ${fehler.message}`)
|
||||
}
|
||||
|
||||
melden(`Ziel ${ZIEL_NAME} angelegt und eingebettet`)
|
||||
melden(`App-Gruppe ${GRUPPE} auf beiden Zielen`)
|
||||
console.log("")
|
||||
console.log(" Einmalig im Apple-Entwicklerportal nötig, sonst schlägt das Signieren fehl:")
|
||||
console.log(` - App-Gruppe ${GRUPPE} anlegen`)
|
||||
console.log(` - App-ID ${BUNDLE_ID} anlegen und der Gruppe zuordnen`)
|
||||
console.log(` - App-ID app.datametric360 ebenfalls der Gruppe zuordnen`)
|
||||
console.log("")
|
||||
Reference in New Issue
Block a user