diff --git a/.env.example b/.env.example index 7a5efe9..1aaa923 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Application APP_NAME=WebDrop Bridge -APP_VERSION=0.9.1 +APP_VERSION=0.9.0 # Web App WEBAPP_URL=file:///./webapp/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 9735517..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## [0.9.1] - 2026-04-15 - -### Changed -- Improved release publishing so release descriptions can be generated from the changelog. -- Updated the release workflow to use a clearer, user-facing summary for update information. -- Added "in App" Branding Management instead of using separated Brand Builds. - -### Fixed -- Removed the generic placeholder release description from published releases. -- Added a reliable fallback message when no detailed notes are available. -- Instead of using "Profiles" and "Configurations" use "Setups" for more clarity. diff --git a/CONFIG_README.md b/CONFIG_README.md index 6572acd..d830dbd 100644 --- a/CONFIG_README.md +++ b/CONFIG_README.md @@ -36,10 +36,7 @@ Create a `config.json` file with the following structure: "log_file": "logs/webdrop_bridge.log", "window_width": 1024, "window_height": 768, - "enable_logging": true, - "language": "auto", - "active_branding_id": "default", - "brand_id": "agravity" + "enable_logging": true } ``` @@ -102,19 +99,6 @@ Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png - **`window_width`**, **`window_height`** (number): Initial window size in pixels - Default: `1024` x `768` -### Language and Branding Settings - -- **`language`** (string): UI language code - - Use `"auto"` to follow the system locale automatically - - Bundled translations currently include `en`, `de`, `fr`, `it`, `ru`, and `zh` - -- **`active_branding_id`** (string): Runtime branding template selected in the Settings dialog - - Default: `"default"` - - Useful when switching between saved branding templates without rebuilding the app - -- **`brand_id`** (string): Stable packaging/update identifier for branded variants - - Usually injected during packaging and normally left unchanged by end users - - **`log_level`** (string): Logging verbosity - Options: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"` - Default: `"INFO"` @@ -165,14 +149,6 @@ You can configure multiple Azure storage accounts: } ``` -## Configuration Priority - -At startup, WebDrop Bridge first loads any bootstrap `.env` defaults and then prefers the persisted JSON config if it exists. This means: - -1. packaged or development defaults can still come from `.env`, -2. the Settings dialog saves the active runtime configuration to JSON, and -3. the JSON file becomes the main configuration source after first save. - ## Environment Variable Fallback If no JSON config exists, WebDrop Bridge will load from `.env`: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57dedbf..8112350 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please be respectful and constructive in all interactions. We're building a welc ## Getting Started ### Prerequisites -- Python 3.9+ +- Python 3.10+ - Git - Familiarity with Qt/PySide6 or willingness to learn diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 3e025ce..e5460e6 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -1051,7 +1051,7 @@ Help Menu **Core:** - PySide6 6.6.0+ -- Python 3.9+ +- Python 3.10+ **Optional:** - PyInstaller (building) diff --git a/QUICKSTART.md b/QUICKSTART.md index 32b4f7e..5c9611e 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -143,19 +143,19 @@ brew upgrade webdrop-bridge brew uninstall webdrop-bridge ``` -For more package manager details and internal hosting options, see [docs/PACKAGE_MANAGER_SUPPORT.md](docs/PACKAGE_MANAGER_SUPPORT.md) +For more package manager details and internal hosting options, see [docs/PACKAGE_MANAGER_SUPPORT.md](../docs/PACKAGE_MANAGER_SUPPORT.md) #### Simplest: Direct wget (if you know the version) ```bash -# Replace X.Y.Z with a release version (e.g., 0.9.1) -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/vX.Y.Z/WebDropBridge-X.Y.Z-win-x64.msi +# Replace VERSION with release tag (e.g., v0.8.0) +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/VERSION/WebDropBridge_Setup.msi -# Real example - download v0.9.1 MSI -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1/WebDropBridge-0.9.1-win-x64.msi +# Real example - download v0.8.0 MSI +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.0/WebDropBridge_Setup.msi -# macOS - download v0.9.1 DMG -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1/WebDropBridge-0.9.1-macos-universal.dmg +# macOS - download v0.8.0 DMG +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.0/WebDropBridge_Setup.dmg ``` #### Windows (PowerShell) - Full Control Script @@ -168,7 +168,7 @@ wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1 .\build\scripts\download_release.ps1 -OutputDir "C:\Installers" # Download specific version -.\build\scripts\download_release.ps1 -Version "0.9.1" +.\build\scripts\download_release.ps1 -Version "0.8.0" # Skip checksum verification .\build\scripts\download_release.ps1 -Verify $false @@ -186,7 +186,7 @@ wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1 ./build/scripts/download_release.sh latest ~/Downloads # Download specific version -./build/scripts/download_release.sh 0.9.1 +./build/scripts/download_release.sh 0.8.0 # Skip checksum verification ./build/scripts/download_release.sh latest --no-verify diff --git a/README.md b/README.md index 21eb28f..976485f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Professional Qt-based desktop application for intelligent drag-and-drop file handling between web applications and desktop clients (InDesign, Word, Notepad++, etc.) -![Status](https://img.shields.io/badge/Status-Phase%205%20RC%20In%20Progress-green) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.9%2B-blue) +![Status](https://img.shields.io/badge/Status-Phase%204%20Complete-green) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.10%2B-blue) ## Overview @@ -19,25 +19,23 @@ WebDrop Bridge embeds a web application in a Qt container with full filesystem a ## Features -- ✅ **Qt-based Architecture** - Professional cross-platform desktop app via PySide6 for Windows and macOS +- ✅ **Qt-based Architecture** - Professional Windows support via PySide6 (macOS support planned) - ✅ **Embedded Web App** - QtWebEngine provides Chromium without browser limitations - ✅ **Drag Interception** - Converts text paths to native file operations - ✅ **Path Whitelist** - Security-conscious file system access control -- ✅ **Configuration Management** - JSON config, profile import/export, and validation -- ✅ **Runtime Branding** - Switch branding templates and packaged variants without code changes -- ✅ **Multilingual UI** - Built-in translations for English, German, French, Italian, Russian, and Chinese -- ✅ **Settings Dialog** - Language, branding, web source, path, URL, logging, and window configuration +- ✅ **Configuration Management** - Profile-based settings with validation +- ✅ **Settings Dialog** - Professional UI for path, URL, logging, and window configuration - ✅ **Auto-Update System** - Automatic release detection via Forgejo API - ✅ **Professional Build Pipeline** - MSI for Windows, DMG for macOS -- ✅ **Comprehensive Testing** - Unit and integration coverage across core modules -- ✅ **Continuous Testing** - Automated CI validation +- ✅ **Comprehensive Testing** - Unit, integration, and end-to-end tests (80%+ coverage) +- ✅ **Continuous Testing** - GitHub Actions test automation - ✅ **Structured Logging** - File-based logging with configurable levels ## Quick Start ### Requirements -- Python 3.9+ -- Windows 10/11 or macOS 12+ +- Python 3.10+ +- Windows 10/11 - 200 MB disk space (includes Chromium from PyInstaller) ### Installation from Pre-Built Release (Recommended) @@ -60,11 +58,11 @@ brew upgrade webdrop-bridge # Update to latest version **Option 2: Direct wget (if you know the version)** ```bash -# Replace X.Y.Z with a release version (e.g., 0.9.1) -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/vX.Y.Z/WebDropBridge-X.Y.Z-win-x64.msi +# Replace VERSION with release tag (e.g., v0.8.0) +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/VERSION/WebDropBridge_Setup.msi -# Example for v0.9.1: -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1/WebDropBridge-0.9.1-win-x64.msi +# Example for v0.8.0: +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.0/WebDropBridge_Setup.msi ``` **Option 3: Automated script (auto-detects platform)** @@ -95,7 +93,6 @@ python -m venv venv # Install dependencies pip install -r requirements.txt -pip install -e . # Run application python -m webdrop_bridge.main @@ -106,7 +103,6 @@ python -m webdrop_bridge.main ```bash # Install development dependencies pip install -r requirements-dev.txt -pip install -e . # Run tests pytest tests -v @@ -182,36 +178,42 @@ webdrop-bridge/ ## Configuration -WebDrop Bridge supports persisted JSON configuration plus optional bootstrap environment defaults. +WebDrop Bridge supports two configuration methods: -### 1. Settings Dialog / JSON Config (Recommended) +### 1. Settings Dialog (Recommended) Launch the application and access the Settings menu to configure: -- **General Tab** - Select the UI language or follow the system locale automatically -- **Branding Tab** - Switch, import, export, and preview runtime branding templates -- **Web Source Tab** - Configure the embedded web application URL -- **Paths / URLs / Logging / Window Tabs** - Control filesystem access, allowed sites, log output, and initial window size -- **Profiles Tab** - Save, load, import, and export complete configuration profiles +- **Paths Tab** - Add/remove allowed root directories +- **URLs Tab** - Configure allowed web URLs (whitelist mode) +- **Logging Tab** - Set log level and file location +- **Window Tab** - Configure window dimensions +- **Profiles Tab** - Save/load/export-import configuration profiles -Saved settings are written to the brand-specific application config directory as `config.json`. +Profiles are saved in `~/.webdrop_bridge/profiles/` -### 2. Bootstrap Environment Variables (`.env`) -A `.env` file is still supported for local development and branded packaged defaults. It is used when no JSON config exists yet. +### 2. Environment Variables +Create a `.env` file in the project root. Available settings: ```bash +# Application APP_NAME=WebDrop Bridge -BRAND_ID=webdrop_bridge -WEBAPP_URL=https://dev.agravity.io/ +APP_VERSION=1.0.0 + +# Paths (comma-separated) ALLOWED_ROOTS=Z:/,C:/Users/Public + +# Web URLs (empty = no restriction, items = kiosk mode) ALLOWED_URLS= -LANGUAGE=auto -LOG_LEVEL=INFO + +# Interface +WEBAPP_URL=file:///./webapp/index.html WINDOW_WIDTH=1024 WINDOW_HEIGHT=768 + +# Logging +LOG_LEVEL=INFO ENABLE_LOGGING=true ``` -For the full JSON structure and branding workflow, see [CONFIG_README.md](CONFIG_README.md) and [BRANDING_AND_RELEASES.md](docs/BRANDING_AND_RELEASES.md). - ## Testing WebDrop Bridge includes comprehensive test coverage with unit, integration, and end-to-end tests. @@ -270,20 +272,11 @@ python build/scripts/build_windows.py --msi --code-sign ``` Output: -- Portable executable: `build/dist/windows/webdrop_bridge/WebDropBridge/WebDropBridge.exe` -- Professional MSI installer: `build/dist/windows/webdrop_bridge/WebDropBridge--win-x64.msi` -- SHA256 checksum: `build/dist/windows/webdrop_bridge/WebDropBridge--win-x64.msi.sha256` +- Portable executable: `build/dist/windows/WebDropBridge/WebDropBridge.exe` (~195 MB) +- Professional MSI installer: `build/dist/windows/WebDropBridge-{version}-Setup.msi` +- SHA256 checksum: `build/dist/windows/WebDropBridge/WebDropBridge.exe.sha256` -### macOS DMG Installer - -```bash -bash build/scripts/build_macos.sh -``` - -Output: -- Application bundle: `build/dist/macos/webdrop_bridge/WebDropBridge.app` -- DMG installer: `build/dist/macos/webdrop_bridge/WebDropBridge--macos-universal.dmg` -- SHA256 checksum: `build/dist/macos/webdrop_bridge/WebDropBridge--macos-universal.dmg.sha256` +**Note on macOS**: Build scripts exist for macOS (DMG generation), but have never been built or tested. macOS support is theoretical at this point. The Qt/PySide6 architecture should support macOS, but platform-specific testing and validation would be required. ### Creating Releases @@ -321,8 +314,8 @@ powershell -ExecutionPolicy Bypass -File build/scripts/create_release.ps1 ## Troubleshooting ### Application won't start -- Ensure Python 3.9+ is installed -- Check the application log in your platform-specific app data directory +- Ensure Python 3.10+ is installed +- Check `logs/webdrop_bridge.log` for errors - Verify all dependencies: `pip list` ### Drag-and-drop not working @@ -339,10 +332,10 @@ powershell -ExecutionPolicy Bypass -File build/scripts/create_release.ps1 | Platform | Version | Status | Notes | |----------|---------|--------|-------| -| Windows | 10, 11 | ✅ Full | Primary target with MSI packaging and update support | -| macOS | 12, 13, 14 | ✅ Supported | Universal DMG builds for Intel and Apple Silicon | +| Windows | 10, 11 | ✅ Full | Tested on x64, MSI installer support | +| macOS | 12+ | ⚠️ **Untested** | Possible via Qt/PySide6, but never built or tested. Theoretical support only. | -**Note**: Release candidates currently target both Windows and macOS. For branded production releases, validate signing assets and installer behavior on the target platform before shipping. +**Note**: WebDrop Bridge is currently developed and tested exclusively on Windows. While the Qt/PySide6 framework supports macOS, we cannot guarantee functionality without actual macOS testing and validation. Contributions for macOS support validation are welcome. ## Contributing @@ -374,4 +367,4 @@ MIT License - see [LICENSE](LICENSE) file for details --- -**Development Phase**: Phase 5 Release Candidates | **Last Updated**: April 16, 2026 | **Current Version**: 0.9.1 | **Python**: 3.9+ | **Qt**: PySide6 (Qt 6) +**Development Phase**: Phase 4 Complete | **Last Updated**: February 18, 2026 | **Current Version**: 1.0.0 | **Python**: 3.10+ | **Qt**: PySide6 (Qt 6) diff --git a/build/package-managers/README.md b/build/package-managers/README.md index 62e5572..7f0a400 100644 --- a/build/package-managers/README.md +++ b/build/package-managers/README.md @@ -27,7 +27,7 @@ build/ 2. **Get SHA256 checksum**: ```powershell - certutil -hashfile build/dist/windows/webdrop_bridge/WebDropBridge--win-x64.msi SHA256 + certutil -hashfile build/dist/windows/WebDropBridge_Setup.msi SHA256 ``` 3. **Update package files**: @@ -42,7 +42,7 @@ build/ 5. **Publish** (requires Chocolatey API key): ```powershell - choco push webdrop-bridge..nupkg --api-key YOUR_KEY + choco push webdrop-bridge.0.8.0.nupkg --api-key YOUR_KEY ``` ### Homebrew Formula (macOS) @@ -54,7 +54,7 @@ build/ 2. **Get SHA256 checksum**: ```bash - shasum -a 256 build/dist/macos/webdrop_bridge/WebDropBridge--macos-universal.dmg + shasum -a 256 build/dist/macos/WebDropBridge_Setup.dmg ``` 3. **Update formula**: diff --git a/build/scripts/README.md b/build/scripts/README.md index 51cac7c..c4fba4c 100644 --- a/build/scripts/README.md +++ b/build/scripts/README.md @@ -75,8 +75,8 @@ The `download_release.ps1` (Windows) and `download_release.sh` (macOS/Linux) scr ```bash # Download directly by version tag -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1/WebDropBridge-0.9.1-win-x64.msi -wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.9.1/WebDropBridge-0.9.1-macos-universal.dmg +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.0/WebDropBridge_Setup.msi +wget https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.0/WebDropBridge_Setup.dmg ``` **If you need to auto-detect latest (with grep/cut, no jq needed)** @@ -111,7 +111,7 @@ sha256sum -c installer.sha256 .\download_release.ps1 # Specific version to Downloads folder -.\download_release.ps1 -Version "0.9.1" -OutputDir "$env:USERPROFILE\Downloads" +.\download_release.ps1 -Version "0.8.0" -OutputDir "$env:USERPROFILE\Downloads" # Skip checksum verification .\download_release.ps1 -Verify $false @@ -124,7 +124,7 @@ sha256sum -c installer.sha256 ./build/scripts/download_release.sh # Specific version to Downloads -./build/scripts/download_release.sh 0.9.1 ~/Downloads +./build/scripts/download_release.sh 0.8.0 ~/Downloads # Skip checksum verification ./build/scripts/download_release.sh latest --no-verify @@ -165,7 +165,7 @@ Automated release creation with versioning and asset uploads. Manages consistent versioning across the project. ```bash -python build/scripts/sync_version.py --version 0.9.1 +python build/scripts/sync_version.py --version 0.8.0 ``` ## Integration Flow diff --git a/build/scripts/create_release.ps1 b/build/scripts/create_release.ps1 index 1cb5940..187c9aa 100644 --- a/build/scripts/create_release.ps1 +++ b/build/scripts/create_release.ps1 @@ -34,18 +34,6 @@ function Get-CurrentVersion { return (& $pythonExe -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$projectRoot/build/scripts').resolve())); from version_utils import get_current_version; print(get_current_version())").Trim() } -function Get-ReleaseNotes { - param([string]$Version) - - $notes = & $pythonExe -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$projectRoot/build/scripts').resolve())); from version_utils import get_release_notes; print(get_release_notes('$Version'))" - - if ($LASTEXITCODE -ne 0) { - return "## WebDrop Bridge v$Version`n`nThis release package contains the latest improvements, fixes, and installer updates for this version." - } - - return ($notes | Out-String).Trim() -} - function Get-LocalReleaseData { $arguments = @($brandHelper, "local-release-data", "--platform", "windows", "--version", $Version) if ($Brands) { @@ -139,11 +127,10 @@ $headers = @{ $releaseLookupUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/tags/v$Version" $releaseUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases" -$releaseBody = Get-ReleaseNotes -Version $Version $releaseData = @{ tag_name = "v$Version" name = "WebDropBridge v$Version" - body = $releaseBody + body = "Shared branded release for WebDrop Bridge v$Version" draft = $false prerelease = $false } | ConvertTo-Json diff --git a/build/scripts/create_release.sh b/build/scripts/create_release.sh index 218de20..2ce39f8 100644 --- a/build/scripts/create_release.sh +++ b/build/scripts/create_release.sh @@ -40,10 +40,6 @@ if [ -z "$VERSION" ]; then VERSION="$(python3 -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$PROJECT_ROOT/build/scripts').resolve())); from version_utils import get_current_version; print(get_current_version())")" fi -get_release_notes() { - python3 -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$PROJECT_ROOT/build/scripts').resolve())); from version_utils import get_release_notes; print(get_release_notes('$VERSION'))" -} - LOCAL_ARGS=("$BRAND_HELPER" "local-release-data" "--platform" "macos" "--version" "$VERSION") if [ ${#BRANDS[@]} -gt 0 ]; then LOCAL_ARGS+=("--brands" "${BRANDS[@]}") @@ -190,19 +186,15 @@ else fi if [ -z "$RELEASE_ID" ]; then - RELEASE_BODY="$(get_release_notes)" - RELEASE_DATA=$(RELEASE_BODY="$RELEASE_BODY" VERSION="$VERSION" python3 - <<'PY' -import json -import os - -print(json.dumps({ - "tag_name": f"v{os.environ['VERSION']}", - "name": f"WebDropBridge v{os.environ['VERSION']}", - "body": os.environ["RELEASE_BODY"], - "draft": False, - "prerelease": False, -})) -PY + RELEASE_DATA=$(cat < str: init_file = project_root / "src" / "webdrop_bridge" / "__init__.py" if not init_file.exists(): - raise FileNotFoundError(f"Cannot find __init__.py at {init_file}") + raise FileNotFoundError( + f"Cannot find __init__.py at {init_file}" + ) content = init_file.read_text(encoding="utf-8") match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) if not match: raise ValueError( - f"Could not find __version__ in {init_file}. " 'Expected: __version__ = "X.Y.Z"' + f"Could not find __version__ in {init_file}. " + "Expected: __version__ = \"X.Y.Z\"" ) return match.group(1) - - -def extract_release_notes(changelog_content: str, version: str) -> str | None: - """Extract the notes for a specific version from changelog content. - - Args: - changelog_content: Full text of CHANGELOG.md - version: Version to extract, e.g. "0.9.1" - - Returns: - The section content for that version, or None if not found. - """ - version_header = re.compile( - rf"^##\s*\[?{re.escape(version)}\]?(?:\s*-\s*.+)?\s*$", - re.MULTILINE, - ) - match = version_header.search(changelog_content) - if not match: - return None - - section_start = match.end() - next_header = re.search(r"^##\s+", changelog_content[section_start:], re.MULTILINE) - section_end = section_start + next_header.start() if next_header else len(changelog_content) - section = changelog_content[section_start:section_end].strip() - return section or None - - -def get_release_notes(version: str, project_root: Path | None = None) -> str: - """Build a readable release body for publishing. - - Prefers the matching version section from CHANGELOG.md. If no changelog - entry exists yet, falls back to a generic but user-facing description. - - Args: - version: Release version string. - project_root: Optional project root override for testing. - - Returns: - Release notes text suitable for Forgejo/GitHub release bodies. - """ - root = project_root or get_project_root() - changelog_file = root / "CHANGELOG.md" - - if changelog_file.exists(): - content = changelog_file.read_text(encoding="utf-8") - notes = extract_release_notes(content, version) - if notes: - return f"## WebDrop Bridge v{version}\n\n{notes}" - - return ( - f"## WebDrop Bridge v{version}\n\n" - "This release package contains the latest improvements, fixes, " - "and installer updates for this version." - ) diff --git a/docs/CONFIGURATION_BUILD.md b/docs/CONFIGURATION_BUILD.md index dedb0af..d69abfa 100644 --- a/docs/CONFIGURATION_BUILD.md +++ b/docs/CONFIGURATION_BUILD.md @@ -1,60 +1,33 @@ # Configuration Management for Builds -This document explains how configuration and branding work for development builds, packaged installers, and installed applications. +This document explains how configuration is handled when building executables and installers for WebDrop Bridge. -## Current Configuration Model +## Overview -WebDrop Bridge now uses a **JSON-first runtime configuration** with optional `.env` bootstrap defaults: +WebDrop Bridge uses `.env` files for runtime configuration. When building distributable packages (exe, MSI, or DMG), the `.env` file is **bundled into the application** so that users receive pre-configured settings. -1. **Bootstrap `.env`** (optional) - - Loaded very early during startup - - Useful for packaged defaults such as `APP_NAME`, `BRAND_ID`, update channel, and default web source - - Commonly used by branded Windows/MSI and macOS/DMG builds +## Configuration File -2. **Persisted JSON config** (preferred) - - Windows: `%APPDATA%\\config.json` - - macOS/Linux: `~/.config//config.json` - - Created and maintained by the Settings dialog - - Takes precedence for day-to-day user settings after first launch - -In practice, installers can ship with a curated `.env`, while user changes are saved into `config.json`. - -## What Belongs Where? - -### Use JSON config for: -- `url_mappings` -- `allowed_roots` -- `allowed_urls` -- window size and logging settings -- `language` -- `active_branding_id` - -### Use `.env` bootstrap defaults for: -- `APP_NAME` -- `BRAND_ID` -- `APP_CONFIG_DIR_NAME` -- update channel and repository defaults -- packaged first-launch defaults for customer-specific builds - -## Example Bootstrap `.env` +The configuration file must be named `.env` and contains settings like: ```dotenv APP_NAME=WebDrop Bridge -BRAND_ID=webdrop_bridge -APP_CONFIG_DIR_NAME=webdrop_bridge +APP_VERSION=0.7.1 WEBAPP_URL=https://example.com ALLOWED_ROOTS=Z:/,C:/Users/Public ALLOWED_URLS= -LANGUAGE=auto LOG_LEVEL=INFO +LOG_FILE=logs/webdrop_bridge.log ENABLE_LOGGING=true WINDOW_WIDTH=1024 WINDOW_HEIGHT=768 ``` +See `.env.example` for a template with all available options. + ## Building with Default Configuration -If you want to use the project's default `.env` file from the repository root: +If you want to use the project's `.env` file (in the project root), simply run: ### Windows ```bash @@ -66,11 +39,11 @@ python build/scripts/build_windows.py --msi bash build/scripts/build_macos.sh ``` -> The build scripts currently expect a `.env` file to exist. This is intentional so packaged builds always have explicit bootstrap defaults. +**Important:** The build will **fail** if `.env` doesn't exist. This prevents accidentally shipping without configuration. -## Building with Custom Customer Defaults +## Building with Custom Configuration -For customer-specific or branded releases, provide a different `.env` file during packaging: +For different customers or deployments, you can specify a custom `.env` file: ### Windows ```bash @@ -82,73 +55,86 @@ python build/scripts/build_windows.py --msi --env-file path/to/customer1.env bash build/scripts/build_macos.sh --env-file path/to/customer1.env ``` -This bundles those bootstrap defaults into the packaged app while still allowing the installed application to persist later changes in JSON. +The custom `.env` file will be bundled into the executable and users will receive those pre-configured settings. ## Example: Multi-Customer Setup -```text +If you have different customer configurations: + +``` webdrop_bridge/ -├── .env +├── .env # Default project configuration +├── .env.example # Template ├── build/ │ └── scripts/ │ ├── build_windows.py │ └── build_macos.sh -├── customer_configs/ +├── customer_configs/ # Create this for customer-specific settings │ ├── acme_corp.env │ ├── globex_corporation.env │ └── initech.env -└── config.example.json +└── ... ``` -Then build per customer or brand: +Then build for each customer: ```bash +# ACME Corp python build/scripts/build_windows.py --msi --env-file customer_configs/acme_corp.env + +# Globex Corporation python build/scripts/build_windows.py --msi --env-file customer_configs/globex_corporation.env -bash build/scripts/build_macos.sh --env-file customer_configs/initech.env + +# Initech +python build/scripts/build_windows.py --msi --env-file customer_configs/initech.env ``` -## What Gets Bundled into Installers? +Each MSI will include that customer's specific configuration (URLs, allowed paths, etc.). -During packaging, the supplied `.env` file is bundled so the application can resolve: -- app display name -- brand/config directory name -- update channel defaults -- initial web source and logging defaults +## What Gets Bundled -After installation, the application normally saves user-controlled settings to the JSON config file in the app data directory. +When building, the `.env` file is: +1. ✅ Copied into the PyInstaller bundle +2. ✅ Extracted to the application's working directory when the app starts +3. ✅ Automatically loaded by `Config.from_env()` at startup -## Recommended Runtime Workflow +Users **do not** need to create their own `.env` files. -1. Package the app with the correct `.env` bootstrap defaults. -2. Launch the app once. -3. Configure URLs, mappings, language, and branding in the Settings dialog. -4. Let the app save `config.json` in the brand-specific config directory. -5. Reuse exported profiles or branding templates for future setups. +## After Installation + +When users run the installed application: +1. The embedded `.env` is automatically available +2. Settings are loaded and applied +3. Users can optionally create a custom `.env` in the installation directory to override settings + +This allows: +- **Pre-configured deployments** for your customers +- **Easy customization** by users (just edit the `.env` file) +- **No manual setup** required after installation ## Build Command Reference ### Windows ```bash -# Default build using the repository root .env +# Default (.env from project root) python build/scripts/build_windows.py --msi -# Customer-specific defaults +# Custom .env file python build/scripts/build_windows.py --msi --env-file customer_configs/acme.env -# Without MSI (just the packaged executable) +# Without MSI (just EXE) python build/scripts/build_windows.py -# Sign executable (requires signing setup) +# Sign executable (requires CODE_SIGN_CERT env var) python build/scripts/build_windows.py --msi --code-sign ``` ### macOS ```bash -# Default build using the repository root .env +# Default (.env from project root) bash build/scripts/build_macos.sh -# Customer-specific defaults +# Custom .env file bash build/scripts/build_macos.sh --env-file customer_configs/acme.env # Sign app (requires Apple developer certificate) @@ -158,11 +144,19 @@ bash build/scripts/build_macos.sh --sign bash build/scripts/build_macos.sh --notarize ``` -## Validation Notes +## Configuration Validation The build process validates that: -1. the specified `.env` file exists, -2. packaging metadata can be resolved, and -3. the resulting installer assets are created successfully. +1. ✅ The specified `.env` file exists +2. ✅ All required environment variables are present +3. ✅ Values are valid (LOG_LEVEL is valid, paths exist for ALLOWED_ROOTS, etc.) -If you need the full runtime JSON schema, see `CONFIG_README.md`. +If validation fails, the build stops with a clear error message. + +## Version Management + +The `APP_VERSION` is read from two places (in order): +1. `.env` file (if specified) +2. `src/webdrop_bridge/__init__.py` (as fallback) + +This allows you to override the version per customer if needed. diff --git a/docs/CUSTOMER_BUILD_EXAMPLES.md b/docs/CUSTOMER_BUILD_EXAMPLES.md index 20146f9..de97cf3 100644 --- a/docs/CUSTOMER_BUILD_EXAMPLES.md +++ b/docs/CUSTOMER_BUILD_EXAMPLES.md @@ -20,7 +20,7 @@ webdrop_bridge/ python build/scripts/build_windows.py --msi ``` -**Result:** `WebDropBridge--win-x64.msi` with your packaged bootstrap defaults bundled. +**Result:** `WebDropBridge-x.x.x-Setup.msi` with your `.env` configuration bundled. --- @@ -47,7 +47,7 @@ webdrop_bridge/ **Customer Config Example:** `deploy/customer_configs/acme_corp.env` ```dotenv APP_NAME=WebDrop Bridge - ACME Corp Edition -BRAND_ID=acme_corp +APP_VERSION=1.0.0 WEBAPP_URL=https://acme-drop.example.com/drop ALLOWED_ROOTS=Z:/acme_files/,C:/Users/Public/ACME LOG_LEVEL=INFO @@ -72,9 +72,9 @@ python build/scripts/build_windows.py --msi --env-file deploy/customer_configs/i python build/scripts/build_windows.py --msi --env-file deploy/customer_configs/wayne_enterprises.env ``` -**Result:** Four separate MSI files, for example: -- `WebDropBridge--win-x64.msi` (default brand) -- `AcmeBridge--win-x64.msi` (if the customer build uses its own asset prefix) +**Result:** Four separate MSI files: +- `WebDropBridge-1.0.0-Setup.msi` (ACME - says "ACME Corp Edition") +- `WebDropBridge-1.0.0-Setup.msi` (Globex - say "Globex Edition") - etc. --- diff --git a/docs/PACKAGE_MANAGER_SUPPORT.md b/docs/PACKAGE_MANAGER_SUPPORT.md index 022fbdf..e49d914 100644 --- a/docs/PACKAGE_MANAGER_SUPPORT.md +++ b/docs/PACKAGE_MANAGER_SUPPORT.md @@ -20,16 +20,16 @@ WebDropBridge supports installation via package managers, making it easier for u # 1. Build the Chocolatey package cd build/chocolatey python ../../build/scripts/build_windows.py --msi -certutil -hashfile "../../build/dist/windows/webdrop_bridge/WebDropBridge--win-x64.msi" SHA256 +certutil -hashfile "../../build/dist/windows/WebDropBridge_Setup.msi" SHA256 # Update checksum in tools/chocolateyInstall.ps1 choco pack webdrop-bridge.nuspec -# 2. Share webdrop-bridge..nupkg +# 2. Share webdrop-bridge.0.8.0.nupkg # File share: \\server\packages\ # USB drive, email, Forgejo releases, etc. # 3. Users install it -# choco install webdrop-bridge..nupkg -s "\\server\packages" +# choco install webdrop-bridge.0.8.0.nupkg -s "\\server\packages" ``` **Advantages:** @@ -56,21 +56,21 @@ choco pack webdrop-bridge.nuspec python build/scripts/build_windows.py --msi # 2. Calculate SHA256 checksum of the MSI -certutil -hashfile "build/dist/windows/webdrop_bridge/WebDropBridge--win-x64.msi" SHA256 +certutil -hashfile "build/dist/windows/WebDropBridge_Setup.msi" SHA256 # 3. Update the checksum in build/chocolatey/tools/chocolateyInstall.ps1 # Replace: $Checksum = '' # With: $Checksum = 'YOUR_SHA256_HASH' # 4. Update version in chocolatey/webdrop-bridge.nuspec -# X.Y.Z +# 0.8.0 # 5. Create the package cd build/chocolatey choco pack webdrop-bridge.nuspec ``` -This creates `webdrop-bridge..nupkg` +This creates `webdrop-bridge.0.8.0.nupkg` ### Publishing to Chocolatey @@ -83,7 +83,7 @@ Host on your own NuGet server (Azure Artifacts, Artifactory, ProGet, etc.): choco source add -n=internal-repo -s "https://your-artifactory.internal/nuget/chocolatey/" # Push package to internal repo -nuget push webdrop-bridge..nupkg -Source https://your-artifactory.internal/nuget/chocolatey/ -ApiKey YOUR_API_KEY +nuget push webdrop-bridge.0.8.0.nupkg -Source https://your-artifactory.internal/nuget/chocolatey/ -ApiKey YOUR_API_KEY # Users install from internal repo (already configured) choco install webdrop-bridge @@ -95,7 +95,7 @@ If you want public distribution (requires community maintainer account): ```bash # Push to community repo -choco push webdrop-bridge..nupkg --api-key YOUR_CHOCOLATEY_API_KEY +choco push webdrop-bridge.0.8.0.nupkg --api-key YOUR_CHOCOLATEY_API_KEY ``` **Option 3: No Repository (Direct Distribution)** @@ -103,8 +103,8 @@ choco push webdrop-bridge..nupkg --api-key YOUR_CHOCOLATEY_API_KEY Share the `.nupkg` file directly, users install locally: ```powershell -# User downloads webdrop-bridge..nupkg and runs: -choco install webdrop-bridge..nupkg -s C:\path\to\package\folder +# User downloads webdrop-bridge.0.8.0.nupkg and runs: +choco install webdrop-bridge.0.8.0.nupkg -s C:\path\to\package\folder ``` ### User Installation @@ -119,7 +119,7 @@ choco install webdrop-bridge choco install webdrop-bridge # If distributing directly -choco install webdrop-bridge..nupkg -s "\\network\share\packages" +choco install webdrop-bridge.0.8.0.nupkg -s "\\network\share\packages" ``` ## Homebrew (macOS) @@ -183,11 +183,11 @@ Submit to `homebrew/casks` (requires more maintenance but no separate tap): bash build/scripts/build_macos.sh # 2. Calculate SHA256 checksum -shasum -a 256 "build/dist/macos/webdrop_bridge/WebDropBridge--macos-universal.dmg" +shasum -a 256 "build/dist/macos/WebDropBridge_Setup.dmg" # 3. Update formula with checksum and URL # build/homebrew/webdrop-bridge.rb -# - url: https://git.him-tools.de/...releases/download/vX.Y.Z/WebDropBridge-X.Y.Z-macos-universal.dmg +# - url: https://git.him-tools.de/...releases/download/vX.X.X/WebDropBridge_Setup.dmg # - sha256: YOUR_SHA256_HASH ``` @@ -210,7 +210,7 @@ webdrop-bridge --version # If CLI exists, or check Applications folder ### Step 1: Build Release ```bash -# Release vX.Y.Z +# Release v0.8.0 # Windows MSI python build/scripts/build_windows.py --msi @@ -224,8 +224,8 @@ bash build/scripts/build_macos.sh Tag and upload installers to Forgejo: ```bash -git tag -a vX.Y.Z -m "Release X.Y.Z" -git push upstream vX.Y.Z +git tag -a v0.8.0 -m "Release 0.8.0" +git push upstream v0.8.0 # Upload MSI and DMG to Forgejo release page ``` @@ -234,10 +234,10 @@ git push upstream vX.Y.Z ```bash # Windows -certutil -hashfile WebDropBridge--win-x64.msi SHA256 +certutil -hashfile WebDropBridge_Setup.msi SHA256 # macOS -shasum -a 256 WebDropBridge--macos-universal.dmg +shasum -a 256 WebDropBridge_Setup.dmg ``` ### Step 4: Update Package Manager Files @@ -258,7 +258,7 @@ sha256 "MACOS_SHA256_HASH" ```powershell cd build/chocolatey choco pack -choco install webdrop-bridge..nupkg -s . +choco install webdrop-bridge.0.8.0.nupkg -s . ``` **Homebrew (with tap):** @@ -270,7 +270,7 @@ brew install ./build/homebrew/webdrop-bridge.rb **Chocolatey:** ```powershell -choco push webdrop-bridge..nupkg --api-key YOUR_KEY +choco push webdrop-bridge.0.8.0.nupkg --api-key YOUR_KEY ``` **Homebrew:** @@ -343,7 +343,7 @@ This works for: ### Chocolatey Issues **Package won't install:** -- Verify checksum: `certutil -hashfile WebDropBridge--win-x64.msi SHA256` +- Verify checksum: `certutil -hashfile WebDropBridge_Setup.msi SHA256` - Check MSI exists at URL: `wget URL` - Verify SHA256 matches in `chocolateyInstall.ps1` @@ -356,7 +356,7 @@ This works for: **Formula won't install:** - Validate syntax: `brew audit --formula webdrop-bridge.rb` - Check URL is accessible: `curl -I URL` -- Verify SHA256: `shasum -a 256 WebDropBridge--macos-universal.dmg` +- Verify SHA256: `shasum -a 256 WebDropBridge_Setup.dmg` **Upgrade fails:** - Remove old version: `brew uninstall webdrop-bridge` @@ -375,7 +375,7 @@ This works for: 1. **Easiest: Direct Distribution** ✅ - Share `.nupkg` file via file share or email - - Users: `choco install webdrop-bridge..nupkg -s "\\share\packages"` + - Users: `choco install webdrop-bridge.0.8.0.nupkg -s "\\share\packages"` - No infrastructure needed - No maintainer account required diff --git a/resources/translations/de.json b/resources/translations/de.json index 5be5e9e..3940c17 100644 --- a/resources/translations/de.json +++ b/resources/translations/de.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Die Spracheinstellung wurde aktualisiert. Starten Sie jetzt neu, um die ausgew\u00e4hlte Sprache \u00fcberall anzuwenden.", "dialog.language_changed.restart_now": "Jetzt neu starten", "dialog.language_changed.restart_later": "Sp\u00e4ter neu starten", - "dialog.branding_changed.title": "Branding ge\u00e4ndert", - "dialog.branding_changed.msg": "Das aktive Branding wurde geändert. Starten Sie jetzt neu, damit die aktualisierte visuelle Identität überall angewendet wird.", - "dialog.branding_changed.restart_now": "Jetzt neu starten", - "dialog.branding_changed.restart_later": "Sp\u00e4ter neu starten", "dialog.restart_failed.title": "Neustart fehlgeschlagen", "dialog.restart_failed.msg": "Die Anwendung konnte nicht automatisch neu gestartet werden:\n\n{error}\n\nBitte starten Sie manuell neu.", "dialog.update_timeout.title": "Zeitüberschreitung bei der Update-Pr\u00fcfung", @@ -88,28 +84,8 @@ "settings.tab.urls": "URLs", "settings.tab.logging": "Protokollierung", "settings.tab.window": "Fenster", - "settings.tab.profiles": "Setups", + "settings.tab.profiles": "Profile", "settings.tab.general": "Allgemein", - "settings.tab.branding": "Branding", - "settings.branding.select_label": "Branding:", - "settings.branding.select_tooltip": "Wählen Sie das Branding, das beim Start automatisch geladen werden soll.", - "settings.branding.help_text": "Branding steuert Name sowie Logo/Icon der App. Änderungen sind klar von den gespeicherten Setups getrennt.", - "settings.branding.display_name_label": "Name:", - "settings.branding.app_name_label": "Anwendungsname:", - "settings.branding.window_title_label": "Fenstertitel (optional):", - "settings.branding.logo_path_label": "Logo/Icon-Datei (optional):", - "settings.branding.save_as_btn": "Branding speichern", - "settings.branding.export_btn": "Branding exportieren", - "settings.branding.import_btn": "Branding importieren", - "settings.branding.delete_btn": "Branding löschen", - "settings.branding.export_title": "Branding exportieren", - "settings.branding.import_title": "Branding importieren", - "settings.branding.preview_label": "Vorschau:", - "settings.branding.no_icon_selected": "Kein Icon ausgewählt", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Branding speichern", - "settings.branding.save_as_prompt": "Name für das Branding eingeben:", - "settings.branding.restart_note": "Branding-Änderungen werden persistent gespeichert und nach einem Neustart vollständig angewendet.", "settings.web_url.label": "Web-Anwendungs-URL:", "settings.web_url.placeholder": "z.B. http://localhost:8080 oder file:///./webapp/index.html", "settings.web_url.open_btn": "\u00d6ffnen", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "Durchsuchen...", "settings.window.width_label": "Fensterbreite:", "settings.window.height_label": "Fensterh\u00f6he:", - "settings.profiles.label": "Gespeicherte Setups auf diesem Ger\u00e4t:", - "settings.profiles.label_tooltip": "Gespeicherte Setups sind benannte Schnappsch\u00fcsse Ihrer aktuellen Einstellungen f\u00fcr den schnellen Wechsel auf diesem Ger\u00e4t.", - "settings.profiles.help_text": "Speichern Sie den aktuellen Stand als benanntes Setup f\u00fcr den schnellen Wechsel auf diesem Ger\u00e4t. Nutzen Sie Export/Import, wenn Sie eine Konfigurationsdatei sichern oder teilen m\u00f6chten.", - "settings.profiles.list_tooltip": "Zeigt die auf diesem Ger\u00e4t verf\u00fcgbaren gespeicherten Setups.", - "settings.profiles.save_btn": "Setup speichern", - "settings.profiles.save_tooltip": "Speichert die aktuellen Einstellungen als benanntes Setup auf diesem Ger\u00e4t.", - "settings.profiles.load_btn": "Setup laden", - "settings.profiles.load_tooltip": "L\u00e4dt das ausgew\u00e4hlte gespeicherte Setup in diesen Dialog.", - "settings.profiles.delete_btn": "Setup l\u00f6schen", - "settings.profiles.delete_tooltip": "L\u00f6scht das ausgew\u00e4hlte gespeicherte Setup von diesem Ger\u00e4t.", - "settings.profiles.transfer_label": "Aktuelle Einstellungen sichern oder teilen:", - "settings.profiles.transfer_tooltip": "Export erstellt eine JSON-Datei zum Sichern oder Teilen. Import liest eine solche Datei ein und wendet sie hier an.", - "settings.profiles.export_btn": "In Datei exportieren", - "settings.profiles.export_tooltip": "Exportiert die aktuellen Einstellungen als JSON-Datei zum Sichern oder Teilen.", - "settings.profiles.import_btn": "Aus Datei importieren", - "settings.profiles.import_tooltip": "Importiert Einstellungen aus einer JSON-Datei und wendet sie hier an.", + "settings.profiles.label": "Gespeicherte Konfigurationsprofile:", + "settings.profiles.save_btn": "Als Profil speichern", + "settings.profiles.load_btn": "Profil laden", + "settings.profiles.delete_btn": "Profil l\u00f6schen", + "settings.profiles.export_btn": "Konfiguration exportieren", + "settings.profiles.import_btn": "Konfiguration importieren", "settings.general.language_label": "Sprache:", "settings.general.language_auto": "Systemstandard (Auto)", "settings.general.language_restart_note": "Sprach\u00e4nderung wirksam nach Neustart.", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "Lokalen Dateisystempfad eingeben:", "settings.add_url.title": "URL hinzuf\u00fcgen", "settings.add_url.prompt": "URL-Muster eingeben (z.B. http://example.com oder http://*.example.com):", - "settings.profile.save.title": "Setup speichern", - "settings.profile.save.prompt": "Namen für das Setup eingeben (z.B. Arbeit, Kunde A):", + "settings.profile.save.title": "Profil speichern", + "settings.profile.save.prompt": "Profilnamen eingeben (z.B. Arbeit, Privat):", "settings.select_directory.title": "Verzeichnis ausw\u00e4hlen", "settings.select_log_file.title": "Protokolldatei ausw\u00e4hlen", - "settings.export_config.title": "Einstellungen exportieren", - "settings.import_config.title": "Einstellungen importieren", + "settings.export_config.title": "Konfiguration exportieren", + "settings.import_config.title": "Konfiguration importieren", "settings.error.select_mapping": "Bitte w\u00e4hlen Sie eine Zuordnung zur Bearbeitung aus", - "settings.error.select_profile_load": "Bitte w\u00e4hlen Sie ein Setup zum Laden aus", - "settings.error.select_profile_delete": "Bitte w\u00e4hlen Sie ein Setup zum L\u00f6schen aus", + "settings.error.select_profile_load": "Bitte w\u00e4hlen Sie ein Profil zum Laden aus", + "settings.error.select_profile_delete": "Bitte w\u00e4hlen Sie ein Profil zum L\u00f6schen aus", "settings.web_source.url_label": "Webanwendungs-URL:", "settings.web_source.open_btn": "\u00d6ffnen", "settings.web_source.url_mappings_label": "URL-Zuordnungen (Azure Blob Storage \u2192 Lokale Pfade):", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "Verzeichnis ausw\u00e4hlen", "settings.urls.add_title": "URL hinzuf\u00fcgen", "settings.urls.add_prompt": "URL-Muster eingeben (z.B. http://example.com oder http://*.example.com):", - "settings.profiles.save_title": "Setup speichern", - "settings.profiles.save_prompt": "Namen für das Setup eingeben (z.B. Arbeit, Kunde A):", - "settings.profiles.select_to_load": "Bitte wählen Sie ein Setup zum Laden aus", - "settings.profiles.select_to_delete": "Bitte wählen Sie ein Setup zum Löschen aus", - "settings.profiles.export_title": "Einstellungen exportieren", - "settings.profiles.import_title": "Einstellungen importieren", + "settings.profiles.save_title": "Profil speichern", + "settings.profiles.save_prompt": "Profilnamen eingeben (z.B. Arbeit, Privat):", + "settings.profiles.select_to_load": "Bitte w\u00e4hlen Sie ein Profil zum Laden aus", + "settings.profiles.select_to_delete": "Bitte w\u00e4hlen Sie ein Profil zum L\u00f6schen aus", + "settings.profiles.export_title": "Konfiguration exportieren", + "settings.profiles.import_title": "Konfiguration importieren", "update.checking.title": "Update-Pr\u00fcfung", "update.checking.label": "Suche nach Updates...", diff --git a/resources/translations/en.json b/resources/translations/en.json index 799c546..3bbfb25 100644 --- a/resources/translations/en.json +++ b/resources/translations/en.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "The language setting was updated. Restart now to apply the selected language everywhere.", "dialog.language_changed.restart_now": "Restart Now", "dialog.language_changed.restart_later": "Restart Later", - "dialog.branding_changed.title": "Branding Changed", - "dialog.branding_changed.msg": "The active branding was changed. Restart now so the updated visual identity is applied everywhere.", - "dialog.branding_changed.restart_now": "Restart Now", - "dialog.branding_changed.restart_later": "Restart Later", "dialog.restart_failed.title": "Restart Failed", "dialog.restart_failed.msg": "Could not automatically restart the application:\n\n{error}\n\nPlease restart manually.", "dialog.update_timeout.title": "Update Check Timeout", @@ -88,28 +84,8 @@ "settings.tab.urls": "URLs", "settings.tab.logging": "Logging", "settings.tab.window": "Window", - "settings.tab.profiles": "Setups", + "settings.tab.profiles": "Profiles", "settings.tab.general": "General", - "settings.tab.branding": "Branding", - "settings.branding.select_label": "Branding:", - "settings.branding.select_tooltip": "Choose the branding that should be loaded automatically on startup.", - "settings.branding.help_text": "Branding controls the app name and logo/icon. It stays clearly separated from your saved setups.", - "settings.branding.display_name_label": "Name:", - "settings.branding.app_name_label": "Application name:", - "settings.branding.window_title_label": "Window title (optional):", - "settings.branding.logo_path_label": "Logo/Icon file (optional):", - "settings.branding.save_as_btn": "Save Branding", - "settings.branding.export_btn": "Export Branding", - "settings.branding.import_btn": "Import Branding", - "settings.branding.delete_btn": "Delete Branding", - "settings.branding.export_title": "Export Branding", - "settings.branding.import_title": "Import Branding", - "settings.branding.preview_label": "Preview:", - "settings.branding.no_icon_selected": "No icon selected", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Save Branding", - "settings.branding.save_as_prompt": "Enter a name for the branding:", - "settings.branding.restart_note": "Branding changes are persisted immediately and are fully applied after restarting the app.", "settings.web_url.label": "Web Application URL:", "settings.web_url.placeholder": "e.g., http://localhost:8080 or file:///./webapp/index.html", "settings.web_url.open_btn": "Open", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "Browse...", "settings.window.width_label": "Window Width:", "settings.window.height_label": "Window Height:", - "settings.profiles.label": "Saved setups on this device:", - "settings.profiles.label_tooltip": "Saved setups are named snapshots of your current settings kept on this device for quick switching.", - "settings.profiles.help_text": "Save the current settings as a named setup for quick switching on this device. Use export/import when you want to back up or share a configuration file.", - "settings.profiles.list_tooltip": "Shows the saved setups available on this device.", - "settings.profiles.save_btn": "Save Setup", - "settings.profiles.save_tooltip": "Save the current settings as a named setup on this device.", - "settings.profiles.load_btn": "Load Setup", - "settings.profiles.load_tooltip": "Load the selected saved setup into the dialog.", - "settings.profiles.delete_btn": "Delete Setup", - "settings.profiles.delete_tooltip": "Delete the selected saved setup from this device.", - "settings.profiles.transfer_label": "Backup or share the current settings:", - "settings.profiles.transfer_tooltip": "Export creates a JSON file for backup or sharing. Import reads such a file and applies it here.", - "settings.profiles.export_btn": "Export to File", - "settings.profiles.export_tooltip": "Export the current settings to a JSON file for backup or sharing.", - "settings.profiles.import_btn": "Import from File", - "settings.profiles.import_tooltip": "Import settings from a JSON file and apply them here.", + "settings.profiles.label": "Saved Configuration Profiles:", + "settings.profiles.save_btn": "Save as Profile", + "settings.profiles.load_btn": "Load Profile", + "settings.profiles.delete_btn": "Delete Profile", + "settings.profiles.export_btn": "Export Configuration", + "settings.profiles.import_btn": "Import Configuration", "settings.general.language_label": "Language:", "settings.general.language_auto": "System Default (Auto)", "settings.general.language_restart_note": "Language change takes effect after restart.", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "Enter local file system path:", "settings.add_url.title": "Add URL", "settings.add_url.prompt": "Enter URL pattern (e.g., http://example.com or http://*.example.com):", - "settings.profile.save.title": "Save Setup", - "settings.profile.save.prompt": "Enter a setup name (e.g., Work, Customer A):", + "settings.profile.save.title": "Save Profile", + "settings.profile.save.prompt": "Enter profile name (e.g., work, personal):", "settings.select_directory.title": "Select Directory to Allow", "settings.select_log_file.title": "Select Log File", - "settings.export_config.title": "Export Settings", - "settings.import_config.title": "Import Settings", + "settings.export_config.title": "Export Configuration", + "settings.import_config.title": "Import Configuration", "settings.error.select_mapping": "Please select a mapping to edit", - "settings.error.select_profile_load": "Please select a setup to load", - "settings.error.select_profile_delete": "Please select a setup to delete", + "settings.error.select_profile_load": "Please select a profile to load", + "settings.error.select_profile_delete": "Please select a profile to delete", "settings.web_source.url_label": "Web Application URL:", "settings.web_source.open_btn": "Open", "settings.web_source.url_mappings_label": "URL Mappings (Azure Blob Storage \u2192 Local Paths):", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "Select Directory to Allow", "settings.urls.add_title": "Add URL", "settings.urls.add_prompt": "Enter URL pattern (e.g., http://example.com or http://*.example.com):", - "settings.profiles.save_title": "Save Setup", - "settings.profiles.save_prompt": "Enter a setup name (e.g., Work, Customer A):", - "settings.profiles.select_to_load": "Please select a setup to load", - "settings.profiles.select_to_delete": "Please select a setup to delete", - "settings.profiles.export_title": "Export Settings", - "settings.profiles.import_title": "Import Settings", + "settings.profiles.save_title": "Save Profile", + "settings.profiles.save_prompt": "Enter profile name (e.g., work, personal):", + "settings.profiles.select_to_load": "Please select a profile to load", + "settings.profiles.select_to_delete": "Please select a profile to delete", + "settings.profiles.export_title": "Export Configuration", + "settings.profiles.import_title": "Import Configuration", "update.checking.title": "Checking for Updates", "update.checking.label": "Checking for updates...", diff --git a/resources/translations/fr.json b/resources/translations/fr.json index 9369246..3e76e2d 100644 --- a/resources/translations/fr.json +++ b/resources/translations/fr.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Le param\u00e8tre de langue a \u00e9t\u00e9 mis \u00e0 jour. Red\u00e9marrez maintenant pour appliquer la langue s\u00e9lectionn\u00e9e partout.", "dialog.language_changed.restart_now": "Red\u00e9marrer maintenant", "dialog.language_changed.restart_later": "Red\u00e9marrer plus tard", - "dialog.branding_changed.title": "Branding modifié", - "dialog.branding_changed.msg": "Le branding actif a été modifié. Redémarrez maintenant pour appliquer partout l’identité visuelle mise à jour.", - "dialog.branding_changed.restart_now": "Redémarrer maintenant", - "dialog.branding_changed.restart_later": "Redémarrer plus tard", "dialog.restart_failed.title": "\u00c9chec du red\u00e9marrage", "dialog.restart_failed.msg": "Impossible de red\u00e9marrer automatiquement l'application\u00a0:\n\n{error}\n\nVeuillez red\u00e9marrer manuellement.", "dialog.update_timeout.title": "D\u00e9lai de v\u00e9rification des mises \u00e0 jour d\u00e9pass\u00e9", @@ -88,28 +84,8 @@ "settings.tab.urls": "URLs", "settings.tab.logging": "Journalisation", "settings.tab.window": "Fen\u00eatre", - "settings.tab.profiles": "Configs", + "settings.tab.profiles": "Profils", "settings.tab.general": "G\u00e9n\u00e9ral", - "settings.tab.branding": "Branding", - "settings.branding.select_label": "Branding :", - "settings.branding.select_tooltip": "Choisissez le modèle de branding qui doit être chargé automatiquement au démarrage.", - "settings.branding.help_text": "Le branding contrôle l’identité visuelle de l’application, comme le nom et les icônes. Il reste séparé de vos configurations enregistrées.", - "settings.branding.display_name_label": "Nom d’affichage :", - "settings.branding.app_name_label": "Nom de l’application :", - "settings.branding.window_title_label": "Titre de la fenêtre (facultatif) :", - "settings.branding.logo_path_label": "Chemin du logo (facultatif) :", - "settings.branding.save_as_btn": "Enregistrer le branding", - "settings.branding.export_btn": "Exporter le branding", - "settings.branding.import_btn": "Importer le branding", - "settings.branding.delete_btn": "Supprimer le branding", - "settings.branding.export_title": "Exporter le branding", - "settings.branding.import_title": "Importer le branding", - "settings.branding.preview_label": "Aperçu :", - "settings.branding.no_icon_selected": "Aucune icône sélectionnée", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Enregistrer le branding", - "settings.branding.save_as_prompt": "Entrez un nom pour le branding :", - "settings.branding.restart_note": "Les changements de branding sont enregistrés de façon persistante et seront entièrement appliqués après le redémarrage de l’application.", "settings.web_url.label": "URL de l'application web\u00a0:", "settings.web_url.placeholder": "p.ex. http://localhost:8080 ou file:///./webapp/index.html", "settings.web_url.open_btn": "Ouvrir", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "Parcourir...", "settings.window.width_label": "Largeur de la fen\u00eatre\u00a0:", "settings.window.height_label": "Hauteur de la fen\u00eatre\u00a0:", - "settings.profiles.label": "Configurations enregistr\u00e9es sur cet appareil\u00a0:", - "settings.profiles.label_tooltip": "Les configurations enregistr\u00e9es sont des instantan\u00e9s nomm\u00e9s de vos r\u00e9glages actuels pour basculer rapidement sur cet appareil.", - "settings.profiles.help_text": "Enregistrez l’\u00e9tat actuel comme configuration nomm\u00e9e pour basculer rapidement sur cet appareil. Utilisez l’export/import pour sauvegarder ou partager un fichier de configuration.", - "settings.profiles.list_tooltip": "Affiche les configurations enregistr\u00e9es disponibles sur cet appareil.", - "settings.profiles.save_btn": "Enregistrer la configuration", - "settings.profiles.save_tooltip": "Enregistre les r\u00e9glages actuels comme configuration nomm\u00e9e sur cet appareil.", - "settings.profiles.load_btn": "Charger la configuration", - "settings.profiles.load_tooltip": "Charge la configuration enregistr\u00e9e s\u00e9lectionn\u00e9e dans cette bo\u00eete de dialogue.", - "settings.profiles.delete_btn": "Supprimer la configuration", - "settings.profiles.delete_tooltip": "Supprime la configuration enregistr\u00e9e s\u00e9lectionn\u00e9e de cet appareil.", - "settings.profiles.transfer_label": "Sauvegarder ou partager les r\u00e9glages actuels\u00a0:", - "settings.profiles.transfer_tooltip": "Exporter cr\u00e9e un fichier JSON pour la sauvegarde ou le partage. Importer lit un tel fichier et l’applique ici.", - "settings.profiles.export_btn": "Exporter vers un fichier", - "settings.profiles.export_tooltip": "Exporte les r\u00e9glages actuels vers un fichier JSON pour sauvegarde ou partage.", - "settings.profiles.import_btn": "Importer depuis un fichier", - "settings.profiles.import_tooltip": "Importe des r\u00e9glages depuis un fichier JSON et les applique ici.", + "settings.profiles.label": "Profils de configuration enregistr\u00e9s\u00a0:", + "settings.profiles.save_btn": "Enregistrer comme profil", + "settings.profiles.load_btn": "Charger le profil", + "settings.profiles.delete_btn": "Supprimer le profil", + "settings.profiles.export_btn": "Exporter la configuration", + "settings.profiles.import_btn": "Importer la configuration", "settings.general.language_label": "Langue\u00a0:", "settings.general.language_auto": "Par d\u00e9faut du syst\u00e8me (Auto)", "settings.general.language_restart_note": "Le changement de langue prend effet apr\u00e8s red\u00e9marrage.", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "Entrez le chemin du syst\u00e8me de fichiers local\u00a0:", "settings.add_url.title": "Ajouter une URL", "settings.add_url.prompt": "Entrez le mod\u00e8le d'URL (p.ex. http://example.com ou http://*.example.com)\u00a0:", - "settings.profile.save.title": "Enregistrer la configuration", - "settings.profile.save.prompt": "Entrez un nom de configuration (p.ex. travail, client A)\u00a0:", + "settings.profile.save.title": "Enregistrer le profil", + "settings.profile.save.prompt": "Entrez le nom du profil (p.ex. travail, personnel)\u00a0:", "settings.select_directory.title": "S\u00e9lectionner un r\u00e9pertoire autoris\u00e9", "settings.select_log_file.title": "S\u00e9lectionner le fichier journal", - "settings.export_config.title": "Exporter les réglages", - "settings.import_config.title": "Importer les réglages", + "settings.export_config.title": "Exporter la configuration", + "settings.import_config.title": "Importer la configuration", "settings.error.select_mapping": "Veuillez s\u00e9lectionner un mappage \u00e0 modifier", - "settings.error.select_profile_load": "Veuillez sélectionner une configuration à charger", - "settings.error.select_profile_delete": "Veuillez sélectionner une configuration à supprimer", + "settings.error.select_profile_load": "Veuillez s\u00e9lectionner un profil \u00e0 charger", + "settings.error.select_profile_delete": "Veuillez s\u00e9lectionner un profil \u00e0 supprimer", "settings.web_source.url_label": "URL de l'application web\u00a0:", "settings.web_source.open_btn": "Ouvrir", "settings.web_source.url_mappings_label": "Mappages d'URL (Azure Blob Storage \u2192 Chemins locaux)\u00a0:", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "S\u00e9lectionner un r\u00e9pertoire autoris\u00e9", "settings.urls.add_title": "Ajouter une URL", "settings.urls.add_prompt": "Entrez le mod\u00e8le d'URL (p.ex. http://example.com ou http://*.example.com)\u00a0:", - "settings.profiles.save_title": "Enregistrer la configuration", - "settings.profiles.save_prompt": "Entrez un nom de configuration (p.ex. travail, client A) :", - "settings.profiles.select_to_load": "Veuillez sélectionner une configuration à charger", - "settings.profiles.select_to_delete": "Veuillez sélectionner une configuration à supprimer", - "settings.profiles.export_title": "Exporter les réglages", - "settings.profiles.import_title": "Importer les réglages", + "settings.profiles.save_title": "Enregistrer le profil", + "settings.profiles.save_prompt": "Entrez le nom du profil (p.ex. travail, personnel)\u00a0:", + "settings.profiles.select_to_load": "Veuillez s\u00e9lectionner un profil \u00e0 charger", + "settings.profiles.select_to_delete": "Veuillez s\u00e9lectionner un profil \u00e0 supprimer", + "settings.profiles.export_title": "Exporter la configuration", + "settings.profiles.import_title": "Importer la configuration", "update.checking.title": "V\u00e9rification des mises \u00e0 jour", "update.checking.label": "Recherche de mises \u00e0 jour...", diff --git a/resources/translations/it.json b/resources/translations/it.json index 0ad9c7d..8695a68 100644 --- a/resources/translations/it.json +++ b/resources/translations/it.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "La lingua è stata aggiornata. Riavvia ora per applicarla ovunque.", "dialog.language_changed.restart_now": "Riavvia ora", "dialog.language_changed.restart_later": "Riavvia più tardi", - "dialog.branding_changed.title": "Branding cambiato", - "dialog.branding_changed.msg": "Il branding attivo è stato modificato. Riavvia ora per applicare ovunque l’identità visiva aggiornata.", - "dialog.branding_changed.restart_now": "Riavvia ora", - "dialog.branding_changed.restart_later": "Riavvia più tardi", "dialog.restart_failed.title": "Riavvio non riuscito", "dialog.restart_failed.msg": "Impossibile riavviare automaticamente l'applicazione:\n\n{error}\n\nRiavvia manualmente.", "dialog.update_timeout.title": "Timeout controllo aggiornamenti", @@ -88,28 +84,8 @@ "settings.tab.urls": "URL", "settings.tab.logging": "Log", "settings.tab.window": "Finestra", - "settings.tab.profiles": "Config", + "settings.tab.profiles": "Profili", "settings.tab.general": "Generale", - "settings.tab.branding": "Branding", - "settings.branding.select_label": "Branding:", - "settings.branding.select_tooltip": "Scegli il modello di branding da caricare automaticamente all’avvio.", - "settings.branding.help_text": "Il branding controlla l’identità visiva dell’app, come nome e icone. Rimane separato dalle configurazioni salvate.", - "settings.branding.display_name_label": "Nome visualizzato:", - "settings.branding.app_name_label": "Nome applicazione:", - "settings.branding.window_title_label": "Titolo finestra (opzionale):", - "settings.branding.logo_path_label": "Percorso logo (opzionale):", - "settings.branding.save_as_btn": "Salva branding", - "settings.branding.export_btn": "Esporta branding", - "settings.branding.import_btn": "Importa branding", - "settings.branding.delete_btn": "Elimina branding", - "settings.branding.export_title": "Esporta branding", - "settings.branding.import_title": "Importa branding", - "settings.branding.preview_label": "Anteprima:", - "settings.branding.no_icon_selected": "Nessuna icona selezionata", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Salva branding", - "settings.branding.save_as_prompt": "Inserisci un nome per il branding:", - "settings.branding.restart_note": "Le modifiche al branding vengono salvate in modo persistente e saranno applicate completamente dopo il riavvio dell’applicazione.", "settings.web_url.label": "URL applicazione web:", "settings.web_url.placeholder": "es. http://localhost:8080 o file:///./webapp/index.html", "settings.web_url.open_btn": "Apri", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "Sfoglia...", "settings.window.width_label": "Larghezza finestra:", "settings.window.height_label": "Altezza finestra:", - "settings.profiles.label": "Configurazioni salvate su questo dispositivo:", - "settings.profiles.label_tooltip": "Le configurazioni salvate sono istantanee con nome delle impostazioni correnti per passare rapidamente da un assetto all’altro su questo dispositivo.", - "settings.profiles.help_text": "Salva lo stato corrente come configurazione con nome per cambiare rapidamente su questo dispositivo. Usa esporta/importa per eseguire un backup o condividere un file di configurazione.", - "settings.profiles.list_tooltip": "Mostra le configurazioni salvate disponibili su questo dispositivo.", - "settings.profiles.save_btn": "Salva configurazione", - "settings.profiles.save_tooltip": "Salva le impostazioni correnti come configurazione con nome su questo dispositivo.", - "settings.profiles.load_btn": "Carica configurazione", - "settings.profiles.load_tooltip": "Carica in questa finestra di dialogo la configurazione salvata selezionata.", - "settings.profiles.delete_btn": "Elimina configurazione", - "settings.profiles.delete_tooltip": "Elimina da questo dispositivo la configurazione salvata selezionata.", - "settings.profiles.transfer_label": "Backup o condivisione delle impostazioni correnti:", - "settings.profiles.transfer_tooltip": "Esporta crea un file JSON per backup o condivisione. Importa legge tale file e lo applica qui.", - "settings.profiles.export_btn": "Esporta in file", - "settings.profiles.export_tooltip": "Esporta le impostazioni correnti in un file JSON per backup o condivisione.", - "settings.profiles.import_btn": "Importa da file", - "settings.profiles.import_tooltip": "Importa impostazioni da un file JSON e le applica qui.", + "settings.profiles.label": "Profili configurazione salvati:", + "settings.profiles.save_btn": "Salva come profilo", + "settings.profiles.load_btn": "Carica profilo", + "settings.profiles.delete_btn": "Elimina profilo", + "settings.profiles.export_btn": "Esporta configurazione", + "settings.profiles.import_btn": "Importa configurazione", "settings.general.language_label": "Lingua:", "settings.general.language_auto": "Predefinita sistema (Auto)", "settings.general.language_restart_note": "La modifica lingua si applica dopo il riavvio.", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "Inserisci percorso file system locale:", "settings.add_url.title": "Aggiungi URL", "settings.add_url.prompt": "Inserisci pattern URL (es. http://example.com o http://*.example.com):", - "settings.profile.save.title": "Salva configurazione", - "settings.profile.save.prompt": "Inserisci un nome per la configurazione (es. Lavoro, Cliente A):", + "settings.profile.save.title": "Salva profilo", + "settings.profile.save.prompt": "Inserisci nome profilo (es. lavoro, personale):", "settings.select_directory.title": "Seleziona directory da consentire", "settings.select_log_file.title": "Seleziona file di log", - "settings.export_config.title": "Esporta impostazioni", - "settings.import_config.title": "Importa impostazioni", + "settings.export_config.title": "Esporta configurazione", + "settings.import_config.title": "Importa configurazione", "settings.error.select_mapping": "Seleziona una mappatura da modificare", - "settings.error.select_profile_load": "Seleziona una configurazione da caricare", - "settings.error.select_profile_delete": "Seleziona una configurazione da eliminare", + "settings.error.select_profile_load": "Seleziona un profilo da caricare", + "settings.error.select_profile_delete": "Seleziona un profilo da eliminare", "settings.web_source.url_label": "URL applicazione web:", "settings.web_source.open_btn": "Apri", "settings.web_source.url_mappings_label": "Mappature URL (Azure Blob Storage \u2192 Percorsi locali):", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "Seleziona directory da consentire", "settings.urls.add_title": "Aggiungi URL", "settings.urls.add_prompt": "Inserisci pattern URL (es. http://example.com o http://*.example.com):", - "settings.profiles.save_title": "Salva configurazione", - "settings.profiles.save_prompt": "Inserisci un nome per la configurazione (es. Lavoro, Cliente A):", - "settings.profiles.select_to_load": "Seleziona una configurazione da caricare", - "settings.profiles.select_to_delete": "Seleziona una configurazione da eliminare", - "settings.profiles.export_title": "Esporta impostazioni", - "settings.profiles.import_title": "Importa impostazioni", + "settings.profiles.save_title": "Salva profilo", + "settings.profiles.save_prompt": "Inserisci nome profilo (es. lavoro, personale):", + "settings.profiles.select_to_load": "Seleziona un profilo da caricare", + "settings.profiles.select_to_delete": "Seleziona un profilo da eliminare", + "settings.profiles.export_title": "Esporta configurazione", + "settings.profiles.import_title": "Importa configurazione", "update.checking.title": "Controllo aggiornamenti", "update.checking.label": "Controllo aggiornamenti...", diff --git a/resources/translations/ru.json b/resources/translations/ru.json index da0b804..9d36a37 100644 --- a/resources/translations/ru.json +++ b/resources/translations/ru.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Настройка языка обновлена. Перезапустите сейчас, чтобы применить язык везде.", "dialog.language_changed.restart_now": "Перезапустить сейчас", "dialog.language_changed.restart_later": "Перезапустить позже", - "dialog.branding_changed.title": "Брендинг изменен", - "dialog.branding_changed.msg": "Активный брендинг был изменен. Перезапустите приложение сейчас, чтобы обновленная визуальная идентичность применялась везде.", - "dialog.branding_changed.restart_now": "Перезапустить сейчас", - "dialog.branding_changed.restart_later": "Перезапустить позже", "dialog.restart_failed.title": "Сбой перезапуска", "dialog.restart_failed.msg": "Не удалось автоматически перезапустить приложение:\n\n{error}\n\nПерезапустите вручную.", "dialog.update_timeout.title": "Таймаут проверки обновлений", @@ -88,28 +84,8 @@ "settings.tab.urls": "URL", "settings.tab.logging": "Логирование", "settings.tab.window": "Окно", - "settings.tab.profiles": "Наборы", + "settings.tab.profiles": "Профили", "settings.tab.general": "Общие настройки", - "settings.tab.branding": "Брендинг", - "settings.branding.select_label": "Брендинг:", - "settings.branding.select_tooltip": "Выберите шаблон брендинга, который должен автоматически загружаться при запуске.", - "settings.branding.help_text": "Брендинг управляет визуальной идентичностью приложения, например названием и иконками. Он отделен от сохраненных наборов настроек.", - "settings.branding.display_name_label": "Отображаемое имя:", - "settings.branding.app_name_label": "Имя приложения:", - "settings.branding.window_title_label": "Заголовок окна (необязательно):", - "settings.branding.logo_path_label": "Путь к логотипу (необязательно):", - "settings.branding.save_as_btn": "Сохранить брендинг", - "settings.branding.export_btn": "Экспортировать брендинг", - "settings.branding.import_btn": "Импортировать брендинг", - "settings.branding.delete_btn": "Удалить брендинг", - "settings.branding.export_title": "Экспортировать брендинг", - "settings.branding.import_title": "Импортировать брендинг", - "settings.branding.preview_label": "Предпросмотр:", - "settings.branding.no_icon_selected": "Значок не выбран", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Сохранить брендинг", - "settings.branding.save_as_prompt": "Введите имя для брендинга:", - "settings.branding.restart_note": "Изменения брендинга сохраняются постоянно и будут полностью применены после перезапуска приложения.", "settings.web_url.label": "URL веб-приложения:", "settings.web_url.placeholder": "например, http://localhost:8080 или file:///./webapp/index.html", "settings.web_url.open_btn": "Открыть", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "Обзор...", "settings.window.width_label": "Ширина окна:", "settings.window.height_label": "Высота окна:", - "settings.profiles.label": "Сохраненные наборы настроек на этом устройстве:", - "settings.profiles.label_tooltip": "Сохраненные наборы — это именованные снимки текущих настроек для быстрого переключения на этом устройстве.", - "settings.profiles.help_text": "Сохраните текущее состояние как именованный набор для быстрого переключения на этом устройстве. Используйте экспорт/импорт для резервного копирования или обмена файлом конфигурации.", - "settings.profiles.list_tooltip": "Показывает сохраненные наборы, доступные на этом устройстве.", - "settings.profiles.save_btn": "Сохранить набор", - "settings.profiles.save_tooltip": "Сохраняет текущие настройки как именованный набор на этом устройстве.", - "settings.profiles.load_btn": "Загрузить набор", - "settings.profiles.load_tooltip": "Загружает выбранный сохраненный набор в это окно.", - "settings.profiles.delete_btn": "Удалить набор", - "settings.profiles.delete_tooltip": "Удаляет выбранный сохраненный набор с этого устройства.", - "settings.profiles.transfer_label": "Сохранить резервную копию или поделиться текущими настройками:", - "settings.profiles.transfer_tooltip": "Экспорт создает JSON-файл для резервного копирования или обмена. Импорт читает такой файл и применяет его здесь.", - "settings.profiles.export_btn": "Экспорт в файл", - "settings.profiles.export_tooltip": "Экспортирует текущие настройки в JSON-файл для резервного копирования или обмена.", - "settings.profiles.import_btn": "Импорт из файла", - "settings.profiles.import_tooltip": "Импортирует настройки из JSON-файла и применяет их здесь.", + "settings.profiles.label": "Сохраненные профили конфигурации:", + "settings.profiles.save_btn": "Сохранить как профиль", + "settings.profiles.load_btn": "Загрузить профиль", + "settings.profiles.delete_btn": "Удалить профиль", + "settings.profiles.export_btn": "Экспорт конфигурации", + "settings.profiles.import_btn": "Импорт конфигурации", "settings.general.language_label": "Язык:", "settings.general.language_auto": "Системный язык (авто)", "settings.general.language_restart_note": "Изменение языка вступает в силу после перезапуска.", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "Введите локальный путь файловой системы:", "settings.add_url.title": "Добавить URL", "settings.add_url.prompt": "Введите шаблон URL (например, http://example.com или http://*.example.com):", - "settings.profile.save.title": "Сохранить набор", - "settings.profile.save.prompt": "Введите имя набора (например, Работа, Клиент A):", + "settings.profile.save.title": "Сохранить профиль", + "settings.profile.save.prompt": "Введите имя профиля (например, работа, личный):", "settings.select_directory.title": "Выберите разрешенную папку", "settings.select_log_file.title": "Выберите файл журнала", - "settings.export_config.title": "Экспорт настроек", - "settings.import_config.title": "Импорт настроек", + "settings.export_config.title": "Экспорт конфигурации", + "settings.import_config.title": "Импорт конфигурации", "settings.error.select_mapping": "Выберите сопоставление для редактирования", - "settings.error.select_profile_load": "Выберите набор для загрузки", - "settings.error.select_profile_delete": "Выберите набор для удаления", + "settings.error.select_profile_load": "Выберите профиль для загрузки", + "settings.error.select_profile_delete": "Выберите профиль для удаления", "settings.web_source.url_label": "URL веб-приложения:", "settings.web_source.open_btn": "Открыть", "settings.web_source.url_mappings_label": "Сопоставления URL (Azure Blob Storage → локальные пути):", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "Выберите разрешенную папку", "settings.urls.add_title": "Добавить URL", "settings.urls.add_prompt": "Введите шаблон URL (например, http://example.com или http://*.example.com):", - "settings.profiles.save_title": "Сохранить набор", - "settings.profiles.save_prompt": "Введите имя набора (например, Работа, Клиент A):", - "settings.profiles.select_to_load": "Выберите набор для загрузки", - "settings.profiles.select_to_delete": "Выберите набор для удаления", - "settings.profiles.export_title": "Экспорт настроек", - "settings.profiles.import_title": "Импорт настроек", + "settings.profiles.save_title": "Сохранить профиль", + "settings.profiles.save_prompt": "Введите имя профиля (например, работа, личный):", + "settings.profiles.select_to_load": "Выберите профиль для загрузки", + "settings.profiles.select_to_delete": "Выберите профиль для удаления", + "settings.profiles.export_title": "Экспорт конфигурации", + "settings.profiles.import_title": "Импорт конфигурации", "update.checking.title": "Проверка обновлений", "update.checking.label": "Проверка обновлений...", diff --git a/resources/translations/zh.json b/resources/translations/zh.json index f3e61fe..62a049d 100644 --- a/resources/translations/zh.json +++ b/resources/translations/zh.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "语言设置已更新。立即重启可在所有界面生效。", "dialog.language_changed.restart_now": "立即重启", "dialog.language_changed.restart_later": "稍后重启", - "dialog.branding_changed.title": "品牌已更改", - "dialog.branding_changed.msg": "当前品牌配置已更改。请立即重启,以便在所有界面应用更新后的视觉标识。", - "dialog.branding_changed.restart_now": "立即重启", - "dialog.branding_changed.restart_later": "稍后重启", "dialog.restart_failed.title": "重启失败", "dialog.restart_failed.msg": "无法自动重启应用:\n\n{error}\n\n请手动重启。", "dialog.update_timeout.title": "更新检查超时", @@ -88,28 +84,8 @@ "settings.tab.urls": "URL", "settings.tab.logging": "日志", "settings.tab.window": "窗口", - "settings.tab.profiles": "设置", + "settings.tab.profiles": "配置档案", "settings.tab.general": "通用", - "settings.tab.branding": "品牌", - "settings.branding.select_label": "品牌:", - "settings.branding.select_tooltip": "选择应用启动时应自动加载的品牌模板。", - "settings.branding.help_text": "品牌控制应用的视觉标识,例如名称和图标,并与已保存的设置保持分离。", - "settings.branding.display_name_label": "显示名称:", - "settings.branding.app_name_label": "应用名称:", - "settings.branding.window_title_label": "窗口标题(可选):", - "settings.branding.logo_path_label": "Logo 路径(可选):", - "settings.branding.save_as_btn": "保存品牌配置", - "settings.branding.export_btn": "导出品牌配置", - "settings.branding.import_btn": "导入品牌配置", - "settings.branding.delete_btn": "删除品牌配置", - "settings.branding.export_title": "导出品牌配置", - "settings.branding.import_title": "导入品牌配置", - "settings.branding.preview_label": "预览:", - "settings.branding.no_icon_selected": "未选择图标", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "保存品牌配置", - "settings.branding.save_as_prompt": "输入品牌名称:", - "settings.branding.restart_note": "品牌更改会被持久保存,并将在应用重启后完整生效。", "settings.web_url.label": "Web 应用 URL:", "settings.web_url.placeholder": "例如: http://localhost:8080 或 file:///./webapp/index.html", "settings.web_url.open_btn": "打开", @@ -130,22 +106,12 @@ "settings.log_file.browse_btn": "浏览...", "settings.window.width_label": "窗口宽度:", "settings.window.height_label": "窗口高度:", - "settings.profiles.label": "此设备上已保存的设置:", - "settings.profiles.label_tooltip": "已保存设置是当前配置的命名快照,可用于在此设备上快速切换。", - "settings.profiles.help_text": "将当前状态保存为命名设置,便于在此设备上快速切换。需要备份或共享配置文件时,请使用导出/导入。", - "settings.profiles.list_tooltip": "显示此设备上可用的已保存设置。", - "settings.profiles.save_btn": "保存设置", - "settings.profiles.save_tooltip": "将当前设置保存为此设备上的命名设置。", - "settings.profiles.load_btn": "加载设置", - "settings.profiles.load_tooltip": "将选中的已保存设置加载到此对话框中。", - "settings.profiles.delete_btn": "删除设置", - "settings.profiles.delete_tooltip": "从此设备删除选中的已保存设置。", - "settings.profiles.transfer_label": "备份或共享当前设置:", - "settings.profiles.transfer_tooltip": "导出会创建一个 JSON 文件用于备份或共享。导入会读取此类文件并在此处应用。", - "settings.profiles.export_btn": "导出到文件", - "settings.profiles.export_tooltip": "将当前设置导出为 JSON 文件,用于备份或共享。", - "settings.profiles.import_btn": "从文件导入", - "settings.profiles.import_tooltip": "从 JSON 文件导入设置并在此处应用。", + "settings.profiles.label": "已保存配置档案:", + "settings.profiles.save_btn": "保存为档案", + "settings.profiles.load_btn": "加载档案", + "settings.profiles.delete_btn": "删除档案", + "settings.profiles.export_btn": "导出配置", + "settings.profiles.import_btn": "导入配置", "settings.general.language_label": "语言:", "settings.general.language_auto": "跟随系统(自动)", "settings.general.language_restart_note": "语言更改将在重启后生效。", @@ -157,15 +123,15 @@ "settings.edit_mapping.path_prompt": "输入本地文件系统路径:", "settings.add_url.title": "添加 URL", "settings.add_url.prompt": "输入 URL 模式(例如: http://example.com 或 http://*.example.com):", - "settings.profile.save.title": "保存设置", - "settings.profile.save.prompt": "输入设置名称(例如:工作、客户A):", + "settings.profile.save.title": "保存档案", + "settings.profile.save.prompt": "输入配置档案名称(例如: 工作, 个人):", "settings.select_directory.title": "选择允许目录", "settings.select_log_file.title": "选择日志文件", - "settings.export_config.title": "导出设置", - "settings.import_config.title": "导入设置", + "settings.export_config.title": "导出配置", + "settings.import_config.title": "导入配置", "settings.error.select_mapping": "请选择要编辑的映射", - "settings.error.select_profile_load": "请选择要加载的设置", - "settings.error.select_profile_delete": "请选择要删除的设置", + "settings.error.select_profile_load": "请选择要加载的档案", + "settings.error.select_profile_delete": "请选择要删除的档案", "settings.web_source.url_label": "Web 应用 URL:", "settings.web_source.open_btn": "打开", "settings.web_source.url_mappings_label": "URL 映射(Azure Blob Storage → 本地路径):", @@ -188,12 +154,12 @@ "settings.paths.select_dir_title": "选择允许目录", "settings.urls.add_title": "添加 URL", "settings.urls.add_prompt": "输入 URL 模式(例如: http://example.com 或 http://*.example.com):", - "settings.profiles.save_title": "保存设置", - "settings.profiles.save_prompt": "输入设置名称(例如:工作、客户A):", - "settings.profiles.select_to_load": "请选择要加载的设置", - "settings.profiles.select_to_delete": "请选择要删除的设置", - "settings.profiles.export_title": "导出设置", - "settings.profiles.import_title": "导入设置", + "settings.profiles.save_title": "保存档案", + "settings.profiles.save_prompt": "输入配置档案名称(例如: 工作, 个人):", + "settings.profiles.select_to_load": "请选择要加载的档案", + "settings.profiles.select_to_delete": "请选择要删除的档案", + "settings.profiles.export_title": "导出配置", + "settings.profiles.import_title": "导入配置", "update.checking.title": "检查更新", "update.checking.label": "正在检查更新...", diff --git a/src/webdrop_bridge/__init__.py b/src/webdrop_bridge/__init__.py index afd4e03..0178dab 100644 --- a/src/webdrop_bridge/__init__.py +++ b/src/webdrop_bridge/__init__.py @@ -1,6 +1,6 @@ """WebDrop Bridge - Qt-based desktop application for intelligent drag-and-drop file handling.""" -__version__ = "0.9.1" +__version__ = "0.9.0" __author__ = "WebDrop Team" __license__ = "MIT" diff --git a/src/webdrop_bridge/config.py b/src/webdrop_bridge/config.py index d8a5740..12bdeb7 100644 --- a/src/webdrop_bridge/config.py +++ b/src/webdrop_bridge/config.py @@ -18,12 +18,6 @@ DEFAULT_UPDATE_BASE_URL = "https://git.him-tools.de" DEFAULT_UPDATE_REPO = "HIM-public/webdrop-bridge" DEFAULT_UPDATE_CHANNEL = "stable" DEFAULT_UPDATE_MANIFEST_NAME = "release-manifest.json" -DEFAULT_ACTIVE_BRANDING_ID = "default" -DEFAULT_APP_ICON_PATH = "resources/icons/app.ico" -DEFAULT_TOOLBAR_ICON_HOME = "resources/icons/home.ico" -DEFAULT_TOOLBAR_ICON_RELOAD = "resources/icons/reload.ico" -DEFAULT_TOOLBAR_ICON_OPEN = "resources/icons/open.ico" -DEFAULT_TOOLBAR_ICON_OPENWITH = "resources/icons/openwith.ico" class ConfigurationError(Exception): @@ -102,14 +96,6 @@ class Config: enable_logging: bool = True enable_checkout: bool = False language: str = "auto" - active_branding_id: str = DEFAULT_ACTIVE_BRANDING_ID - branding_display_name: str = "Default" - logo_path: str = "" - app_icon_path: str = DEFAULT_APP_ICON_PATH - toolbar_icon_home: str = DEFAULT_TOOLBAR_ICON_HOME - toolbar_icon_reload: str = DEFAULT_TOOLBAR_ICON_RELOAD - toolbar_icon_open: str = DEFAULT_TOOLBAR_ICON_OPEN - toolbar_icon_openwith: str = DEFAULT_TOOLBAR_ICON_OPENWITH brand_id: str = DEFAULT_BRAND_ID config_dir_name: str = DEFAULT_CONFIG_DIR_NAME update_base_url: str = DEFAULT_UPDATE_BASE_URL @@ -193,7 +179,7 @@ class Config: # No window title specified, use default window_title = f"{app_name} v{__version__}" - config = cls( + return cls( app_name=app_name, app_version=__version__, log_level=data.get("log_level", "INFO").upper(), @@ -211,13 +197,6 @@ class Config: enable_logging=data.get("enable_logging", True), enable_checkout=data.get("enable_checkout", False), language=data.get("language", "auto"), - active_branding_id=data.get("active_branding_id", DEFAULT_ACTIVE_BRANDING_ID), - logo_path=data.get("logo_path", ""), - app_icon_path=data.get("app_icon_path", DEFAULT_APP_ICON_PATH), - toolbar_icon_home=data.get("toolbar_icon_home", DEFAULT_TOOLBAR_ICON_HOME), - toolbar_icon_reload=data.get("toolbar_icon_reload", DEFAULT_TOOLBAR_ICON_RELOAD), - toolbar_icon_open=data.get("toolbar_icon_open", DEFAULT_TOOLBAR_ICON_OPEN), - toolbar_icon_openwith=data.get("toolbar_icon_openwith", DEFAULT_TOOLBAR_ICON_OPENWITH), brand_id=brand_id, config_dir_name=config_dir_name, update_base_url=data.get("update_base_url", DEFAULT_UPDATE_BASE_URL), @@ -225,7 +204,6 @@ class Config: update_channel=data.get("update_channel", DEFAULT_UPDATE_CHANNEL), update_manifest_name=data.get("update_manifest_name", DEFAULT_UPDATE_MANIFEST_NAME), ) - return cls._apply_runtime_branding(config) @classmethod def from_env(cls, env_file: str | None = None) -> "Config": @@ -268,12 +246,6 @@ class Config: enable_logging = os.getenv("ENABLE_LOGGING", "true").lower() == "true" enable_checkout = os.getenv("ENABLE_CHECKOUT", "false").lower() == "true" language = os.getenv("LANGUAGE", "auto") - active_branding_id = os.getenv("BRAND_TEMPLATE", DEFAULT_ACTIVE_BRANDING_ID) - app_icon_path = os.getenv("APP_ICON_PATH", DEFAULT_APP_ICON_PATH) - toolbar_icon_home = os.getenv("TOOLBAR_ICON_HOME", DEFAULT_TOOLBAR_ICON_HOME) - toolbar_icon_reload = os.getenv("TOOLBAR_ICON_RELOAD", DEFAULT_TOOLBAR_ICON_RELOAD) - toolbar_icon_open = os.getenv("TOOLBAR_ICON_OPEN", DEFAULT_TOOLBAR_ICON_OPEN) - toolbar_icon_openwith = os.getenv("TOOLBAR_ICON_OPENWITH", DEFAULT_TOOLBAR_ICON_OPENWITH) update_base_url = os.getenv("UPDATE_BASE_URL", DEFAULT_UPDATE_BASE_URL) update_repo = os.getenv("UPDATE_REPO", DEFAULT_UPDATE_REPO) update_channel = os.getenv("UPDATE_CHANNEL", DEFAULT_UPDATE_CHANNEL) @@ -356,7 +328,7 @@ class Config: f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}" ) from e - config = cls( + return cls( app_name=app_name, app_version=app_version, log_level=log_level, @@ -371,12 +343,6 @@ class Config: enable_logging=enable_logging, enable_checkout=enable_checkout, language=language, - active_branding_id=active_branding_id, - app_icon_path=app_icon_path, - toolbar_icon_home=toolbar_icon_home, - toolbar_icon_reload=toolbar_icon_reload, - toolbar_icon_open=toolbar_icon_open, - toolbar_icon_openwith=toolbar_icon_openwith, brand_id=brand_id, config_dir_name=config_dir_name, update_base_url=update_base_url, @@ -384,7 +350,6 @@ class Config: update_channel=update_channel, update_manifest_name=update_manifest_name, ) - return cls._apply_runtime_branding(config) def to_file(self, config_path: Path) -> None: """Save configuration to JSON file. @@ -413,7 +378,6 @@ class Config: "enable_logging": self.enable_logging, "enable_checkout": self.enable_checkout, "language": self.language, - "active_branding_id": self.active_branding_id, "brand_id": self.brand_id, "config_dir_name": self.config_dir_name, "update_base_url": self.update_base_url, @@ -426,17 +390,6 @@ class Config: with open(config_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) - @staticmethod - def _apply_runtime_branding(config: "Config") -> "Config": - """Apply the persisted runtime branding template to cosmetic fields.""" - try: - from webdrop_bridge.core.branding_manager import BrandingManager - - BrandingManager().apply_to_config(config) - except Exception as e: - logger.warning(f"Failed to apply runtime branding: {e}") - return config - @staticmethod def load_bootstrap_env(env_file: str | None = None) -> Path | None: """Load a bootstrap .env before configuration path lookup. diff --git a/src/webdrop_bridge/core/branding_manager.py b/src/webdrop_bridge/core/branding_manager.py deleted file mode 100644 index ae1726f..0000000 --- a/src/webdrop_bridge/core/branding_manager.py +++ /dev/null @@ -1,406 +0,0 @@ -"""Runtime branding template management for the shared application.""" - -from __future__ import annotations - -import json -import logging -import os -import platform -import shutil -from dataclasses import asdict, dataclass -from pathlib import Path -from typing import Any - -from webdrop_bridge.config import DEFAULT_CONFIG_DIR_NAME, Config, ConfigurationError - -logger = logging.getLogger(__name__) - -DEFAULT_BRANDING_TEMPLATE_ID = "default" -DEFAULT_LOGO_PATH = "resources/icons/app.png" -SUPPORTED_LOGO_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".ico", ".icns"} - - -@dataclass(frozen=True) -class BrandingTemplate: - """Serializable runtime branding template.""" - - template_id: str - display_name: str - app_name: str - window_title: str = "" - logo_path: str = "" - app_icon_path_windows: str = "resources/icons/app.ico" - app_icon_path_macos: str = "resources/icons/app.icns" - toolbar_icon_home: str = "resources/icons/home.ico" - toolbar_icon_reload: str = "resources/icons/reload.ico" - toolbar_icon_open: str = "resources/icons/open.ico" - toolbar_icon_openwith: str = "resources/icons/openwith.ico" - accent_color: str = "#667eea" - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "BrandingTemplate": - """Create a template from a JSON-compatible dictionary.""" - template_id = str(data.get("template_id") or data.get("id") or "").strip() - display_name = str(data.get("display_name") or template_id or "").strip() - app_name = str(data.get("app_name") or display_name or "").strip() - - if not template_id: - raise ConfigurationError("Branding template requires a template_id") - if not display_name: - raise ConfigurationError("Branding template requires a display_name") - if not app_name: - raise ConfigurationError("Branding template requires an app_name") - - return cls( - template_id=template_id, - display_name=display_name, - app_name=app_name, - window_title=str(data.get("window_title", "")), - logo_path=str(data.get("logo_path", "")), - app_icon_path_windows=str(data.get("app_icon_path_windows", "resources/icons/app.ico")), - app_icon_path_macos=str(data.get("app_icon_path_macos", "resources/icons/app.icns")), - toolbar_icon_home=str(data.get("toolbar_icon_home", "resources/icons/home.ico")), - toolbar_icon_reload=str(data.get("toolbar_icon_reload", "resources/icons/reload.ico")), - toolbar_icon_open=str(data.get("toolbar_icon_open", "resources/icons/open.ico")), - toolbar_icon_openwith=str( - data.get("toolbar_icon_openwith", "resources/icons/openwith.ico") - ), - accent_color=str(data.get("accent_color", "#667eea")), - ) - - def to_dict(self) -> dict[str, Any]: - """Convert the template to a JSON-compatible dictionary.""" - return asdict(self) - - def get_app_icon_path(self) -> str: - """Return the best app icon path for the current platform.""" - if platform.system() == "Darwin": - return self.app_icon_path_macos - return self.app_icon_path_windows - - -BUILTIN_BRANDING_TEMPLATES: dict[str, BrandingTemplate] = { - "default": BrandingTemplate( - template_id="default", - display_name="Default", - app_name="WebDrop Bridge", - window_title="", - logo_path=DEFAULT_LOGO_PATH, - accent_color="#667eea", - ), - "agravity": BrandingTemplate( - template_id="agravity", - display_name="Agravity", - app_name="Agravity Bridge", - window_title="", - logo_path=DEFAULT_LOGO_PATH, - accent_color="#2d7d6e", - ), -} - - -class BrandingManager: - """Manage runtime branding templates independently from saved setups.""" - - def __init__(self, base_dir: Path | None = None) -> None: - env_dir = os.getenv("WEBDROP_BRANDING_DIR") - resolved_base = Path(env_dir).resolve() if env_dir and base_dir is None else base_dir - self.base_dir = resolved_base or self._default_base_dir() - self.templates_dir = self.base_dir / "templates" - self.assets_dir = self.base_dir / "assets" - self.active_branding_path = self.base_dir / "active_branding.json" - self.ensure_builtin_templates() - - @staticmethod - def _default_base_dir() -> Path: - """Return the shared branding storage directory.""" - return Config.get_default_config_path(DEFAULT_CONFIG_DIR_NAME).parent / "branding" - - def ensure_builtin_templates(self) -> None: - """Ensure built-in templates exist on disk for discovery and later editing.""" - self.templates_dir.mkdir(parents=True, exist_ok=True) - self.assets_dir.mkdir(parents=True, exist_ok=True) - - for template in BUILTIN_BRANDING_TEMPLATES.values(): - template_path = self.templates_dir / f"{template.template_id}.json" - if not template_path.exists(): - template_path.write_text(json.dumps(template.to_dict(), indent=2), encoding="utf-8") - - def list_templates(self) -> list[BrandingTemplate]: - """List all available templates with built-ins guaranteed.""" - self.ensure_builtin_templates() - - templates: dict[str, BrandingTemplate] = {} - for template_path in self.templates_dir.glob("*.json"): - try: - data = json.loads(template_path.read_text(encoding="utf-8")) - template = BrandingTemplate.from_dict(data) - templates[template.template_id] = template - except (OSError, json.JSONDecodeError, ConfigurationError) as exc: - logger.warning("Skipping invalid branding template %s: %s", template_path, exc) - - for template_id, template in BUILTIN_BRANDING_TEMPLATES.items(): - templates.setdefault(template_id, template) - - ordered_templates = sorted( - templates.values(), - key=lambda template: ( - template.template_id != DEFAULT_BRANDING_TEMPLATE_ID, - template.display_name.lower(), - ), - ) - return ordered_templates - - def has_template(self, template_id: str) -> bool: - """Return whether a template with the given id exists.""" - return any(template.template_id == template_id for template in self.list_templates()) - - def load_template(self, template_id: str) -> BrandingTemplate: - """Load a template by id, falling back to the default template if missing.""" - for template in self.list_templates(): - if template.template_id == template_id: - return template - - logger.warning("Branding template '%s' not found. Falling back to default.", template_id) - return BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID] - - def save_template(self, template: BrandingTemplate) -> Path: - """Save or update a branding template on disk.""" - if not template.template_id: - raise ConfigurationError("Branding template requires a template_id") - if template.template_id in BUILTIN_BRANDING_TEMPLATES: - raise ConfigurationError(f"Cannot overwrite built-in branding: {template.template_id}") - - stored_logo_path = "" - if template.logo_path: - stored_logo_path = self._copy_logo_asset(template.logo_path, template.template_id) - - stored_template = BrandingTemplate( - template_id=template.template_id, - display_name=template.display_name, - app_name=template.app_name, - window_title=template.window_title, - logo_path=stored_logo_path or template.logo_path, - app_icon_path_windows=stored_logo_path or template.app_icon_path_windows, - app_icon_path_macos=stored_logo_path or template.app_icon_path_macos, - toolbar_icon_home=template.toolbar_icon_home, - toolbar_icon_reload=template.toolbar_icon_reload, - toolbar_icon_open=template.toolbar_icon_open, - toolbar_icon_openwith=template.toolbar_icon_openwith, - accent_color=template.accent_color, - ) - - self.templates_dir.mkdir(parents=True, exist_ok=True) - template_path = self.templates_dir / f"{stored_template.template_id}.json" - template_path.write_text(json.dumps(stored_template.to_dict(), indent=2), encoding="utf-8") - logger.info("Branding template saved: %s", stored_template.template_id) - return template_path - - def build_template( - self, - *, - template_id: str, - display_name: str, - app_name: str = "", - window_title: str = "", - logo_path: str = "", - ) -> BrandingTemplate: - """Build a validated branding template from editable UI fields.""" - safe_id = self._slugify(template_id.strip() or display_name) - safe_name = display_name.strip() - logo = logo_path.strip() - resolved_app_name = (app_name or display_name).strip() - - if not safe_id: - raise ConfigurationError("Branding requires a name") - if not safe_name: - raise ConfigurationError("Branding requires a display name") - if logo: - logo_file = self._resolve_asset_path(logo) - if logo_file is None or not logo_file.exists() or not logo_file.is_file(): - raise ConfigurationError(f"Logo file not found: {logo}") - if logo_file.suffix.lower() not in SUPPORTED_LOGO_SUFFIXES: - raise ConfigurationError( - "Unsupported logo format. Use PNG, JPG, BMP, SVG, ICO, or ICNS." - ) - - return BrandingTemplate( - template_id=safe_id, - display_name=safe_name, - app_name=resolved_app_name, - window_title=window_title.strip(), - logo_path=logo, - app_icon_path_windows=logo or "resources/icons/app.ico", - app_icon_path_macos=logo or "resources/icons/app.icns", - ) - - @staticmethod - def _slugify(value: str) -> str: - """Convert a human-readable branding name into a stable id.""" - return "".join(c.lower() if c.isalnum() else "_" for c in value).strip("_") - - @staticmethod - def _resolve_asset_path(configured_path: str) -> Path | None: - """Resolve a branding asset path in dev and packaged layouts.""" - if not configured_path: - return None - - path = Path(configured_path) - candidates = [path] if path.is_absolute() else [Path.cwd() / path] - if not path.is_absolute(): - project_root = Path(__file__).resolve().parents[3] - candidates.append(project_root / path) - - for candidate in candidates: - if candidate.exists(): - return candidate - - return None - - def _copy_logo_asset(self, configured_path: str, template_id: str) -> str: - """Copy a user-selected logo into managed branding storage.""" - resolved_path = self._resolve_asset_path(configured_path) - if resolved_path is None or not resolved_path.exists() or not resolved_path.is_file(): - raise ConfigurationError(f"Logo file not found: {configured_path}") - - self.assets_dir.mkdir(parents=True, exist_ok=True) - target_path = self.assets_dir / f"{template_id}{resolved_path.suffix.lower()}" - if resolved_path.resolve() != target_path.resolve(): - shutil.copy2(resolved_path, target_path) - - return str(target_path) - - def export_template(self, template_id: str, export_path: Path) -> Path: - """Export a branding into a shareable JSON file plus optional logo asset.""" - template = self.load_template(template_id) - export_path.parent.mkdir(parents=True, exist_ok=True) - - export_data = template.to_dict() - if template.logo_path: - resolved_logo = self._resolve_asset_path(template.logo_path) - if resolved_logo and resolved_logo.exists() and resolved_logo.is_file(): - export_logo_path = export_path.parent / resolved_logo.name - if resolved_logo.resolve() != export_logo_path.resolve(): - shutil.copy2(resolved_logo, export_logo_path) - export_data["logo_path"] = export_logo_path.name - export_data["app_icon_path_windows"] = export_logo_path.name - export_data["app_icon_path_macos"] = export_logo_path.name - - export_path.write_text(json.dumps(export_data, indent=2), encoding="utf-8") - logger.info("Branding template exported: %s -> %s", template_id, export_path) - return export_path - - def import_template(self, import_path: Path) -> BrandingTemplate: - """Import a branding from a previously exported JSON file.""" - if not import_path.exists() or not import_path.is_file(): - raise ConfigurationError(f"Branding import file not found: {import_path}") - - try: - data = json.loads(import_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - raise ConfigurationError(f"Invalid branding import file: {import_path}") from exc - - template = BrandingTemplate.from_dict(data) - imported_template_id = template.template_id - if imported_template_id in BUILTIN_BRANDING_TEMPLATES: - imported_template_id = self._slugify(f"{template.display_name}_imported") - - imported_logo_path = "" - if template.logo_path: - candidate_logo = Path(template.logo_path) - if not candidate_logo.is_absolute(): - candidate_logo = import_path.parent / candidate_logo - imported_logo_path = self._copy_logo_asset(str(candidate_logo), imported_template_id) - - imported_template = BrandingTemplate( - template_id=imported_template_id, - display_name=template.display_name, - app_name=template.app_name, - window_title=template.window_title, - logo_path=imported_logo_path, - app_icon_path_windows=imported_logo_path or template.app_icon_path_windows, - app_icon_path_macos=imported_logo_path or template.app_icon_path_macos, - toolbar_icon_home=template.toolbar_icon_home, - toolbar_icon_reload=template.toolbar_icon_reload, - toolbar_icon_open=template.toolbar_icon_open, - toolbar_icon_openwith=template.toolbar_icon_openwith, - accent_color=template.accent_color, - ) - self.save_template(imported_template) - logger.info("Branding template imported: %s", imported_template.template_id) - return imported_template - - def delete_template(self, template_id: str) -> None: - """Delete a user template while protecting built-ins.""" - if template_id in BUILTIN_BRANDING_TEMPLATES: - raise ConfigurationError(f"Cannot delete built-in branding template: {template_id}") - - template_path = self.templates_dir / f"{template_id}.json" - if not template_path.exists(): - raise ConfigurationError(f"Branding template not found: {template_id}") - - template_path.unlink() - logger.info("Branding template deleted: %s", template_id) - - def get_active_branding_id(self) -> str: - """Return the persisted active branding selection.""" - if not self.active_branding_path.exists(): - return DEFAULT_BRANDING_TEMPLATE_ID - - try: - data = json.loads(self.active_branding_path.read_text(encoding="utf-8")) - template_id = str(data.get("active_branding_id", DEFAULT_BRANDING_TEMPLATE_ID)) - return template_id if self.has_template(template_id) else DEFAULT_BRANDING_TEMPLATE_ID - except (OSError, json.JSONDecodeError): - return DEFAULT_BRANDING_TEMPLATE_ID - - def set_active_branding_id(self, template_id: str) -> None: - """Persist the active branding selection.""" - if not self.has_template(template_id): - raise ConfigurationError(f"Branding template not found: {template_id}") - - self.base_dir.mkdir(parents=True, exist_ok=True) - self.active_branding_path.write_text( - json.dumps({"active_branding_id": template_id}, indent=2), - encoding="utf-8", - ) - logger.info("Active branding set to %s", template_id) - - def apply_to_config(self, config: Config) -> BrandingTemplate: - """Apply the active branding template to cosmetic config fields only.""" - requested_id = (getattr(config, "active_branding_id", "") or "").strip() - if not requested_id or requested_id == DEFAULT_BRANDING_TEMPLATE_ID: - requested_id = self.get_active_branding_id() - - template = self.load_template(requested_id) - default_app_name = BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID].app_name - known_app_names = {known_template.app_name for known_template in self.list_templates()} - known_title_prefixes = {f"{app_name} v" for app_name in known_app_names} - - config.active_branding_id = template.template_id - config.branding_display_name = template.display_name - - if ( - template.template_id != DEFAULT_BRANDING_TEMPLATE_ID - or not config.app_name - or config.app_name in known_app_names - ): - config.app_name = template.app_name or default_app_name - - if ( - template.template_id != DEFAULT_BRANDING_TEMPLATE_ID - or not config.window_title - or any(config.window_title.startswith(prefix) for prefix in known_title_prefixes) - ): - config.window_title = ( - template.window_title or f"{config.app_name} v{config.app_version}" - ) - - config.logo_path = template.logo_path - config.app_icon_path = template.get_app_icon_path() - config.toolbar_icon_home = template.toolbar_icon_home - config.toolbar_icon_reload = template.toolbar_icon_reload - config.toolbar_icon_open = template.toolbar_icon_open - config.toolbar_icon_openwith = template.toolbar_icon_openwith - - return template diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index a32a09e..f75d872 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -427,9 +427,7 @@ class MainWindow(QMainWindow): self._background_threads = [] # Keep references to background threads self._background_workers = {} # Keep references to background workers self._bridge_script_source = "" # Cache combined bridge source for recovery injection - self._bridge_script_re_registered = ( - False # Flag to prevent duplicate re-registration on same load - ) + self._bridge_script_re_registered = False # Flag to prevent duplicate re-registration on same load self._is_page_loading = False # Track if a page load is currently in progress self._pending_reload = False # Coalesce multiple rapid reload requests into one self._load_sequence = 0 # Monotonic counter to ignore stale async recovery callbacks @@ -446,13 +444,22 @@ class MainWindow(QMainWindow): config.window_height, ) - # Set window icon from the active runtime branding - icon_path = self._resolve_toolbar_icon_path(config.app_icon_path) - if icon_path is not None: + # Set window icon + # Support both development mode and PyInstaller bundle + if hasattr(sys, "_MEIPASS"): + # Running as PyInstaller bundle + icon_path = Path(sys._MEIPASS) / "resources" / "icons" / "app.ico" # type: ignore + else: + # Running in development mode + icon_path = ( + Path(__file__).parent.parent.parent.parent / "resources" / "icons" / "app.ico" + ) + + if icon_path.exists(): self.setWindowIcon(QIcon(str(icon_path))) logger.debug(f"Window icon set from {icon_path}") else: - logger.warning(f"Window icon not found for configured path: {config.app_icon_path}") + logger.warning(f"Window icon not found at {icon_path}") # Create web engine view with URL for profile isolation self.web_view = RestrictedWebEngineView( @@ -1182,9 +1189,7 @@ class MainWindow(QMainWindow): # This more reliably opens files with chosen applications. # Use a simple, more direct approach # Get the chosen app via AppleScript, then use open command - get_app_script = ( - '''choose application with title "Select an application to open the file"''' - ) + get_app_script = '''choose application with title "Select an application to open the file"''' try: # Get the chosen application app_result = subprocess.run( @@ -1194,21 +1199,19 @@ class MainWindow(QMainWindow): text=True, timeout=30, ) - + if app_result.returncode != 0: - logger.warning( - f"User cancelled app chooser or error occurred: {app_result.stderr}" - ) + logger.warning(f"User cancelled app chooser or error occurred: {app_result.stderr}") return False - + # Get the application name (strip whitespace) chosen_app = app_result.stdout.strip() if not chosen_app: logger.warning("No application was selected") return False - + logger.info(f"User selected app: {chosen_app}") - + # Now open the file with the chosen app using the 'open' command open_result = subprocess.run( ["open", "-a", chosen_app, normalized_path], @@ -1217,16 +1220,14 @@ class MainWindow(QMainWindow): text=True, timeout=10, ) - + if open_result.returncode == 0: logger.info(f"Opened '{normalized_path}' with '{chosen_app}'") return True else: - logger.warning( - f"Failed to open file with '{chosen_app}': {open_result.stderr}" - ) + logger.warning(f"Failed to open file with '{chosen_app}': {open_result.stderr}") return False - + except subprocess.TimeoutExpired: logger.warning("App chooser timed out") return False @@ -1392,7 +1393,7 @@ class MainWindow(QMainWindow): Re-registers the bridge script to ensure it will be injected on reload, page navigation, or any load event. - + Uses a flag to prevent duplicate re-registrations if loadStarted fires multiple times. """ self._is_page_loading = True @@ -1411,7 +1412,7 @@ class MainWindow(QMainWindow): Checks if the bridge script was successfully injected, with automatic recovery for page reloads and redirects. - + Resets the re-registration flag for the next load cycle. Args: @@ -1432,11 +1433,9 @@ class MainWindow(QMainWindow): logger.warning("Page failed to load") return - def _verify_bridge_loaded( - stage: str, attempt: int = 1, sequence: int = finished_sequence - ) -> None: + def _verify_bridge_loaded(stage: str, attempt: int = 1, sequence: int = finished_sequence) -> None: """Check if bridge marker exists and optionally recover script injection. - + Implements multi-attempt recovery strategy: - initial: First check after page load (50ms delay) - recovery_N: Recovery attempts with progressive delays @@ -1486,7 +1485,9 @@ class MainWindow(QMainWindow): delay = int(100 * (1.5 ** (attempt - 1))) QTimer.singleShot( delay, - lambda: _verify_bridge_loaded("recovery", attempt + 1, sequence), + lambda: _verify_bridge_loaded( + "recovery", attempt + 1, sequence + ), ) self.web_view.page().runJavaScript(self._bridge_script_source, after_retry) @@ -1506,15 +1507,11 @@ class MainWindow(QMainWindow): ) self._re_register_bridge_script() - self.web_view.page().runJavaScript( - self._bridge_script_source, after_re_register - ) + self.web_view.page().runJavaScript(self._bridge_script_source, after_re_register) return # All recovery attempts exhausted - logger.error( - "❌ WebDrop Bridge script failed to inject after all recovery attempts!" - ) + logger.error("❌ WebDrop Bridge script failed to inject after all recovery attempts!") logger.error(" Drag-and-drop functionality is DISABLED") logger.debug(f" Stage: {stage}, Attempt: {attempt}") @@ -1546,21 +1543,21 @@ class MainWindow(QMainWindow): def _ensure_bridge_script_exists(self, verbose: bool = False) -> None: """Ensure bridge script exists in QWebEngineScript collection (idempotent). - + Checks if the script already exists. If not, adds it. Never removes/re-adds to avoid race conditions with Qt's injection mechanism. - + This is safer than removing+re-adding because: - Avoids concurrent access conflicts with Qt's internal injection - Prevents missing injections during rapid reloads - Guarantees script is available without timing gaps - + Args: verbose: If True, use debug logging; otherwise use minimal logging """ try: scripts = self.web_view.page().scripts() - + # Check if script already exists already_exists = False for script in scripts.toList(): # type: ignore @@ -1569,7 +1566,7 @@ class MainWindow(QMainWindow): if verbose: logger.debug("Bridge script already exists in page().scripts()") break - + # If script doesn't exist, add it if not already_exists and self._bridge_script_source: new_script = QWebEngineScript() @@ -1585,18 +1582,16 @@ class MainWindow(QMainWindow): new_script.setSourceCode(self._bridge_script_source) scripts.insert(new_script) - logger.debug( - f"✓ Added bridge script to collection ({len(self._bridge_script_source)} chars)" - ) + logger.debug(f"✓ Added bridge script to collection ({len(self._bridge_script_source)} chars)") except Exception as e: logger.error(f"Failed to ensure bridge script exists: {e}") def _re_register_bridge_script(self, verbose: bool = False) -> None: """Force re-registration of bridge script in QWebEngineScript collection. - + Removes old script and re-adds it to ensure it's injected on next page load. This is a fallback for recovery mechanics when normal injection fails. - + Args: verbose: If True, use debug logging; otherwise use minimal logging """ @@ -1627,9 +1622,7 @@ class MainWindow(QMainWindow): scripts.insert(new_script) if verbose or removed: - logger.debug( - f"✓ Re-registered webdrop-bridge script ({len(self._bridge_script_source)} chars)" - ) + logger.debug(f"✓ Re-registered webdrop-bridge script ({len(self._bridge_script_source)} chars)") except Exception as e: logger.error(f"Failed to re-register bridge script: {e}") @@ -1656,7 +1649,9 @@ class MainWindow(QMainWindow): toolbar.addSeparator() # Home button - home_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_home) + home_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_HOME", "resources/icons/home.ico") + ) home_icon = ( QIcon(str(home_icon_path)) if home_icon_path is not None @@ -1668,7 +1663,9 @@ class MainWindow(QMainWindow): # Refresh button refresh_action = toolbar.addAction("") - reload_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_reload) + reload_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_RELOAD", "resources/icons/reload.ico") + ) if reload_icon_path is not None: refresh_action.setIcon(QIcon(str(reload_icon_path))) else: @@ -1680,7 +1677,9 @@ class MainWindow(QMainWindow): # Open-with-default-app drop zone (right of Reload) self._open_drop_zone = OpenDropZone() - open_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_open) + open_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPEN", "resources/icons/open.ico") + ) if open_icon_path is not None: self._open_drop_zone.set_icon(QIcon(str(open_icon_path))) self._open_drop_zone.file_opened.connect(self._on_file_opened_via_drop) @@ -1691,7 +1690,9 @@ class MainWindow(QMainWindow): # Open-with chooser drop zone (right of Open-with-default-app) self._open_with_drop_zone = OpenWithDropZone() - open_with_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_openwith) + open_with_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPENWITH", "resources/icons/openwith.ico") + ) if open_with_icon_path is not None: self._open_with_drop_zone.set_icon(QIcon(str(open_with_icon_path))) self._open_with_drop_zone.file_open_with_requested.connect( @@ -1958,7 +1959,6 @@ class MainWindow(QMainWindow): # Store current URL before opening dialog old_webapp_url = self.config.webapp_url old_language = self.config.language - old_branding_id = self.config.active_branding_id # Show dialog dialog = SettingsDialog(self.config, self) @@ -1967,9 +1967,6 @@ class MainWindow(QMainWindow): # Check if webapp URL changed new_webapp_url = self.config.webapp_url language_changed = old_language != self.config.language - branding_changed = old_branding_id != self.config.active_branding_id - restart_prompt_shown = False - if old_webapp_url != new_webapp_url: logger.info(f"Web application URL changed: {old_webapp_url} → {new_webapp_url}") @@ -1979,7 +1976,6 @@ class MainWindow(QMainWindow): if domain_changed: logger.warning("Domain has changed - recommending restart") self._handle_domain_change_restart() - restart_prompt_shown = True else: logger.info("Path changed but domain is same - reloading...") # Clear cache and navigate to home asynchronously @@ -1987,16 +1983,7 @@ class MainWindow(QMainWindow): self.web_view.clear_cache_and_cookies() QTimer.singleShot(100, self._navigate_home) - if not restart_prompt_shown and branding_changed: - logger.info( - "Branding changed: %s → %s", - old_branding_id, - self.config.active_branding_id, - ) - self._handle_branding_change_restart() - restart_prompt_shown = True - - if not restart_prompt_shown and language_changed: + if language_changed: logger.info(f"Language changed: {old_language} → {self.config.language}") self._handle_language_change_restart() @@ -2060,42 +2047,21 @@ class MainWindow(QMainWindow): self.web_view.clear_cache_and_cookies() self._navigate_home() - def _handle_branding_change_restart(self) -> None: - """Handle branding change by prompting for an optional restart.""" - self._show_restart_prompt( - title_key="dialog.branding_changed.title", - message_key="dialog.branding_changed.msg", - restart_now_key="dialog.branding_changed.restart_now", - restart_later_key="dialog.branding_changed.restart_later", - ) - def _handle_language_change_restart(self) -> None: """Handle language change by prompting for an optional restart.""" - self._show_restart_prompt( - title_key="dialog.language_changed.title", - message_key="dialog.language_changed.msg", - restart_now_key="dialog.language_changed.restart_now", - restart_later_key="dialog.language_changed.restart_later", - ) - - def _show_restart_prompt( - self, - *, - title_key: str, - message_key: str, - restart_now_key: str, - restart_later_key: str, - ) -> None: - """Show a restart prompt for settings that require a full restart.""" from PySide6.QtWidgets import QMessageBox msg = QMessageBox(self) - msg.setWindowTitle(tr(title_key)) + msg.setWindowTitle(tr("dialog.language_changed.title")) msg.setIcon(QMessageBox.Icon.Information) - msg.setText(tr(message_key)) + msg.setText(tr("dialog.language_changed.msg")) - restart_now_btn = msg.addButton(tr(restart_now_key), QMessageBox.ButtonRole.AcceptRole) - msg.addButton(tr(restart_later_key), QMessageBox.ButtonRole.RejectRole) + restart_now_btn = msg.addButton( + tr("dialog.language_changed.restart_now"), QMessageBox.ButtonRole.AcceptRole + ) + msg.addButton( + tr("dialog.language_changed.restart_later"), QMessageBox.ButtonRole.RejectRole + ) msg.exec() diff --git a/src/webdrop_bridge/ui/settings_dialog.py b/src/webdrop_bridge/ui/settings_dialog.py index 9830cff..99f5241 100644 --- a/src/webdrop_bridge/ui/settings_dialog.py +++ b/src/webdrop_bridge/ui/settings_dialog.py @@ -4,14 +4,12 @@ import logging from pathlib import Path from typing import Any, Dict, Optional -from PySide6.QtGui import QIcon, QPixmap from PySide6.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, - QInputDialog, QLabel, QLineEdit, QListWidget, @@ -25,7 +23,6 @@ from PySide6.QtWidgets import ( ) from webdrop_bridge.config import Config, ConfigurationError -from webdrop_bridge.core.branding_manager import BrandingManager from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator from webdrop_bridge.utils.i18n import get_available_languages, tr from webdrop_bridge.utils.logging import reconfigure_logging @@ -45,7 +42,6 @@ class SettingsDialog(QDialog): """ super().__init__(parent) self.config = config - self.branding_manager = BrandingManager() self.profile_manager = ConfigProfile(config.config_dir_name) self.setWindowTitle(tr("settings.title")) self.setGeometry(100, 100, 600, 500) @@ -58,7 +54,6 @@ class SettingsDialog(QDialog): self.tabs = QTabWidget() self.tabs.addTab(self._create_general_tab(), tr("settings.tab.general")) - self.tabs.addTab(self._create_branding_tab(), tr("settings.tab.branding")) self.tabs.addTab(self._create_web_source_tab(), tr("settings.tab.web_source")) self.tabs.addTab(self._create_paths_tab(), tr("settings.tab.paths")) self.tabs.addTab(self._create_urls_tab(), tr("settings.tab.urls")) @@ -88,14 +83,6 @@ class SettingsDialog(QDialog): for m in config_data["url_mappings"] ] - selected_branding_id = config_data.get( - "active_branding_id", self.config.active_branding_id - ) - old_branding_id = self.config.active_branding_id - self.branding_manager.set_active_branding_id(selected_branding_id) - self.config.active_branding_id = selected_branding_id - self.branding_manager.apply_to_config(self.config) - old_log_level = self.config.log_level self.config.language = config_data["language"] self.config.log_level = config_data["log_level"] @@ -115,12 +102,6 @@ class SettingsDialog(QDialog): logger.info(f"Configuration saved to {config_path}") logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})") logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}") - if old_branding_id != self.config.active_branding_id: - logger.info( - " Active branding changed: %s -> %s", - old_branding_id, - self.config.active_branding_id, - ) if old_log_level != self.config.log_level: reconfigure_logging( @@ -170,273 +151,6 @@ class SettingsDialog(QDialog): widget.setLayout(layout) return widget - def _create_branding_tab(self) -> QWidget: - """Create runtime branding tab.""" - widget = QWidget() - layout = QVBoxLayout() - - label = QLabel(tr("settings.branding.select_label")) - label.setToolTip(tr("settings.branding.select_tooltip")) - layout.addWidget(label) - - help_label = QLabel(tr("settings.branding.help_text")) - help_label.setWordWrap(True) - help_label.setStyleSheet("color: gray; font-size: 11px;") - layout.addWidget(help_label) - - self.branding_combo = QComboBox() - self.branding_combo.setToolTip(tr("settings.branding.select_tooltip")) - self._refresh_branding_combo() - self.branding_combo.currentIndexChanged.connect(self._on_branding_selection_changed) - layout.addWidget(self.branding_combo) - - self.branding_display_name_input = QLineEdit() - self.branding_display_name_input.setPlaceholderText( - tr("settings.branding.display_name_label") - ) - self.branding_display_name_input.textChanged.connect(self._update_branding_preview) - layout.addWidget(QLabel(tr("settings.branding.display_name_label"))) - layout.addWidget(self.branding_display_name_input) - - self.branding_app_name_input = QLineEdit() - self.branding_app_name_input.setPlaceholderText(tr("settings.branding.app_name_label")) - self.branding_app_name_input.textChanged.connect(self._update_branding_preview) - layout.addWidget(QLabel(tr("settings.branding.app_name_label"))) - layout.addWidget(self.branding_app_name_input) - - self.branding_window_title_input = QLineEdit() - self.branding_window_title_input.setPlaceholderText( - tr("settings.branding.window_title_label") - ) - self.branding_window_title_input.textChanged.connect(self._update_branding_preview) - layout.addWidget(QLabel(tr("settings.branding.window_title_label"))) - layout.addWidget(self.branding_window_title_input) - - layout.addWidget(QLabel(tr("settings.branding.logo_path_label"))) - logo_layout = QHBoxLayout() - self.branding_logo_path_input = QLineEdit() - self.branding_logo_path_input.setPlaceholderText(tr("settings.branding.logo_path_label")) - self.branding_logo_path_input.textChanged.connect(self._update_branding_preview) - logo_layout.addWidget(self.branding_logo_path_input) - - self.browse_branding_logo_btn = QPushButton(tr("settings.log_file.browse_btn")) - self.browse_branding_logo_btn.clicked.connect(self._browse_branding_logo) - logo_layout.addWidget(self.browse_branding_logo_btn) - layout.addLayout(logo_layout) - - layout.addWidget(QLabel(tr("settings.branding.preview_label"))) - self.branding_preview_name_label = QLabel() - self.branding_preview_name_label.setStyleSheet("font-weight: bold;") - layout.addWidget(self.branding_preview_name_label) - - self.branding_preview_title_label = QLabel() - self.branding_preview_title_label.setStyleSheet("color: gray;") - layout.addWidget(self.branding_preview_title_label) - - self.branding_preview_icon_label = QLabel(tr("settings.branding.no_icon_selected")) - self.branding_preview_icon_label.setFixedSize(72, 72) - self.branding_preview_icon_label.setStyleSheet( - "border: 1px solid #ccc; padding: 4px; background: #fafafa;" - ) - layout.addWidget(self.branding_preview_icon_label) - - branding_button_layout = QHBoxLayout() - self.save_branding_as_btn = QPushButton(tr("settings.branding.save_as_btn")) - self.save_branding_as_btn.clicked.connect(self._save_branding_as) - branding_button_layout.addWidget(self.save_branding_as_btn) - - self.export_branding_btn = QPushButton(tr("settings.branding.export_btn")) - self.export_branding_btn.clicked.connect(self._export_branding) - branding_button_layout.addWidget(self.export_branding_btn) - - self.import_branding_btn = QPushButton(tr("settings.branding.import_btn")) - self.import_branding_btn.clicked.connect(self._import_branding) - branding_button_layout.addWidget(self.import_branding_btn) - - self.delete_branding_btn = QPushButton(tr("settings.branding.delete_btn")) - self.delete_branding_btn.clicked.connect(self._delete_branding) - branding_button_layout.addWidget(self.delete_branding_btn) - layout.addLayout(branding_button_layout) - - self._load_branding_into_editor(self.branding_combo.currentData() or "default") - - note = QLabel(tr("settings.branding.restart_note")) - note.setWordWrap(True) - note.setStyleSheet("color: gray; font-size: 11px;") - layout.addWidget(note) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def _refresh_branding_combo(self, selected_template_id: Optional[str] = None) -> None: - """Refresh the branding template selector.""" - current = selected_template_id or self.config.active_branding_id or "default" - self.branding_combo.blockSignals(True) - self.branding_combo.clear() - for template in self.branding_manager.list_templates(): - self.branding_combo.addItem(template.display_name, template.template_id) - - idx = self.branding_combo.findData(current) - if idx < 0: - idx = self.branding_combo.findData("default") - if idx >= 0: - self.branding_combo.setCurrentIndex(idx) - self.branding_combo.blockSignals(False) - - def _load_branding_into_editor(self, template_id: str) -> None: - """Load the selected branding into the editable fields.""" - template = self.branding_manager.load_template(template_id) - self.branding_display_name_input.setText(template.display_name) - self.branding_app_name_input.setText(template.app_name) - self.branding_window_title_input.setText( - template.window_title or f"{template.app_name} v{self.config.app_version}" - ) - self.branding_logo_path_input.setText(template.logo_path or template.get_app_icon_path()) - self._update_branding_preview() - - def _resolve_branding_preview_path(self, configured_path: str) -> Optional[Path]: - """Resolve a branding preview path in both dev and packaged layouts.""" - if not configured_path: - return None - - path = Path(configured_path) - candidates = [path] if path.is_absolute() else [Path.cwd() / path] - if not path.is_absolute(): - project_root = Path(__file__).resolve().parents[3] - candidates.append(project_root / path) - - for candidate in candidates: - if candidate.exists() and candidate.is_file(): - return candidate - - return None - - def _update_branding_preview(self) -> None: - """Refresh the small branding preview for name and icon.""" - display_name = self.branding_display_name_input.text().strip() or tr( - "settings.branding.preview_default_name" - ) - self.branding_preview_name_label.setText(display_name) - - effective_title = self.branding_window_title_input.text().strip() or ( - self.branding_app_name_input.text().strip() or display_name - ) - self.branding_preview_title_label.setText(effective_title) - - logo_path = self.branding_logo_path_input.text().strip() - resolved_logo_path = self._resolve_branding_preview_path(logo_path) - if resolved_logo_path: - pixmap = QPixmap(str(resolved_logo_path)) - if pixmap.isNull(): - icon = QIcon(str(resolved_logo_path)) - pixmap = icon.pixmap(64, 64) - - if not pixmap.isNull(): - self.branding_preview_icon_label.setPixmap(pixmap.scaled(64, 64)) - self.branding_preview_icon_label.setText("") - return - - self.branding_preview_icon_label.setPixmap(QPixmap()) - self.branding_preview_icon_label.setText(tr("settings.branding.no_icon_selected")) - - def _on_branding_selection_changed(self) -> None: - """Update editable branding fields when a different template is selected.""" - template_id = self.branding_combo.currentData() - if template_id: - self._load_branding_into_editor(template_id) - - def _browse_branding_logo(self) -> None: - """Select an external logo or icon file for the current branding.""" - file_path, _ = QFileDialog.getOpenFileName( - self, - tr("settings.branding.logo_path_label"), - str(Path.home()), - "Image Files (*.png *.jpg *.jpeg *.svg *.ico *.icns *.bmp);;All Files (*)", - ) - if file_path: - self.branding_logo_path_input.setText(file_path) - - def _save_branding_as(self) -> None: - """Save the edited branding as a new reusable branding entry.""" - branding_name, ok = QInputDialog.getText( - self, - tr("settings.branding.save_as_title"), - tr("settings.branding.save_as_prompt"), - text=self.branding_display_name_input.text().strip(), - ) - - if not ok or not branding_name: - return - - try: - display_name = self.branding_display_name_input.text().strip() or branding_name - app_name = self.branding_app_name_input.text().strip() or display_name - window_title = self.branding_window_title_input.text().strip() - template = self.branding_manager.build_template( - template_id=branding_name, - display_name=display_name, - app_name=app_name, - window_title=window_title, - logo_path=self.branding_logo_path_input.text(), - ) - self.branding_manager.save_template(template) - self._refresh_branding_combo(template.template_id) - self._load_branding_into_editor(template.template_id) - except ConfigurationError as e: - self._show_error(f"Failed to save branding: {e}") - - def _export_branding(self) -> None: - """Export the selected branding so it can be shared with other users.""" - template_id = self.branding_combo.currentData() - if not template_id: - return - - file_path, _ = QFileDialog.getSaveFileName( - self, - tr("settings.branding.export_title"), - str(Path.home() / f"{template_id}.json"), - "JSON Files (*.json);;All Files (*)", - ) - if not file_path: - return - - try: - self.branding_manager.export_template(template_id, Path(file_path)) - except ConfigurationError as e: - self._show_error(f"Failed to export branding: {e}") - - def _import_branding(self) -> None: - """Import a branding package from another user.""" - file_path, _ = QFileDialog.getOpenFileName( - self, - tr("settings.branding.import_title"), - str(Path.home()), - "JSON Files (*.json);;All Files (*)", - ) - if not file_path: - return - - try: - template = self.branding_manager.import_template(Path(file_path)) - self._refresh_branding_combo(template.template_id) - self._load_branding_into_editor(template.template_id) - except ConfigurationError as e: - self._show_error(f"Failed to import branding: {e}") - - def _delete_branding(self) -> None: - """Delete the currently selected custom branding.""" - template_id = self.branding_combo.currentData() - if not template_id: - return - - try: - self.branding_manager.delete_template(template_id) - self._refresh_branding_combo("default") - self._load_branding_into_editor("default") - except ConfigurationError as e: - self._show_error(f"Failed to delete branding: {e}") - def _create_web_source_tab(self) -> QWidget: """Create web source configuration tab.""" widget = QWidget() @@ -682,58 +396,41 @@ class SettingsDialog(QDialog): return widget def _create_profiles_tab(self) -> QWidget: - """Create setups/import-export tab with clearer guidance.""" + """Create profiles management tab.""" widget = QWidget() layout = QVBoxLayout() - saved_setups_label = QLabel(tr("settings.profiles.label")) - saved_setups_label.setToolTip(tr("settings.profiles.label_tooltip")) - layout.addWidget(saved_setups_label) - - self.profiles_help_label = QLabel(tr("settings.profiles.help_text")) - self.profiles_help_label.setWordWrap(True) - self.profiles_help_label.setStyleSheet("color: gray; font-size: 11px;") - layout.addWidget(self.profiles_help_label) + layout.addWidget(QLabel(tr("settings.profiles.label"))) self.profiles_list = QListWidget() - self.profiles_list.setToolTip(tr("settings.profiles.list_tooltip")) self._refresh_profiles_list() layout.addWidget(self.profiles_list) button_layout = QHBoxLayout() - self.save_profile_btn = QPushButton(tr("settings.profiles.save_btn")) - self.save_profile_btn.setToolTip(tr("settings.profiles.save_tooltip")) - self.save_profile_btn.clicked.connect(self._save_profile) - button_layout.addWidget(self.save_profile_btn) + save_profile_btn = QPushButton(tr("settings.profiles.save_btn")) + save_profile_btn.clicked.connect(self._save_profile) + button_layout.addWidget(save_profile_btn) - self.load_profile_btn = QPushButton(tr("settings.profiles.load_btn")) - self.load_profile_btn.setToolTip(tr("settings.profiles.load_tooltip")) - self.load_profile_btn.clicked.connect(self._load_profile) - button_layout.addWidget(self.load_profile_btn) + load_profile_btn = QPushButton(tr("settings.profiles.load_btn")) + load_profile_btn.clicked.connect(self._load_profile) + button_layout.addWidget(load_profile_btn) - self.delete_profile_btn = QPushButton(tr("settings.profiles.delete_btn")) - self.delete_profile_btn.setToolTip(tr("settings.profiles.delete_tooltip")) - self.delete_profile_btn.clicked.connect(self._delete_profile) - button_layout.addWidget(self.delete_profile_btn) + delete_profile_btn = QPushButton(tr("settings.profiles.delete_btn")) + delete_profile_btn.clicked.connect(self._delete_profile) + button_layout.addWidget(delete_profile_btn) layout.addLayout(button_layout) - export_label = QLabel(tr("settings.profiles.transfer_label")) - export_label.setToolTip(tr("settings.profiles.transfer_tooltip")) - layout.addWidget(export_label) - export_layout = QHBoxLayout() - self.export_btn = QPushButton(tr("settings.profiles.export_btn")) - self.export_btn.setToolTip(tr("settings.profiles.export_tooltip")) - self.export_btn.clicked.connect(self._export_config) - export_layout.addWidget(self.export_btn) + export_btn = QPushButton(tr("settings.profiles.export_btn")) + export_btn.clicked.connect(self._export_config) + export_layout.addWidget(export_btn) - self.import_btn = QPushButton(tr("settings.profiles.import_btn")) - self.import_btn.setToolTip(tr("settings.profiles.import_tooltip")) - self.import_btn.clicked.connect(self._import_config) - export_layout.addWidget(self.import_btn) + import_btn = QPushButton(tr("settings.profiles.import_btn")) + import_btn.clicked.connect(self._import_config) + export_layout.addWidget(import_btn) layout.addLayout(export_layout) layout.addStretch() @@ -909,7 +606,6 @@ class SettingsDialog(QDialog): "app_name": self.config.app_name, "app_version": self.config.app_version, "language": self.language_combo.currentData(), - "active_branding_id": self.branding_combo.currentData(), "log_level": self.log_level_combo.currentText(), "log_file": self.log_file_input.text() or None, "allowed_roots": [ diff --git a/tests/unit/test_branding_manager.py b/tests/unit/test_branding_manager.py deleted file mode 100644 index 60997d4..0000000 --- a/tests/unit/test_branding_manager.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Tests for runtime branding template management.""" - -from pathlib import Path - -import pytest - -from webdrop_bridge.config import Config, ConfigurationError -from webdrop_bridge.core.branding_manager import BrandingManager - - -def test_builtin_brandings_are_available(tmp_path): - """Built-in default and Agravity templates should always be available.""" - manager = BrandingManager(base_dir=tmp_path) - - brandings = manager.list_templates() - template_ids = [template.template_id for template in brandings] - - assert "default" in template_ids - assert "agravity" in template_ids - - -def test_active_branding_persists_across_manager_instances(tmp_path): - """Selected active branding should persist on disk.""" - manager = BrandingManager(base_dir=tmp_path) - manager.set_active_branding_id("agravity") - - reloaded_manager = BrandingManager(base_dir=tmp_path) - - assert reloaded_manager.get_active_branding_id() == "agravity" - - -def test_apply_branding_updates_cosmetic_fields_only(tmp_path): - """Applying a branding template should not overwrite setup-specific values.""" - allowed_root = tmp_path / "allowed" - allowed_root.mkdir() - - config = Config( - app_name="WebDrop Bridge", - app_version="1.0.0", - log_level="INFO", - log_file=None, - allowed_roots=[allowed_root], - allowed_urls=["example.com"], - webapp_url="http://localhost:8080", - window_width=1024, - window_height=768, - enable_logging=True, - active_branding_id="agravity", - ) - - manager = BrandingManager(base_dir=tmp_path) - manager.apply_to_config(config) - - assert config.active_branding_id == "agravity" - assert config.app_name == "Agravity Bridge" - assert config.webapp_url == "http://localhost:8080" - assert config.allowed_roots == [allowed_root] - assert config.toolbar_icon_home.endswith("home.ico") - - -def test_config_from_env_uses_persisted_active_branding(tmp_path, monkeypatch): - """Config loading should apply the persisted active branding automatically.""" - branding_dir = tmp_path / "branding-state" - manager = BrandingManager(base_dir=branding_dir) - manager.set_active_branding_id("agravity") - - monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(branding_dir)) - - root = tmp_path / "root" - root.mkdir() - env_file = tmp_path / ".env" - env_file.write_text(f"ALLOWED_ROOTS={root}\n", encoding="utf-8") - - config = Config.from_env(str(env_file)) - - assert config.active_branding_id == "agravity" - assert config.app_name == "Agravity Bridge" - assert config.get_config_path().name == "config.json" - - -def test_switching_back_to_default_restores_default_branding(tmp_path): - """Switching from a custom branding back to default should restore the default name.""" - manager = BrandingManager(base_dir=tmp_path) - config = Config( - app_name="WebDrop Bridge", - app_version="1.0.0", - log_level="INFO", - log_file=None, - allowed_roots=[], - allowed_urls=[], - webapp_url="http://localhost:8080", - enable_logging=True, - active_branding_id="agravity", - ) - - manager.apply_to_config(config) - assert config.app_name == "Agravity Bridge" - - config.active_branding_id = "default" - manager.apply_to_config(config) - - assert config.app_name == "WebDrop Bridge" - assert config.branding_display_name == "Default" - - -def test_delete_custom_branding_removes_it(tmp_path): - """Custom brandings should be removable while built-ins stay protected.""" - manager = BrandingManager(base_dir=tmp_path) - template = manager.build_template(template_id="Customer B", display_name="Customer B") - manager.save_template(template) - - assert manager.has_template("customer_b") - - manager.delete_template("customer_b") - - assert not manager.has_template("customer_b") - - -def test_build_template_preserves_app_and_window_titles(tmp_path): - """Custom brandings should keep their editable app and window title values.""" - manager = BrandingManager(base_dir=tmp_path) - - template = manager.build_template( - template_id="Customer C", - display_name="Customer C", - app_name="Customer Bridge", - window_title="Customer Bridge Desktop", - ) - - assert template.app_name == "Customer Bridge" - assert template.window_title == "Customer Bridge Desktop" - - -def test_invalid_logo_file_is_rejected(tmp_path): - """Non-existent logo files should not be accepted for saved brandings.""" - manager = BrandingManager(base_dir=tmp_path) - - with pytest.raises(ConfigurationError): - manager.build_template( - template_id="customer_c", - display_name="Customer C", - logo_path=str(tmp_path / "missing-logo.png"), - ) - - -def test_exported_branding_can_be_imported_for_another_user(tmp_path): - """Exported brandings should be shareable and importable by another user.""" - source_logo = tmp_path / "shared-logo.png" - source_logo.write_bytes(b"fake-png-data") - - source_manager = BrandingManager(base_dir=tmp_path / "source") - template = source_manager.build_template( - template_id="Customer D", - display_name="Customer D", - app_name="Customer Bridge", - window_title="Customer Window", - logo_path=str(source_logo), - ) - source_manager.save_template(template) - - export_path = tmp_path / "export" / "customer_d.json" - source_manager.export_template("customer_d", export_path) - - target_manager = BrandingManager(base_dir=tmp_path / "target") - imported = target_manager.import_template(export_path) - - assert imported.template_id == "customer_d" - assert imported.display_name == "Customer D" - assert imported.app_name == "Customer Bridge" - assert imported.window_title == "Customer Window" - assert Path(imported.logo_path).exists() - assert target_manager.has_template("customer_d") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 065caee..09e4d6d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -9,14 +9,11 @@ from webdrop_bridge.config import Config, ConfigurationError @pytest.fixture(autouse=True) -def clear_env(tmp_path): +def clear_env(): """Clear environment variables before each test to avoid persistence.""" # Save current env saved_env = os.environ.copy() - # Isolate runtime branding state from the developer machine - os.environ["WEBDROP_BRANDING_DIR"] = str(tmp_path / "branding") - # Clear relevant variables for key in list(os.environ.keys()): if key.startswith( diff --git a/tests/unit/test_main_window.py b/tests/unit/test_main_window.py index 1fa7d66..be89ab6 100644 --- a/tests/unit/test_main_window.py +++ b/tests/unit/test_main_window.py @@ -82,25 +82,6 @@ class TestMainWindowInitialization: assert window.drag_interceptor is not None -class TestSettingsRestartBehavior: - """Test restart prompts for settings changes that require a restart.""" - - def test_branding_change_prompts_restart(self, qtbot, sample_config): - """Changing the active branding should trigger the restart flow.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - with patch.object(window, "_handle_branding_change_restart") as mock_restart: - with patch("webdrop_bridge.ui.settings_dialog.SettingsDialog") as mock_dialog_cls: - mock_dialog = mock_dialog_cls.return_value - mock_dialog.exec.side_effect = lambda: setattr( - window.config, "active_branding_id", "agravity" - ) - window._show_settings_dialog() - - mock_restart.assert_called_once() - - class TestMainWindowDragIntegration: """Test drag-and-drop integration.""" @@ -226,15 +207,15 @@ class TestMainWindowOpenWith: test_file.write_text("test") call_count = [0] # Use list to make it mutable in nested function - + class _AppChooseResult: returncode = 0 stdout = "TextEdit" # Simulated chosen app name - + class _OpenResult: returncode = 0 stdout = "" - + def mock_run(*args, **kwargs): """Mock subprocess.run with two different behaviors per call.""" call_count[0] += 1 @@ -246,7 +227,8 @@ class TestMainWindowOpenWith: return _OpenResult() else: raise AssertionError(f"Unexpected call #{call_count[0]} to subprocess.run") - + + with patch("webdrop_bridge.ui.main_window.sys.platform", "darwin"): with patch("webdrop_bridge.ui.main_window.subprocess.run", side_effect=mock_run): assert window._open_with_app_chooser(str(test_file)) is True diff --git a/tests/unit/test_settings_dialog.py b/tests/unit/test_settings_dialog.py index b05239a..06d78d0 100644 --- a/tests/unit/test_settings_dialog.py +++ b/tests/unit/test_settings_dialog.py @@ -1,11 +1,11 @@ """Tests for settings dialog.""" from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from webdrop_bridge.config import Config +from webdrop_bridge.config import Config, ConfigurationError from webdrop_bridge.ui.settings_dialog import SettingsDialog @@ -44,7 +44,7 @@ class TestSettingsDialogInitialization: qtbot.addWidget(dialog) assert dialog.tabs is not None - assert dialog.tabs.count() == 8 # General + Branding + previous 6 tabs + assert dialog.tabs.count() == 7 # General + previous 6 tabs def test_dialog_has_general_tab(self, qtbot, sample_config): """Test General tab exists.""" @@ -53,103 +53,47 @@ class TestSettingsDialogInitialization: assert dialog.tabs.tabText(0) == "General" - def test_dialog_has_branding_tab(self, qtbot, sample_config): - """Test Branding tab exists.""" - dialog = SettingsDialog(sample_config) - qtbot.addWidget(dialog) - - assert dialog.tabs.tabText(1) == "Branding" - def test_dialog_has_web_source_tab(self, qtbot, sample_config): """Test Web Source tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(2) == "Web Source" + assert dialog.tabs.tabText(1) == "Web Source" def test_dialog_has_paths_tab(self, qtbot, sample_config): """Test Paths tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(3) == "Paths" + assert dialog.tabs.tabText(2) == "Paths" def test_dialog_has_urls_tab(self, qtbot, sample_config): """Test URLs tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(4) == "URLs" + assert dialog.tabs.tabText(3) == "URLs" def test_dialog_has_logging_tab(self, qtbot, sample_config): """Test Logging tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(5) == "Logging" + assert dialog.tabs.tabText(4) == "Logging" def test_dialog_has_window_tab(self, qtbot, sample_config): """Test Window tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(6) == "Window" + assert dialog.tabs.tabText(5) == "Window" def test_dialog_has_profiles_tab(self, qtbot, sample_config): - """Test Setups tab exists with clearer wording.""" + """Test Profiles tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(7) == "Setups" - - def test_profiles_actions_have_explanatory_tooltips(self, qtbot, sample_config): - """Test profile/config actions expose helpful explanations.""" - dialog = SettingsDialog(sample_config) - qtbot.addWidget(dialog) - - assert "this device" in dialog.save_profile_btn.toolTip().lower() - assert "backup" in dialog.export_btn.toolTip().lower() - assert "json" in dialog.import_btn.toolTip().lower() - - def test_branding_editor_fields_are_initialized( - self, qtbot, sample_config, monkeypatch, tmp_path - ): - """Test branding tab exposes editable fields for the selected template.""" - monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(tmp_path / "branding")) - dialog = SettingsDialog(sample_config) - qtbot.addWidget(dialog) - - assert dialog.branding_display_name_input.text() == "Default" - assert dialog.branding_app_name_input.text() == "WebDrop Bridge" - assert "WebDrop Bridge" in dialog.branding_window_title_input.text() - assert dialog.branding_logo_path_input is not None - assert dialog.browse_branding_logo_btn is not None - assert dialog.branding_preview_name_label.text() == "Default" - assert dialog.branding_preview_icon_label.pixmap() is not None - assert not dialog.branding_preview_icon_label.pixmap().isNull() - assert dialog.export_branding_btn is not None - assert dialog.import_branding_btn is not None - assert dialog.delete_branding_btn is not None - - def test_save_branding_as_creates_custom_template( - self, qtbot, sample_config, monkeypatch, tmp_path - ): - """Test edited branding can be saved as a new reusable template.""" - monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(tmp_path / "branding")) - dialog = SettingsDialog(sample_config) - qtbot.addWidget(dialog) - - logo_path = tmp_path / "customer-logo.png" - logo_path.write_bytes(b"fake-png-data") - - dialog.branding_display_name_input.setText("Customer A") - dialog.branding_logo_path_input.setText(str(logo_path)) - - with patch("PySide6.QtWidgets.QInputDialog.getText", return_value=("Customer A", True)): - dialog._save_branding_as() - - assert dialog.branding_manager.has_template("customer_a") - assert dialog.branding_combo.findData("customer_a") >= 0 + assert dialog.tabs.tabText(6) == "Profiles" class TestPathsTab: @@ -246,7 +190,7 @@ class TestWindowTab: class TestProfilesTab: - """Test profiles management tab.""" + """Test Profiles management tab.""" def test_profiles_list_initialized(self, qtbot, sample_config): """Test profiles list is initialized.""" @@ -254,7 +198,6 @@ class TestProfilesTab: qtbot.addWidget(dialog) assert dialog.profiles_list is not None - assert dialog.profiles_help_label.wordWrap() is True class TestConfigDataRetrieval: diff --git a/tests/unit/test_version_utils.py b/tests/unit/test_version_utils.py deleted file mode 100644 index cedf2fc..0000000 --- a/tests/unit/test_version_utils.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Tests for build script version utilities.""" - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "build" / "scripts")) - -from version_utils import get_release_notes - - -class TestReleaseNotes: - """Test release note extraction for published releases.""" - - def test_get_release_notes_from_changelog(self, tmp_path): - """Extract only the selected version section from the changelog.""" - changelog = tmp_path / "CHANGELOG.md" - changelog.write_text( - """## [0.9.1] - 2026-04-15 - -### Added -- Better update text -- New installer checks - -### Fixed -- Upload retries - -## [0.9.0] - 2026-04-01 - -### Added -- Older changes -""", - encoding="utf-8", - ) - - notes = get_release_notes("0.9.1", project_root=tmp_path) - - assert "Better update text" in notes - assert "New installer checks" in notes - assert "Older changes" not in notes - - def test_get_release_notes_uses_fallback_when_missing(self, tmp_path): - """Return a readable fallback when no changelog entry exists.""" - notes = get_release_notes("0.9.1", project_root=tmp_path) - - assert "WebDrop Bridge v0.9.1" in notes - assert "release package" in notes.lower()