From f196e0a39d8e2ba1be38a2e0b316e34801b8fd20 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Fri, 9 Oct 2026 16:55:30 +0200 Subject: [PATCH 01/14] =?UTF-8?q?Erg=C3=A4nzt=20plattform=C3=BCbergreifend?= =?UTF-8?q?e=20App=20f=C3=BCr=20Suchk=C3=BCrzel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/suchkuerzel-transfer.yml | 44 +++ browser-suchmigration/AGENTS.md | 25 ++ browser-suchmigration/README.md | 11 + browser-suchmigration/app/.gitignore | 5 + browser-suchmigration/app/README.md | 63 +++++ .../app/SuchkuerzelTransfer.spec | 54 ++++ browser-suchmigration/app/main.py | 250 ++++++++++++++++++ browser-suchmigration/app/requirements.txt | 2 + 8 files changed, 454 insertions(+) create mode 100644 .github/workflows/suchkuerzel-transfer.yml create mode 100644 browser-suchmigration/AGENTS.md create mode 100644 browser-suchmigration/README.md create mode 100644 browser-suchmigration/app/.gitignore create mode 100644 browser-suchmigration/app/README.md create mode 100644 browser-suchmigration/app/SuchkuerzelTransfer.spec create mode 100644 browser-suchmigration/app/main.py create mode 100644 browser-suchmigration/app/requirements.txt diff --git a/.github/workflows/suchkuerzel-transfer.yml b/.github/workflows/suchkuerzel-transfer.yml new file mode 100644 index 0000000..0154bc1 --- /dev/null +++ b/.github/workflows/suchkuerzel-transfer.yml @@ -0,0 +1,44 @@ +name: Suchkürzel-Transfer App + +on: + push: + paths: + - "browser-suchmigration/app/**" + - "browser-suchmigration/Suchkuerzel-Transfer.command" + - ".github/workflows/suchkuerzel-transfer.yml" + pull_request: + paths: + - "browser-suchmigration/app/**" + - "browser-suchmigration/Suchkuerzel-Transfer.command" + - ".github/workflows/suchkuerzel-transfer.yml" + workflow_dispatch: + +jobs: + package: + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: browser-suchmigration/app + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Linux Qt-Laufzeit installieren + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libegl1 libgl1 libxkbcommon-x11-0 libdbus-1-3 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1 + - run: python -m pip install -r requirements.txt + - name: Core-Selbsttest + run: python main.py --self-test + env: + QT_QPA_PLATFORM: offscreen + - name: Plattformpaket bauen + run: pyinstaller --noconfirm --clean SuchkuerzelTransfer.spec + - uses: actions/upload-artifact@v4 + with: + name: SuchkuerzelTransfer-${{ matrix.os }} + path: browser-suchmigration/app/dist/ diff --git a/browser-suchmigration/AGENTS.md b/browser-suchmigration/AGENTS.md new file mode 100644 index 0000000..8250722 --- /dev/null +++ b/browser-suchmigration/AGENTS.md @@ -0,0 +1,25 @@ +# Browser-Suchmigration + +Lokale Werkzeuge zum Übertragen benutzerdefinierter Suchmaschinen und Suchkürzel zwischen Chromium-Profilen. Die Desktop-App liegt in `app/`, nutzt PySide6 und bindet die vorhandene Transferlogik aus `Suchkuerzel-Transfer.command` ein. Die übrigen Skripte sind bestehende macOS-Werkzeuge. + +## Grenzen + +- Browserprofil-Daten bleiben lokal. Nie ein gesamtes Profil ersetzen. +- Vor jedem Import oder Wiederherstellen in der Desktop-App Browserbeendigung prüfen, Änderungen bestätigen lassen und eine SQLite-Sicherung anlegen. +- Beim Wiederherstellen nur eigene Suchkürzel ändern; Browser- und andere Profiltabellen unverändert lassen. +- Exportformat in der bestehenden Transferdatei kompatibel halten. Standard-Suchmaschinen-Einstellung muss der Nutzer im Browser bestätigen. +- Keine Zugangsdaten oder Browserinhalte außerhalb der Suchmaschinendaten erfassen. + +## App-Befehle + +Im Verzeichnis `app/`: + +```bash +python -m venv .venv +python -m pip install -r requirements.txt +python main.py +python main.py --self-test +pyinstaller --noconfirm --clean SuchkuerzelTransfer.spec +``` + +Pakete für Windows, macOS und Linux jeweils auf dem Zielsystem bauen. Der GitHub-Workflow erledigt das für alle drei Systeme und führt den Kern-Selbsttest aus. diff --git a/browser-suchmigration/README.md b/browser-suchmigration/README.md new file mode 100644 index 0000000..5273115 --- /dev/null +++ b/browser-suchmigration/README.md @@ -0,0 +1,11 @@ +# Browser-Suchmigration + +Werkzeuge zum lokalen Übertragen und Sichern eigener Browser-Suchkürzel. + +- [Suchkürzel-Transfer Desktop-App](app/README.md): Windows, macOS und Linux +- `Suchkuerzel-Transfer.command`: bisheriges macOS-Einzeldateiwerkzeug +- `Sicheres-Migrationsskript/`: bisherige macOS-GUI und CLI + +Die neue Desktop-App verwendet dasselbe XML-Format wie `Suchkuerzel-Transfer.command`. Sie zeigt Änderungen vor dem Import an, erstellt vor dem Schreiben eine SQLite-Sicherung und ändert nur Suchkürzel. Änderungen an der Standardsuche müssen anschließend in den Browser-Einstellungen bestätigt werden. + +Entwicklung und plattformspezifische Paketierung sind in [app/README.md](app/README.md) beschrieben. Das CI baut unter Windows, macOS und Ubuntu. diff --git a/browser-suchmigration/app/.gitignore b/browser-suchmigration/app/.gitignore new file mode 100644 index 0000000..9b22b07 --- /dev/null +++ b/browser-suchmigration/app/.gitignore @@ -0,0 +1,5 @@ +.venv/ +build/ +dist/ +__pycache__/ +*.py[cod] diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md new file mode 100644 index 0000000..a367cb8 --- /dev/null +++ b/browser-suchmigration/app/README.md @@ -0,0 +1,63 @@ +# Suchkürzel-Transfer + +Desktop-App zum Sichern und Übertragen eigener Suchkürzel in Chromium-Browsern. Die Daten bleiben auf dem Rechner. Die Oberfläche und Profil-Erkennung laufen unter Windows, macOS und Linux. + +## Unterstützte Browser + +- Google Chrome, Brave, Microsoft Edge, Vivaldi und Chromium auf Windows, macOS und Linux +- Ego Lite auf macOS +- Firefox erhält Suchkürzel über eine exportierte Lesezeichen-HTML-Datei. Ein direkter Firefox-Datenbankimport wird nicht vorgenommen. + +Die App erkennt übliche Profilordner automatisch. Über „Profilordner manuell auswählen“ lassen sich abweichende Installationspfade und Linux-Flatpak-Profile angeben; dazu den konkreten `Default`- oder `Profile N`-Ordner mit der Datei `Web Data` auswählen. + +## Funktionen + +- Suchkürzel als XML exportieren und aus XML importieren +- Vor dem Import Änderungen und Konflikte anzeigen +- Vor einem Schreibzugriff die Browserdatenbank sichern +- Bestehende Kürzel aktualisieren, neue hinzufügen und nichts löschen +- Suchkürzel für Firefox als HTML-Lesezeichen exportieren +- Profile über den üblichen Browserpfad erkennen oder manuell auswählen +- Standardsuche aus der Quelle in der Zielprofil-Datenbank verfügbar machen; die Auswahl als Standardsuche bestätigt der Nutzer anschließend im Browser + +Der Zielbrowser muss für den Import vollständig beendet sein. Das Programm bricht bei geschützten oder mehrdeutigen Kürzel-Kollisionen ab. Backups liegen unter `Documents/Suchkuerzel-Backups`. + +## Start aus dem Quellcode + +Python 3.11 oder neuer wird benötigt. + +```bash +cd app +python -m venv .venv +``` + +macOS/Linux: + +```bash +source .venv/bin/activate +python -m pip install -r requirements.txt +python main.py +``` + +Windows PowerShell: + +```powershell +.\.venv\Scripts\Activate.ps1 +python -m pip install -r requirements.txt +python main.py +``` + +## Eigenständige Pakete bauen + +PyInstaller muss auf dem jeweiligen Zielbetriebssystem ausgeführt werden. Auf jedem System im `app`-Ordner: + +```bash +python -m pip install -r requirements.txt +pyinstaller --noconfirm --clean SuchkuerzelTransfer.spec +``` + +Das Paket liegt danach in `dist/`. Die GitHub-Aktion baut getrennte Pakete auf Windows, macOS und Ubuntu und führt dabei den vorhandenen Kern-Selbsttest aus. macOS-Signierung/Notarisierung und Windows-Signierung sind für eine öffentliche Verteilung separat einzurichten. + +## Datenformat und Grenzen + +Das XML-Format der App bleibt mit dem bisherigen `Suchkuerzel-Transfer.command` kompatibel. SQLite-Backups werden vor Änderungen erstellt. Die App ersetzt keine Systemeinstellung für die Standardsuche: Chromium-Browser verlangen dafür eine Bestätigung in ihren Einstellungen. Flatpak-Sandboxen können den Zugriff auf Browserprofile verhindern; in diesem Fall muss die App mit passendem Profilzugriff gestartet oder ein XML-Transfer verwendet werden. diff --git a/browser-suchmigration/app/SuchkuerzelTransfer.spec b/browser-suchmigration/app/SuchkuerzelTransfer.spec new file mode 100644 index 0000000..bb7801f --- /dev/null +++ b/browser-suchmigration/app/SuchkuerzelTransfer.spec @@ -0,0 +1,54 @@ +# PyInstaller-Spezifikation: auf Windows, macOS und Linux jeweils lokal bauen. +from pathlib import Path + +app_dir = Path(SPECPATH) +source_root = app_dir.parent + +a = Analysis( + [str(app_dir / "main.py")], + pathex=[str(app_dir)], + binaries=[], + datas=[(str(source_root / "Suchkuerzel-Transfer.command"), ".")], + hiddenimports=[ + "argparse", + "contextlib", + "datetime", + "hashlib", + "html", + "json", + "os", + "re", + "shutil", + "sqlite3", + "subprocess", + "sys", + "tempfile", + "time", + "typing", + "uuid", + "xml.etree.ElementTree", + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) +pyz = PYZ(a.pure) +exe = EXE( + pyz, + a.scripts, + [], + [], + name="SuchkuerzelTransfer", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + console=False, + exclude_binaries=True, + disable_windowed_traceback=False, +) +coll = COLLECT(exe, a.binaries, a.datas, strip=False, upx=False, name="SuchkuerzelTransfer") +if __import__("sys").platform == "darwin": + app = BUNDLE(coll, name="SuchkuerzelTransfer.app", bundle_identifier="de.suchkuerzel.transfer") diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py new file mode 100644 index 0000000..68897f6 --- /dev/null +++ b/browser-suchmigration/app/main.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Plattformübergreifende Oberfläche für den vorhandenen Suchkürzel-Transfer.""" + +from __future__ import annotations + +import importlib.machinery +import importlib.util +import os +import sqlite3 +import subprocess +import sys +from pathlib import Path + +from PySide6.QtCore import QUrl +from PySide6.QtGui import QDesktopServices +from PySide6.QtWidgets import QApplication, QFileDialog, QInputDialog, QMessageBox + + +ROOT = Path(__file__).resolve().parents[1] +bundle_root = Path(getattr(sys, "_MEIPASS", ROOT)) +transfer_candidates = ( + bundle_root / "Suchkuerzel-Transfer.command", + Path(sys.executable).resolve().parent.parent / "Resources" / "Suchkuerzel-Transfer.command", +) +TRANSFER_FILE = next((path for path in transfer_candidates if path.is_file()), transfer_candidates[0]) + + +def browser_roots() -> dict[str, dict[str, object]]: + home = Path.home() + if sys.platform == "win32": + local = Path(os.environ.get("LOCALAPPDATA", home / "AppData/Local")) + roaming = Path(os.environ.get("APPDATA", home / "AppData/Roaming")) + entries = { + "chrome": ("Google Chrome", "chrome.exe", local / "Google/Chrome/User Data", "chrome://settings/search"), + "brave": ("Brave", "brave.exe", local / "BraveSoftware/Brave-Browser/User Data", "brave://settings/search"), + "edge": ("Microsoft Edge", "msedge.exe", local / "Microsoft/Edge/User Data", "edge://settings/search"), + "vivaldi": ("Vivaldi", "vivaldi.exe", local / "Vivaldi/User Data", "vivaldi://settings/search"), + "chromium": ("Chromium", "chrome.exe", local / "Chromium/User Data", "chrome://settings/search"), + } + elif sys.platform == "darwin": + support = home / "Library/Application Support" + entries = { + "chrome": ("Google Chrome", "Google Chrome", support / "Google/Chrome", "chrome://settings/search"), + "brave": ("Brave", "Brave Browser", support / "BraveSoftware/Brave-Browser", "brave://settings/search"), + "edge": ("Microsoft Edge", "Microsoft Edge", support / "Microsoft Edge", "edge://settings/search"), + "vivaldi": ("Vivaldi", "Vivaldi", support / "Vivaldi", "vivaldi://settings/search"), + "chromium": ("Chromium", "Chromium", support / "Chromium", "chrome://settings/search"), + "ego-lite": ("Ego Lite", "ego lite", support / "Citro Labs/ego lite", "chrome://settings/search"), + } + else: + config = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")) + entries = { + "chrome": ("Google Chrome", "chrome", config / "google-chrome", "chrome://settings/search"), + "brave": ("Brave", "brave", config / "BraveSoftware/Brave-Browser", "brave://settings/search"), + "edge": ("Microsoft Edge", "msedge", config / "microsoft-edge", "edge://settings/search"), + "vivaldi": ("Vivaldi", "vivaldi", config / "vivaldi", "vivaldi://settings/search"), + "chromium": ("Chromium", "chromium", config / "chromium", "chrome://settings/search"), + "chromium-snap": ("Chromium (Snap)", "chromium", home / "snap/chromium/common/chromium", "chrome://settings/search"), + } + return { + key: {"label": label, "process": process, "root": path, "settings": settings} + for key, (label, process, path, settings) in entries.items() + } + + +def process_is_running(executable: str) -> bool: + if sys.platform == "win32": + result = subprocess.run(["tasklist", "/FI", f"IMAGENAME eq {executable}", "/NH"], capture_output=True, text=True) + return executable.casefold() in result.stdout.casefold() + if sys.platform == "darwin": + return subprocess.run(["pgrep", "-f", executable], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + proc = Path("/proc") + if proc.is_dir(): + for entry in proc.iterdir(): + if entry.name.isdigit(): + try: + if executable.casefold() in (entry / "comm").read_text().casefold(): + return True + except OSError: + continue + return False + return subprocess.run(["pgrep", "-f", executable], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + + +def load_transfer(): + loader = importlib.machinery.SourceFileLoader("suchkuerzel_transfer_core", str(TRANSFER_FILE)) + spec = importlib.util.spec_from_loader(loader.name, loader) + if spec is None: + raise RuntimeError("Die Transferlogik konnte nicht geladen werden.") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + module.BROWSERS = browser_roots() + module.BACKUP_DIR = Path.home() / "Documents" / "Suchkuerzel-Backups" + original_wait_until_import_ready = module.wait_until_import_ready + module.wait_until_import_ready = lambda profile, require_closed: original_wait_until_import_ready(profile, require_closed=True) + + def choose_from_list(title: str, prompt: str, choices: list[str]) -> str: + choice, accepted = QInputDialog.getItem(None, title, prompt, choices, 0, False) + if not accepted: + raise module.Cancelled + return choice + + def choose_profile(profiles, purpose: str): + labels = [f"{item['label']} — {item['count']} Kürzel" for item in profiles] + manual = "Profilordner manuell auswählen …" + choice, accepted = QInputDialog.getItem(None, "Suchkürzel-Transfer", purpose, labels + [manual], 0, False) + if not accepted: + raise module.Cancelled + if choice != manual: + return profiles[labels.index(choice)] + browser_labels = [str(item["label"]) for item in module.BROWSERS.values()] + browser_label, accepted = QInputDialog.getItem(None, "Browser auswählen", "Zu welchem Browser gehört das Profil?", browser_labels, 0, False) + if not accepted: + raise module.Cancelled + browser_key = next(key for key, item in module.BROWSERS.items() if item["label"] == browser_label) + directory = QFileDialog.getExistingDirectory(None, "Browser-Profilordner auswählen", str(Path.home())) + if not directory: + raise module.Cancelled + profile_dir = Path(directory) + database = profile_dir / "Web Data" + if not database.is_file(): + raise module.TransferError("Der ausgewählte Ordner enthält keine Chromium-Suchdatenbank („Web Data“). Bitte direkt den Ordner Default oder Profile N auswählen.") + count = len(module.read_shortcuts(database)) + return {"token": f"{browser_key}:{profile_dir}", "browser_key": browser_key, "label": f"{browser_label} — {module.profile_name(profile_dir)}", "directory": profile_dir, "path": database, "count": count} + + def choose_save_path(default_name: str) -> Path: + filename, _ = QFileDialog.getSaveFileName(None, "XML-Export speichern", str(Path.home() / default_name), "Suchkürzel-XML (*.xml)") + if not filename: + raise module.Cancelled + path = Path(filename) + return path if path.suffix.lower() == ".xml" else path.with_suffix(".xml") + + def choose_firefox_save_path(default_name: str) -> Path: + filename, _ = QFileDialog.getSaveFileName(None, "Firefox-Importdatei speichern", str(Path.home() / default_name), "Firefox-Lesezeichen (*.html)") + if not filename: + raise module.Cancelled + path = Path(filename) + return path if path.suffix.lower() in {".html", ".htm"} else path.with_suffix(".html") + + def choose_xml_file() -> Path: + filename, _ = QFileDialog.getOpenFileName(None, "Suchkürzel-XML auswählen", str(Path.home()), "Suchkürzel-XML (*.xml)") + if not filename: + raise module.Cancelled + return Path(filename) + + def confirm(title: str, message: str, action: str) -> None: + result = QMessageBox.question(None, title, message, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel) + if result != QMessageBox.StandardButton.Yes: + raise module.Cancelled + + def alert(title: str, message: str, kind: str = "informational") -> None: + box = QMessageBox() + box.setWindowTitle(title) + box.setText(message) + box.setIcon(QMessageBox.Icon.Critical if kind == "error" else QMessageBox.Icon.Information) + box.exec() + + def ask_retry_locked(label: str) -> None: + result = QMessageBox.warning(None, "Browser schließen", f"{label} ist noch geöffnet.\n\nBitte vollständig beenden und erneut prüfen.", QMessageBox.StandardButton.Retry | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Retry) + if result != QMessageBox.StandardButton.Retry: + raise module.Cancelled + + module.choose_from_list = choose_from_list + module.choose_profile = choose_profile + module.choose_save_path = choose_save_path + module.choose_firefox_save_path = choose_firefox_save_path + module.choose_xml_file = choose_xml_file + module.confirm = confirm + module.alert = alert + module.ask_retry_locked = ask_retry_locked + module.browser_running = lambda profile: process_is_running(module.BROWSERS[profile["browser_key"]]["process"]) + module.open_search_settings = lambda profile: QDesktopServices.openUrl(QUrl(module.BROWSERS[profile["browser_key"]]["settings"])) + + def firefox_export_flow(profiles) -> None: + profile = module.choose_profile(profiles, "Chrome-/Brave-Quellprofil für Firefox auswählen:") + shortcuts = module.read_shortcuts(profile["path"]) + if not shortcuts: + raise module.TransferError("Dieses Profil enthält keine aktiven eigenen Suchkürzel.") + filename = f"Firefox-Suchkuerzel-{module.safe_component(profile['label'])}-{module.dt.date.today().isoformat()}.html" + destination = choose_firefox_save_path(filename) + destination.write_text(module.to_firefox_html(shortcuts, profile["label"]), encoding="utf-8") + QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) + alert("Firefox-Datei erstellt", f"{len(shortcuts)} Suchkürzel gespeichert:\n{destination}\n\nIn Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.") + + def restore_flow(profiles) -> None: + profile = module.choose_profile(profiles, "Zielprofil für die Wiederherstellung auswählen:") + filename, _ = QFileDialog.getOpenFileName(None, "Suchkürzel-Sicherung auswählen", str(module.BACKUP_DIR), "SQLite-Sicherung (*.sqlite)") + if not filename: + raise module.Cancelled + backup_path = Path(filename) + try: + entries = module.read_shortcuts(backup_path) + except (OSError, sqlite3.Error, module.TransferError) as error: + raise module.TransferError("Die ausgewählte Datei ist keine lesbare Suchkürzel-Sicherung.") from error + if not entries: + raise module.TransferError("Die Sicherung enthält keine eigenen Suchkürzel.") + result = module.preview(profile["path"], entries) + if result["conflicts"]: + raise module.TransferError("Geschützte oder doppelte Kürzel kollidieren: " + ", ".join(result["conflicts"][:5])) + confirm("Sicherung wiederherstellen", f"Ziel: {profile['label']}\nSicherung: {backup_path.name}\n\n{result['added']} neu\n{result['updated']} werden zurückgesetzt\n{result['unchanged']} unverändert\n\nAndere Profil- und Browserdaten bleiben unberührt.", "Wiederherstellen") + module.wait_until_import_ready(profile, require_closed=True) + final, current_backup = module.apply_import(profile["path"], profile["label"], entries) + alert("Wiederherstellung abgeschlossen", f"{final['added']} neu, {final['updated']} zurückgesetzt, {final['unchanged']} unverändert.\n\nSicherung des vorherigen Stands:\n{current_backup}") + + def app_main() -> int: + try: + profiles = module.discover_profiles() + action = choose_from_list("Suchkürzel-Transfer", "Was möchtest du tun?", ["Als XML exportieren", "Aus XML importieren", "Für Firefox exportieren", "Sicherung wiederherstellen"]) + if action == "Als XML exportieren": + module.export_flow(profiles) + elif action == "Aus XML importieren": + module.import_flow(profiles) + elif action == "Für Firefox exportieren": + firefox_export_flow(profiles) + else: + restore_flow(profiles) + return 0 + except module.Cancelled: + return 0 + except Exception as error: + try: + alert("Suchkürzel-Transfer", str(error), "error") + except module.Cancelled: + pass + return 1 + + module.main = app_main + return module + + +def main() -> int: + app = QApplication(sys.argv) + app.setApplicationName("Suchkürzel-Transfer") + app.setOrganizationName("Suchkürzel-Transfer") + try: + transfer = load_transfer() + if "--self-test" in sys.argv: + transfer.self_test() + return 0 + return transfer.main() + except Exception as error: + if "--self-test" in sys.argv: + print(f"Selbsttest fehlgeschlagen: {type(error).__name__}: {error}", file=sys.stderr) + return 1 + QMessageBox.critical(None, "Suchkürzel-Transfer", str(error)) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/browser-suchmigration/app/requirements.txt b/browser-suchmigration/app/requirements.txt new file mode 100644 index 0000000..64407e9 --- /dev/null +++ b/browser-suchmigration/app/requirements.txt @@ -0,0 +1,2 @@ +PySide6-Essentials>=6.8,<7 +pyinstaller>=6.10,<7 From 8b7d32fe29bba3b54b2d38d93747f2065b91285e Mon Sep 17 00:00:00 2001 From: ysamjo Date: Fri, 9 Oct 2026 16:57:14 +0200 Subject: [PATCH 02/14] =?UTF-8?q?Schlie=C3=9Ft=20SQLite-Snapshots=20plattf?= =?UTF-8?q?orm=C3=BCbergreifend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/Suchkuerzel-Transfer.command | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/browser-suchmigration/Suchkuerzel-Transfer.command b/browser-suchmigration/Suchkuerzel-Transfer.command index da134c2..4472b70 100755 --- a/browser-suchmigration/Suchkuerzel-Transfer.command +++ b/browser-suchmigration/Suchkuerzel-Transfer.command @@ -251,8 +251,11 @@ def with_read_db(path: Path, operation): if connection is not None: connection.close() with stable_snapshot(path) as snapshot: - with open_db(snapshot, read_only=True) as connection: + connection = open_db(snapshot, read_only=True) + try: return operation(connection) + finally: + connection.close() def custom_conditions(existing_columns: set[str]) -> list[str]: From 641c0226fb4314b933a60b3afb32ff662125748e Mon Sep 17 00:00:00 2001 From: ysamjo Date: Fri, 9 Oct 2026 16:58:29 +0200 Subject: [PATCH 03/14] =?UTF-8?q?Schlie=C3=9Ft=20SQLite=20im=20plattform?= =?UTF-8?q?=C3=BCbergreifenden=20Selbsttest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/Suchkuerzel-Transfer.command | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/browser-suchmigration/Suchkuerzel-Transfer.command b/browser-suchmigration/Suchkuerzel-Transfer.command index 4472b70..1941f50 100755 --- a/browser-suchmigration/Suchkuerzel-Transfer.command +++ b/browser-suchmigration/Suchkuerzel-Transfer.command @@ -849,11 +849,15 @@ def self_test() -> None: enforced_by_policy INTEGER DEFAULT 0, featured_by_policy INTEGER DEFAULT 0, url_hash BLOB ); """ - with sqlite3.connect(db_path) as connection: + connection = sqlite3.connect(db_path) + try: connection.executescript(schema) connection.execute( "INSERT INTO keywords (short_name,keyword,favicon_url,url,input_encodings,is_active,sync_guid) VALUES ('Test','t','','https://example.test/?q={searchTerms}','UTF-8',1,'test-guid')" ) + connection.commit() + finally: + connection.close() (profile_dir / "Preferences").write_text( json.dumps({"default_search_provider": {"guid": "test-guid"}}), encoding="utf-8" ) From faab45af13e0011101eb4d1c2d8a3a43491ad423 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sat, 10 Oct 2026 17:19:48 +0200 Subject: [PATCH 04/14] Lokale Synchronisierung 2026-10-10 17:19:48 --- ...here_suchkuerzel_migration.cpython-314.pyc | Bin 21266 -> 21182 bytes ..._suchkuerzel_migration_gui.cpython-314.pyc | Bin 29904 -> 29820 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/browser-suchmigration/Sicheres-Migrationsskript/__pycache__/sichere_suchkuerzel_migration.cpython-314.pyc b/browser-suchmigration/Sicheres-Migrationsskript/__pycache__/sichere_suchkuerzel_migration.cpython-314.pyc index 400a3ea0b95d0b5b7489859d78620457c35570c6..f5aaf3d92e81745e13d5383d28b22d54cf266308 100644 GIT binary patch delta 16 YcmbQVjB(#m#tl3yj0&3*SvL9s05ewwmH+?% delta 100 zcmdnDlyTBB#tl3yp(XmE#i>Qb`e})|nK_mEE~#ayIr#;tMf#;6;pC#sf|6qWq@w(C sAXm4zG&v(TGrcIWBr`uxKR7cvBef{CSl1UOR9u`5(xAVYmt~_L0I4M={{R30 diff --git a/browser-suchmigration/Sicheres-Migrationsskript/__pycache__/sichere_suchkuerzel_migration_gui.cpython-314.pyc b/browser-suchmigration/Sicheres-Migrationsskript/__pycache__/sichere_suchkuerzel_migration_gui.cpython-314.pyc index 4e48f42a6b30915d8cb7f125ae3603073f3ba94f..981f96995cac2a89d42c7a011f3609fda6f16eb9 100644 GIT binary patch delta 16 YcmccclJU;07p9qH~;_u delta 100 zcmezKg7Lyj#tkQ#Lo4({i&Kk=_0tk_Gjl5ST~fvwxW_nR#NoIbYesE@TMru)Nv92#nsJJ*Aq(Oi4DdtTD093sxpa1{> From c75b552c4fb9cf5efd21fa7ee53dedcebf827658 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 03:14:35 +0200 Subject: [PATCH 05/14] =?UTF-8?q?=C3=9Cberspringt=20gesperrte=20Browserpro?= =?UTF-8?q?file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/AGENTS.md | 1 + .../Suchkuerzel-Transfer.command | 19 ++++++++++++++----- browser-suchmigration/app/README.md | 4 ++++ browser-suchmigration/app/main.py | 8 ++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/browser-suchmigration/AGENTS.md b/browser-suchmigration/AGENTS.md index 8250722..b59b4f4 100644 --- a/browser-suchmigration/AGENTS.md +++ b/browser-suchmigration/AGENTS.md @@ -6,6 +6,7 @@ Lokale Werkzeuge zum Übertragen benutzerdefinierter Suchmaschinen und Suchkürz - Browserprofil-Daten bleiben lokal. Nie ein gesamtes Profil ersetzen. - Vor jedem Import oder Wiederherstellen in der Desktop-App Browserbeendigung prüfen, Änderungen bestätigen lassen und eine SQLite-Sicherung anlegen. +- Nicht zugängliche Browserprofilordner bei der automatischen Suche überspringen und im UI melden, damit andere Browserprofile weiter nutzbar bleiben. - Beim Wiederherstellen nur eigene Suchkürzel ändern; Browser- und andere Profiltabellen unverändert lassen. - Exportformat in der bestehenden Transferdatei kompatibel halten. Standard-Suchmaschinen-Einstellung muss der Nutzer im Browser bestätigen. - Keine Zugangsdaten oder Browserinhalte außerhalb der Suchmaschinendaten erfassen. diff --git a/browser-suchmigration/Suchkuerzel-Transfer.command b/browser-suchmigration/Suchkuerzel-Transfer.command index 1941f50..0951ffb 100755 --- a/browser-suchmigration/Suchkuerzel-Transfer.command +++ b/browser-suchmigration/Suchkuerzel-Transfer.command @@ -26,6 +26,7 @@ MAX_XML_SIZE = 750_000 MAX_FIREFOX_HTML_SIZE = 1_500_000 MAX_SHORTCUTS = 1_000 BACKUP_DIR = Path.home() / "Documents" / "Suchkuerzel-Backups" +DISCOVERY_WARNINGS: list[str] = [] BROWSERS = { "chrome": { "label": "Google Chrome", @@ -381,14 +382,22 @@ def choose_default_search(profile: dict[str, Any]) -> dict[str, str]: def discover_profiles() -> list[dict[str, Any]]: result = [] + DISCOVERY_WARNINGS.clear() for browser_key, browser in BROWSERS.items(): root = browser["root"] - if not root.is_dir(): + try: + if not root.is_dir(): + continue + candidates = sorted( + (item for item in root.iterdir() if item.is_dir() and (item / "Web Data").is_file()), + key=profile_sort_key, + ) + except PermissionError as error: + DISCOVERY_WARNINGS.append(f"{browser['label']}: kein Zugriff auf {root} ({error})") + continue + except OSError as error: + DISCOVERY_WARNINGS.append(f"{browser['label']}: Profilordner nicht lesbar: {error}") continue - candidates = sorted( - (item for item in root.iterdir() if item.is_dir() and (item / "Web Data").is_file()), - key=profile_sort_key, - ) for directory in candidates: try: count = len(read_shortcuts(directory / "Web Data")) diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md index a367cb8..e14a243 100644 --- a/browser-suchmigration/app/README.md +++ b/browser-suchmigration/app/README.md @@ -22,6 +22,10 @@ Die App erkennt übliche Profilordner automatisch. Über „Profilordner manuell Der Zielbrowser muss für den Import vollständig beendet sein. Das Programm bricht bei geschützten oder mehrdeutigen Kürzel-Kollisionen ab. Backups liegen unter `Documents/Suchkuerzel-Backups`. +### macOS: Zugriff auf Browserprofile + +macOS vergibt den Profilzugriff pro App. Wenn ein Browserordner wie Brave gesperrt ist, wird dieser Browser jetzt übersprungen und die zugänglichen Profile bleiben auswählbar. Für Zugriff auf weitere Profile muss `SuchkuerzelTransfer.app` unter **Systemeinstellungen → Datenschutz & Sicherheit → Festplattenvollzugriff** freigegeben und danach neu gestartet werden. + ## Start aus dem Quellcode Python 3.11 oder neuer wird benötigt. diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py index 68897f6..03f120e 100644 --- a/browser-suchmigration/app/main.py +++ b/browser-suchmigration/app/main.py @@ -205,6 +205,14 @@ def restore_flow(profiles) -> None: def app_main() -> int: try: profiles = module.discover_profiles() + if module.DISCOVERY_WARNINGS: + QMessageBox.warning( + None, + "Browserzugriff eingeschränkt", + "Einige Browserprofile sind für diese App gesperrt und wurden übersprungen.\n\n" + + "\n".join(module.DISCOVERY_WARNINGS) + + "\n\nDie zugänglichen Profile können trotzdem verwendet werden. Für die gesperrten Ordner erteile der App in Systemeinstellungen → Datenschutz & Sicherheit den nötigen Zugriff.", + ) action = choose_from_list("Suchkürzel-Transfer", "Was möchtest du tun?", ["Als XML exportieren", "Aus XML importieren", "Für Firefox exportieren", "Sicherung wiederherstellen"]) if action == "Als XML exportieren": module.export_flow(profiles) From 9848689d21b0700cfd7d637f9411e9d0a6b9991a Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 03:44:05 +0200 Subject: [PATCH 06/14] =?UTF-8?q?=C3=96ffnet=20bei=20Zugriffssperre=20die?= =?UTF-8?q?=20macOS-Einstellungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/app/README.md | 2 +- browser-suchmigration/app/main.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md index e14a243..010520c 100644 --- a/browser-suchmigration/app/README.md +++ b/browser-suchmigration/app/README.md @@ -24,7 +24,7 @@ Der Zielbrowser muss für den Import vollständig beendet sein. Das Programm bri ### macOS: Zugriff auf Browserprofile -macOS vergibt den Profilzugriff pro App. Wenn ein Browserordner wie Brave gesperrt ist, wird dieser Browser jetzt übersprungen und die zugänglichen Profile bleiben auswählbar. Für Zugriff auf weitere Profile muss `SuchkuerzelTransfer.app` unter **Systemeinstellungen → Datenschutz & Sicherheit → Festplattenvollzugriff** freigegeben und danach neu gestartet werden. +macOS vergibt den Profilzugriff pro App. Wenn ein Browserordner wie Brave gesperrt ist, öffnet die App beim Start automatisch **Systemeinstellungen → Datenschutz & Sicherheit → Festplattenvollzugriff**. Füge dort `SuchkuerzelTransfer.app` hinzu und starte die App danach neu. Zugängliche Profile bleiben währenddessen auswählbar. ## Start aus dem Quellcode diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py index 03f120e..de1da4b 100644 --- a/browser-suchmigration/app/main.py +++ b/browser-suchmigration/app/main.py @@ -206,12 +206,27 @@ def app_main() -> int: try: profiles = module.discover_profiles() if module.DISCOVERY_WARNINGS: + settings_opened = False + if sys.platform == "darwin": + settings_url = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" + settings_opened = subprocess.run( + ["open", settings_url], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode == 0 + settings_hint = ( + "Die Systemeinstellungen wurden geöffnet. Füge dort „SuchkuerzelTransfer.app“ zum Festplattenvollzugriff hinzu und starte die App danach neu." + if settings_opened + else "Öffne Systemeinstellungen → Datenschutz & Sicherheit → Festplattenvollzugriff, füge „SuchkuerzelTransfer.app“ hinzu und starte die App danach neu." + ) QMessageBox.warning( None, "Browserzugriff eingeschränkt", "Einige Browserprofile sind für diese App gesperrt und wurden übersprungen.\n\n" + "\n".join(module.DISCOVERY_WARNINGS) - + "\n\nDie zugänglichen Profile können trotzdem verwendet werden. Für die gesperrten Ordner erteile der App in Systemeinstellungen → Datenschutz & Sicherheit den nötigen Zugriff.", + + "\n\nDie zugänglichen Profile können trotzdem verwendet werden. " + + settings_hint, ) action = choose_from_list("Suchkürzel-Transfer", "Was möchtest du tun?", ["Als XML exportieren", "Aus XML importieren", "Für Firefox exportieren", "Sicherung wiederherstellen"]) if action == "Als XML exportieren": From a2e08b2910e16420945bfdc535983e56ea225cb3 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 03:49:08 +0200 Subject: [PATCH 07/14] =?UTF-8?q?=C3=9Cbertr=C3=A4gt=20Suchk=C3=BCrzel=20d?= =?UTF-8?q?irekt=20zwischen=20Browsern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/AGENTS.md | 2 +- browser-suchmigration/app/README.md | 3 +- browser-suchmigration/app/main.py | 61 ++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/browser-suchmigration/AGENTS.md b/browser-suchmigration/AGENTS.md index b59b4f4..6077a1c 100644 --- a/browser-suchmigration/AGENTS.md +++ b/browser-suchmigration/AGENTS.md @@ -1,6 +1,6 @@ # Browser-Suchmigration -Lokale Werkzeuge zum Übertragen benutzerdefinierter Suchmaschinen und Suchkürzel zwischen Chromium-Profilen. Die Desktop-App liegt in `app/`, nutzt PySide6 und bindet die vorhandene Transferlogik aus `Suchkuerzel-Transfer.command` ein. Die übrigen Skripte sind bestehende macOS-Werkzeuge. +Lokale Werkzeuge zum Übertragen benutzerdefinierter Suchmaschinen und Suchkürzel zwischen Chromium-Profilen. Die Desktop-App in `app/` lässt Quellbrowser, Zielbrowser und bei Bedarf die Profile direkt auswählen; sie nutzt PySide6 und bindet die vorhandene Transferlogik aus `Suchkuerzel-Transfer.command` ein. Die übrigen Skripte sind bestehende macOS-Werkzeuge. ## Grenzen diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md index 010520c..9bbfb43 100644 --- a/browser-suchmigration/app/README.md +++ b/browser-suchmigration/app/README.md @@ -12,7 +12,8 @@ Die App erkennt übliche Profilordner automatisch. Über „Profilordner manuell ## Funktionen -- Suchkürzel als XML exportieren und aus XML importieren +- Quellbrowser und Zielbrowser auswählen und Suchkürzel direkt übertragen +- Quellprofil und Zielprofil auswählen, wenn ein Browser mehrere Profile enthält - Vor dem Import Änderungen und Konflikte anzeigen - Vor einem Schreibzugriff die Browserdatenbank sichern - Bestehende Kürzel aktualisieren, neue hinzufügen und nichts löschen diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py index de1da4b..ddcb443 100644 --- a/browser-suchmigration/app/main.py +++ b/browser-suchmigration/app/main.py @@ -123,6 +123,23 @@ def choose_profile(profiles, purpose: str): count = len(module.read_shortcuts(database)) return {"token": f"{browser_key}:{profile_dir}", "browser_key": browser_key, "label": f"{browser_label} — {module.profile_name(profile_dir)}", "directory": profile_dir, "path": database, "count": count} + def choose_browser(profiles, purpose: str) -> str: + keys = list(dict.fromkeys(str(profile["browser_key"]) for profile in profiles)) + choices = [ + f"{module.BROWSERS[key]['label']} ({sum(profile['browser_key'] == key for profile in profiles)} Profile)" + for key in keys + ] + picked = choose_from_list("Suchkürzel-Transfer", purpose, choices) + return keys[choices.index(picked)] + + def choose_browser_profile(profiles, browser_key: str, purpose: str): + matching = [profile for profile in profiles if profile["browser_key"] == browser_key] + if not matching: + raise module.TransferError(f"Für {module.BROWSERS[browser_key]['label']} wurde kein zugängliches Profil gefunden.") + if len(matching) == 1: + return matching[0] + return choose_profile(matching, purpose) + def choose_save_path(default_name: str) -> Path: filename, _ = QFileDialog.getSaveFileName(None, "XML-Export speichern", str(Path.home() / default_name), "Suchkürzel-XML (*.xml)") if not filename: @@ -182,6 +199,40 @@ def firefox_export_flow(profiles) -> None: QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) alert("Firefox-Datei erstellt", f"{len(shortcuts)} Suchkürzel gespeichert:\n{destination}\n\nIn Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.") + def direct_transfer_flow(profiles) -> None: + source_browser = choose_browser(profiles, "Von welchem Browser sollen die Suchkürzel übernommen werden?") + target_browser = choose_browser(profiles, "In welchen Browser sollen die Suchkürzel übertragen werden?") + source = choose_browser_profile(profiles, source_browser, "Quellprofil auswählen:") + target = choose_browser_profile(profiles, target_browser, "Zielprofil auswählen:") + + entries = module.read_shortcuts(source["path"]) + if not entries: + raise module.TransferError(f"Im Quellprofil {source['label']} gibt es keine eigenen Suchkürzel.") + default_search = module.read_default_search(source) or module.choose_default_search(source) + entries = module.entries_for_import(target["path"], entries, default_search) + result = module.preview(target["path"], entries) + if result["conflicts"]: + raise module.TransferError("Geschützte oder mehrdeutige Kürzel-Kollisionen: " + ", ".join(result["conflicts"][:5])) + + confirm( + "Übertragung bestätigen", + f"Von: {source['label']}\nNach: {target['label']}\n\n" + f"{result['added']} neu\n{result['updated']} aktualisiert\n" + f"{result['unchanged']} unverändert\n\n" + "Nichts wird gelöscht. Vorher wird eine Sicherung des Zielprofils erstellt.", + "Übertragen", + ) + module.wait_until_import_ready(target, require_closed=True) + final, backup_path = module.apply_import(target["path"], target["label"], entries) + alert( + "Übertragung abgeschlossen", + f"Von {source['label']} nach {target['label']} übertragen:\n" + f"{final['added']} neu, {final['updated']} aktualisiert, {final['unchanged']} unverändert.\n\n" + f"Sicherung des Zielprofils:\n{backup_path}\n\n" + f"Öffne im Zielbrowser die Sucheinstellungen und wähle dort „{default_search['short_name']}“ als Standardsuche.", + ) + module.open_search_settings(target) + def restore_flow(profiles) -> None: profile = module.choose_profile(profiles, "Zielprofil für die Wiederherstellung auswählen:") filename, _ = QFileDialog.getOpenFileName(None, "Suchkürzel-Sicherung auswählen", str(module.BACKUP_DIR), "SQLite-Sicherung (*.sqlite)") @@ -228,15 +279,7 @@ def app_main() -> int: + "\n\nDie zugänglichen Profile können trotzdem verwendet werden. " + settings_hint, ) - action = choose_from_list("Suchkürzel-Transfer", "Was möchtest du tun?", ["Als XML exportieren", "Aus XML importieren", "Für Firefox exportieren", "Sicherung wiederherstellen"]) - if action == "Als XML exportieren": - module.export_flow(profiles) - elif action == "Aus XML importieren": - module.import_flow(profiles) - elif action == "Für Firefox exportieren": - firefox_export_flow(profiles) - else: - restore_flow(profiles) + direct_transfer_flow(profiles) return 0 except module.Cancelled: return 0 From 6320fadfba12b04841fef7908f55d768e5f070ca Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 03:51:54 +0200 Subject: [PATCH 08/14] =?UTF-8?q?Erg=C3=A4nzt=20Firefox=20als=20ausw=C3=A4?= =?UTF-8?q?hlbares=20=C3=9Cbertragungsziel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/app/README.md | 2 +- browser-suchmigration/app/main.py | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md index 9bbfb43..cfbbe7b 100644 --- a/browser-suchmigration/app/README.md +++ b/browser-suchmigration/app/README.md @@ -12,7 +12,7 @@ Die App erkennt übliche Profilordner automatisch. Über „Profilordner manuell ## Funktionen -- Quellbrowser und Zielbrowser auswählen und Suchkürzel direkt übertragen +- Quellbrowser und Zielbrowser auswählen und Suchkürzel direkt zwischen Chromium-Browsern übertragen - Quellprofil und Zielprofil auswählen, wenn ein Browser mehrere Profile enthält - Vor dem Import Änderungen und Konflikte anzeigen - Vor einem Schreibzugriff die Browserdatenbank sichern diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py index ddcb443..f2ba732 100644 --- a/browser-suchmigration/app/main.py +++ b/browser-suchmigration/app/main.py @@ -123,12 +123,12 @@ def choose_profile(profiles, purpose: str): count = len(module.read_shortcuts(database)) return {"token": f"{browser_key}:{profile_dir}", "browser_key": browser_key, "label": f"{browser_label} — {module.profile_name(profile_dir)}", "directory": profile_dir, "path": database, "count": count} - def choose_browser(profiles, purpose: str) -> str: + def choose_browser(profiles, purpose: str, allow_firefox: bool = False) -> str: keys = list(dict.fromkeys(str(profile["browser_key"]) for profile in profiles)) - choices = [ - f"{module.BROWSERS[key]['label']} ({sum(profile['browser_key'] == key for profile in profiles)} Profile)" - for key in keys - ] + choices = [f"{module.BROWSERS[key]['label']} ({sum(profile['browser_key'] == key for profile in profiles)} Profile)" for key in keys] + if allow_firefox: + keys.append("firefox") + choices.append("Firefox (Lesezeichen-HTML)") picked = choose_from_list("Suchkürzel-Transfer", purpose, choices) return keys[choices.index(picked)] @@ -201,13 +201,25 @@ def firefox_export_flow(profiles) -> None: def direct_transfer_flow(profiles) -> None: source_browser = choose_browser(profiles, "Von welchem Browser sollen die Suchkürzel übernommen werden?") - target_browser = choose_browser(profiles, "In welchen Browser sollen die Suchkürzel übertragen werden?") + target_browser = choose_browser(profiles, "In welchen Browser sollen die Suchkürzel übertragen werden?", allow_firefox=True) source = choose_browser_profile(profiles, source_browser, "Quellprofil auswählen:") - target = choose_browser_profile(profiles, target_browser, "Zielprofil auswählen:") - entries = module.read_shortcuts(source["path"]) if not entries: raise module.TransferError(f"Im Quellprofil {source['label']} gibt es keine eigenen Suchkürzel.") + if target_browser == "firefox": + filename = f"Firefox-Suchkuerzel-{module.safe_component(source['label'])}-{module.dt.date.today().isoformat()}.html" + destination = choose_firefox_save_path(filename) + destination.write_text(module.to_firefox_html(entries, source["label"]), encoding="utf-8") + QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) + alert( + "Firefox-Importdatei erstellt", + f"Von {source['label']} exportiert:\n{len(entries)} Suchkürzel\n\n" + f"Datei: {destination}\n\n" + "In Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.", + ) + return + + target = choose_browser_profile(profiles, target_browser, "Zielprofil auswählen:") default_search = module.read_default_search(source) or module.choose_default_search(source) entries = module.entries_for_import(target["path"], entries, default_search) result = module.preview(target["path"], entries) From fcb53e0961ac0bc562486f387760c305839f55de Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 11:12:55 +0200 Subject: [PATCH 09/14] bd init: initialize beads issue tracking --- .beads/.gitignore | 82 ++++++++++++++++++++++++++++++++++++++++++++ .beads/README.md | 81 +++++++++++++++++++++++++++++++++++++++++++ .beads/config.yaml | 75 ++++++++++++++++++++++++++++++++++++++++ .beads/metadata.json | 7 ++++ .gitignore | 7 ++++ 5 files changed, 252 insertions(+) create mode 100644 .beads/.gitignore create mode 100644 .beads/README.md create mode 100644 .beads/config.yaml create mode 100644 .beads/metadata.json diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..1ce0e03 --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,82 @@ +# Dolt database (managed by Dolt, not git) +dolt/ +embeddeddolt/ +proxieddb/ + +# Runtime files +bd.sock +bd.sock.startlock +sync-state.json +last-touched +.exclusive-lock + +# Daemon runtime (lock, log, pid) +daemon.* + +# Push state (runtime, per-machine) +push-state.json + +# Lock files (various runtime locks) +*.lock + +# Credential key (encryption key for federation peer auth — never commit) +.beads-credential-key + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +proxied_server_client_info.json + +# Worktree redirect file (contains relative path to main repo's .beads/) +# Must not be committed as paths would be wrong in other clones +redirect + +# Sync state (local-only, per-machine) +# These files are machine-specific and should not be shared across clones +.sync.lock + +# Workspace operation gate (internal/workspacegate): physical-root gate +# files live beside the guarded root inside .beads (e.g. dolt.gate.lock) +*.gate.lock* +export-state/ +export-state.json +last_pull + +# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned) +ephemeral.sqlite3 +ephemeral.sqlite3-journal +ephemeral.sqlite3-wal +ephemeral.sqlite3-shm + +# Dolt server management (auto-started by bd) +dolt-server.pid +dolt-server.log +dolt-server.lock +dolt-server.port +dolt-server.activity +dolt-server-config.yaml + +# Debug-mode pprof artifacts (written when dolt.debug: true in config.yaml) +dolt-pprof/ + +# Corrupt backup directories (created by bd doctor --fix recovery) +*.corrupt.backup/ + +# Backup data (auto-exported JSONL, local-only) +backup/ + +# Per-project environment file (Dolt connection config, GH#2520) +.env + +# Legacy files (from pre-Dolt versions) +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm +db.sqlite +bd.db +# NOTE: Do NOT add negation patterns here. +# They would override fork protection in .git/info/exclude. +# Config files (metadata.json, config.yaml) are tracked by git by default +# since no pattern above ignores them. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000..63e8f4c --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --claim +bd update --status done + +# Sync with Dolt remote +bd dolt push +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in Dolt database with version control and branching +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Sync-ready**: Uses Dolt remotes for backup and team sharing + +## Why Beads? + +✨ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** +- Dolt-native sync via bd dolt push / bd dolt pull +- Branch-aware issue tracking +- Dolt-native three-way merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..4920446 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,75 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: JSONL-only, no Dolt database +# When true, .beads/issues.jsonl is the only local store +# no-db: false + +# Enable JSON output by default +# json: false + +# Feedback title formatting for mutating commands (create/update/close/dep/edit) +# 0 = hide titles, N > 0 = truncate to N characters +# output: +# title-length: 255 + +# Default actor for audit trails (overridden by BEADS_ACTOR or --actor) +# actor: "" + +# Optional JSONL sidecar for explicit agent/tool interaction audit records. +# Issue history is always recorded in the database and is visible with +# bd history --events; this only controls .beads/interactions.jsonl. +# audit: +# enabled: false + +# Export events (audit trail) to .beads/events.jsonl on each flush/sync +# When enabled, new events are appended incrementally using a high-water mark. +# Use 'bd export --events' to trigger manually regardless of this setting. +# events-export: false + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct database +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# Dolt-native backup (periodic backup for off-machine recovery) +# This is full database backup only. Cross-machine sync uses Dolt remotes. +# backup: +# enabled: false # Disable auto-backup entirely +# interval: 15m # Minimum time between auto-backups +# git-push: false # Disable git push (backup locally only) +# git-repo: "" # Separate git repo for backups (default: project repo) + +# Optional JSONL auto-export for viewers, interchange, and issue-level migration. +# Disabled by default; enable only when an integration needs fresh .beads/issues.jsonl. +# Use relative paths under .beads/ for JSONL import/export filenames. +# export: +# auto: false +# path: issues.jsonl +# interval: 60s +# git-add: false +# import: +# path: issues.jsonl + +# Integration settings (access with 'bd config get/set') +# Non-secret keys (stored in the database): +# - jira.url, jira.project +# - linear.team_id +# - github.org, github.repo +# +# Secret keys (stored in this file but prefer env vars to avoid git exposure): +# - linear.api_key → use LINEAR_API_KEY env var instead +# - github.token → use GITHUB_TOKEN env var instead + +sync: + remote: "git+https://github.com/ysamjo/userscripts.git" diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..2117bd8 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,7 @@ +{ + "database": "dolt", + "backend": "dolt", + "dolt_mode": "embedded", + "dolt_database": "userscripts", + "project_id": "e17ae08b-405b-4833-9545-2a71288c4f43" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b9286eb..5cf9ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ # Userscripts .DS_Store *.log + +# Beads / Dolt files (added by bd init) +.dolt/ +*.db +.beads-credential-key +.beads/proxieddb/ +*.gate.lock* From 258a9d321742fecf56e9d8e19b7be062541654cd Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 11 Oct 2026 11:12:56 +0200 Subject: [PATCH 10/14] Beads-Projektkontext lokal einrichten --- .beads/README.md | 84 ++++-------------------------------------------- 1 file changed, 7 insertions(+), 77 deletions(-) diff --git a/.beads/README.md b/.beads/README.md index 63e8f4c..1497043 100644 --- a/.beads/README.md +++ b/.beads/README.md @@ -1,81 +1,11 @@ -# Beads - AI-Native Issue Tracking +# Beads-Kontext für userscripts -Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. +Diese lokale Beads-Datenbank speichert Aufgaben, Abhängigkeiten und ausdrücklich hinterlegtes Projektwissen. Harness Router lädt ihren Kontext vor jedem Auftrag automatisch; eine Aufgabenauswahl ist optional. -## What is Beads? +- Kontext prüfen: `bd prime --full --readonly` +- Verfügbare Aufgaben lesen: `bd ready --json --readonly` +- Wissen ausdrücklich hinterlegen: `bd remember "Projektwissen"` -Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. +Projektregeln bleiben in `AGENTS.md`. Initialisierung ohne Änderungen an Regeln und Git-Hooks. Aufgaben werden vom Router weder automatisch übernommen noch geschlossen. -**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) - -## Quick Start - -### Essential Commands - -```bash -# Create new issues -bd create "Add user authentication" - -# View all issues -bd list - -# View issue details -bd show - -# Update issue status -bd update --claim -bd update --status done - -# Sync with Dolt remote -bd dolt push -``` - -### Working with Issues - -Issues in Beads are: -- **Git-native**: Stored in Dolt database with version control and branching -- **AI-friendly**: CLI-first design works perfectly with AI coding agents -- **Branch-aware**: Issues can follow your branch workflow -- **Sync-ready**: Uses Dolt remotes for backup and team sharing - -## Why Beads? - -✨ **AI-Native Design** -- Built specifically for AI-assisted development workflows -- CLI-first interface works seamlessly with AI coding agents -- No context switching to web UIs - -🚀 **Developer Focused** -- Issues live in your repo, right next to your code -- Works offline, syncs when you push -- Fast, lightweight, and stays out of your way - -🔧 **Git Integration** -- Dolt-native sync via bd dolt push / bd dolt pull -- Branch-aware issue tracking -- Dolt-native three-way merge resolution - -## Get Started with Beads - -Try Beads in your own projects: - -```bash -# Install Beads -curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash - -# Initialize in your repo -bd init - -# Create your first issue -bd create "Try out Beads" -``` - -## Learn More - -- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) -- **Quick Start Guide**: Run `bd quickstart` -- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) - ---- - -*Beads: Issue tracking that moves at the speed of thought* ⚡ +Die Datenbank unter `embeddeddolt/` bleibt lokal und wird nicht durch normale Git-Commits gesichert. Versionierte Konfigurationsdateien ersetzen kein Datenbank-Backup; Datenübertragung und Sicherung erfolgen separat über Beads/Dolt. From 260955ce938f416abb17fa2a35962fda3d5b5cc0 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Mon, 12 Oct 2026 01:38:42 +0200 Subject: [PATCH 11/14] =?UTF-8?q?=C3=9Cbertr=C3=A4gt=20die=20Standardsuche?= =?UTF-8?q?=20nur,=20wenn=20die=20Quelle=20eine=20hat,=20und=20w=C3=A4hlt?= =?UTF-8?q?=20Quell-=20und=20Zielprofil=20direkt=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser-suchmigration/AGENTS.md | 2 +- browser-suchmigration/app/README.md | 2 +- browser-suchmigration/app/main.py | 261 +++++++++++++++++++++------- 3 files changed, 204 insertions(+), 61 deletions(-) diff --git a/browser-suchmigration/AGENTS.md b/browser-suchmigration/AGENTS.md index 6077a1c..fec0857 100644 --- a/browser-suchmigration/AGENTS.md +++ b/browser-suchmigration/AGENTS.md @@ -8,7 +8,7 @@ Lokale Werkzeuge zum Übertragen benutzerdefinierter Suchmaschinen und Suchkürz - Vor jedem Import oder Wiederherstellen in der Desktop-App Browserbeendigung prüfen, Änderungen bestätigen lassen und eine SQLite-Sicherung anlegen. - Nicht zugängliche Browserprofilordner bei der automatischen Suche überspringen und im UI melden, damit andere Browserprofile weiter nutzbar bleiben. - Beim Wiederherstellen nur eigene Suchkürzel ändern; Browser- und andere Profiltabellen unverändert lassen. -- Exportformat in der bestehenden Transferdatei kompatibel halten. Standard-Suchmaschinen-Einstellung muss der Nutzer im Browser bestätigen. +- Exportformat in der bestehenden Transferdatei kompatibel halten. Standardsuche nur übernehmen, wenn die Quelle eine hat; die Auswahl muss der Nutzer im Browser bestätigen. - Keine Zugangsdaten oder Browserinhalte außerhalb der Suchmaschinendaten erfassen. ## App-Befehle diff --git a/browser-suchmigration/app/README.md b/browser-suchmigration/app/README.md index cfbbe7b..d3ef025 100644 --- a/browser-suchmigration/app/README.md +++ b/browser-suchmigration/app/README.md @@ -19,7 +19,7 @@ Die App erkennt übliche Profilordner automatisch. Über „Profilordner manuell - Bestehende Kürzel aktualisieren, neue hinzufügen und nichts löschen - Suchkürzel für Firefox als HTML-Lesezeichen exportieren - Profile über den üblichen Browserpfad erkennen oder manuell auswählen -- Standardsuche aus der Quelle in der Zielprofil-Datenbank verfügbar machen; die Auswahl als Standardsuche bestätigt der Nutzer anschließend im Browser +- Standardsuche aus der Quelle in der Zielprofil-Datenbank verfügbar machen, sofern die Quelle eine hat; die Auswahl als Standardsuche bestätigt der Nutzer anschließend im Browser Der Zielbrowser muss für den Import vollständig beendet sein. Das Programm bricht bei geschützten oder mehrdeutigen Kürzel-Kollisionen ab. Backups liegen unter `Documents/Suchkuerzel-Backups`. diff --git a/browser-suchmigration/app/main.py b/browser-suchmigration/app/main.py index f2ba732..e9d2739 100644 --- a/browser-suchmigration/app/main.py +++ b/browser-suchmigration/app/main.py @@ -100,14 +100,7 @@ def choose_from_list(title: str, prompt: str, choices: list[str]) -> str: raise module.Cancelled return choice - def choose_profile(profiles, purpose: str): - labels = [f"{item['label']} — {item['count']} Kürzel" for item in profiles] - manual = "Profilordner manuell auswählen …" - choice, accepted = QInputDialog.getItem(None, "Suchkürzel-Transfer", purpose, labels + [manual], 0, False) - if not accepted: - raise module.Cancelled - if choice != manual: - return profiles[labels.index(choice)] + def choose_manual_profile(): browser_labels = [str(item["label"]) for item in module.BROWSERS.values()] browser_label, accepted = QInputDialog.getItem(None, "Browser auswählen", "Zu welchem Browser gehört das Profil?", browser_labels, 0, False) if not accepted: @@ -123,9 +116,68 @@ def choose_profile(profiles, purpose: str): count = len(module.read_shortcuts(database)) return {"token": f"{browser_key}:{profile_dir}", "browser_key": browser_key, "label": f"{browser_label} — {module.profile_name(profile_dir)}", "directory": profile_dir, "path": database, "count": count} + def choose_profile(profiles, purpose: str): + labels = [f"{item['label']} — {item['count']} Kürzel" for item in profiles] + manual = "Profilordner manuell auswählen …" + choice, accepted = QInputDialog.getItem(None, "Suchkürzel-Transfer", purpose, labels + [manual], 0, False) + if not accepted: + raise module.Cancelled + if choice != manual: + return profiles[labels.index(choice)] + return choose_manual_profile() + + def choose_source_profile(profiles): + labels = [f"{item['label']} — {item['count']} Kürzel" for item in profiles] + actions = [ + "Profilordner manuell auswählen …", + "Aus XML-Datei importieren …", + "Sicherung wiederherstellen …", + ] + choice, accepted = QInputDialog.getItem( + None, + "Suchkürzel-Transfer", + "Quell-Browserprofil auswählen:", + labels + actions, + 0, + False, + ) + if not accepted: + raise module.Cancelled + if choice in labels: + return "profile", profiles[labels.index(choice)] + return choice, None + + def choose_target(profiles, source): + source_token = str(source.get("token")) + candidate_profiles = [ + p for p in profiles if str(p.get("token")) != source_token and p.get("path") != source.get("path") + ] + labels = [f"{item['label']} — {item['count']} Kürzel" for item in candidate_profiles] + options = [ + "Firefox (Lesezeichen-HTML)", + "Als XML exportieren", + "Profilordner manuell auswählen …", + ] + choice, accepted = QInputDialog.getItem( + None, + "Suchkürzel-Transfer", + "Ziel für die Suchkürzel auswählen:", + labels + options, + 0, + False, + ) + if not accepted: + raise module.Cancelled + if choice in labels: + return "profile", candidate_profiles[labels.index(choice)] + return choice, None + def choose_browser(profiles, purpose: str, allow_firefox: bool = False) -> str: keys = list(dict.fromkeys(str(profile["browser_key"]) for profile in profiles)) - choices = [f"{module.BROWSERS[key]['label']} ({sum(profile['browser_key'] == key for profile in profiles)} Profile)" for key in keys] + choices = [ + f"{module.BROWSERS[key]['label']} ({sum(profile['browser_key'] == key for profile in profiles)} {'Profil' if sum(profile['browser_key'] == key for profile in profiles) == 1 else 'Profile'})" + for key in keys + ] if allow_firefox: keys.append("firefox") choices.append("Firefox (Lesezeichen-HTML)") @@ -179,6 +231,8 @@ def ask_retry_locked(label: str) -> None: module.choose_from_list = choose_from_list module.choose_profile = choose_profile + module.choose_source_profile = choose_source_profile + module.choose_target = choose_target module.choose_save_path = choose_save_path module.choose_firefox_save_path = choose_firefox_save_path module.choose_xml_file = choose_xml_file @@ -188,65 +242,83 @@ def ask_retry_locked(label: str) -> None: module.browser_running = lambda profile: process_is_running(module.BROWSERS[profile["browser_key"]]["process"]) module.open_search_settings = lambda profile: QDesktopServices.openUrl(QUrl(module.BROWSERS[profile["browser_key"]]["settings"])) - def firefox_export_flow(profiles) -> None: - profile = module.choose_profile(profiles, "Chrome-/Brave-Quellprofil für Firefox auswählen:") - shortcuts = module.read_shortcuts(profile["path"]) - if not shortcuts: - raise module.TransferError("Dieses Profil enthält keine aktiven eigenen Suchkürzel.") - filename = f"Firefox-Suchkuerzel-{module.safe_component(profile['label'])}-{module.dt.date.today().isoformat()}.html" + def export_xml_flow(source, entries) -> None: + default_search = module.read_default_search(source) or module.choose_default_search(source) + date = module.dt.date.today().isoformat() + filename = f"Suchkuerzel-{module.safe_component(source['label'])}-{date}.xml" + destination = choose_save_path(filename) + destination.write_text(module.to_xml(entries, source["label"], default_search), encoding="utf-8") + QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) + alert( + "XML-Export abgeschlossen", + f"Von {source['label']} exportiert:\n{len(entries)} Suchkürzel gespeichert\n\n" + f"Standardsuche: {default_search['short_name']} ({default_search['keyword']})\n\n" + f"Datei:\n{destination}", + ) + + def export_firefox_flow(source, entries) -> None: + filename = f"Firefox-Suchkuerzel-{module.safe_component(source['label'])}-{module.dt.date.today().isoformat()}.html" destination = choose_firefox_save_path(filename) - destination.write_text(module.to_firefox_html(shortcuts, profile["label"]), encoding="utf-8") + destination.write_text(module.to_firefox_html(entries, source["label"]), encoding="utf-8") QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) - alert("Firefox-Datei erstellt", f"{len(shortcuts)} Suchkürzel gespeichert:\n{destination}\n\nIn Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.") + alert( + "Firefox-Importdatei erstellt", + f"Von {source['label']} exportiert:\n{len(entries)} Suchkürzel\n\n" + f"Datei: {destination}\n\n" + "In Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.", + ) - def direct_transfer_flow(profiles) -> None: - source_browser = choose_browser(profiles, "Von welchem Browser sollen die Suchkürzel übernommen werden?") - target_browser = choose_browser(profiles, "In welchen Browser sollen die Suchkürzel übertragen werden?", allow_firefox=True) - source = choose_browser_profile(profiles, source_browser, "Quellprofil auswählen:") + def firefox_export_flow(profiles) -> None: + source = choose_profile(profiles, "Chrome-/Brave-Quellprofil für Firefox auswählen:") entries = module.read_shortcuts(source["path"]) if not entries: - raise module.TransferError(f"Im Quellprofil {source['label']} gibt es keine eigenen Suchkürzel.") - if target_browser == "firefox": - filename = f"Firefox-Suchkuerzel-{module.safe_component(source['label'])}-{module.dt.date.today().isoformat()}.html" - destination = choose_firefox_save_path(filename) - destination.write_text(module.to_firefox_html(entries, source["label"]), encoding="utf-8") - QDesktopServices.openUrl(QUrl.fromLocalFile(str(destination.parent))) - alert( - "Firefox-Importdatei erstellt", - f"Von {source['label']} exportiert:\n{len(entries)} Suchkürzel\n\n" - f"Datei: {destination}\n\n" - "In Firefox die Lesezeichenverwaltung öffnen und „Lesezeichen von HTML importieren“ wählen.", - ) - return + raise module.TransferError("Dieses Profil enthält keine aktiven eigenen Suchkürzel.") + export_firefox_flow(source, entries) - target = choose_browser_profile(profiles, target_browser, "Zielprofil auswählen:") - default_search = module.read_default_search(source) or module.choose_default_search(source) + def export_flow(profiles) -> None: + source = choose_profile(profiles, "Quellprofil für den XML-Export auswählen:") + entries = module.read_shortcuts(source["path"]) + if not entries: + raise module.TransferError("Dieses Profil enthält keine aktiven eigenen Suchkürzel.") + export_xml_flow(source, entries) + + def import_xml_flow(profiles) -> None: + xml_path = choose_xml_file() + entries, default_search = module.parse_transfer_xml(xml_path.read_text(encoding="utf-8")) + target = choose_profile(profiles, "Zielprofil für den XML-Import auswählen:") entries = module.entries_for_import(target["path"], entries, default_search) result = module.preview(target["path"], entries) if result["conflicts"]: raise module.TransferError("Geschützte oder mehrdeutige Kürzel-Kollisionen: " + ", ".join(result["conflicts"][:5])) - + default_message = ( + f"\nStandardsuche: {default_search['short_name']} ({default_search['keyword']})\n" + if default_search + else "\nStandardsuche: nicht in dieser XML enthalten\n" + ) confirm( - "Übertragung bestätigen", - f"Von: {source['label']}\nNach: {target['label']}\n\n" - f"{result['added']} neu\n{result['updated']} aktualisiert\n" - f"{result['unchanged']} unverändert\n\n" + "Import bestätigen", + f"Ziel: {target['label']}\n\n{result['added']} neu\n{result['updated']} zu aktualisieren\n" + f"{result['unchanged']} unverändert\n{default_message}\n" "Nichts wird gelöscht. Vorher wird eine Sicherung des Zielprofils erstellt.", - "Übertragen", + "Importieren", ) - module.wait_until_import_ready(target, require_closed=True) + module.wait_until_import_ready(target, require_closed=bool(default_search)) final, backup_path = module.apply_import(target["path"], target["label"], entries) + default_finish = ( + f"\n\nAls Nächstes öffnen sich die Browser-Einstellungen. Dort bitte „{default_search['short_name']}“ als Standardsuche auswählen." + if default_search + else "" + ) alert( - "Übertragung abgeschlossen", - f"Von {source['label']} nach {target['label']} übertragen:\n" + "Import abgeschlossen", f"{final['added']} neu, {final['updated']} aktualisiert, {final['unchanged']} unverändert.\n\n" - f"Sicherung des Zielprofils:\n{backup_path}\n\n" - f"Öffne im Zielbrowser die Sucheinstellungen und wähle dort „{default_search['short_name']}“ als Standardsuche.", + f"Sicherung:\n{backup_path}{default_finish}", ) - module.open_search_settings(target) + if default_search: + module.open_search_settings(target) def restore_flow(profiles) -> None: - profile = module.choose_profile(profiles, "Zielprofil für die Wiederherstellung auswählen:") + target = choose_profile(profiles, "Zielprofil für die Wiederherstellung auswählen:") filename, _ = QFileDialog.getOpenFileName(None, "Suchkürzel-Sicherung auswählen", str(module.BACKUP_DIR), "SQLite-Sicherung (*.sqlite)") if not filename: raise module.Cancelled @@ -257,13 +329,84 @@ def restore_flow(profiles) -> None: raise module.TransferError("Die ausgewählte Datei ist keine lesbare Suchkürzel-Sicherung.") from error if not entries: raise module.TransferError("Die Sicherung enthält keine eigenen Suchkürzel.") - result = module.preview(profile["path"], entries) + result = module.preview(target["path"], entries) if result["conflicts"]: raise module.TransferError("Geschützte oder doppelte Kürzel kollidieren: " + ", ".join(result["conflicts"][:5])) - confirm("Sicherung wiederherstellen", f"Ziel: {profile['label']}\nSicherung: {backup_path.name}\n\n{result['added']} neu\n{result['updated']} werden zurückgesetzt\n{result['unchanged']} unverändert\n\nAndere Profil- und Browserdaten bleiben unberührt.", "Wiederherstellen") - module.wait_until_import_ready(profile, require_closed=True) - final, current_backup = module.apply_import(profile["path"], profile["label"], entries) - alert("Wiederherstellung abgeschlossen", f"{final['added']} neu, {final['updated']} zurückgesetzt, {final['unchanged']} unverändert.\n\nSicherung des vorherigen Stands:\n{current_backup}") + confirm( + "Sicherung wiederherstellen", + f"Ziel: {target['label']}\nSicherung: {backup_path.name}\n\n{result['added']} neu\n{result['updated']} werden zurückgesetzt\n{result['unchanged']} unverändert\n\nAndere Profil- und Browserdaten bleiben unberührt.", + "Wiederherstellen", + ) + module.wait_until_import_ready(target, require_closed=True) + final, current_backup = module.apply_import(target["path"], target["label"], entries) + alert( + "Wiederherstellung abgeschlossen", + f"{final['added']} neu, {final['updated']} zurückgesetzt, {final['unchanged']} unverändert.\n\nSicherung des vorherigen Stands:\n{current_backup}", + ) + + def direct_transfer_flow(profiles) -> None: + source_kind, source = choose_source_profile(profiles) + if source_kind == "Profilordner manuell auswählen …": + source = choose_manual_profile() + elif source_kind == "Aus XML-Datei importieren …": + import_xml_flow(profiles) + return + elif source_kind == "Sicherung wiederherstellen …": + restore_flow(profiles) + return + + entries = module.read_shortcuts(source["path"]) + if not entries: + raise module.TransferError(f"Im Quellprofil {source['label']} gibt es keine eigenen Suchkürzel.") + + target_kind, target = choose_target(profiles, source) + if target_kind == "Firefox (Lesezeichen-HTML)": + export_firefox_flow(source, entries) + return + elif target_kind == "Als XML exportieren": + export_xml_flow(source, entries) + return + elif target_kind == "Profilordner manuell auswählen …": + target = choose_manual_profile() + if target["path"] == source["path"]: + raise module.TransferError("Quell- und Zielprofil dürfen nicht identisch sein.") + + # Standardsuche nur übernehmen, wenn die Quelle überhaupt eine hat. Sonst bleibt die + # Standardsuche des Zielprofils unangetastet, statt eine erfundene einzutragen. + default_search = module.read_default_search(source) + import_entries = module.entries_for_import(target["path"], entries, default_search) + result = module.preview(target["path"], import_entries) + if result["conflicts"]: + raise module.TransferError("Geschützte oder mehrdeutige Kürzel-Kollisionen: " + ", ".join(result["conflicts"][:5])) + + default_info = ( + f"\nStandardsuche: {default_search['short_name']} ({default_search['keyword']})\n" + if default_search + else "" + ) + confirm( + "Übertragung bestätigen", + f"Von: {source['label']}\nNach: {target['label']}\n\n" + f"{result['added']} neu\n{result['updated']} aktualisiert\n" + f"{result['unchanged']} unverändert{default_info}\n\n" + "Nichts wird gelöscht. Vorher wird eine Sicherung des Zielprofils erstellt.", + "Übertragen", + ) + module.wait_until_import_ready(target, require_closed=True) + final, backup_path = module.apply_import(target["path"], target["label"], import_entries) + default_note = ( + f"\n\nÖffne im Zielbrowser die Sucheinstellungen und wähle dort „{default_search['short_name']}“ als Standardsuche." + if default_search + else "" + ) + alert( + "Übertragung abgeschlossen", + f"Von {source['label']} nach {target['label']} übertragen:\n" + f"{final['added']} neu, {final['updated']} aktualisiert, {final['unchanged']} unverändert.\n\n" + f"Sicherung des Zielprofils:\n{backup_path}{default_note}", + ) + if default_search: + module.open_search_settings(target) def app_main() -> int: try: @@ -279,16 +422,16 @@ def app_main() -> int: check=False, ).returncode == 0 settings_hint = ( - "Die Systemeinstellungen wurden geöffnet. Füge dort „SuchkuerzelTransfer.app“ zum Festplattenvollzugriff hinzu und starte die App danach neu." - if settings_opened - else "Öffne Systemeinstellungen → Datenschutz & Sicherheit → Festplattenvollzugriff, füge „SuchkuerzelTransfer.app“ hinzu und starte die App danach neu." + "Falls „SuchkuerzelTransfer.app“ bereits in der Liste steht und aktiv ist:\n" + "Bitte den Eintrag mit „-“ entfernen und mit „+“ neu hinzufügen (macOS erfordert dies nach App-Updates), danach die App neu starten.\n\n" + "Alternativ: Du kannst Chrome- und Brave-Profile jederzeit direkt über „Profilordner manuell auswählen …“ anwählen." ) QMessageBox.warning( None, "Browserzugriff eingeschränkt", - "Einige Browserprofile sind für diese App gesperrt und wurden übersprungen.\n\n" + "Einige Browserprofile sind für diese App durch macOS gesperrt und wurden übersprungen.\n\n" + "\n".join(module.DISCOVERY_WARNINGS) - + "\n\nDie zugänglichen Profile können trotzdem verwendet werden. " + + "\n\n" + settings_hint, ) direct_transfer_flow(profiles) From e1c78f8f4d98490bf4ec78df4eaa0d1d9fda6376 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sat, 24 Oct 2026 14:29:55 +0200 Subject: [PATCH 12/14] =?UTF-8?q?Paketda:=20MutationObserver-Fallback=20f?= =?UTF-8?q?=C3=BCr=20KNS/Packlink=20falls=20Tabellen=20beim=20Script-Start?= =?UTF-8?q?=20noch=20nicht=20im=20DOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paketda/Paketda.user.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/paketda/Paketda.user.js b/paketda/Paketda.user.js index b3a86dc..c778bce 100644 --- a/paketda/Paketda.user.js +++ b/paketda/Paketda.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Paketda Packlink Button & Adblock Remover // @namespace http://tampermonkey.net/ -// @version 2.0 +// @version 2.1 // @description Ergänzt Packlink-Buttons, Klick-n-Schick als passendes Angebot und entfernt den Adblock-Hinweis. // @match https://www.paketda.de/paket-preis-rechner.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=paketda.de @@ -283,6 +283,17 @@ addKlickNSchickOffer(); + // Falls die Tabelle noch nicht im DOM ist (Seite noch nicht fertig), nochmal versuchen + if (fitsKlickNSchick && !document.querySelector('.kns-offer-row')) { + const knsRetry = new MutationObserver(() => { + if (document.querySelector('#desktop_tabelle tbody') || document.querySelector('#tabelle_porto_online tbody')) { + addKlickNSchickOffer(); + if (document.querySelector('.kns-offer-row')) knsRetry.disconnect(); + } + }); + knsRetry.observe(document.body, { childList: true, subtree: true }); + } + // --- 5. Öffentlichen Packlink-Ab-Preis in die Angebotsliste einfügen --- function packlinkOfferDetails(productName) { const wrapper = document.createDocumentFragment(); @@ -355,6 +366,17 @@ addPacklinkOffer(); + // Falls die Tabelle noch nicht im DOM ist, nochmal versuchen + if (fitsPacklink && !document.querySelector('.packlink-offer-row')) { + const plRetry = new MutationObserver(() => { + if (document.querySelector('#desktop_tabelle tbody') || document.querySelector('#tabelle_porto_online tbody')) { + addPacklinkOffer(); + if (document.querySelector('.packlink-offer-row')) plRetry.disconnect(); + } + }); + plRetry.observe(document.body, { childList: true, subtree: true }); + } + // --- 6. Novapost-Preise zusätzlich mit 25 % Rabatt anzeigen --- function parseEuro(value) { const normalized = String(value || '') From 69a7ca1f92d0844b6f86dcafd119d524b8d2ffdf Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sat, 24 Oct 2026 14:37:10 +0200 Subject: [PATCH 13/14] Paketda Loader: Pfad auf /Developer/userscripts/paketda/ korrigiert (Documents/Code existiert nicht) --- paketda/Paketda lokaler Loader.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paketda/Paketda lokaler Loader.user.js b/paketda/Paketda lokaler Loader.user.js index ed9e50c..4a131eb 100644 --- a/paketda/Paketda lokaler Loader.user.js +++ b/paketda/Paketda lokaler Loader.user.js @@ -1,10 +1,10 @@ // ==UserScript== // @name Paketda Packlink & Klick-n-Schick (lokaler Loader) // @namespace http://tampermonkey.net/ -// @version 1.0.0 +// @version 1.0.1 // @description Lädt das Paketda-Skript direkt aus dem lokalen Ordner. // @match https://www.paketda.de/paket-preis-rechner.php* -// @require file:///Users/family/Documents/Code/Paketda/Paketda.user.js +// @require file:///Users/family/Developer/userscripts/paketda/Paketda.user.js // @run-at document-idle // @grant none // @noframes From 563414070b8eb1f456c0ead39fc5ca1af8174a60 Mon Sep 17 00:00:00 2001 From: ysamjo Date: Sun, 25 Oct 2026 06:37:46 +0100 Subject: [PATCH 14/14] =?UTF-8?q?Paketda:=20Klick-n-Schick=20Button=20und?= =?UTF-8?q?=20Tabelleneinf=C3=BCgung=20f=C3=BCr=20Startseite=20und=20Tabel?= =?UTF-8?q?len-Updates=20gefixt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paketda/Paketda.user.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/paketda/Paketda.user.js b/paketda/Paketda.user.js index c778bce..59d0268 100644 --- a/paketda/Paketda.user.js +++ b/paketda/Paketda.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Paketda Packlink Button & Adblock Remover // @namespace http://tampermonkey.net/ -// @version 2.1 +// @version 2.2 // @description Ergänzt Packlink-Buttons, Klick-n-Schick als passendes Angebot und entfernt den Adblock-Hinweis. // @match https://www.paketda.de/paket-preis-rechner.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=paketda.de @@ -55,7 +55,7 @@ const [longest, middle, shortest] = dimensions; const girth = longest + (2 * middle) + (2 * shortest); const hasPackageData = [weight, longest, middle, shortest].every(Number.isFinite); - const isGermany = !land; + const isGermany = !land || String(land).trim() === '' || String(land).toLowerCase() === 'de' || String(land).toLowerCase() === 'deutschland'; const fitsKlickNSchick = hasPackageData && isGermany && weight > 0 @@ -235,14 +235,14 @@ } function addKlickNSchickOffer() { - if (!fitsKlickNSchick || document.querySelector('.kns-offer-row')) return; + if (!fitsKlickNSchick) return; const price = weight <= 2 ? 4.90 : 5.80; const netPrice = price / 1.19; const productName = `Klick-n-Schick Paket bis ${weight <= 2 ? 2 : 10} kg`; const desktopBody = document.querySelector('#desktop_tabelle tbody'); - if (desktopBody) { + if (desktopBody && !desktopBody.querySelector('.kns-offer-row')) { const row = document.createElement('tr'); row.className = 'kns-offer-row'; @@ -266,7 +266,7 @@ } const mobileBody = document.querySelector('#tabelle_porto_online tbody'); - if (mobileBody) { + if (mobileBody && !mobileBody.querySelector('.kns-offer-row')) { const row = document.createElement('tr'); row.className = 'kns-offer-row kns-offer-row-mobile'; @@ -284,11 +284,13 @@ addKlickNSchickOffer(); // Falls die Tabelle noch nicht im DOM ist (Seite noch nicht fertig), nochmal versuchen - if (fitsKlickNSchick && !document.querySelector('.kns-offer-row')) { + if (fitsKlickNSchick) { const knsRetry = new MutationObserver(() => { - if (document.querySelector('#desktop_tabelle tbody') || document.querySelector('#tabelle_porto_online tbody')) { - addKlickNSchickOffer(); - if (document.querySelector('.kns-offer-row')) knsRetry.disconnect(); + addKlickNSchickOffer(); + const hasDesktop = !document.querySelector('#desktop_tabelle') || document.querySelector('#desktop_tabelle .kns-offer-row'); + const hasMobile = !document.querySelector('#tabelle_porto_online') || document.querySelector('#tabelle_porto_online .kns-offer-row'); + if (hasDesktop && hasMobile && document.querySelector('.kns-offer-row')) { + knsRetry.disconnect(); } }); knsRetry.observe(document.body, { childList: true, subtree: true }); @@ -318,14 +320,14 @@ } function addPacklinkOffer() { - if (!fitsPacklink || document.querySelector('.packlink-offer-row')) return; + if (!fitsPacklink) return; const netPrice = packlinkDpdTariff.netPrice; const price = netPrice * 1.19; const productName = `Packlink DPD Paketshop ${packlinkDpdTariff.size} (bis 20 kg)`; const desktopBody = document.querySelector('#desktop_tabelle tbody'); - if (desktopBody) { + if (desktopBody && !desktopBody.querySelector('.packlink-offer-row')) { const row = document.createElement('tr'); row.className = 'packlink-offer-row'; @@ -349,7 +351,7 @@ } const mobileBody = document.querySelector('#tabelle_porto_online tbody'); - if (mobileBody) { + if (mobileBody && !mobileBody.querySelector('.packlink-offer-row')) { const row = document.createElement('tr'); row.className = 'packlink-offer-row packlink-offer-row-mobile'; @@ -367,11 +369,13 @@ addPacklinkOffer(); // Falls die Tabelle noch nicht im DOM ist, nochmal versuchen - if (fitsPacklink && !document.querySelector('.packlink-offer-row')) { + if (fitsPacklink) { const plRetry = new MutationObserver(() => { - if (document.querySelector('#desktop_tabelle tbody') || document.querySelector('#tabelle_porto_online tbody')) { - addPacklinkOffer(); - if (document.querySelector('.packlink-offer-row')) plRetry.disconnect(); + addPacklinkOffer(); + const hasDesktop = !document.querySelector('#desktop_tabelle') || document.querySelector('#desktop_tabelle .packlink-offer-row'); + const hasMobile = !document.querySelector('#tabelle_porto_online') || document.querySelector('#tabelle_porto_online .packlink-offer-row'); + if (hasDesktop && hasMobile && document.querySelector('.packlink-offer-row')) { + plRetry.disconnect(); } }); plRetry.observe(document.body, { childList: true, subtree: true }); @@ -436,8 +440,6 @@ addNovapostDiscounts(); // --- 7. Vorhandenen Brief-Link durch die drei Schnellbuttons ersetzen --- - if (!hasPackageData) return; - const briefLink = document.querySelector('a[href*="deutschepost.de/de/p/portoberater.html"]'); const container = briefLink?.parentElement; if (!briefLink || !container || document.querySelector('.custom-btn-modern')) return;