wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)

6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
This commit is contained in:
Bernd Steckmeister
2026-07-03 05:47:18 +02:00
parent d706ace35f
commit 25ed765a03
195 changed files with 47784 additions and 1236 deletions
+182
View File
@@ -0,0 +1,182 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:image/image.dart' as img;
// ── Windows constants ────────────────────────────────────────────────────────
const _CF_DIB = 8;
const _CF_DIBV4 = 20;
// ── Native function typedefs ─────────────────────────────────────────────────
typedef _OpenClipboardNative = Int32 Function(IntPtr hwnd);
typedef _OpenClipboard = int Function(int hwnd);
typedef _CloseClipboardNative = Int32 Function();
typedef _CloseClipboard = int Function();
typedef _IsFormatAvailableNative = Int32 Function(Uint32 format);
typedef _IsFormatAvailable = int Function(int format);
typedef _GetClipboardDataNative = IntPtr Function(Uint32 format);
typedef _GetClipboardData = int Function(int format);
typedef _GlobalSizeNative = IntPtr Function(IntPtr hMem);
typedef _GlobalSize = int Function(int hMem);
typedef _GlobalLockNative = Pointer<Uint8> Function(IntPtr hMem);
typedef _GlobalLock = Pointer<Uint8> Function(int hMem);
typedef _GlobalUnlockNative = Int32 Function(IntPtr hMem);
typedef _GlobalUnlock = int Function(int hMem);
typedef _RegisterFormatNative = Uint32 Function(Pointer<Utf16> name);
typedef _RegisterFormat = int Function(Pointer<Utf16> name);
// ── Public API ───────────────────────────────────────────────────────────────
/// Returns image bytes (PNG) from the Windows clipboard, or null if there is
/// no image or the platform is not Windows.
///
/// Checks the following formats in order:
/// 1. "PNG" custom registered format used by browsers / screenshot tools
/// 2. CF_DIBV4 / CF_DIB Device-Independent Bitmap (Win+Shift+S, etc.)
Uint8List? getClipboardImageBytes() {
if (!Platform.isWindows) return null;
try {
return _readClipboardImage();
} catch (_) {
return null;
}
}
// ── Implementation ───────────────────────────────────────────────────────────
Uint8List? _readClipboardImage() {
final user32 = DynamicLibrary.open('user32.dll');
final kernel32 = DynamicLibrary.open('kernel32.dll');
final openClipboard =
user32.lookupFunction<_OpenClipboardNative, _OpenClipboard>('OpenClipboard');
final closeClipboard =
user32.lookupFunction<_CloseClipboardNative, _CloseClipboard>('CloseClipboard');
final isFormatAvailable =
user32.lookupFunction<_IsFormatAvailableNative, _IsFormatAvailable>(
'IsClipboardFormatAvailable');
final getClipboardData =
user32.lookupFunction<_GetClipboardDataNative, _GetClipboardData>('GetClipboardData');
final registerFormat =
user32.lookupFunction<_RegisterFormatNative, _RegisterFormat>(
'RegisterClipboardFormatW');
final globalSize =
kernel32.lookupFunction<_GlobalSizeNative, _GlobalSize>('GlobalSize');
final globalLock =
kernel32.lookupFunction<_GlobalLockNative, _GlobalLock>('GlobalLock');
final globalUnlock =
kernel32.lookupFunction<_GlobalUnlockNative, _GlobalUnlock>('GlobalUnlock');
// Register custom "PNG" format (Chrome, Edge, Firefox, Snipping Tool export)
final pngNamePtr = 'PNG'.toNativeUtf16();
final pngFormat = registerFormat(pngNamePtr);
malloc.free(pngNamePtr);
if (openClipboard(0) == 0) return null;
try {
// 1. Try custom PNG format — bytes are ready-to-use PNG
if (pngFormat != 0 && isFormatAvailable(pngFormat) != 0) {
final bytes = _readGlobalBytes(
getClipboardData(pngFormat), globalSize, globalLock, globalUnlock);
if (bytes != null && bytes.isNotEmpty) return bytes;
}
// 2. Try CF_DIBV4 then CF_DIB — convert DIB to PNG via the image package
for (final fmt in [_CF_DIBV4, _CF_DIB]) {
if (isFormatAvailable(fmt) != 0) {
final dibBytes = _readGlobalBytes(
getClipboardData(fmt), globalSize, globalLock, globalUnlock);
if (dibBytes != null && dibBytes.isNotEmpty) {
final png = _dibToPng(dibBytes);
if (png != null) return png;
}
}
}
return null;
} finally {
closeClipboard();
}
}
/// Copy the bytes pointed to by a global memory handle.
Uint8List? _readGlobalBytes(
int handle,
_GlobalSize globalSize,
_GlobalLock globalLock,
_GlobalUnlock globalUnlock,
) {
if (handle == 0) return null;
final size = globalSize(handle);
if (size == 0) return null;
final ptr = globalLock(handle);
if (ptr.address == 0) return null;
try {
return Uint8List.fromList(ptr.asTypedList(size));
} finally {
globalUnlock(handle);
}
}
/// Convert a CF_DIB / CF_DIBV4 block to PNG bytes using the `image` package.
///
/// A DIB is essentially a BMP without the 14-byte file header. We prepend the
/// header and let the `image` package decode it.
Uint8List? _dibToPng(Uint8List dib) {
if (dib.length < 40) return null; // minimum BITMAPINFOHEADER size
final bd = ByteData.sublistView(dib);
final headerSize = bd.getUint32(0, Endian.little); // biSize
final bitCount = bd.getUint16(14, Endian.little); // biBitCount
final compression = bd.getUint32(16, Endian.little); // biCompression
final clrUsed = bd.getUint32(32, Endian.little); // biClrUsed
// Calculate color table size (in bytes, each entry = 4 bytes RGBQUAD)
int colorTableEntries;
if (bitCount <= 8) {
colorTableEntries = clrUsed != 0 ? clrUsed : (1 << bitCount);
} else if (compression == 3 /* BI_BITFIELDS */ || compression == 6 /* BI_ALPHABITFIELDS */) {
colorTableEntries = 0;
} else {
colorTableEntries = clrUsed;
}
final colorTableBytes = colorTableEntries * 4;
// Offset to pixel data from start of FILE (= 14 file header bytes + DIB)
final pixelOffset = 14 + headerSize + colorTableBytes;
// Build the 14-byte BITMAPFILEHEADER
final fileSize = 14 + dib.length;
final fileHeader = Uint8List(14);
final fh = ByteData.sublistView(fileHeader);
fileHeader[0] = 0x42; // 'B'
fileHeader[1] = 0x4D; // 'M'
fh.setUint32(2, fileSize, Endian.little);
fh.setUint32(6, 0, Endian.little);
fh.setUint32(10, pixelOffset, Endian.little);
final bmp = Uint8List(fileHeader.length + dib.length);
bmp.setRange(0, fileHeader.length, fileHeader);
bmp.setRange(fileHeader.length, bmp.length, dib);
try {
final decoded = img.decodeBmp(bmp);
if (decoded == null) return null;
return Uint8List.fromList(img.encodePng(decoded));
} catch (_) {
return null;
}
}