Files
adminandClaude Opus 5 865dd61535 Einfuehrung fuer Einsteiger: Folien mit echten Aufnahmen und Markierungen
Neue Seite "Einfuehrung" im Menue: 31 Folien in zwei Teilen - Bedienen im
Alltag (Grundriss, Raum, Rollos, Heizung, Licht, Zeitleiste, Solar, Auto,
Heizstab und Wasser, Statistik, Historie, Heizung, Wetter, Fahrzeug, Hilfe)
und Einrichten und Verwalten (Preise, Grundriss, Kacheln, Geraete,
Automatik anlegen in Wenn/Wann/Dann, Ketten, Fahrzeugschluessel,
Protokolle).

- Bilder sind echte Aufnahmen der Oberflaeche mit Livewerten, Handy- und
  Desktopgroesse. tools/einfuehrung/aufnehmen.ps1 steuert Edge ohne
  Fenster ueber das DevTools-Protokoll nach plan.json und schreibt zu jedem
  Bild die Lage der markierten Elemente; daraus sitzen die nummerierten
  Punkte. Wetterort und Schluesselendungen werden vor der Aufnahme
  abgedeckt.
- Blaettern per Knopf, Pfeiltaste oder Wischen; Kapitelsprung; die Folie
  steht in der Adresse (#folie-7). Erklaerung und Punkt heben sich
  gegenseitig hervor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 14:32:14 +02:00

99 lines
5.2 KiB
PowerShell

param(
[string]$Plan = "$PSScriptRoot\plan.json",
[string]$Ausgabe = "$PSScriptRoot\bilder"
)
# Edge ohne Fenster über das DevTools-Protokoll steuern: laden, echt warten,
# optional JavaScript ausführen (klicken, scrollen), Screenshot speichern.
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force $Ausgabe | Out-Null
$edge = "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe"
$profil = Join-Path $env:TEMP ("edge-cdp-" + [guid]::NewGuid())
$port = 9333
$proc = Start-Process $edge -ArgumentList @("--headless=new", "--disable-gpu", "--hide-scrollbars",
"--remote-debugging-port=$port", "--user-data-dir=$profil", "--no-first-run", "about:blank") -PassThru
function Warte-Auf-Port {
for ($i = 0; $i -lt 40; $i++) {
try { return Invoke-RestMethod "http://127.0.0.1:$port/json/list" } catch { Start-Sleep -Milliseconds 250 }
}
throw "Edge antwortet nicht"
}
$script:id = 0
function Sende($ws, $methode, $parameter) {
$script:id++
$nachricht = @{ id = $script:id; method = $methode; params = $parameter } | ConvertTo-Json -Depth 20 -Compress
$bytes = [Text.Encoding]::UTF8.GetBytes($nachricht)
$ws.SendAsync([ArraySegment[byte]]$bytes, [Net.WebSockets.WebSocketMessageType]::Text, $true, [Threading.CancellationToken]::None).Wait()
$puffer = New-Object byte[] (1024 * 1024)
while ($true) {
$ms = New-Object IO.MemoryStream
do {
$teil = $ws.ReceiveAsync([ArraySegment[byte]]$puffer, [Threading.CancellationToken]::None)
$teil.Wait()
$ms.Write($puffer, 0, $teil.Result.Count)
} while (-not $teil.Result.EndOfMessage)
$antwort = [Text.Encoding]::UTF8.GetString($ms.ToArray()) | ConvertFrom-Json
if ($antwort.id -eq $script:id) { return $antwort }
}
}
try {
$ziele = Warte-Auf-Port
$seite = $ziele | Where-Object { $_.type -eq "page" } | Select-Object -First 1
$ws = New-Object Net.WebSockets.ClientWebSocket
$ws.Options.KeepAliveInterval = [TimeSpan]::FromSeconds(20)
$ws.ConnectAsync([Uri]$seite.webSocketDebuggerUrl, [Threading.CancellationToken]::None).Wait()
Sende $ws "Page.enable" @{} | Out-Null
Sende $ws "Runtime.enable" @{} | Out-Null
$schritte = Get-Content $Plan -Raw -Encoding UTF8 | ConvertFrom-Json
$breit = 1280; $hoch = 900; $handy = $false
foreach ($s in $schritte) {
# Ein Schritt mit eigener Adresse legt die Größe fest; Schritte ohne
# Adresse (klicken, scrollen) behalten die des vorigen.
if ($s.url) {
$breit = if ($s.breit) { $s.breit } else { 1280 }
$hoch = if ($s.hoch) { $s.hoch } else { 900 }
$handy = [bool]$s.handy
}
$em = Sende $ws "Emulation.setDeviceMetricsOverride" @{ width = [int]$breit; height = [int]$hoch; deviceScaleFactor = $(if ($handy) { 2 } else { 1 }); mobile = $handy }
if ($em.error) { Write-Output ("Emulation: " + ($em.error | ConvertTo-Json -Compress)) }
if ($s.url) {
Sende $ws "Page.navigate" @{ url = $s.url } | Out-Null
Start-Sleep -Seconds $(if ($s.warte) { $s.warte } else { 6 })
}
# Einstellungsreiter per Klick öffnen - ein Anker in der Adresse greift
# nicht, wenn nur er sich ändert.
if ($s.vorher) {
Sende $ws "Runtime.evaluate" @{ expression = "new bootstrap.Tab(document.getElementById('einstellung-$($s.vorher)-tab')).show()" } | Out-Null
Start-Sleep -Seconds 2
}
if ($s.js) {
$r = Sende $ws "Runtime.evaluate" @{ expression = $s.js; awaitPromise = $true; returnByValue = $true }
if ($r.result.exceptionDetails) { Write-Output ("JS-Fehler in " + $s.datei + ": " + $r.result.exceptionDetails.exception.description) }
if ($s.zeige) { Write-Output ("JS " + $s.datei + ": " + ($r.result.result.value | ConvertTo-Json -Compress)) }
Start-Sleep -Seconds $(if ($s.danach) { $s.danach } else { 2 })
}
if ($s.datei) {
# Lage der markierten Elemente in Prozent des Bildausschnitts - daraus
# setzt die Einführung ihre nummerierten Punkte.
if ($s.marken) {
$sel = $s.marken | ConvertTo-Json -Compress
$ausdruck = "(() => { const m = $sel; const aus = {}; for (const k in m) { const el = document.querySelector(m[k]); if (!el) { aus[k] = null; continue; } const r = el.getBoundingClientRect(); aus[k] = { x: +((r.left + r.width / 2) / innerWidth * 100).toFixed(1), y: +((r.top + r.height / 2) / innerHeight * 100).toFixed(1) }; } return JSON.stringify(aus); })()"
$r = Sende $ws "Runtime.evaluate" @{ expression = $ausdruck; returnByValue = $true }
$name = [IO.Path]::GetFileNameWithoutExtension($s.datei)
[IO.File]::WriteAllText((Join-Path $Ausgabe "$name.json"), $r.result.result.value, [Text.Encoding]::UTF8)
if ($r.result.result.value -match "null") { Write-Output ("Marke fehlt in " + $s.datei + ": " + $r.result.result.value) }
}
$bild = Sende $ws "Page.captureScreenshot" @{ format = "jpeg"; quality = 82 }
[IO.File]::WriteAllBytes((Join-Path $Ausgabe $s.datei), [Convert]::FromBase64String($bild.result.data))
Write-Output ("ok " + $s.datei)
}
}
} finally {
if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
Get-Process msedge -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like "*$profil*" } | Stop-Process -Force -ErrorAction SilentlyContinue
}