Native App — Geräte-Sign-off-Checkliste¶
Stand: 2026-06-08 · Commit
a78e3f6· App-Suite 98 + Engine 25 grün, iOS-Sim-Build grün. Audio-Feel-Rework (Loudness-Response + globale Config) gelandet; Per-Band-Balance noch offen. Zweck: Was die XCTest-Suite nicht abdeckt — alles, was nur per Auge, Ohr, Touch, echter Fotos-Bibliothek und echtem Mikrofon verifizierbar ist. Jeder Punkt ist aus demapple/-Code abgeleitet (sieheBeleg-Zeile).So nutzen: Pro Gerät ein Block. 🔴 = kritisch (neu/riskant), zuerst durchgehen. ⚠ = bekanntes Risiko, da besonders hinschauen. Bei Abweichung nennt die
Beleg-Zeile die Code-Stelle zum Nachfassen.
📱 iPhone (JK) — voller Durchlauf¶
Fundament & Rotation¶
- [x] 🔴 App in Portrait starten, einmal drehen, zurückdrehen. Simulation füllt das Display randlos bis in alle Ecken (kein Letterbox, kein kleiner zentrierter 320×480-Block, keine schwarzen Balken). Beim Drehen bleibt der Player randlos; nach ~0,3 s passt sich das Feld dem neuen Seitenverhältnis an (kurzes Re-Seed sichtbar).
⚠ Fällt der Player in den 320×480-Compat-Mode (kleiner upscalter Block mit Rändern), fehlt der
UILaunchScreen-Key im gebauten Bundle — nur auf echter HW/Sim sichtbar.Beleg: Resources/Info.plist:UILaunchScreen; RootView.swift fullW/fullH+ignoresSafeArea; RenderCoordinator.setField/recreate
Audio-Reaktivität (Schwerpunkt — neu)¶
Update 2026-06-08: Auto-gain ist jetzt eine Loudness-Response (leise=sanft / laut=voll / Stille=ruhig), und die Audio-Config ist global (überlebt Preset-Wechsel). Band-Balance (high soll mehr ausschlagen) ist noch offen → eigene gerätegetunte Runde; der Meter bleibt vorerst Bass-lastig. - [ ] 🔴 Bei jungfräulichem Preset (keine Routen) den „Enabled"-Toggle einschalten, Mic-Permission erlauben. Dialogtext exakt „Physarum uses the microphone so the animation can react to sound." Danach reagiert die Animation sofort (ohne erst „Default" drücken zu müssen): 5 Routen werden automatisch angelegt, Auto-gain auf AN. ⚠ Bei Ablehnung startet die Engine nicht (kein Crash, aber keine Reaktion, Meter bleibt leer).
Beleg: AppState.setAudioEnabled (routes.isEmpty → defaultAudioRoutes + autoGain=true); AudioInput.requestPermission- [ ] 🔴 Die 5 Routen-Karten ablesen (Reihenfolge + Werte). Exakt: 1) Bass→Bloom · Trans · +1.2 — 2) Bass→Move · Trans · +0.8 — 3) High→Expo · Trans · +0.6 — 4) Mid→Chaos · Trans · +0.7 — 5) Mid→Sat · Lvl · +0.4. Quell-Labels kapitalisiert (Bass/Mid/High), Depth mit Vorzeichen + 1 Nachkommastelle. ⚠ Karte 5 ist als einzige Lvl, die anderen 4 Trans. Sehen alle 5 gleich aus, stimmt die Mode-Zuordnung nicht.Beleg: AppState.swift:103-109 defaultAudioRoutes; AudioReactiveSection.swift:89/128-129/144- [ ] Ziel-Picker einer Route öffnen. Sektion „Agent": Move, Turn, Deposit, Sensor, Rhythm, Chaos. Sektion „Post": Expo, Bloom, Sat, Contr (bewusst gekürzt).Beleg: AudioReactiveSection.swift:153-164 shortTarget- [ ] 🔴 Musik mit klarem Beat abspielen (Lautsprecher nah ans Mic). Die 3 Meter (Bass/Mid/High) füllen sich pegelabhängig 0..1. Mit den 4 Transient-Routen bekommt die Animation einen sichtbaren Punch genau auf den Beat (Bloom + Move zucken auf Bass-Hits, Exposure auf Highs), kein konstanter Dauer-Boost. Mid→Sat (Lvl) gibt dagegen eine ruhigere, gehaltene Sättigungs-Anhebung. ⚠ Punch muss beat-synchron sein, nicht träge nachziehen (Release ~0,25 s). Nur per Ohr/Auge prüfbar.Beleg: AudioAnalyzer.transientValue; AudioFactors.evaluateAudioFactors (transient vs level)- [ ] 🔴 Musik komplett stoppen, ~5–10 s beobachten. Loudness-Response fährt die Gesamt-Reaktion bei Stille gegen 0 → Animation kehrt in Ruhe zurück, Meter fallen fast auf 0. ⚠ Loudness-Response ersetzt das alte ×20-Auto-Gain (kein Silence-Over-Boost mehr). Achte: bleibt Stille wirklich ruhig? Zappelt Grundrauschen noch →gateLo(0.04) anheben; schneidet es zu abrupt ab →gateLosenken.Beleg: AudioFactors.swift smoothstep + loudnessEnv (gateLo 0.04 / gateHi 0.22 / envCoef 0.15)- [ ] Sehr leise Musik abspielen. Loudness-Response: leise = sanfte, proportionale Reaktion (nicht mehr über-geboostet). ⚠ Der frühere Low-Loudness-Over-Boost ist mit der Loudness-Response behoben. Fühlt sich leise Musik proportional an? Reagiert zu spät/zu wenig →gateHi(0.22) senken; flasht zu früh voll →gateHianheben.Beleg: AudioFactors.swift evaluateAudioFactors (resp = smoothstep(gateLo, gateHi, loudnessEnv))- [ ] Auto-gain AUS, gleichen Loop abspielen, dann Gain-Slider verschieben. Mit Auto-gain AUS kein Loudness-Gating → immer volle Reaktion; Stärke hängt nur am manuellen Gain (0.1×..10×, Anzeige z.B. „1.00x").Beleg: AudioReactiveSection Gain-Slider; AudioFactors effGain = gain · (autoGain ? resp : 1)- [ ] Depth-Slider einer Route ans negative Ende ziehen (−2.0). Bereich −2.0..+2.0; bei negativem Depth invertiert sich die Modulation (Ziel sinkt auf Hits statt zu steigen). Wert zeigt „−2.0". ⚠ Prüfen, dass das segmentierte Lvl/Trans-Picker (116 pt fest) + Depth-Slider + Wert in Zeile 2 auf dem schmalen iPhone-Layout nicht abgeschnitten werden.Beleg: AudioReactiveSection.swift:141/131- [ ] 🔴 Bei bestehenden (selbst geänderten) Routen Audio AUS- und wieder EINschalten. Vorhandene Routen werden nicht überschrieben (Defaults nur bei leerer Liste), Auto-gain nicht zwangsweise erzwungen. Beim Disable stoppt das Mic, Animation kehrt sofort in unmodulierten Zustand zurück. ⚠ Nicht verwechseln mit dem „Default"-Button, der die Routen immer auf die 5 Defaults zurücksetzt. Der Enable-Toggle darf bestehende Routen nicht anfassen.Beleg: AppState.setAudioEnabled / applyDefaultAudioWiring; AudioInput.stop()- [ ] 🔴 Globale Config: Audio einrichten, dann mehrere Presets durchschalten (und App neu starten). Die Audio-Verdrahtung (Routen/Gain/Auto-gain/Enabled) bleibt über Preset-Wechsel + Neustart hinweg erhalten — nicht mehr pro Preset. Genau das zum Durchtesten verschiedener Looks mit einem Audio-Setup. ⚠ Neu in dieser Runde (früher überschrieb ein Preset-Wechsel die Audio-Config).Beleg: AppState.audioReactive (global, persistiert via PresetStore _audio.json)
Capture / Export (neu)¶
- [ ] Capture öffnen (Share-Icon Topbar), „Photo" gewählt lassen, Defaults prüfen. Resolution default 2048 (Optionen 1080 / 2048 / „4K · 4096"), Format default PNG (PNG / HEIC / Live Photo). Button „Capture".
⚠ Default-Resolution ist 2048, nicht der kleinste Wert — steht 1080 vorgewählt, ist der State-Default verrutscht.
Beleg: CaptureConfig.stillSizes/defaultStill; PhotoFormat.label - [ ] 🔴 PNG „Capture", Fortschritt abwarten, Vorschau-Karte ansehen. Linearer ember-Progress mit „n %", danach Done-Karte mit echtem Thumbnail (kein Platzhalter) + Dateiname
physarum-<preset>-YYYYMMDD-HHmmss.png. Look muss visuell mit dem Live-Bild übereinstimmen (Farben, Trail-Dichte, Post-FX/Feedback-Glow). ⚠ Wegen 180 Warmup-Frames + density-skaliertem agentCount kann der Export anfangs dünner/blasser/dichter wirken — auf Farbtreue + Trail-Dichte achten (Presets sind auf Feld=512 getuned).Beleg: CaptureService warmupFrames=180; CaptureMath.captureFilename - [ ] 🔴 „Save to Photos" tippen (erstes Mal: Foto-Berechtigung erteilen). Button → „Saving…" → „Saved" (deaktiviert), grünes „Saved to Photos" mit Häkchen, Erfolgs-Haptik spürbar. PNG liegt in der Fotos-Bibliothek.
⚠ Bei verweigerter Berechtigung kein Crash — stattdessen „Couldn't save — check permissions."
Beleg: CaptureSaver.saveToPhotos (.addOnly); CaptureSheet.swift:178 (success haptic) - [ ] 🔴 Direkt danach erneut „Save to Photos" (schnell mehrfach tippen). Button bleibt deaktiviert/„Saved", kein zweites Speichern. In Fotos liegt das Bild genau einmal, kein Duplikat.
⚠ Doppel-Guard ist UI-State-basiert; bei sehr schnellem Doppeltipp vor
.savingauf echtes Duplikat prüfen.Beleg: CaptureSheet.swift:142-143 - [ ] Foto mit Format „HEIC" aufnehmen + speichern, Datei-Info in Fotos prüfen. Endung
.heic, in Fotos als HEIF/HEIC geführt, visuell identisch zum PNG. ⚠ Auf Geräten ohne HEIC-Encoder fällt der Writer still auf.jpgzurück — dann ist die Endung.jpg(kein Fehler, aber notieren).Beleg: ImageWriter.writeStill (heicSupported, JPEG-Fallback q0.9) - [ ] 🔴 Foto mit Format „Live Photo" aufnehmen + speichern, Asset in Fotos öffnen. Render ~2 s @ 60 fps. In Fotos erscheint ein Asset mit LIVE-Badge (oben links) — nicht getrenntes Foto + Video. Standbild = mittlerer Frame (Frame 60).
⚠ Paarung hängt an 3 Metadaten-Haken (content.identifier, still-image-time-Track, Maker-Dict „17", gleiche assetID). XCTest prüft nur das .mov; ob Fotos die Paarung akzeptiert, ist nur hier sichtbar.
Beleg: CaptureService.captureLivePhoto (keyFrameIndex frames/2); CaptureSaver.saveLivePhoto - [ ] 🔴 Das Live Photo in Fotos lange gedrückt halten. Die ~2 s Physarum-Bewegung spielt flüssig ab (Trails bewegen sich), Standbild davor = Bewegungsmitte.
⚠ Animiert der Long-Press nichts, ist die Still↔Video-Paarung gebrochen (assetID-Mismatch / Timing) — das ist der Kernbeweis des Live-Photo-Pfads.
Beleg: VideoClipRecorder stillImageTimeItem/contentIdentifierItem; ImageWriter Maker-Dict „17" - [ ] Auf „Video" umschalten, Defaults prüfen. Resolution default 1920 (1280/1920/2560), Duration default „10 s" (5/10/15/30), Codec default „HEVC (.mov)" (oder „H.264 (.mp4)"). Button „Render clip".
Beleg: CaptureConfig.videoSizes/videoDurations; VideoCodec.label - [ ] 🔴 HEVC-Clip 5 s rendern + speichern + in Fotos abspielen. Fortschritt über 300 Frames. Done-Karte mit Video-Thumbnail +
*.mov. In Fotos ~5 s flüssig @ 60 fps, gleicher Aspekt + Look wie Live (keine Farb-/Helligkeitsverschiebung). ⚠ Recorder taggt BT.709 — bei sichtbarer Farb-/Gamma-Verschiebung ist die Color-Pipeline schief.Beleg: VideoClipRecorder (HEVC→.mov, BT.709) - [ ] Zweiten Clip als „H.264 (.mp4)" rendern + abspielen. Endung
.mp4, spielt in Fotos, visuell gleichwertig zum HEVC.Beleg: VideoClipRecorder.begin (h264→.mp4) - [ ] Fill/Crop testen: je ein Foto mit „Fill (crop edges)" AUS (fit) und AN (cover), Ränder vergleichen. fit: ganzer Sim-Inhalt sichtbar, Export-Aspekt = Feld-Aspekt, kein Beschnitt, kein Letterbox. cover: view-aspekt-beschnittener Ausschnitt, exakt wie das Live-Composite (gleiche Crop-Kanten).
⚠ Default ist fit. Export muss exakt denselben Ausschnitt wie das Live-Bild zeigen.
Beleg: CaptureService.swift:54 (cover?viewAspect:fieldAspect); RootView renderCover default false - [ ] format-Menü auf „Portrait" bzw. „Square", je ein Foto, Seitenverhältnis prüfen. Square: quadratisch (720×720). Portrait: hochkant 9:16 (720×1280). Export-Aspekt folgt dem SimFormat, stimmt mit Live überein.
⚠ Bei fit bestimmt das Feld-Aspekt, bei cover das View-Aspekt — Format + Fill-Schalter zusammen denken.
Beleg: SimFormat.targetFieldSize; CaptureService.swift:49
Editor & Interaktion (Touch)¶
- [ ] 🔴 Editor öffnen (Slider-Icon
slider.horizontal.3rechts in der Topbar), in Portrait UND Landscape. Editor erscheint immer als Bottom-Sheet (nie Inspector-Spalte), Detents medium/large, Player läuft sichtbar weiter. Beim App-Start war das Sheet zu (player-first). ⚠ Erschiene der Editor auf iPhone als Inspector, ist die Size-Class-Erkennung kaputt.Beleg: EditorPresentation (isCompactWidth→.sheet); RootView .sheet(presentationDetents/backgroundInteraction) - [ ] 🔴 Slider verschieben; Zahlenwert rechts antippen + Wert eintippen + Return; dann Slider-Zeile doppelt antippen. Wert-Tap öffnet 64-pt-Textfeld mit aktuellem Wert; Eingabe + Return übernimmt geclampt. Doppeltap auf die Zeile setzt auf Preset-Default zurück (mit Undo). Int-Params ganze Zahlen, sonst %.3g.
⚠ Doppeltap-Reset darf nicht mit dem Single-Tap aufs Wertfeld kollidieren — gezielter Wert-Tap muss das Textfeld öffnen, nicht versehentlich resetten.
Beleg: ParamSlider.swift valueLabel/onTapGesture(count:2)/formatted - [ ] Kleines ⓘ neben einem Param-Label antippen. Popover (260 pt) mit Hilfetext per Tap. Labels ohne Hilfetext zeigen kein ⓘ.
⚠ Auf iPhone compact muss
presentationCompactAdaptation(.popover)es wirklich als Popover (nicht Full-Sheet) zeigen — visuell prüfen.Beleg: HelpLabel.swift onTapGesture/.popover(260) - [ ] 🔴 Library öffnen (Preset-Name-Button in der Topbar) bei geöffnetem Editor-Sheet. Zuerst schließt das Editor-Sheet, dann öffnet die Library (verzögert über pendingLibrary). Überschrift „PICK A PRESET", Suchfeld „Search…", Tag-Pills, Karten-Grid.
⚠ Bekanntes iOS-17-Risiko (im Code dokumentiert): Sheet-dismiss + sofortiges present verschluckt die Library — prüfen, dass sie wirklich erscheint und nicht „nichts passiert".
Beleg: RootView.openLibrary (pendingLibrary); LibraryView.swift:49/53 - [ ] 🔴 Mehrere Tag-Pills aktivieren, Tag-Leiste scrollen, im Suchfeld tippen. Aktive Pills orange, inaktive grau. Mehrere Tags = UND-Filter (nur Presets mit allen aktiven Tags). Grid auf iPhone = eine Spalte (vertikal gestapelt).
⚠ UND, nicht ODER — bei vielen Tags kann das Grid leer werden (erwartet).
Beleg: LibraryView.swift:41 (activeTags.isSubset); :20-28 columns - [ ] 🔴 Vollbild aktivieren (Icon
arrow.up.left.and.arrow.down.right), dann einmal auf die Simulation tippen. Topbar verschwindet, Player vollformatig. Ein Tap blendet unten zwei Glass-Pills ein: „Capture" + „Exit fullscreen", die nach 2,5 s wieder verschwinden. (Editor offen, nicht-Vollbild: Canvas-Tap schließt den Editor.) ⚠ Der Reveal-Tap ist die einzige Art, Vollbild zu verlassen — trifft der Tap auf dem MetalView nicht, sitzt man fest. Pills müssen unten thumb-erreichbar sitzen (auch Landscape).Beleg: RootView.handleCanvasTap/revealFsHint (2.5s) - [ ] iPhone Landscape mit langem (Custom-)Preset-Namen — Topbar ansehen. Name 1-zeilig, am Ende mit … abgeschnitten; Format-Menü + Capture + Vollbild + Editor-Icon bleiben als 34×30-Touchtargets sichtbar. Name truncatet, statt die Icons rauszudrücken.
⚠ Zwischen Name und den 4 Controls steht nur ein Spacer — bei sehr schmaler Breite prüfen, dass keine Icons abgeschnitten werden.
Beleg: RootView.swift:134-135 (lineLimit(1)/truncationMode); :140 Spacer
📱 iPad — Layout-Deltas + Smoke¶
Audio + Capture nutzen dieselben Code-Pfade wie iPhone — hier reicht ein Smoke-Test. Der echte iPad-Fokus ist die adaptive Präsentation (Inspector ↔ Sheet) und das Grid.
- [ ] 🔴 Vollbild (kein Split-View): Editor öffnen, Landscape → Portrait → zurück drehen. In Landscape (Breite > Höhe): Editor als Inspector-Spalte rechts (Icon
sidebar.right, min 300 / ideal 340 pt). In Portrait (Höhe ≥ Breite): derselbe Editor als Bottom-Sheet. Beim Drehen live umgeschaltet, Editor-Inhalt bleibt erhalten. ⚠ Heikelste Stelle: beide Bindings (Inspector & Sheet) hängen am selbenshowEditor/mode. Beim Rotieren mit offenem Editor muss genau eine Präsentation aktiv sein — Risiko: kurz doppelt, leer, oder Sheet klappt sofort wieder zu (iOS-Sheet-dismiss-vor-present).Beleg: EditorPresentation.swift:14; RootView .inspector(...)/.sheet(...) - [ ] 🔴 iPad in Split-View/Slide-Over ziehen (Physarum schmal), dann zurück auf Vollbild. Bei kompakter Breite: Editor wechselt zwingend auf Sheet, die PHYSARUM-Eyebrow in der Topbar verschwindet. Zurück auf Vollbild-Landscape: Inspector + Eyebrow kehren zurück.
⚠ Beim Compact-Werden mit offenem Inspector muss der Inspector verschwinden und das Sheet übernehmen (gleicher mode-Handoff wie bei Rotation).
Beleg: EditorPresentation.swift:13; RootView.showEyebrow (hSizeClass != .compact) - [ ] 🔴 Library öffnen, iPad drehen. Karten-Grid auf iPad-regular = adaptives Mehrspalten-Grid (min 150 pt); bei Rotation schaltet die Spaltenzahl um. (Auf compact/Split eine Spalte.)
Beleg: LibraryView.swift:20-28 columns (.adaptive(min:150)) - [ ] Format-Menü + Editor-Segmented-Picker synchron? Topbar-Menü (4 Optionen Auto/Square/Portrait/Landscape + „Fill"-Toggle) und der Segmented-Picker oben im Editor zeigen dieselbe Wahl (gleiche @AppStorage-Quelle); Änderung an einer Stelle ändert die andere + das Feld live.
⚠ Auf schmaler Sheet-Breite prüfen, dass „Landscape" im Segmented-Picker nicht abgeschnitten wird.
Beleg: EditorView.swift:16-23 @AppStorage('simFormat') - [ ] Smoke Audio: Enable → 5 Routen + Reaktion auf Musik (wie iPhone), Meter laufen.
- [ ] Smoke Capture: je ein PNG + ein Live Photo + ein Clip aufnehmen, in Fotos speichern, Live-Photo-Long-Press animiert.
🖥 macOS — voll (bisher nur build-verifiziert)¶
Fenster, Format & Viewport¶
- [ ] 🔴 Fenster von quadratisch auf breit und auf schmal-hoch ziehen, Look beobachten; dann „Fill (crop edges)" einschalten. Format „Auto": Simulation füllt das Fenster ohne Verzerrung. Fill AUS (Default): ganzes Feld sichtbar, Überstand zeigt Preset-Hintergrundfarbe (Letterbox, kein Schwarz). Fill AN: Feld füllt das Fenster, Ränder werden abgeschnitten, kein Verzerren/Stauchen beim Resize.
⚠ cover-Viewport wurde nie auf einem echten macOS-Fenster geprüft — auf Verzerrung beim Live-Resize + korrekte Crop- statt Stretch-Geometrie achten.
Beleg: MetalView.sync (cover/format); Renderer.encodeFrame(cover:) - [ ] Format-Menü (
aspectratio) Square / Portrait / Landscape durchgehen. Square → 720×720. Portrait → 720×1280. Landscape → 1280×720. Ohne Fill erscheint bei abweichendem Fensterformat die Letterbox in Preset-Bg. Auswahl überlebt App-Neustart (@AppStorage).Beleg: SimFormat.targetFieldSize; RootView.formatMenu
Slider-Präzision (Maus + Tastatur)¶
- [ ] 🔴 Mauszeiger über einen Param-Slider, Scrollrad/Zwei-Finger hoch/runter. Wert nudged pro Tick um base (
baseStep ?? range/100). Der Editor-ScrollView scrollt dabei nicht mit (Event konsumiert). Nudge wirkt nur, wenn der Zeiger über dem Slider-Frame im selben Fenster steht. ⚠ macOS-only NSEvent-Monitor, nie auf HW geprüft. (1) Slider-Drag per Maus muss weiter funktionieren (Monitor liegt hinter dem Slider), (2) Containment korrekt, (3) Zeiger nicht über Slider → Liste scrollt normal.Beleg: SliderPrecision.SliderScrollMonitor (addLocalMonitorForEvents) - [ ] 🔴 Beim Scrollen über einem Slider Shift halten, dann Option, dann beide. Shift = fein ×0.1, Option = grob ×10, beide = fein hat Vorrang (×0.1). Inkrement pro Tick ändert sich sichtbar.
⚠ Prüfen, dass fein wirklich Vorrang vor grob hat.
Beleg: SliderPrecision.precisionStep (fine?0.1:(coarse?10:1)) - [ ] 🔴 Slider per Maus/Tab fokussieren, Pfeil-Links/Rechts nudgen, Home/End drücken (auch mit Shift/Option); danach Cmd+Z. Rechts = +step, Links = −step (Shift fein, Option grob). Home → Minimum, End → Maximum. Jede Aktion ist undo-bar.
⚠
.focusable()auf Slidern ist auf macOS heikel — prüfen, ob der Slider Fokus annimmt und Pfeiltasten nicht vom umgebenden ScrollView abgefangen werden.Beleg: SliderPrecision.precisionControls (.focusable/.onKeyPress); ParamSlider (onEditStart: pushUndo)
Hilfe, Library-Kontextmenü¶
- [ ] Über ein ⓘ-Label hovern (nicht klicken), dann anklicken. Hover → natives macOS-Tooltip (
.help). Klick → zusätzlich Popover (260 pt, .callout) mit demselben Text. Labels ohne Hilfetext zeigen kein ⓘ. ⚠ Doppel-Affordanz (Hover-Tooltip UND Klick-Popover) — beide müssen unabhängig erscheinen, sich nicht blockieren.Beleg: HelpLabel.swift #if os(macOS) .help + .popover - [ ] 🔴 Library öffnen, auf einer eigenen (Custom-)Karte rechtsklicken. Kontextmenü nur bei Custom-Presets: „Rename", „Move Up", „Move Down", ─, „Delete" (rot). Built-ins zeigen kein Menü. Oberste Karte: „Move Up" ausgegraut; unterste: „Move Down" ausgegraut.
⚠ Rechtsklick ist der macOS-Hauptpfad (kein Long-Press) — prüfen, dass es nur auf Customs erscheint.
Beleg: LibraryView.swift .contextMenu - [ ] 🔴 „Move Up"/„Move Down" ohne Filter ausführen; dann Such-/Tag-Filter setzen, Menü erneut öffnen. Ohne Filter: Reihenfolge ändert sich sichtbar (eine Position pro Klick), bleibt nach Neustart erhalten. Mit aktivem Filter (Suche nicht leer ODER ≥1 Tag): beide Move-Einträge ausgegraut.
⚠ Verifizieren, dass beide Move-Buttons im gefilterten Zustand wirklich nicht klickbar sind.
Beleg: LibraryView.isFiltering; AppState.moveCustom→reorderedIDs/store.reorder - [ ] „Rename" wählen, neuen Namen + Enter; separat Dialog öffnen + „Cancel". Dialog (360 pt, „RENAME PRESET") vorbefüllt mit aktuellem Namen. Enter/„Rename" committet (getrimmt, max 64; leer → kein Rename). „Cancel" schließt ohne Änderung. Neuer Name sofort auf Karte + Topbar-Pille.
Beleg: RenameDialog.commit (trim + prefix(64))
Capture & Audio auf macOS¶
- [ ] 🔴 Share-Icon → Capture-UI öffnen, „Photo", „Capture". Auf macOS ein Popover (kein Sheet, kein .fileExporter), am Share-Button verankert, minWidth 280. Photo-Defaults wie iOS (Res 2048, PNG). Nach „Capture" Linear-Progress + Done-Karte mit Thumbnail + Dateiname. Nur „Share" (ShareLink) + „Done" — kein „Save to Photos".
⚠ Popover-Capture nie geprüft: (1) Popover-Größe/Clipping, (2) GPU-Still sieht aus wie der Live-Preview, (3) ShareLink öffnet das Share-Sheet mit gültiger Datei-URL.
Beleg: RootView .popover(showCapture); CaptureSheet #if os(iOS) save-button - [ ] In der Capture-UI auf „Video", kurzen Clip rendern (1920 / 10 s / HEVC). Video-Defaults wie iOS. Button „Render clip". Done-Karte mit Video-Frame-Thumbnail, Endung
.mov(bzw..mp4). Share teilt die Datei. ⚠ Render-Performance/GPU-Look des Video-Pfads auf echter macOS-GPU bestätigen.Beleg: CaptureConfig.videoSizes/Durations; CaptureSheet.videoFrame - [ ] 🔴 Audio-Sektion → „Enabled", macOS-Mic-Permission erlauben, ins Mic sprechen/Musik, Meter beobachten. macOS-Permission via
AVCaptureDevice.requestAccess(.audio). Leere Routen → 5 Defaults + autoGain. Meter Bass/Mid/High füllen sich live, fallen bei Stille auf 0. ⚠ macOS-Mic-Flow nur per HW prüfbar (anderer Pfad als iOS): (1) Dialog genau beim ersten Enable, (2) Ablehnung → kein Crash, (3) Meter-Cadence (nur jeder 4. Tick) ruckelt nicht sichtbar.Beleg: AudioInput.requestPermission #else AVCaptureDevice; AppState.setAudioEnabled - [ ] „Default" klicken (5 Routen prüfen), dann Route per
+hinzufügen + editieren. „Default" = die 5 bekannten Routen. Neue Route per „+" = Bass→Move depth 1 Trans. Pro Block: Source-Menü → Target-Menü (Sektionen Agent/Post) + minus zum Entfernen; Zeile 2 Mode-Segmented Lvl/Trans + Depth-Slider (−2..2) + Wert. Max 16 Routen, dann „+" ausgegraut. ⚠ Picker-Menü mit Sektionen Agent/Post per Maus prüfen; Segmented Lvl/Trans (116 pt) klickbar.Beleg: AppState.defaultAudioRoutes; AudioReactiveSection.routeRow
Befund-Notizen (für die nächste Code-Runde)¶
Hier eintragen, was abweicht — dann kann ich es gezielt fixen.
- Audio-Feel: (Stille-Zappeln? Leise-Musik-Übersteuern? → Silence-Gate / Transient-Peak-Normalisierung sind die zwei bekannten Hebel)
- iPad Inspector↔Sheet-Handoff: (doppelt / leer / klappt zu beim Rotieren?)
- macOS Slider-Präzision: (Drag noch ok? Fokus/Pfeiltasten? Scroll-Containment?)
- macOS cover-Viewport: (Verzerrung beim Resize?)
- Sonstiges: