feat: Implement default welcome page for missing web application
- Added a professional HTML welcome page displayed when no web application is configured. - Enhanced `_load_webapp()` method to support improved path resolution for both development and bundled modes. - Updated error handling to show the welcome page instead of a bare error message when the webapp file is not found. - Modified unit tests to verify the welcome page is displayed in error scenarios. build: Complete Windows and macOS build scripts - Created `build_windows.py` for building Windows executable and optional MSI installer using PyInstaller. - Developed `build_macos.sh` for creating macOS application bundle and DMG image. - Added logging and error handling to build scripts for better user feedback. docs: Add build and icon requirements documentation - Created `PHASE_3_BUILD_SUMMARY.md` detailing the build process, results, and next steps. - Added `resources/icons/README.md` outlining icon requirements and creation guidelines. chore: Sync remotes script for repository maintenance - Introduced `sync_remotes.ps1` PowerShell script to fetch updates from origin and upstream remotes.
This commit is contained in:
parent
90dc09eb4d
commit
f0c96f15b8
10 changed files with 1415 additions and 39 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -8,7 +8,10 @@ __pycache__/
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/dist/
|
||||||
|
build/windows/
|
||||||
|
build/macos/
|
||||||
|
build/temp/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
|
|
|
||||||
|
|
@ -537,41 +537,100 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
## Phase 3: Build & Distribution (Weeks 7-8)
|
## Phase 3: Build & Distribution (Weeks 7-8)
|
||||||
|
|
||||||
### 3.1 Windows Installer (MSI)
|
### 3.1 Windows Installer (Executable + MSI)
|
||||||
|
|
||||||
**Setup:**
|
|
||||||
```bash
|
|
||||||
pip install pyinstaller
|
|
||||||
```
|
|
||||||
|
|
||||||
**Build Script** (`build/scripts/build_windows.py`):
|
**Build Script** (`build/scripts/build_windows.py`):
|
||||||
- Compile with PyInstaller
|
- PyInstaller compilation with proper spec file
|
||||||
- Create MSI with WiX (optional: advanced features)
|
- Standalone executable generation
|
||||||
- Code signing (optional: professional deployment)
|
- Optional WiX MSI installer creation
|
||||||
- Output: `WebDropBridge-1.0.0-Setup.exe`
|
- Optional code signing support
|
||||||
|
- Clean build management
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Automatic dependency bundling (PySide6, Qt, Chromium)
|
||||||
|
- ✅ Resource embedding (webapp, icons, stylesheets)
|
||||||
|
- ✅ Hidden imports configuration for Qt Web Engine
|
||||||
|
- ✅ Output validation and size reporting
|
||||||
|
- ✅ WiX support for professional MSI creation
|
||||||
|
|
||||||
|
**PyInstaller Configuration** (`build/webdrop_bridge.spec`):
|
||||||
|
- Bundles all dependencies (PySide6, Qt6 libraries)
|
||||||
|
- Includes webapp files and resources
|
||||||
|
- Sets up GUI mode (no console window)
|
||||||
|
- Cross-platform compatible
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Build executable only
|
||||||
|
python build/scripts/build_windows.py
|
||||||
|
|
||||||
|
# Build with MSI installer (requires WiX)
|
||||||
|
python build/scripts/build_windows.py --msi
|
||||||
|
|
||||||
|
# Build with code signing (requires certificate)
|
||||||
|
python build/scripts/build_windows.py --sign
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build Results:**
|
||||||
|
- ✅ **Executable**: `WebDropBridge.exe` (195.66 MB)
|
||||||
|
- ✅ **Output Directory**: `build/dist/windows/`
|
||||||
|
- ✅ Contains all dependencies including Chromium engine
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- Executable runs standalone
|
- [x] Executable builds successfully
|
||||||
- Installer installs to Program Files
|
- [x] Executable runs standalone (no Python required)
|
||||||
- Uninstaller removes all files
|
- [x] All dependencies bundled correctly
|
||||||
- Shortcuts created in Start Menu
|
- [ ] MSI installer creation (requires WiX installation)
|
||||||
|
- [ ] Code signing (requires certificate)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 macOS DMG Package
|
### 3.2 macOS DMG Package
|
||||||
|
|
||||||
**Build Script** (`build/scripts/build_macos.sh`):
|
**Build Script** (`build/scripts/build_macos.sh`):
|
||||||
- Compile with PyInstaller
|
- PyInstaller for .app bundle creation
|
||||||
- Create `.app` bundle
|
- DMG image generation
|
||||||
- Generate DMG image
|
- Professional DMG styling (optional via create-dmg)
|
||||||
- Code signing (optional)
|
- Code signing and notarization support
|
||||||
- Output: `WebDropBridge-1.0.0.dmg`
|
- Comprehensive error handling
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Creates proper macOS .app bundle
|
||||||
|
- ✅ DMG image for distribution
|
||||||
|
- ✅ Professional volume icon and layout
|
||||||
|
- ✅ Code signing with signing identities
|
||||||
|
- ✅ Apple notarization support
|
||||||
|
- ✅ Checksum verification
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Build .app bundle and DMG
|
||||||
|
bash build/scripts/build_macos.sh
|
||||||
|
|
||||||
|
# With code signing
|
||||||
|
bash build/scripts/build_macos.sh --sign
|
||||||
|
|
||||||
|
# With notarization
|
||||||
|
bash build/scripts/build_macos.sh --notarize
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration (Environment Variables):**
|
||||||
|
```bash
|
||||||
|
# For code signing
|
||||||
|
export APPLE_SIGNING_ID="Developer ID Application: Company Name"
|
||||||
|
|
||||||
|
# For notarization
|
||||||
|
export APPLE_ID="your@apple.id"
|
||||||
|
export APPLE_PASSWORD="app-specific-password"
|
||||||
|
export APPLE_TEAM_ID="XXXXXXXXXX"
|
||||||
|
```
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- App bundle signed (if applicable)
|
- [ ] .app bundle builds successfully
|
||||||
- DMG opens in Finder
|
- [ ] DMG image creates without errors
|
||||||
- Drag-to-Applications works
|
- [ ] DMG mounts and shows contents properly
|
||||||
- Notarization passes (if applicable)
|
- [ ] Code signing works
|
||||||
|
- [ ] Notarization passes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
280
PHASE_3_BUILD_SUMMARY.md
Normal file
280
PHASE_3_BUILD_SUMMARY.md
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
# Phase 3: Build & Distribution - Completion Summary
|
||||||
|
|
||||||
|
**Status**: ✅ WINDOWS BUILD COMPLETE | ⏳ MACOS PENDING | ⏳ CI/CD PENDING
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
|
||||||
|
### 1. PyInstaller Specification File
|
||||||
|
**File**: `build/webdrop_bridge.spec`
|
||||||
|
- Cross-platform spec supporting Windows and macOS
|
||||||
|
- Uses `SPECPATH` variable for proper path resolution
|
||||||
|
- Bundles all dependencies: PySide6, Qt6 libraries, Chromium
|
||||||
|
- Includes data files: `webapp/`, `resources/`
|
||||||
|
- Configured for GUI mode (no console window)
|
||||||
|
- **Status**: ✅ Functional
|
||||||
|
|
||||||
|
### 2. Windows Build Script
|
||||||
|
**File**: `build/scripts/build_windows.py` (315 lines)
|
||||||
|
- Encapsulated in `WindowsBuilder` class
|
||||||
|
- Methods:
|
||||||
|
- `clean()` - Remove previous builds
|
||||||
|
- `build_executable()` - Run PyInstaller
|
||||||
|
- `create_msi()` - WiX Toolset integration (optional)
|
||||||
|
- `sign_executable()` - Code signing (optional)
|
||||||
|
- CLI Arguments:
|
||||||
|
- `--msi` - Create MSI installer
|
||||||
|
- `--sign` - Sign executable
|
||||||
|
- Unicode emoji support (UTF-8 encoding for Windows console)
|
||||||
|
- **Status**: ✅ Tested & Working
|
||||||
|
|
||||||
|
### 3. macOS Build Script
|
||||||
|
**File**: `build/scripts/build_macos.sh` (240+ lines)
|
||||||
|
- Creates .app bundle and DMG image
|
||||||
|
- Functions:
|
||||||
|
- `check_prerequisites()` - Verify required tools
|
||||||
|
- `clean_builds()` - Remove previous builds
|
||||||
|
- `build_executable()` - PyInstaller compilation
|
||||||
|
- `create_dmg()` - DMG image generation (professional or fallback)
|
||||||
|
- `sign_app()` - Code signing support
|
||||||
|
- `notarize_app()` - Apple notarization support
|
||||||
|
- Color-coded output for visibility
|
||||||
|
- Comprehensive error handling
|
||||||
|
- **Status**: ✅ Implemented (untested - requires macOS)
|
||||||
|
|
||||||
|
### 4. Documentation
|
||||||
|
**File**: `resources/icons/README.md`
|
||||||
|
- Icon requirements and specifications
|
||||||
|
- Tools and commands for icon creation
|
||||||
|
- Design guidelines for both platforms
|
||||||
|
- **Status**: ℹ️ Reference documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Results
|
||||||
|
|
||||||
|
### Windows Executable (✅ Complete)
|
||||||
|
|
||||||
|
```
|
||||||
|
Build Output Directory: build/dist/windows/
|
||||||
|
├── WebDropBridge.exe (195.66 MB) - Main executable
|
||||||
|
└── WebDropBridge/ - Dependency directory
|
||||||
|
├── PySide6/ (Qt6 libraries)
|
||||||
|
├── python3.13.zip (Python runtime)
|
||||||
|
└── [other dependencies]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Standalone executable (no Python installation required on user's machine)
|
||||||
|
- Includes Chromium WebEngine (explains large file size)
|
||||||
|
- All dependencies bundled
|
||||||
|
- GUI application (runs without console window)
|
||||||
|
- Ready for distribution or MSI packaging
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
```bash
|
||||||
|
# File size
|
||||||
|
PS> Get-Item "build\dist\windows\WebDropBridge.exe" |
|
||||||
|
Select-Object Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}}
|
||||||
|
# Result: WebDropBridge.exe (195.66 MB)
|
||||||
|
|
||||||
|
# Execution test
|
||||||
|
PS> .\build\dist\windows\WebDropBridge.exe --version
|
||||||
|
# Exit code: 0 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate (Phase 3 Continuation)
|
||||||
|
|
||||||
|
1. **Test Windows Executable Functionality**
|
||||||
|
```bash
|
||||||
|
# Run the application
|
||||||
|
.\build\dist\windows\WebDropBridge.exe
|
||||||
|
|
||||||
|
# Verify:
|
||||||
|
# - Main window opens
|
||||||
|
# - Web view loads
|
||||||
|
# - Settings accessible
|
||||||
|
# - Drag-and-drop works
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **macOS Build Testing** (requires macOS machine)
|
||||||
|
```bash
|
||||||
|
bash build/scripts/build_macos.sh
|
||||||
|
# Should create: build/dist/macos/WebDropBridge.dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Optional: Create MSI Installer**
|
||||||
|
```bash
|
||||||
|
# Install WiX Toolset first
|
||||||
|
python build/scripts/build_windows.py --msi
|
||||||
|
# Output: WebDropBridge-Setup.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deferred Tasks
|
||||||
|
|
||||||
|
4. **GitHub Actions CI/CD Pipeline** (`.github/workflows/build.yml`)
|
||||||
|
- Automated Windows builds on release tag
|
||||||
|
- macOS builds on release tag
|
||||||
|
- Checksum generation
|
||||||
|
- Upload to releases
|
||||||
|
|
||||||
|
5. **Code Signing & Notarization**
|
||||||
|
- Windows: Requires code signing certificate
|
||||||
|
- macOS: Requires Apple Developer ID and notarization credentials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Files Added
|
||||||
|
|
||||||
|
### For Windows Builds
|
||||||
|
```python
|
||||||
|
# build/scripts/build_windows.py
|
||||||
|
class WindowsBuilder:
|
||||||
|
def __init__(self, project_root: Path):
|
||||||
|
self.project_root = project_root
|
||||||
|
self.build_dir = project_root / "build"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### For macOS Builds
|
||||||
|
```bash
|
||||||
|
# build/scripts/build_macos.sh
|
||||||
|
PROJECT_ROOT="$(dirname "$(dirname "$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )")")"
|
||||||
|
APP_NAME="WebDropBridge"
|
||||||
|
DMG_NAME="WebDropBridge.dmg"
|
||||||
|
```
|
||||||
|
|
||||||
|
### PyInstaller Configuration
|
||||||
|
```python
|
||||||
|
# build/webdrop_bridge.spec
|
||||||
|
SPECPATH = os.path.dirname(os.path.abspath(spec_file))
|
||||||
|
project_root = os.path.dirname(SPECPATH)
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
[os.path.join(project_root, 'src/webdrop_bridge/main.py')],
|
||||||
|
...
|
||||||
|
datas=[
|
||||||
|
(os.path.join(project_root, 'webapp'), 'webapp'),
|
||||||
|
(os.path.join(project_root, 'resources'), 'resources'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Decisions & Rationale
|
||||||
|
|
||||||
|
### 1. PyInstaller Spec File (Not CLI Arguments)
|
||||||
|
- **Decision**: Use .spec file instead of CLI args
|
||||||
|
- **Rationale**: Better cross-platform compatibility, easier to maintain, supports complex bundling
|
||||||
|
- **Result**: Unified spec works for both Windows and macOS
|
||||||
|
|
||||||
|
### 2. Separate Build Scripts (Windows Python, macOS Bash)
|
||||||
|
- **Decision**: Python for Windows, Bash for macOS
|
||||||
|
- **Rationale**: Windows Python is most portable, macOS scripts integrate better with shell tools
|
||||||
|
- **Result**: Platform-native experience, easier CI/CD integration
|
||||||
|
|
||||||
|
### 3. Large Executable Size (195.66 MB)
|
||||||
|
- **Expected**: Yes, includes:
|
||||||
|
- Python runtime (~50 MB)
|
||||||
|
- PySide6/Qt6 libraries (~80 MB)
|
||||||
|
- Embedded Chromium browser (~50 MB)
|
||||||
|
- Application code and resources (~15 MB)
|
||||||
|
- **Mitigation**: Users get single-file download, no external dependencies
|
||||||
|
|
||||||
|
### 4. Cross-Platform Data File Bundling
|
||||||
|
- **Decision**: Include webapp/ and resources/ in executables
|
||||||
|
- **Rationale**: Self-contained distribution, no external file dependencies
|
||||||
|
- **Result**: Users can place executable anywhere, always works
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Limitations & Future Work
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- [ ] MSI installer requires WiX Toolset installation on build machine
|
||||||
|
- [ ] Code signing requires code signing certificate
|
||||||
|
- [ ] No automatic updater yet (Phase 4.1)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- [ ] build_macos.sh script is implemented but untested (no macOS machine in workflow)
|
||||||
|
- [ ] Code signing requires macOS machine and certificate
|
||||||
|
- [ ] Notarization requires Apple Developer account
|
||||||
|
- [ ] Professional DMG requires create-dmg tool installation
|
||||||
|
|
||||||
|
### General
|
||||||
|
- [ ] CI/CD pipeline not yet implemented
|
||||||
|
- [ ] Auto-update system not yet implemented (Phase 4.1)
|
||||||
|
- [ ] Icon files not yet created (resources/icons/app.ico, app.icns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use These Build Scripts
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows only - build executable
|
||||||
|
cd "c:\Development\VS Code Projects\webdrop_bridge"
|
||||||
|
python build/scripts/build_windows.py
|
||||||
|
|
||||||
|
# Windows - create MSI (requires WiX)
|
||||||
|
python build/scripts/build_windows.py --msi
|
||||||
|
|
||||||
|
# macOS only - create .app and DMG
|
||||||
|
bash build/scripts/build_macos.sh
|
||||||
|
|
||||||
|
# macOS - with code signing
|
||||||
|
bash build/scripts/build_macos.sh --sign
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Locations
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
- Executable: `build/dist/windows/WebDropBridge.exe`
|
||||||
|
- MSI: `build/dist/windows/WebDropBridge-Setup.exe` (if --msi used)
|
||||||
|
|
||||||
|
macOS:
|
||||||
|
- App Bundle: `build/dist/macos/WebDropBridge.app`
|
||||||
|
- DMG: `build/dist/macos/WebDropBridge.dmg`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
### Windows Build Machine
|
||||||
|
```powershell
|
||||||
|
# Install PyInstaller (already in requirements-dev.txt)
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
# Optional: Install WiX for MSI creation
|
||||||
|
# Download from: https://github.com/wixtoolset/wix3/releases
|
||||||
|
# Or: choco install wixtoolset
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS Build Machine
|
||||||
|
```bash
|
||||||
|
# PyInstaller is in requirements-dev.txt
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
# Optional: Install create-dmg for professional DMG
|
||||||
|
brew install create-dmg
|
||||||
|
|
||||||
|
# For code signing and notarization:
|
||||||
|
# - macOS Developer Certificate (in Keychain)
|
||||||
|
# - Apple ID + app-specific password
|
||||||
|
# - Team ID
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version: 1.0.0
|
||||||
|
|
||||||
|
**Build Date**: January 2026
|
||||||
|
**Built With**: PyInstaller 6.18.0, PySide6 6.10.1, Python 3.13.11
|
||||||
|
|
||||||
148
WEBAPP_LOADING_FIX.md
Normal file
148
WEBAPP_LOADING_FIX.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# WebApp Loading - Issue & Fix Summary
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
When running the Windows executable, the embedded web view displayed:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error
|
||||||
|
Web application file not found: C:\Development\VS Code Projects\webdrop_bridge\file:\webapp\index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Root Causes
|
||||||
|
|
||||||
|
1. **Path Resolution Issue**: When the app runs from a bundled executable (PyInstaller), the default webapp path `file:///./webapp/index.html` is resolved relative to the current working directory, not relative to the executable location.
|
||||||
|
|
||||||
|
2. **No Fallback UI**: When the webapp file wasn't found, users saw a bare error page instead of a helpful welcome/status page.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
### 1. Improved Path Resolution (main_window.py)
|
||||||
|
|
||||||
|
Enhanced `_load_webapp()` method to:
|
||||||
|
- First try the configured path as-is
|
||||||
|
- If not found, try relative to the application package root
|
||||||
|
- Handle both development mode and PyInstaller bundled mode
|
||||||
|
- Work with `file://` URLs and relative paths
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _load_webapp(self) -> None:
|
||||||
|
if not file_path.exists():
|
||||||
|
# Try relative to application package root
|
||||||
|
# This handles both development and bundled (PyInstaller) modes
|
||||||
|
app_root = Path(__file__).parent.parent.parent.parent
|
||||||
|
relative_path = app_root / webapp_url.lstrip("file:///").lstrip("./")
|
||||||
|
|
||||||
|
if relative_path.exists():
|
||||||
|
file_path = relative_path
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Beautiful Default Welcome Page
|
||||||
|
|
||||||
|
Created `DEFAULT_WELCOME_PAGE` constant with professional UI including:
|
||||||
|
- **Status message**: Shows when no web app is configured
|
||||||
|
- **Application info**: Name, version, description
|
||||||
|
- **Key features**: Drag-drop, validation, cross-platform support
|
||||||
|
- **Configuration guide**: Instructions to set up custom webapp
|
||||||
|
- **Professional styling**: Gradient background, clean layout, accessibility
|
||||||
|
|
||||||
|
### 3. Updated Error Handling
|
||||||
|
|
||||||
|
When webapp file is not found, the app now:
|
||||||
|
- Shows the welcome page instead of a bare error message
|
||||||
|
- Provides clear instructions on how to configure a web app
|
||||||
|
- Displays the version number
|
||||||
|
- Gives users a professional first impression
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### `src/webdrop_bridge/ui/main_window.py`
|
||||||
|
- Added `DEFAULT_WELCOME_PAGE` HTML constant with professional styling
|
||||||
|
- Enhanced `_load_webapp()` method with multi-path resolution
|
||||||
|
- Added welcome page as fallback for missing/error conditions
|
||||||
|
|
||||||
|
### `tests/unit/test_main_window.py`
|
||||||
|
- Renamed test: `test_load_nonexistent_file_shows_error` → `test_load_nonexistent_file_shows_welcome_page`
|
||||||
|
- Updated assertions to verify welcome page is shown instead of error
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
```
|
||||||
|
User runs: python -m webdrop_bridge
|
||||||
|
Config: WEBAPP_URL=file:///./webapp/index.html
|
||||||
|
Resolution: C:\...\webdrop_bridge\webapp\index.html
|
||||||
|
Result: ✅ Loads local webapp from source
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bundled Executable (PyInstaller)
|
||||||
|
```
|
||||||
|
User runs: WebDropBridge.exe
|
||||||
|
PyInstaller unpacks to: _internal/webapp/
|
||||||
|
Resolution logic:
|
||||||
|
1. Try: C:\current\working\dir\webapp\index.html (fails)
|
||||||
|
2. Try: C:\path\to\executable\webapp\index.html (succeeds!)
|
||||||
|
Result: ✅ Loads bundled webapp from PyInstaller bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Webapp Configured
|
||||||
|
```
|
||||||
|
User runs: WebDropBridge.exe
|
||||||
|
No WEBAPP_URL or file not found
|
||||||
|
Display: Beautiful welcome page with instructions
|
||||||
|
Result: ✅ Professional fallback instead of error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
All 99 tests pass:
|
||||||
|
- ✅ 99 passed in 2.26s
|
||||||
|
- ✅ Coverage: 84%
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
Before:
|
||||||
|
```
|
||||||
|
Error
|
||||||
|
Web application file not found: C:\...\file:\webapp\index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
```
|
||||||
|
🌉 WebDrop Bridge
|
||||||
|
Professional Web-to-File Drag-and-Drop Bridge
|
||||||
|
|
||||||
|
✓ Application Ready
|
||||||
|
No web application is currently configured.
|
||||||
|
Configure WEBAPP_URL in your .env file to load your custom application.
|
||||||
|
|
||||||
|
[Features list]
|
||||||
|
[Configuration instructions]
|
||||||
|
[Version info]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration for Users
|
||||||
|
|
||||||
|
To use a custom web app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create .env file in application directory
|
||||||
|
WEBAPP_URL=file:///path/to/your/app.html
|
||||||
|
# Or use remote URL
|
||||||
|
WEBAPP_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
- CSS selectors escaped with double braces `{{ }}` for `.format()` compatibility
|
||||||
|
- Works with both relative paths (`./webapp/`) and absolute paths
|
||||||
|
- Handles `file://` URLs and raw file paths
|
||||||
|
- Graceful fallback when webapp is missing
|
||||||
|
- Professional welcome page generates on-the-fly from template
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- **Date Fixed**: January 28, 2026
|
||||||
|
- **Executable Built**: ✅ WebDropBridge.exe (195.7 MB)
|
||||||
|
- **Tests**: ✅ 99/99 passing
|
||||||
|
- **Coverage**: ✅ 84%
|
||||||
295
build/scripts/build_macos.sh
Normal file
295
build/scripts/build_macos.sh
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build macOS DMG package using PyInstaller
|
||||||
|
#
|
||||||
|
# This script builds the WebDrop Bridge application for macOS and creates
|
||||||
|
# a distributable DMG image.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - PyInstaller 6.0+
|
||||||
|
# - Python 3.10+
|
||||||
|
# - Xcode Command Line Tools (for code signing)
|
||||||
|
# - create-dmg (optional, for custom DMG: brew install create-dmg)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash build_macos.sh [--sign] [--notarize]
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
BUILD_DIR="$PROJECT_ROOT/build"
|
||||||
|
DIST_DIR="$BUILD_DIR/dist/macos"
|
||||||
|
TEMP_BUILD="$BUILD_DIR/temp/macos"
|
||||||
|
SPECS_DIR="$BUILD_DIR/specs"
|
||||||
|
SPEC_FILE="$BUILD_DIR/webdrop_bridge.spec"
|
||||||
|
|
||||||
|
APP_NAME="WebDropBridge"
|
||||||
|
DMG_VOLUME_NAME="WebDrop Bridge"
|
||||||
|
VERSION="1.0.0"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
SIGN_APP=0
|
||||||
|
NOTARIZE_APP=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--sign)
|
||||||
|
SIGN_APP=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--notarize)
|
||||||
|
NOTARIZE_APP=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}ℹ️${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}✅${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}⚠️${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}❌${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main build function
|
||||||
|
main() {
|
||||||
|
echo "=========================================="
|
||||||
|
echo "🚀 WebDrop Bridge macOS Build"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
check_prerequisites
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
clean_builds
|
||||||
|
|
||||||
|
# Build with PyInstaller
|
||||||
|
build_executable
|
||||||
|
|
||||||
|
# Create DMG
|
||||||
|
create_dmg
|
||||||
|
|
||||||
|
# Optional: Sign and notarize
|
||||||
|
if [ $SIGN_APP -eq 1 ]; then
|
||||||
|
sign_app
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $NOTARIZE_APP -eq 1 ]; then
|
||||||
|
notarize_app
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
log_success "Build completed successfully"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
log_info "Output: $DIST_DIR/"
|
||||||
|
ls -lh "$DIST_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_prerequisites() {
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
|
||||||
|
# Check Python
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
log_error "Python 3 not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Python 3 found: $(python3 --version)"
|
||||||
|
|
||||||
|
# Check PyInstaller
|
||||||
|
if ! python3 -m pip show pyinstaller &> /dev/null; then
|
||||||
|
log_error "PyInstaller not installed. Run: pip install pyinstaller"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "PyInstaller installed"
|
||||||
|
|
||||||
|
# Check spec file
|
||||||
|
if [ ! -f "$SPEC_FILE" ]; then
|
||||||
|
log_error "Spec file not found: $SPEC_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Spec file found"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_builds() {
|
||||||
|
log_info "Cleaning previous builds..."
|
||||||
|
|
||||||
|
for dir in "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
rm -rf "$dir"
|
||||||
|
log_success "Removed $dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
build_executable() {
|
||||||
|
log_info "Building macOS executable with PyInstaller..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
python3 -m PyInstaller \
|
||||||
|
--distpath="$DIST_DIR" \
|
||||||
|
--buildpath="$TEMP_BUILD" \
|
||||||
|
--specpath="$SPECS_DIR" \
|
||||||
|
"$SPEC_FILE"
|
||||||
|
|
||||||
|
if [ ! -d "$DIST_DIR/$APP_NAME.app" ]; then
|
||||||
|
log_error "Application bundle not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Application bundle built successfully"
|
||||||
|
log_info "Output: $DIST_DIR/$APP_NAME.app"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dmg() {
|
||||||
|
log_info "Creating DMG package..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg"
|
||||||
|
|
||||||
|
# Remove existing DMG
|
||||||
|
if [ -f "$DMG_FILE" ]; then
|
||||||
|
rm -f "$DMG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if create-dmg is available
|
||||||
|
if command -v create-dmg &> /dev/null; then
|
||||||
|
log_info "Using create-dmg for professional DMG..."
|
||||||
|
|
||||||
|
create-dmg \
|
||||||
|
--volname "$DMG_VOLUME_NAME" \
|
||||||
|
--icon-size 128 \
|
||||||
|
--window-size 512 400 \
|
||||||
|
--app-drop-link 380 200 \
|
||||||
|
"$DMG_FILE" \
|
||||||
|
"$DIST_DIR/$APP_NAME.app"
|
||||||
|
else
|
||||||
|
log_warning "create-dmg not found, using hdiutil (less stylish)"
|
||||||
|
log_info "For professional DMG: brew install create-dmg"
|
||||||
|
|
||||||
|
# Create temporary DMG directory structure
|
||||||
|
DMG_TEMP="$TEMP_BUILD/dmg_contents"
|
||||||
|
mkdir -p "$DMG_TEMP"
|
||||||
|
|
||||||
|
# Copy app bundle
|
||||||
|
cp -r "$DIST_DIR/$APP_NAME.app" "$DMG_TEMP/"
|
||||||
|
|
||||||
|
# Create symlink to Applications folder
|
||||||
|
ln -s /Applications "$DMG_TEMP/Applications"
|
||||||
|
|
||||||
|
# Create DMG
|
||||||
|
hdiutil create \
|
||||||
|
-volname "$DMG_VOLUME_NAME" \
|
||||||
|
-srcfolder "$DMG_TEMP" \
|
||||||
|
-ov \
|
||||||
|
-format UDZO \
|
||||||
|
"$DMG_FILE"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
rm -rf "$DMG_TEMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DMG_FILE" ]; then
|
||||||
|
log_error "DMG file not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
SIZE=$(du -h "$DMG_FILE" | cut -f1)
|
||||||
|
log_success "DMG created successfully"
|
||||||
|
log_info "Output: $DMG_FILE (Size: $SIZE)"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_app() {
|
||||||
|
log_info "Signing application..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get signing identity from environment or use ad-hoc
|
||||||
|
SIGNING_ID="${APPLE_SIGNING_ID:--}"
|
||||||
|
|
||||||
|
codesign \
|
||||||
|
--deep \
|
||||||
|
--force \
|
||||||
|
--verify \
|
||||||
|
--verbose \
|
||||||
|
--options=runtime \
|
||||||
|
--sign "$SIGNING_ID" \
|
||||||
|
"$DIST_DIR/$APP_NAME.app"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "Code signing failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Application signed successfully"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
notarize_app() {
|
||||||
|
log_info "Notarizing application..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Requires:
|
||||||
|
# - APPLE_ID environment variable
|
||||||
|
# - APPLE_PASSWORD environment variable (app-specific password)
|
||||||
|
# - APPLE_TEAM_ID environment variable
|
||||||
|
|
||||||
|
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
|
||||||
|
log_error "APPLE_ID and APPLE_PASSWORD environment variables required for notarization"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg"
|
||||||
|
|
||||||
|
# Upload for notarization
|
||||||
|
log_info "Uploading to Apple Notarization Service..."
|
||||||
|
xcrun notarytool submit "$DMG_FILE" \
|
||||||
|
--apple-id "$APPLE_ID" \
|
||||||
|
--password "$APPLE_PASSWORD" \
|
||||||
|
--team-id "${APPLE_TEAM_ID}" \
|
||||||
|
--wait
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "Notarization failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Staple the notarization
|
||||||
|
xcrun stapler staple "$DMG_FILE"
|
||||||
|
|
||||||
|
log_success "Application notarized successfully"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main
|
||||||
321
build/scripts/build_windows.py
Normal file
321
build/scripts/build_windows.py
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
"""Build Windows installer (MSI) using PyInstaller.
|
||||||
|
|
||||||
|
This script builds the WebDrop Bridge application for Windows using PyInstaller.
|
||||||
|
It creates both a standalone executable and optionally an MSI installer.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- PyInstaller 6.0+
|
||||||
|
- Python 3.10+
|
||||||
|
- For MSI: WiX Toolset (optional, requires separate installation)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python build_windows.py [--msi] [--code-sign]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Fix Unicode output on Windows
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsBuilder:
|
||||||
|
"""Build Windows installer using PyInstaller."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize builder paths."""
|
||||||
|
self.project_root = Path(__file__).parent.parent.parent
|
||||||
|
self.build_dir = self.project_root / "build"
|
||||||
|
self.dist_dir = self.build_dir / "dist" / "windows"
|
||||||
|
self.temp_dir = self.build_dir / "temp" / "windows"
|
||||||
|
self.spec_file = self.build_dir / "webdrop_bridge.spec"
|
||||||
|
self.version = self._get_version()
|
||||||
|
|
||||||
|
def _get_version(self) -> str:
|
||||||
|
"""Get version from config.py."""
|
||||||
|
config_file = self.project_root / "src" / "webdrop_bridge" / "config.py"
|
||||||
|
for line in config_file.read_text().split("\n"):
|
||||||
|
if "app_version" in line and "1.0.0" in line:
|
||||||
|
# Extract default version from config
|
||||||
|
return "1.0.0"
|
||||||
|
return "1.0.0"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Clean previous builds."""
|
||||||
|
print("🧹 Cleaning previous builds...")
|
||||||
|
for path in [self.dist_dir, self.temp_dir]:
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
print(f" Removed {path}")
|
||||||
|
|
||||||
|
def build_executable(self) -> bool:
|
||||||
|
"""Build executable using PyInstaller."""
|
||||||
|
print("\n🔨 Building Windows executable with PyInstaller...")
|
||||||
|
|
||||||
|
self.dist_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# PyInstaller command using spec file
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"PyInstaller",
|
||||||
|
"--distpath",
|
||||||
|
str(self.dist_dir),
|
||||||
|
"--workpath",
|
||||||
|
str(self.temp_dir),
|
||||||
|
str(self.spec_file),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f" Command: {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(cmd, cwd=str(self.project_root))
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ PyInstaller build failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
exe_path = self.dist_dir / "WebDropBridge.exe"
|
||||||
|
if not exe_path.exists():
|
||||||
|
print(f"❌ Executable not found at {exe_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Executable built successfully")
|
||||||
|
print(f"📦 Output: {exe_path}")
|
||||||
|
print(f" Size: {exe_path.stat().st_size / 1024 / 1024:.1f} MB")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_msi(self) -> bool:
|
||||||
|
"""Create MSI installer using WiX Toolset.
|
||||||
|
|
||||||
|
This requires WiX Toolset to be installed:
|
||||||
|
https://wixtoolset.org/releases/
|
||||||
|
"""
|
||||||
|
print("\n📦 Creating MSI installer with WiX...")
|
||||||
|
|
||||||
|
# Check if WiX is installed
|
||||||
|
heat_exe = shutil.which("heat.exe")
|
||||||
|
candle_exe = shutil.which("candle.exe")
|
||||||
|
light_exe = shutil.which("light.exe")
|
||||||
|
|
||||||
|
if not all([heat_exe, candle_exe, light_exe]):
|
||||||
|
print("⚠️ WiX Toolset not found in PATH")
|
||||||
|
print(" Install from: https://wixtoolset.org/releases/")
|
||||||
|
print(" Or use: choco install wixtoolset")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create WiX source file
|
||||||
|
if not self._create_wix_source():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Compile and link
|
||||||
|
wix_obj = self.build_dir / "WebDropBridge.wixobj"
|
||||||
|
msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi"
|
||||||
|
|
||||||
|
# Run candle (compiler)
|
||||||
|
candle_cmd = [
|
||||||
|
str(candle_exe),
|
||||||
|
"-o",
|
||||||
|
str(wix_obj),
|
||||||
|
str(self.build_dir / "WebDropBridge.wxs"),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f" Compiling WiX source...")
|
||||||
|
result = subprocess.run(candle_cmd)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ WiX compilation failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Run light (linker)
|
||||||
|
light_cmd = [
|
||||||
|
str(light_exe),
|
||||||
|
"-o",
|
||||||
|
str(msi_output),
|
||||||
|
str(wix_obj),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f" Linking MSI installer...")
|
||||||
|
result = subprocess.run(light_cmd)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ MSI linking failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not msi_output.exists():
|
||||||
|
print(f"❌ MSI not found at {msi_output}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ MSI installer created successfully")
|
||||||
|
print(f"📦 Output: {msi_output}")
|
||||||
|
print(f" Size: {msi_output.stat().st_size / 1024 / 1024:.1f} MB")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _create_wix_source(self) -> bool:
|
||||||
|
"""Create WiX source file for MSI generation."""
|
||||||
|
wix_content = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="{self.version}"
|
||||||
|
Manufacturer="HIM-Tools"
|
||||||
|
UpgradeCode="12345678-1234-1234-1234-123456789012">
|
||||||
|
|
||||||
|
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
|
||||||
|
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
|
||||||
|
|
||||||
|
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
||||||
|
<ComponentRef Id="MainExecutable" />
|
||||||
|
<ComponentRef Id="ProgramMenuShortcut" />
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
|
<Directory Id="ProgramFilesFolder">
|
||||||
|
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
|
||||||
|
</Directory>
|
||||||
|
<Directory Id="ProgramMenuFolder">
|
||||||
|
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<DirectoryRef Id="INSTALLFOLDER">
|
||||||
|
<Component Id="MainExecutable" Guid="*">
|
||||||
|
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\\WebDropBridge.exe" KeyPath="yes"/>
|
||||||
|
</Component>
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||||
|
<Component Id="ProgramMenuShortcut" Guid="*">
|
||||||
|
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||||
|
Name="WebDrop Bridge"
|
||||||
|
Description="Web Drag-and-Drop Bridge"
|
||||||
|
Target="[INSTALLFOLDER]WebDropBridge.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
<RemoveFolder Id="ApplicationProgramsFolderRemove"
|
||||||
|
On="uninstall" />
|
||||||
|
<RegistryValue Root="HKCU"
|
||||||
|
Key="Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\WebDropBridge"
|
||||||
|
Name="installed"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</DirectoryRef>
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
||||||
|
'''
|
||||||
|
|
||||||
|
wix_file = self.build_dir / "WebDropBridge.wxs"
|
||||||
|
wix_file.write_text(wix_content)
|
||||||
|
print(f" Created WiX source: {wix_file}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sign_executable(self, cert_path: str, password: str) -> bool:
|
||||||
|
"""Sign executable with certificate (optional).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cert_path: Path to code signing certificate
|
||||||
|
password: Certificate password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if signing successful
|
||||||
|
"""
|
||||||
|
print("\n🔐 Signing executable...")
|
||||||
|
|
||||||
|
signtool = shutil.which("signtool.exe")
|
||||||
|
if not signtool:
|
||||||
|
print("⚠️ signtool.exe not found (part of Windows SDK)")
|
||||||
|
print(" Skipping code signing")
|
||||||
|
return True
|
||||||
|
|
||||||
|
exe_path = self.dist_dir / "WebDropBridge.exe"
|
||||||
|
cmd = [
|
||||||
|
signtool,
|
||||||
|
"sign",
|
||||||
|
"/f",
|
||||||
|
cert_path,
|
||||||
|
"/p",
|
||||||
|
password,
|
||||||
|
"/t",
|
||||||
|
"http://timestamp.comodoca.com/authenticode",
|
||||||
|
str(exe_path),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ Code signing failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Executable signed successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def build(self, create_msi: bool = False, sign: bool = False) -> bool:
|
||||||
|
"""Run complete build process.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
create_msi: Whether to create MSI installer
|
||||||
|
sign: Whether to sign executable (requires certificate)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if build successful
|
||||||
|
"""
|
||||||
|
start_time = datetime.now()
|
||||||
|
print("=" * 60)
|
||||||
|
print("🚀 WebDrop Bridge Windows Build")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
self.clean()
|
||||||
|
|
||||||
|
if not self.build_executable():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if create_msi:
|
||||||
|
if not self.create_msi():
|
||||||
|
print("⚠️ MSI creation failed, but executable is available")
|
||||||
|
|
||||||
|
if sign:
|
||||||
|
# Would need certificate path from environment
|
||||||
|
cert_path = os.getenv("CODE_SIGN_CERT")
|
||||||
|
if cert_path:
|
||||||
|
self.sign_executable(cert_path, os.getenv("CODE_SIGN_PASSWORD", ""))
|
||||||
|
|
||||||
|
elapsed = (datetime.now() - start_time).total_seconds()
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"✅ Build completed in {elapsed:.1f}s")
|
||||||
|
print("=" * 60)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Build WebDrop Bridge for Windows"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--msi",
|
||||||
|
action="store_true",
|
||||||
|
help="Create MSI installer (requires WiX Toolset)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sign",
|
||||||
|
action="store_true",
|
||||||
|
help="Sign executable (requires CODE_SIGN_CERT environment variable)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
builder = WindowsBuilder()
|
||||||
|
success = builder.build(create_msi=args.msi, sign=args.sign)
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
55
build/scripts/sync_remotes.ps1
Normal file
55
build/scripts/sync_remotes.ps1
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Sync script to keep origin and upstream remotes in sync
|
||||||
|
# Usage: .\sync_remotes.ps1 [--push-to-origin]
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$PushToOrigin
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$repoRoot = Split-Path -Parent (Split-Path -Parent $scriptPath)
|
||||||
|
|
||||||
|
Write-Host "🔄 WebDrop Bridge - Remote Sync Script" -ForegroundColor Cyan
|
||||||
|
Write-Host "Repository: $repoRoot`n" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# Change to repo directory
|
||||||
|
Push-Location $repoRoot
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Fetch from both remotes
|
||||||
|
Write-Host "📥 Fetching from origin..." -ForegroundColor Yellow
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
Write-Host "📥 Fetching from upstream..." -ForegroundColor Yellow
|
||||||
|
git fetch upstream
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
Write-Host "`n📊 Remote Status:" -ForegroundColor Cyan
|
||||||
|
git remote -v
|
||||||
|
|
||||||
|
# Show branch comparison
|
||||||
|
Write-Host "`n📋 Branch Comparison:" -ForegroundColor Cyan
|
||||||
|
Write-Host "Local branches vs origin:" -ForegroundColor Gray
|
||||||
|
git log --oneline origin/main -5 | ForEach-Object { Write-Host " origin: $_" }
|
||||||
|
Write-Host ""
|
||||||
|
git log --oneline upstream/main -5 | ForEach-Object { Write-Host " upstream: $_" }
|
||||||
|
|
||||||
|
# Optionally push to origin
|
||||||
|
if ($PushToOrigin) {
|
||||||
|
Write-Host "`n📤 Pushing current branch to origin..." -ForegroundColor Yellow
|
||||||
|
$currentBranch = git rev-parse --abbrev-ref HEAD
|
||||||
|
git push origin $currentBranch
|
||||||
|
Write-Host "✅ Pushed $currentBranch to origin" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "`n💡 Tip: Use --push-to-origin flag to push current branch to origin" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n✅ Sync complete!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "`n❌ Error: $_" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
47
resources/icons/README.md
Normal file
47
resources/icons/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Icon Files for WebDrop Bridge
|
||||||
|
|
||||||
|
This directory should contain application icon files for the build process.
|
||||||
|
|
||||||
|
## Required Icons
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- **app.ico** - 256x256 or larger, ICO format
|
||||||
|
- Used by PyInstaller and Windows installer
|
||||||
|
- Can contain multiple resolutions (16, 32, 48, 64, 128, 256)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- **app.icns** - Apple icon set
|
||||||
|
- Required for macOS .app bundle
|
||||||
|
- Must include at least: 16, 32, 48, 64, 128, 256, 512, 1024px sizes
|
||||||
|
- Tools: `iconutil` on macOS, or `png2icns`
|
||||||
|
|
||||||
|
## Creating Icons
|
||||||
|
|
||||||
|
### From PNG on macOS:
|
||||||
|
```bash
|
||||||
|
png2icns resources/icons/app.icns resources/icons/app-1024.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Multiple PNGs on macOS:
|
||||||
|
```bash
|
||||||
|
mkdir app.iconset
|
||||||
|
# Add PNG files: 16x16, 32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024
|
||||||
|
iconutil -c icns app.iconset -o resources/icons/app.icns
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating ICO from PNG on Windows:
|
||||||
|
- Use tools like: ImageMagick, GIMP, or online converters
|
||||||
|
- Ensure high quality for professional appearance
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
⚠️ Icon files are currently missing.
|
||||||
|
- The build scripts will work without them (PyInstaller uses default icon)
|
||||||
|
- Replace these files before distribution for professional appearance
|
||||||
|
|
||||||
|
## Design Guidelines
|
||||||
|
|
||||||
|
- Use consistent branding/color scheme
|
||||||
|
- Ensure visibility at small sizes (16x16)
|
||||||
|
- Include transparency for professional look
|
||||||
|
- Test on both light and dark backgrounds (macOS)
|
||||||
|
|
@ -11,6 +11,160 @@ from webdrop_bridge.core.drag_interceptor import DragInterceptor
|
||||||
from webdrop_bridge.core.validator import PathValidator
|
from webdrop_bridge.core.validator import PathValidator
|
||||||
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
|
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
|
||||||
|
|
||||||
|
# Default welcome page HTML when no webapp is configured
|
||||||
|
DEFAULT_WELCOME_PAGE = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebDrop Bridge</title>
|
||||||
|
<style>
|
||||||
|
* {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.container {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 60px 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
h1 {{
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.version {{
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
p {{
|
||||||
|
color: #555;
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features {{
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: left;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features h2 {{
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features ul {{
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features li {{
|
||||||
|
color: #666;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features li:last-child {{
|
||||||
|
border-bottom: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.features li:before {{
|
||||||
|
content: "✓ ";
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.status {{
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #856404;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.status strong {{
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.footer {{
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🌉 WebDrop Bridge</h1>
|
||||||
|
<div class="version">Professional Web-to-File Drag-and-Drop Bridge</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<strong>✓ Application Ready</strong>
|
||||||
|
No web application is currently configured.
|
||||||
|
Configure WEBAPP_URL in your .env file to load your custom application.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>WebDrop Bridge is a professional desktop application that seamlessly converts web-based drag-and-drop interactions into native file operations on Windows and macOS.</p>
|
||||||
|
|
||||||
|
<div class="features">
|
||||||
|
<h2>Key Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Drag-and-drop from web interface to desktop</li>
|
||||||
|
<li>Real-time drag state monitoring</li>
|
||||||
|
<li>Path validation and security controls</li>
|
||||||
|
<li>Cross-platform support (Windows & macOS)</li>
|
||||||
|
<li>Professional production-grade architecture</li>
|
||||||
|
<li>Comprehensive logging and monitoring</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>To configure your web application:</strong></p>
|
||||||
|
<ol style="text-align: left; color: #666; margin-top: 15px;">
|
||||||
|
<li>Create a <code>.env</code> file in your application directory</li>
|
||||||
|
<li>Set <code>WEBAPP_URL</code> to your HTML file path or HTTP URL</li>
|
||||||
|
<li>Example: <code>WEBAPP_URL=file:///./webapp/index.html</code></li>
|
||||||
|
<li>Restart the application</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<strong>Version:</strong> {version}<br>
|
||||||
|
<strong>Status:</strong> Ready for configuration
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
"""Main application window for WebDrop Bridge.
|
"""Main application window for WebDrop Bridge.
|
||||||
|
|
@ -77,6 +231,8 @@ class MainWindow(QMainWindow):
|
||||||
"""Load the web application.
|
"""Load the web application.
|
||||||
|
|
||||||
Loads HTML from the configured webapp URL or from local file.
|
Loads HTML from the configured webapp URL or from local file.
|
||||||
|
Supports both bundled apps (PyInstaller) and development mode.
|
||||||
|
Falls back to default welcome page if webapp not found.
|
||||||
"""
|
"""
|
||||||
webapp_url = self.config.webapp_url
|
webapp_url = self.config.webapp_url
|
||||||
|
|
||||||
|
|
@ -87,12 +243,26 @@ class MainWindow(QMainWindow):
|
||||||
# Local file path
|
# Local file path
|
||||||
try:
|
try:
|
||||||
file_path = Path(webapp_url).resolve()
|
file_path = Path(webapp_url).resolve()
|
||||||
|
|
||||||
|
# If path doesn't exist, try relative to application root
|
||||||
|
# This handles both development and bundled (PyInstaller) modes
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
self.web_view.setHtml(
|
# Try relative to application package root
|
||||||
f"<html><body><h1>Error</h1>"
|
app_root = Path(__file__).parent.parent.parent.parent
|
||||||
f"<p>Web application file not found: {file_path}</p>"
|
relative_path = app_root / webapp_url.lstrip("file:///").lstrip("./")
|
||||||
f"</body></html>"
|
|
||||||
)
|
if relative_path.exists():
|
||||||
|
file_path = relative_path
|
||||||
|
else:
|
||||||
|
# Try without leading "./"
|
||||||
|
alt_path = Path(webapp_url.lstrip("file:///").lstrip("./")).resolve()
|
||||||
|
if alt_path.exists():
|
||||||
|
file_path = alt_path
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
# Show welcome page with instructions
|
||||||
|
welcome_html = DEFAULT_WELCOME_PAGE.format(version=self.config.app_version)
|
||||||
|
self.web_view.setHtml(welcome_html)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Load local file as file:// URL
|
# Load local file as file:// URL
|
||||||
|
|
@ -100,11 +270,9 @@ class MainWindow(QMainWindow):
|
||||||
self.web_view.load(QUrl(file_url))
|
self.web_view.load(QUrl(file_url))
|
||||||
|
|
||||||
except (OSError, ValueError) as e:
|
except (OSError, ValueError) as e:
|
||||||
self.web_view.setHtml(
|
# Show welcome page on error
|
||||||
f"<html><body><h1>Error</h1>"
|
welcome_html = DEFAULT_WELCOME_PAGE.format(version=self.config.app_version)
|
||||||
f"<p>Failed to load web application: {e}</p>"
|
self.web_view.setHtml(welcome_html)
|
||||||
f"</body></html>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _apply_stylesheet(self) -> None:
|
def _apply_stylesheet(self) -> None:
|
||||||
"""Apply application stylesheet if available."""
|
"""Apply application stylesheet if available."""
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ class TestMainWindowWebAppLoading:
|
||||||
|
|
||||||
assert window.web_view is not None
|
assert window.web_view is not None
|
||||||
|
|
||||||
def test_load_nonexistent_file_shows_error(self, qtbot, tmp_path):
|
def test_load_nonexistent_file_shows_welcome_page(self, qtbot, tmp_path):
|
||||||
"""Test loading nonexistent file shows error HTML."""
|
"""Test loading nonexistent file shows welcome page HTML."""
|
||||||
config = Config(
|
config = Config(
|
||||||
app_name="Test",
|
app_name="Test",
|
||||||
app_version="1.0.0",
|
app_version="1.0.0",
|
||||||
|
|
@ -205,10 +205,10 @@ class TestMainWindowWebAppLoading:
|
||||||
window._load_webapp()
|
window._load_webapp()
|
||||||
mock_set_html.assert_called_once()
|
mock_set_html.assert_called_once()
|
||||||
|
|
||||||
# Verify error message
|
# Verify welcome page is shown instead of error
|
||||||
call_args = mock_set_html.call_args[0][0]
|
call_args = mock_set_html.call_args[0][0]
|
||||||
assert "Error" in call_args
|
assert "WebDrop Bridge" in call_args
|
||||||
assert "not found" in call_args
|
assert "Application Ready" in call_args
|
||||||
|
|
||||||
|
|
||||||
class TestMainWindowDragIntegration:
|
class TestMainWindowDragIntegration:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue