diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3a61b95..7cfac82 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -104,3 +104,97 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ publish-linux-appimage:
+ name: Linux AppImage x86_64
+ needs: publish-tauri
+ permissions:
+ contents: write
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Проверка исходного кода
+ uses: actions/checkout@v4
+
+ - name: Установка системных зависимостей
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \
+ patchelf libmpv-dev mpv ffmpeg mediainfo libfuse2 curl git cmake ninja-build \
+ meson python3-dev cython3 nasm libvulkan-dev glslang-tools libavcodec-dev \
+ libavformat-dev libavutil-dev libswresample-dev libswscale-dev libass-dev \
+ libplacebo-dev libpipewire-0.3-dev libpulse-dev libx11-dev libwayland-dev \
+ wayland-protocols libegl1-mesa-dev libzimg-dev
+
+ - name: Настройка Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Настройка Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Кэширование сборок Rust
+ uses: swatinem/rust-cache@v2
+ with:
+ workspaces: './src-tauri -> target'
+
+ - name: Синхронизация версии
+ shell: bash
+ run: |
+ tag='${{ github.ref_name }}'
+ version="${tag#v}"
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ version="$(node -p "require('./package.json').version")"
+ fi
+ npm version "$version" --no-git-tag-version
+ sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$version\"/" src-tauri/Cargo.toml
+ echo "VERSION=$version" >> "$GITHUB_ENV"
+
+ - name: Сборка самодостаточного AppImage
+ run: bash scripts/build-linux-appimage.sh
+
+ - name: Публикация AppImage
+ run: |
+ tag='${{ github.ref_name }}'
+ [[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION"
+ gh release upload "$tag" dist-linux/*.AppImage --clobber
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ publish-arch-linux:
+ name: Arch Linux package x86_64
+ needs: publish-tauri
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ container:
+ image: archlinux:base-devel
+ steps:
+ - name: Установка инструментов CI
+ run: pacman -Syu --noconfirm git github-cli nodejs npm rust webkit2gtk-4.1 gtk3 ffmpeg mediainfo cmake ninja meson python cython vulkan-headers glslang libplacebo libass zimg patchelf
+
+ - name: Проверка исходного кода
+ uses: actions/checkout@v4
+
+ - name: Сборка pacman-пакета
+ shell: bash
+ run: |
+ tag='${{ github.ref_name }}'
+ version="${tag#v}"
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ version="$(node -p "require('./package.json').version")"
+ fi
+ sed -i "s/^pkgver=.*/pkgver=$version/" packaging/arch/PKGBUILD
+ useradd -m builder
+ chown -R builder:builder "$GITHUB_WORKSPACE"
+ su builder -c "cd '$GITHUB_WORKSPACE/packaging/arch' && makepkg --printsrcinfo > .SRCINFO && makepkg --noconfirm --cleanbuild --nodeps"
+ echo "VERSION=$version" >> "$GITHUB_ENV"
+
+ - name: Публикация Arch Linux пакета и PKGBUILD
+ shell: bash
+ run: |
+ tag='${{ github.ref_name }}'
+ [[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION"
+ gh release upload "$tag" packaging/arch/*.pkg.tar.zst packaging/arch/PKGBUILD packaging/arch/.SRCINFO packaging/arch/l-mpv.desktop --clobber
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index dcc86e4..ec807c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,9 @@ Portable-L-MPV*
*.tar.gz
*.rar
scratch/
+/dist-linux/
+/packaging/arch/*.pkg.tar.zst
+/.runtime/
@@ -98,4 +101,3 @@ scratch/
.agents/rules/
agents/rules/
.codebase-memory/*
-
diff --git a/README.md b/README.md
index 26247bf..f0600fc 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@
🎬 L-MPV — Modern & Portable Media Player
- Высокопроизводительный, эстетичный и портативный медиаплеер нового поколения.
- Построен на базе Tauri v2 , React 19 , Direct3D 11 и нативного движка libmpv (C-FFI) с поддержкой Real-Time 4K AI Upscaling .
+ Высокопроизводительный медиаплеер для Windows и Linux с нативным выводом видео.
+ Построен на базе Tauri v2 , React 19 и libmpv с кроссплатформенным Real-Time 4K AI Upscaling .
@@ -16,8 +16,8 @@
-
-
+
+
@@ -26,9 +26,39 @@
## 🌟 О проекте
-**L-MPV** — это современный настольный медиаплеер для Windows, объединяющий эталонное качество воспроизведения нативного видеодвижка **MPV** (`vo=gpu-next`, Direct3D 11, HDR, WASAPI, студийный 32-tap sinc-ресемплинг), аппаратный **Real-Time 4K AI Апскейлинг** ( ONNX инференс через DirectML и TensorRT) с утонченным, отзывчивым интерфейсом в стиле **Glassmorphism**, созданным на **React 19** и **TypeScript**.
-
-Плеер спроектирован по строгой концепции **True Portable Architecture (Zero-Install)**: он полностью автономен, не привязан к системному реестру Windows и не создает мусор в системных каталогах пользователя. Все конфигурации, кэш, скриншоты, универсальная папка ONNX-моделей (`models/onnx/`) и нативные бинарные библиотеки (`libmpv-2.dll`, `ffmpeg.exe`, `mediainfo.dll`) расположены непосредственно в каталоге приложения.
+**L-MPV** — современный настольный медиаплеер для Windows 10/11 и Linux x86_64. Windows использует прямое встраивание libmpv в HWND, Direct3D 11 и ONNX-инференс через DirectML/TensorRT. Linux выводит видео через libmpv render API в GTK `GLArea` на нативных Wayland и X11, а AI-апскейлинг выполняет через NCNN/Vulkan на NVIDIA, AMD и Intel.
+
+Windows-редакция сохраняет концепцию **True Portable Architecture (Zero-Install)**: настройки, скриншоты, ONNX-модели и нативные библиотеки расположены рядом с приложением. Linux следует XDG Base Directory, хранит пользовательские данные в `$XDG_CONFIG_HOME/l-mpv` (по умолчанию `~/.config/l-mpv`) и поставляется как AppImage либо пакет Arch Linux.
+
+## 🖥️ Совместимость платформ
+
+| Возможность | Windows 10/11 x64 | Linux x86_64 |
+| :--- | :---: | :---: |
+| Основное воспроизведение через libmpv | ✅ `libmpv-2.dll` | ✅ `libmpv.so` |
+| Графическая сессия | ✅ Win32 / DWM | ✅ нативные Wayland и X11 |
+| Нативный Wayland | — | ✅ GTK `GLArea` + libmpv render API |
+| Аппаратное декодирование | ✅ D3D11VA / DXVA2 | ✅ VA-API / NVDEC — через mpv |
+| GPU-рендеринг | ✅ D3D11 | ✅ OpenGL/EGL; Vulkan compute для NCNN |
+| HDR и tone mapping | ✅ | ✅ Зависит от mpv, драйвера и окружения рабочего стола |
+| AI-апскейлинг ONNX | ✅ | ❌ |
+| DirectML | ✅ AMD / Intel / NVIDIA | ❌ Windows-only |
+| TensorRT | ✅ NVIDIA | ❌ Текущая реализация Windows-only |
+| NCNN / Vulkan AI | ❌ | ✅ NVIDIA / AMD / Intel, модели `.param` + `.bin` |
+| Авто-fallback AI | ❌ | ✅ FSRCNNX / Anime4K при пропусках кадров |
+| Шейдерный Ambient Light | ✅ | ✅ |
+| WASAPI-аудиовизуализатор | ✅ | ❌ Windows-only |
+| Скриншоты и копирование кадра | ✅ | ✅ |
+| Извлечение дорожек через FFmpeg | ✅ Встроенный `ffmpeg.exe` | ✅ Встроенный в AppImage или системный `ffmpeg` |
+| Подробный отчёт MediaInfo | ✅ `mediainfo.dll` | ✅ MediaInfo CLI |
+| Плейлисты, главы и внешние дорожки | ✅ | ✅ |
+| Несколько экземпляров приложения | ✅ | ✅ |
+| Ассоциации файлов из настроек приложения | ✅ Реестр Windows | ❌ |
+| Desktop-файл и MIME-типы | ❌ | ✅ При установке Arch-пакета |
+| Встроенное portable-автообновление | ✅ | ❌ Обновление через релиз или пакетный менеджер |
+| Расположение настроек | Каталог приложения | `$XDG_CONFIG_HOME/l-mpv` или `~/.config/l-mpv` |
+| Форматы сборок | NSIS, portable ZIP, `.exe` | AppImage, Arch `pkg.tar.zst`, нативный бинарник |
+
+> **Примечание:** Linux использует libmpv render API, а не X11 Window ID, поэтому работает без XWayland. AI-фильтр принимает заранее конвертированные пары NCNN `.param`/`.bin`; ONNX, DirectML и TensorRT остаются Windows-функциями.
@@ -97,25 +127,28 @@
🚀 Real-Time 4K AI Upscaling (Нейросетевой Апскейлинг Аниме и Видео)
-- **Нативный инференс в видеопотоке:** интеграция фильтра `vf_animejanai` на базе `the-database/mpv-AnimeJaNai` и моста `aji.dll` с аппаратным выводом кадров в 4K Ultra HD в реальном времени.
+- **Нативный инференс в видеопотоке:** Windows использует `vf_animejanai`/`aji.dll`; Linux — приватный mpv/VapourSynth runtime и NCNN через Vulkan.
- **Два режима работы:** переключатель **«Выкл»** и **«AI Upscaling»** в Настройках (`F2`) — пользователь сам решает, когда задействовать искусственный интеллект.
-- **Два движка инференса (Backend):**
+- **Windows-движки инференса (Backend):**
- **DirectML:** универсальный инференс через DirectX 12 для любых видеокарт (AMD Radeon, Intel Arc/Iris, NVIDIA GeForce) с максимальной стабильностью.
- **TensorRT:** максимальная скорость и частота кадров для видеокарт NVIDIA GeForce RTX (поддержка архитектур от `sm75` до новейшей Blackwell `sm120`).
-- **Умный автозагрузчик компонентов («Скачать движок»):**
+- **Linux backend — NCNN Vulkan:** заранее конвертированные пары `.param` + `.bin`, работающие на NVIDIA, AMD и Intel; при недостаточной скорости автоматически включаются FSRCNNX/Anime4K.
+- **Умный автозагрузчик компонентов Windows («Скачать движок»):**
- Автоматическое определение установленного GPU и скачивание точного набора библиотек.
- Потоковая индикация процентов, объема (МБ), аккуратный статус распаковки и фиксация 100% готовности в течение 3 секунд.
- Неблокирующая фоновая распаковка через `spawn_blocking` без зависаний UI и без всплывающих консольных окон.
-- **Предварительная компиляция TensorRT 1080p (.engine):**
+- **Предварительная компиляция TensorRT 1080p (.engine, Windows):**
- Фоновая оптимизация ONNX моделей под разрешение 1080p -> 4K с многоступенчатым отслеживанием фаз (разбор ONNX, тактики CUDA, оптимизация графа, сериализация).
- Компиляция через нативный `aji_harness.exe` с точным соответствием динамических осей `dyn-HW` и оптимизационных параметров.
- Устранение задержек при первом включении видео и поддержка перекомпиляции в один клик.
-- **Автоматическая FP16-нормализация и поддержка моделей всех поколений (V1, V2, V3):**
+- **Автоматическая FP16-нормализация ONNX (Windows):**
- Встроенный алгоритм аппаратной валидации и автоконвертации типов ONNX: модели в формате FP32 автоматически приводятся к стандарту IEEE Float16 перед компиляцией.
- Полное устранение артефактов «радужного шума» и расхождения памяти шейдера: идеальная четкость и чистота картинки как на компактных сетях V2, так и на тяжелых моделях V1 и HD V3 / V3Sharp1.
-- **Универсальная библиотека ONNX (`models/onnx/`):**
- - Возможность использования любых сторонних моделей формата `.onnx`.
- - Кнопка **«Папка моделей»** для быстрого перехода в Проводник Windows.
+- **Библиотека моделей:**
+ - Windows: ONNX в `models/onnx/`.
+ - Linux: NCNN `.param` + `.bin` в `models/ncnn/`, включая загрузку проверенной AnimeJaNai V2 из интерфейса.
+ - Windows принимает сторонние модели `.onnx`; Linux требует заранее конвертированную совместимую пару `.param` + `.bin` с масштабом 2×.
+ - Кнопка **«Папка моделей»** открывает каталог в Проводнике Windows или через `xdg-open` в Linux.
- Переключатель скрытия/отображения названий моделей (маскировка точками).
- **Переключение нейросетей на лету по горячим клавишам:**
- `Ctrl+J` — Информационный OSD-оверлей статуса и статистики апскейлинга (активный движок, выбранная модель, слот, разрешение видео -> 4K);
@@ -129,13 +162,13 @@
-⚡ Рендеринг Нового Поколения (vo=gpu-next)
+⚡ Нативный GPU-рендеринг через libmpv
-- **GPU-HQ пайплайн:** профиль `profile=gpu-hq`, нативный Direct3D 11 (`gpu-api=d3d11`) и аппаратное декодирование `hwdec=auto-safe`.
+- **GPU-HQ пайплайн:** Direct3D 11 в Windows; OpenGL/EGL через libmpv render API в Linux; аппаратное декодирование `hwdec=auto-safe` вне AI-фильтра.
- **Прецизионное масштабирование:** алгоритмы интерполяции `scale=spline36` и `cscale=spline36` для идеальной четкости деталей и цветовых переходов.
- **Интеллектуальный HDR:** автоматическая передача метаданных в дисплей (`target-colorspace-hint=yes`), динамический расчет пиков яркости (`hdr-compute-peak=yes`) и адаптивный tone mapping.
- **Zero-Flicker жизненный цикл:** окно создается в скрытом режиме, рассчитывает истинный Display Aspect Ratio видео, центрируется и отображается (`window.show()`) строго тогда, когда интерфейс и первый видеокадр готовы к показу, исключая любые рывки геометрии и мерцания.
-- **Прямой вывод в окно:** видеопоток отрисовывается в нативный Win32 HWND через C-FFI с нулевой задержкой под полностью прозрачным DOM-слоем WebView2 (`transparent: true`).
+- **Прямой вывод в окно:** Win32 HWND/WebView2 в Windows; GTK `GLArea` внутри `GtkOverlay` в нативных Wayland и X11 в Linux.
@@ -168,7 +201,7 @@
🎧 Студийный Аудиофильский Звук и Усиление до 150% (Audiophile Profile)
-- **Низколатентный вывод:** нативный драйвер Windows WASAPI (`ao=wasapi`) с оптимизированным буфером 0.2 с.
+- **Низколатентный вывод:** Windows использует WASAPI (`ao=wasapi`), Linux выбирает PipeWire, PulseAudio или ALSA; аудиобуфер настроен на 0.2 с.
- **Софтверный буст громкости (до 150%):** возможность усиления тихих аудиодорожек до 150% без искажений и клиппинга (`volume-max=150.0`).
- **Студийный 32-точечный sinc-ресемплинг:** фильтр `audio-resample-filter-size=32`, 16 384 фазы сдвига (`audio-resample-phase-shift=14`) и линейная интерполяция между отсчетами (`audio-resample-linear=yes`).
- **Нормализованный даунмикс:** автоматическое безопасное сведение многоканального аудио 5.1/7.1 в стерео (`audio-normalize-downmix=yes`) с аппаратной защитой от перегрузок и клиппинга.
@@ -307,37 +340,37 @@
Backend & Shell
Rust (2021 edition), Tauri v2, Tokio
- Низкоуровневая интеграция с Win32 API, многопоточный IPC-мост, управление окнами и DWM
+ IPC-мост, управление окнами, Win32/DWM в Windows и GTK/WebKitGTK в Linux
Media Engine
- libmpv-2.dll (сборка the-database/mpv-winbuild) via dynamic FFI (libloading)
- Аппаратный рендеринг vo=gpu-next, Direct3D 11, HDR tone-mapping, demuxing
+ libmpv-2.dll в Windows; приватный libmpv.so.2 и render API в Linux
+ Direct3D 11/HWND либо OpenGL/EGL/GTK GLArea, HDR tone mapping и demuxing
AI Upscaling
- vf_animejanai + aji.dll (DirectML / TensorRT) + ONNX
- Аппаратный апскейлинг видео в реальном времени до 4K на любых GPU
+ DirectML/TensorRT + ONNX в Windows; VapourSynth + NCNN/Vulkan в Linux
+ Аппаратный апскейлинг 2× и автоматический fallback на FSRCNNX/Anime4K в Linux
MediaInfo Engine
- mediainfo.dll via dynamic C-FFI
+ mediainfo.dll через C-FFI либо MediaInfo CLI
Извлечение исчерпывающего технического отчёта о видеоконтейнере и потоках данных
Audio Engine
- Windows WASAPI, 32-tap Sinc Resampler, Scaletempo2
+ WASAPI либо PipeWire/PulseAudio/ALSA, 32-tap Sinc Resampler, Scaletempo2
Студийный 32-точечный ресемплинг, безопасный даунмикс 5.1/7.1 в стерео, pitch correction
Track Extraction
- Встроенный FFmpeg (ffmpeg.exe)
+ Встроенный FFmpeg (ffmpeg.exe или AppImage ffmpeg)
Прямой экспорт потоков аудио/субтитров (-c copy) и интеллектуальный fallback-транскодинг
Платформа
- Windows 10 / 11 x64
- Аппаратное ускорение DXVA2/D3D11VA, Windows Explorer Context Menu API, Taskbar API
+ Windows 10/11 x64, Linux x86_64 (Wayland/X11)
+ D3D11VA/DXVA2 либо VA-API/NVDEC; платформенные интеграции включаются условно
@@ -355,11 +388,11 @@ L-MPV/
│ │ ├── PlayerControls.tsx # Плавающая панель управления (кнопки, громкость, треки, скорость)
│ │ ├── ContextMenu.tsx # Кастомное ПКМ-меню (масштаб, пропорции, поворот, дорожки, подсветка)
│ │ ├── SettingsModal.tsx # Настройки (скриншоты, цвета, подсветка полос, апскейлинг, хоткеи)
-│ │ ├── UpscalingSettingsSection.tsx # Управление 4K AI апскейлингом и библиотекой ONNX-моделей
+│ │ ├── UpscalingSettingsSection.tsx # Управление AI-апскейлингом и библиотекой ONNX/NCNN-моделей
│ │ ├── upscale/ # Модульные подкомпоненты апскейлинга:
│ │ │ ├── types.ts # Модели данных и событий прогресса
│ │ │ ├── GpuHardwareCard.tsx # Информационная карточка обнаруженного GPU (VRAM, архитектура, рекомендации)
-│ │ │ └── BackendSelector.tsx # Селектор DirectML / TensorRT, скачивание движков, удаление, индикация прогресса
+│ │ │ └── BackendSelector.tsx # DirectML / TensorRT / NCNN Vulkan и состояние runtime
│ │ ├── MediaInfoModal.tsx # Компактное окно технической информации о медиафайле
│ │ ├── StandaloneMediaInfoWindow.tsx # Автономное окно просмотра MediaInfo из проводника
│ │ ├── ChaptersModal.tsx # Модальное окно навигации по главам видео
@@ -397,24 +430,31 @@ L-MPV/
├── src-tauri/ # Бэкенд (Rust + Tauri v2)
│ ├── capabilities/default.json # Манифест разрешений Tauri v2
│ ├── src/
-│ │ ├── main.rs # Точка входа приложения
-│ │ ├── lib.rs # Инициализация Tauri, HWND-привязка, фокус и реестр 103 IPC-команд
+│ │ ├── main.rs # Точка входа и настройка приватного Linux runtime
+│ │ ├── lib.rs # Инициализация Tauri, HWND либо GTK GLArea и IPC-команды
│ │ ├── upscale/ # Модульная подсистема 4K AI апскейлинга:
│ │ │ ├── mod.rs # Фасад подсистемы, IPC-команды, unit-тесты
│ │ │ ├── types.rs # Модели данных: ModelFileItem, UpscaleSettings, GpuHardwareInfo, Progress
-│ │ │ ├── hardware.rs # Диагностика GPU через Win32 DXGI, определение SM-архитектуры NVIDIA
-│ │ │ ├── config.rs # Разрешение путей, окружение DLL PATH, сканирование models/onnx/, upscale.conf
+│ │ │ ├── hardware.rs # Диагностика GPU через DXGI либо vulkaninfo/lspci
+│ │ │ ├── config.rs # Пути и сканирование models/onnx либо models/ncnn
│ │ │ ├── downloader.rs # Асинхронная потоковая загрузка DirectML/TensorRT, распаковка архивов
-│ │ │ └── controller.rs # Управление libmpv фильтром, фоновая компиляция TensorRT .engine, хоткеи
+│ │ │ ├── controller.rs # Платформенная маршрутизация AI-фильтров и хоткеи
+│ │ │ └── linux.rs # VapourSynth/NCNN, загрузка модели и watchdog fallback
│ │ ├── ambient.rs # Контроллер подсветки черных полос (GPU Blur / Color / Off)
│ │ ├── audio_capture.rs # Нативный захват звука WASAPI Loopback, быстрый БПФ (FFT Radix-2), 32 полосы
-│ │ ├── mediainfo.rs # FFI-интеграция с mediainfo.dll и управление автономным окном
-│ │ ├── mpv_manager.rs # FFI-обертка libmpv (vo=gpu-next, WASAPI, D3D11, vf_animejanai, sinc-фильтр)
+│ │ ├── audio_capture_stub.rs # Linux-заглушка Windows-only WASAPI-визуализатора
+│ │ ├── mediainfo.rs # mediainfo.dll в Windows и MediaInfo CLI в Linux
+│ │ ├── mpv_manager.rs # Динамический libmpv API, HWND и OpenGL render context
│ │ ├── system_integration.rs # Интеграция с Проводником Windows (контекстное меню, ассоциации файлов)
│ │ ├── updater.rs # Модуль фонового и ручного обновления
│ │ └── commands.rs # IPC #[tauri::command] обработчики, экспорт дорожек через FFmpeg
│ ├── Cargo.toml # Зависимости бэкенда Rust
-│ └── tauri.conf.json # Конфигурация Tauri v2
+│ ├── tauri.conf.json # Основная конфигурация Tauri v2
+│ └── tauri.linux.conf.json # Linux-конфигурация окна и AppImage
+├── native/linux-ncnn/ # VapourSynth API 4 плагин NCNN/Vulkan
+├── scripts/ # Сборка приватного runtime и AppImage
+├── packaging/arch/ # PKGBUILD, .SRCINFO и desktop-файл
+├── vendor/tauri-runtime-wry/ # GTK-патч иерархии GtkOverlay/GLArea
├── models/ # Каталог нейросетей
│ └── onnx/ # Универсальная папка для размещения ONNX-моделей
├── inference/ # Папка библиотек инференса (aji.dll, DirectML, TensorRT)
@@ -478,9 +518,10 @@ L-MPV/
## ⚡ IPC-Архитектура (Rust ↔ React)
-Связь интерфейса React с движком MPV, подсистемой апскейлинга и системными модулями осуществляется через **91 нативную IPC-команду**, гарантирующую мгновенный отклик и отсутствие задержек:
+Связь интерфейса React с движком MPV, подсистемой апскейлинга и системными модулями осуществляется через нативные IPC-команды Tauri:
-- **Апскейлинг и AI Модели (6 команд):** `get_upscale_status`, `scan_onnx_models`, `open_models_folder`, `apply_upscale_settings`, `download_recommended_models`, `switch_upscale_network_hotkey`.
+- **Апскейлинг и AI-модели:** `get_upscale_status`, `scan_onnx_models`, `open_models_folder`, `apply_upscale_settings`, `download_recommended_models`, `download_curated_ncnn_model`, `switch_upscale_network_hotkey`.
+- **Определение платформы:** `get_runtime_platform` адаптирует интерфейс без изменения поведения Windows-сборки.
- **Воспроизведение и Плейлист (16 команд):** `open_file`, `toggle_pause`, `set_pause`, `seek`, `seek_absolute`, `frame_step`, `frame_back_step`, `playlist_prev`, `playlist_next`, `get_playlist`, `play_playlist_item`, `set_loop_file`, `set_loop_playlist`, `toggle_shuffle`, `get_play_next_on_end`, `set_play_next_on_end`.
- **Громкость и Скорость (2 команды):** `set_volume`, `set_speed`.
- **Дорожки и FFmpeg Извлечение (11 команд):** `get_tracks`, `set_audio_track`, `set_subtitle_track`, `disable_subtitles`, `load_subtitle_file`, `load_audio_file`, `set_video_track`, `extract_track`, `get_auto_load_tracks`, `set_auto_load_tracks`, `load_external_tracks_for_file`.
@@ -500,9 +541,74 @@ L-MPV/
### Требования к окружению
- **Node.js** v20+ и менеджер пакетов **npm**
-- **Rust** (toolchain `stable-x86_64-pc-windows-msvc`)
-- Нативная библиотека `libmpv-2.dll` из релиза `the-database/mpv-winbuild` (с фильтром `vf_animejanai`)
-- Утилита `ffmpeg.exe` для прямого экспорта дорожек и `mediainfo.dll` для детального анализа
+- **Rust stable** (`x86_64-pc-windows-msvc` или `x86_64-unknown-linux-gnu`)
+- Windows: `libmpv-2.dll`, `ffmpeg.exe` и `mediainfo.dll`
+- Linux: GTK 3, WebKitGTK 4.1, FFmpeg, MediaInfo и Vulkan-драйвер GPU
+
+### Linux
+
+В релизах публикуются два варианта для `x86_64`:
+
+- самодостаточный `AppImage` с приватными libmpv 0.41, VapourSynth R79, NCNN/Vulkan, Python runtime, FFmpeg, MediaInfo и fallback-шейдерами;
+- пакет Arch Linux `pkg.tar.zst` с тем же приватным runtime в `/usr/lib/l-mpv` и desktop/MIME-интеграцией.
+
+AppImage включает пользовательские библиотеки, но не поставляет драйвер GPU: в системе должен быть установлен рабочий Vulkan ICD для NVIDIA, AMD или Intel. Воспроизведение работает нативно в Wayland и X11 через GTK `GLArea` и libmpv render API, без обязательного XWayland.
+
+#### Установка и запуск
+
+```bash
+# AppImage
+chmod +x L-MPV-*-linux-x86_64.AppImage
+./L-MPV-*-linux-x86_64.AppImage
+
+# Arch Linux
+sudo pacman -U l-mpv-*-x86_64.pkg.tar.zst
+```
+
+Пользовательские настройки, история, скриншоты и модели находятся в
+`$XDG_CONFIG_HOME/l-mpv` или `~/.config/l-mpv`. Linux-версия не изменяет системный
+реестр и не использует portable-настройки из каталога исполняемого файла.
+
+#### NCNN/Vulkan AI
+
+1. Откройте **Настройки → Апскейлинг**.
+2. Нажмите **AnimeJaNai V2**, чтобы скачать проверенную модель с валидацией SHA-256, либо поместите собственную совместимую пару `имя.param` + `имя.bin` в `~/.config/l-mpv/models/ncnn/`.
+3. Выберите модель и включите **AI Upscaling**. Модель должна принимать RGB float и выдавать изображение с масштабом 2×.
+
+Во время работы VapourSynth передаёт кадры через системную память, поэтому аппаратное
+декодирование временно отключается. Watchdog отслеживает счётчики пропущенных кадров;
+если NCNN не справляется с воспроизведением, фильтр автоматически снимается и включаются
+пакетные FSRCNNX/Anime4K-шейдеры с уведомлением в OSD. DirectML, TensorRT и
+WASAPI-визуализатор остаются Windows-only.
+
+#### Сборка из исходников
+
+Версии ключевых компонентов закреплены в `scripts/build-linux-runtime.sh`: mpv 0.41,
+VapourSynth R79, NCNN и GLSL-шейдеры собираются из фиксированных ревизий.
+
+Сборка приватного runtime:
+
+```bash
+bash scripts/build-linux-runtime.sh
+```
+
+Сборка готового AppImage в `dist-linux/`:
+
+```bash
+bash scripts/build-linux-appimage.sh
+```
+
+Сборка пакета Arch Linux:
+
+```bash
+cd packaging/arch
+makepkg -s
+```
+
+Для разработки необходимы GTK 3, WebKitGTK 4.1, OpenGL/EGL и Vulkan headers/loader.
+Полная сборка runtime дополнительно использует CMake, Ninja, Meson, Python/Cython,
+FFmpeg development libraries, libplacebo, libass и zimg. Актуальный список пакетов
+для Ubuntu 22.04 и Arch Linux приведён в `.github/workflows/release.yml`.
### Режим разработки (Development)
```bash
@@ -557,4 +663,3 @@ Copy-Item -Path "src-tauri/target/release/l-mpv.exe" -Destination "Portable-L-MP
## 📄 Лицензия
Проект распространяется под свободной лицензией **MIT**. Подробная информация доступна в файле [LICENSE](./LICENSE).
-
diff --git a/native/linux-ncnn/CMakeLists.txt b/native/linux-ncnn/CMakeLists.txt
new file mode 100644
index 0000000..20233f5
--- /dev/null
+++ b/native/linux-ncnn/CMakeLists.txt
@@ -0,0 +1,20 @@
+cmake_minimum_required(VERSION 3.20)
+project(lmpv_ncnn LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+find_package(ncnn CONFIG REQUIRED)
+find_path(VAPOURSYNTH_INCLUDE_DIR VapourSynth4.h PATH_SUFFIXES vapoursynth REQUIRED)
+
+add_library(lmpv_ncnn SHARED lmpv_ncnn.cpp)
+target_include_directories(lmpv_ncnn PRIVATE "${VAPOURSYNTH_INCLUDE_DIR}")
+target_link_libraries(lmpv_ncnn PRIVATE ncnn)
+set_target_properties(lmpv_ncnn PROPERTIES
+ PREFIX "lib"
+ OUTPUT_NAME "lmpv_ncnn"
+ INSTALL_RPATH "$ORIGIN/../lib"
+)
+
+install(TARGETS lmpv_ncnn LIBRARY DESTINATION vapoursynth)
diff --git a/native/linux-ncnn/lmpv_ncnn.cpp b/native/linux-ncnn/lmpv_ncnn.cpp
new file mode 100644
index 0000000..c5ef5e4
--- /dev/null
+++ b/native/linux-ncnn/lmpv_ncnn.cpp
@@ -0,0 +1,179 @@
+// L-MPV NCNN/Vulkan VapourSynth filter.
+// Accepts planar RGB float frames and preconverted NCNN .param/.bin models.
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+struct FilterData {
+ VSNode *node{};
+ VSVideoInfo vi{};
+ ncnn::Net net;
+ int input_index{};
+ int output_index{};
+ int scale{2};
+ int tile_size{256};
+ int overlap{16};
+};
+
+static void set_filter_error(VSFrameContext *ctx, const VSAPI *vsapi, const std::string &message) {
+ vsapi->setFilterError(("L-MPV NCNN: " + message).c_str(), ctx);
+}
+
+static const VSFrame *VS_CC get_frame(int n, int activation_reason, void *instance_data,
+ void **, VSFrameContext *frame_ctx, VSCore *core,
+ const VSAPI *vsapi) {
+ auto *d = static_cast(instance_data);
+ if (activation_reason == arInitial) {
+ vsapi->requestFrameFilter(n, d->node, frame_ctx);
+ return nullptr;
+ }
+ if (activation_reason != arAllFramesReady)
+ return nullptr;
+
+ const VSFrame *src = vsapi->getFrameFilter(n, d->node, frame_ctx);
+ const int src_w = vsapi->getFrameWidth(src, 0);
+ const int src_h = vsapi->getFrameHeight(src, 0);
+ const int dst_w = src_w * d->scale;
+ const int dst_h = src_h * d->scale;
+ VSFrame *dst = vsapi->newVideoFrame(&d->vi.format, dst_w, dst_h, src, core);
+
+ const int tile = std::max(32, d->tile_size);
+ const int overlap = std::clamp(d->overlap, 0, tile / 3);
+ const int step = std::max(1, tile - overlap * 2);
+
+ for (int top = 0;; top = std::min(top + step, std::max(0, src_h - tile))) {
+ const int tile_h = std::min(tile, src_h - top);
+ for (int left = 0;; left = std::min(left + step, std::max(0, src_w - tile))) {
+ const int tile_w = std::min(tile, src_w - left);
+ ncnn::Mat input(tile_w, tile_h, 3, sizeof(float), 1);
+ for (int plane = 0; plane < 3; ++plane) {
+ const auto *source = reinterpret_cast(vsapi->getReadPtr(src, plane));
+ const ptrdiff_t stride = vsapi->getStride(src, plane) / sizeof(float);
+ float *target = input.channel(plane);
+ for (int y = 0; y < tile_h; ++y)
+ std::copy_n(source + (top + y) * stride + left, tile_w, target + y * tile_w);
+ }
+
+ ncnn::Extractor extractor = d->net.create_extractor();
+ if (extractor.input(d->input_index, input) != 0) {
+ vsapi->freeFrame(src);
+ vsapi->freeFrame(dst);
+ set_filter_error(frame_ctx, vsapi, "failed to upload an input tile");
+ return nullptr;
+ }
+ ncnn::Mat output;
+ if (extractor.extract(d->output_index, output) != 0 || output.empty()) {
+ vsapi->freeFrame(src);
+ vsapi->freeFrame(dst);
+ set_filter_error(frame_ctx, vsapi, "Vulkan inference failed");
+ return nullptr;
+ }
+ if (output.w != tile_w * d->scale || output.h != tile_h * d->scale || output.c < 3) {
+ vsapi->freeFrame(src);
+ vsapi->freeFrame(dst);
+ set_filter_error(frame_ctx, vsapi, "model output shape does not match the selected 2x scale");
+ return nullptr;
+ }
+
+ const int crop_l = left == 0 ? 0 : overlap * d->scale;
+ const int crop_t = top == 0 ? 0 : overlap * d->scale;
+ const int crop_r = left + tile_w == src_w ? 0 : overlap * d->scale;
+ const int crop_b = top + tile_h == src_h ? 0 : overlap * d->scale;
+ for (int plane = 0; plane < 3; ++plane) {
+ auto *target = reinterpret_cast(vsapi->getWritePtr(dst, plane));
+ const ptrdiff_t stride = vsapi->getStride(dst, plane) / sizeof(float);
+ const float *source = output.channel(plane);
+ for (int y = crop_t; y < output.h - crop_b; ++y) {
+ std::copy_n(source + y * output.w + crop_l, output.w - crop_l - crop_r,
+ target + (top * d->scale + y) * stride + left * d->scale + crop_l);
+ }
+ }
+
+ if (left + tile_w == src_w)
+ break;
+ }
+ if (top + tile_h == src_h)
+ break;
+ }
+
+ vsapi->freeFrame(src);
+ return dst;
+}
+
+static void VS_CC free_filter(void *instance_data, VSCore *, const VSAPI *vsapi) {
+ auto *d = static_cast(instance_data);
+ vsapi->freeNode(d->node);
+ delete d;
+}
+
+static void VS_CC create_filter(const VSMap *in, VSMap *out, void *, VSCore *core,
+ const VSAPI *vsapi) {
+ auto d = std::make_unique();
+ d->node = vsapi->mapGetNode(in, "clip", 0, nullptr);
+ const VSVideoInfo *input_vi = vsapi->getVideoInfo(d->node);
+ if (!vsh::isConstantVideoFormat(input_vi) || input_vi->format.colorFamily != cfRGB ||
+ input_vi->format.sampleType != stFloat || input_vi->format.bitsPerSample != 32) {
+ vsapi->mapSetError(out, "L-MPV NCNN: input must be constant RGBS");
+ vsapi->freeNode(d->node);
+ return;
+ }
+
+ int error = 0;
+ const char *param_path = vsapi->mapGetData(in, "param_path", 0, &error);
+ const char *model_path = vsapi->mapGetData(in, "model_path", 0, &error);
+ d->scale = static_cast(vsapi->mapGetInt(in, "scale", 0, &error));
+ if (error) d->scale = 2;
+ d->tile_size = static_cast(vsapi->mapGetInt(in, "tile_size", 0, &error));
+ if (error) d->tile_size = 256;
+ d->overlap = static_cast(vsapi->mapGetInt(in, "overlap", 0, &error));
+ if (error) d->overlap = 16;
+ const bool fp16 = !!vsapi->mapGetInt(in, "fp16", 0, &error);
+ int device_error = 0;
+ const int requested_device = static_cast(vsapi->mapGetInt(in, "device_id", 0, &device_error));
+
+ ncnn::create_gpu_instance();
+ const int device_id = device_error ? ncnn::get_default_gpu_index() : requested_device;
+ ncnn::VulkanDevice *device = ncnn::get_gpu_device(device_id);
+ if (!device) {
+ vsapi->mapSetError(out, "L-MPV NCNN: no Vulkan compute device found");
+ vsapi->freeNode(d->node);
+ return;
+ }
+ d->net.opt.use_vulkan_compute = true;
+ d->net.opt.use_fp16_packed = fp16;
+ d->net.opt.use_fp16_storage = fp16;
+ d->net.set_vulkan_device(device);
+ if (d->net.load_param(param_path) != 0 || d->net.load_model(model_path) != 0 ||
+ d->net.input_indexes().empty() || d->net.output_indexes().empty()) {
+ vsapi->mapSetError(out, "L-MPV NCNN: failed to load .param/.bin model pair");
+ vsapi->freeNode(d->node);
+ return;
+ }
+ d->input_index = d->net.input_indexes().front();
+ d->output_index = d->net.output_indexes().front();
+ d->vi = *input_vi;
+ d->vi.width *= d->scale;
+ d->vi.height *= d->scale;
+
+ FilterData *data = d.release();
+ VSFilterDependency dependency[] = {{data->node, rpStrictSpatial}};
+ vsapi->createVideoFilter(out, "LmpvNcnn", &data->vi, get_frame, free_filter,
+ fmParallelRequests, dependency, 1, data, core);
+}
+
+VS_EXTERNAL_API(void) VapourSynthPluginInit2(VSPlugin *plugin, const VSPLUGINAPI *vspapi) {
+ vspapi->configPlugin("io.github.menely.lmpv.ncnn", "lmpvncnn",
+ "L-MPV NCNN Vulkan Upscaler", VS_MAKE_VERSION(1, 0),
+ VAPOURSYNTH_API_VERSION, 0, plugin);
+ vspapi->registerFunction(
+ "Model",
+ "clip:vnode;param_path:data;model_path:data;scale:int:opt;tile_size:int:opt;overlap:int:opt;fp16:int:opt;device_id:int:opt;",
+ "clip:vnode;", create_filter, nullptr, plugin);
+}
diff --git a/packaging/arch/.SRCINFO b/packaging/arch/.SRCINFO
new file mode 100644
index 0000000..0472581
--- /dev/null
+++ b/packaging/arch/.SRCINFO
@@ -0,0 +1,33 @@
+pkgbase = l-mpv
+ pkgdesc = Modern desktop media player powered by Tauri and libmpv
+ pkgver = 2.0.2
+ pkgrel = 1
+ url = https://github.com/Menely/L-MPV
+ arch = x86_64
+ license = MIT
+ makedepends = cargo
+ makedepends = npm
+ makedepends = rust
+ makedepends = git
+ makedepends = cmake
+ makedepends = ninja
+ makedepends = meson
+ makedepends = cython
+ makedepends = vulkan-headers
+ makedepends = glslang
+ makedepends = patchelf
+ depends = ffmpeg
+ depends = mediainfo
+ depends = webkit2gtk-4.1
+ depends = gtk3
+ depends = python
+ depends = vulkan-icd-loader
+ depends = libplacebo
+ depends = libass
+ depends = zimg
+ source = l-mpv-2.0.2.tar.gz::https://github.com/Menely/L-MPV/archive/refs/tags/v2.0.2.tar.gz
+ source = l-mpv.desktop
+ sha256sums = SKIP
+ sha256sums = SKIP
+
+pkgname = l-mpv
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
new file mode 100644
index 0000000..3a2d1b0
--- /dev/null
+++ b/packaging/arch/PKGBUILD
@@ -0,0 +1,39 @@
+# Maintainer: L-MPV contributors
+pkgname=l-mpv
+pkgver=2.0.2
+pkgrel=1
+pkgdesc='Modern desktop media player powered by Tauri and libmpv'
+arch=('x86_64')
+url='https://github.com/Menely/L-MPV'
+license=('MIT')
+depends=('ffmpeg' 'mediainfo' 'webkit2gtk-4.1' 'gtk3' 'python' 'vulkan-icd-loader' 'libplacebo' 'libass' 'zimg')
+makedepends=('cargo' 'npm' 'rust' 'git' 'cmake' 'ninja' 'meson' 'cython' 'vulkan-headers' 'glslang' 'patchelf')
+source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz"
+ 'l-mpv.desktop')
+sha256sums=('SKIP' 'SKIP')
+
+prepare() {
+ cd "L-MPV-$pkgver"
+ npm version "$pkgver" --no-git-tag-version
+ sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$pkgver\"/" src-tauri/Cargo.toml
+ npm ci
+}
+
+build() {
+ cd "L-MPV-$pkgver"
+ L_MPV_RUNTIME_BUILD_DIR="$srcdir/runtime-build" \
+ L_MPV_RUNTIME_PREFIX="$srcdir/runtime" \
+ bash scripts/build-linux-runtime.sh
+ npm run build
+ cargo build --manifest-path src-tauri/Cargo.toml --release
+}
+
+package() {
+ cd "L-MPV-$pkgver"
+ install -Dm755 src-tauri/target/release/l-mpv "$pkgdir/usr/bin/l-mpv"
+ install -d "$pkgdir/usr/lib/l-mpv"
+ cp -a "$srcdir/runtime/." "$pkgdir/usr/lib/l-mpv/"
+ install -Dm644 src-tauri/icons/128x128.png "$pkgdir/usr/share/icons/hicolor/128x128/apps/l-mpv.png"
+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
+ install -Dm644 "$srcdir/l-mpv.desktop" "$pkgdir/usr/share/applications/l-mpv.desktop"
+}
diff --git a/packaging/arch/l-mpv.desktop b/packaging/arch/l-mpv.desktop
new file mode 100644
index 0000000..ebf2efa
--- /dev/null
+++ b/packaging/arch/l-mpv.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Name=L-MPV
+Comment=Modern media player powered by libmpv
+Exec=l-mpv %U
+Icon=l-mpv
+Terminal=false
+Categories=AudioVideo;Player;
+MimeType=video/mp4;video/x-matroska;video/x-msvideo;video/quicktime;video/webm;video/mp2t;video/mpeg;audio/mpeg;audio/flac;audio/x-wav;audio/aac;audio/ogg;audio/mp4;audio/opus;
+StartupNotify=true
diff --git a/scripts/build-linux-appimage.sh b/scripts/build-linux-appimage.sh
new file mode 100755
index 0000000..10b434b
--- /dev/null
+++ b/scripts/build-linux-appimage.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+VERSION="${VERSION:-$(node -p "require('${ROOT_DIR}/package.json').version")}"
+ARCH="${ARCH:-x86_64}"
+TOOLS_DIR="${RUNNER_TEMP:-/tmp}/l-mpv-appimage-tools"
+OUTPUT_DIR="${ROOT_DIR}/dist-linux"
+
+mkdir -p "${TOOLS_DIR}" "${OUTPUT_DIR}"
+cd "${ROOT_DIR}"
+
+RUNTIME_DIR="${L_MPV_RUNTIME_PREFIX:-${ROOT_DIR}/.runtime/linux}"
+if [[ ! -f "${RUNTIME_DIR}/lib/libmpv.so.2" && ! -f "${RUNTIME_DIR}/lib64/libmpv.so.2" ]]; then
+ L_MPV_RUNTIME_PREFIX="${RUNTIME_DIR}" bash scripts/build-linux-runtime.sh
+fi
+
+npm ci
+export NO_STRIP=1
+TAURI_APPDIR="${ROOT_DIR}/src-tauri/target/release/bundle/appimage/L-MPV.AppDir"
+if ! npm run tauri -- build --bundles appimage; then
+ # Newer appimagetool validates icon filename casing after linuxdeploy has
+ # already produced a complete AppDir. Reuse that staging tree and let the
+ # final packaging pass below normalize the icon and create the image.
+ if [[ ! -x "${TAURI_APPDIR}/usr/bin/l-mpv" ]]; then
+ echo 'Tauri failed before producing a usable AppDir' >&2
+ exit 1
+ fi
+ echo 'Tauri AppImage wrapper failed; continuing from its completed AppDir' >&2
+fi
+
+BASE_IMAGE="$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit)"
+
+rm -rf "${TOOLS_DIR}/squashfs-root"
+cd "${TOOLS_DIR}"
+if [[ -n "${BASE_IMAGE}" ]]; then
+ BASE_IMAGE="$(realpath "${BASE_IMAGE}")"
+ "${BASE_IMAGE}" --appimage-extract >/dev/null
+else
+ cp -a "${TAURI_APPDIR}" squashfs-root
+fi
+
+# appimagetool requires the root icon filename to match Desktop Entry Icon=.
+DESKTOP_FILE="$(find squashfs-root -maxdepth 1 -name '*.desktop' -print -quit)"
+if [[ -n "${DESKTOP_FILE}" ]]; then
+ ICON_NAME="$(sed -n 's/^Icon=//p' "${DESKTOP_FILE}" | head -1)"
+ if [[ -n "${ICON_NAME}" && ! -e "squashfs-root/${ICON_NAME}.png" ]]; then
+ ROOT_ICON="$(find squashfs-root -maxdepth 1 -iname '*.png' -print -quit)"
+ [[ -z "${ROOT_ICON}" ]] || cp "${ROOT_ICON}" "squashfs-root/${ICON_NAME}.png"
+ fi
+fi
+
+install -Dm755 "$(command -v ffmpeg)" squashfs-root/usr/bin/ffmpeg
+install -Dm755 "$(command -v mediainfo)" squashfs-root/usr/bin/mediainfo
+
+LIBMPV="$(find "${RUNTIME_DIR}" -name 'libmpv.so.2' -print -quit)"
+if [[ -z "${LIBMPV}" ]]; then
+ echo 'libmpv was not found by ldconfig' >&2
+ exit 1
+fi
+
+LINUXDEPLOY="${TOOLS_DIR}/linuxdeploy-${ARCH}.AppImage"
+if [[ ! -x "${LINUXDEPLOY}" ]]; then
+ curl --fail --location --retry 3 \
+ "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage" \
+ --output "${LINUXDEPLOY}"
+ chmod +x "${LINUXDEPLOY}"
+fi
+
+export OUTPUT="${OUTPUT_DIR}/L-MPV-v${VERSION}-linux-${ARCH}.AppImage"
+
+# Private runtime: custom libmpv with VapourSynth, NCNN/Vulkan plugin and
+# packaged shader fallback. Keep it isolated from host mpv.
+mkdir -p squashfs-root/usr/lib/l-mpv
+cp -a "${RUNTIME_DIR}/." squashfs-root/usr/lib/l-mpv/
+find squashfs-root/usr/lib/l-mpv -type f -name 'lib*.so*' -exec patchelf --set-rpath '$ORIGIN' {} \; 2>/dev/null || true
+if [[ -f squashfs-root/usr/lib/l-mpv/vapoursynth/liblmpv_ncnn.so ]]; then
+ patchelf --set-rpath '$ORIGIN/../lib' squashfs-root/usr/lib/l-mpv/vapoursynth/liblmpv_ncnn.so
+fi
+
+"${LINUXDEPLOY}" --appimage-extract-and-run \
+ --appdir squashfs-root \
+ --executable squashfs-root/usr/bin/ffmpeg \
+ --executable squashfs-root/usr/bin/mediainfo \
+ --library "${LIBMPV}" \
+ --output appimage
+
+chmod +x "${OUTPUT}"
+echo "Created ${OUTPUT}"
diff --git a/scripts/build-linux-runtime.sh b/scripts/build-linux-runtime.sh
new file mode 100755
index 0000000..13ae8de
--- /dev/null
+++ b/scripts/build-linux-runtime.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+BUILD_DIR="${L_MPV_RUNTIME_BUILD_DIR:-${RUNNER_TEMP:-/tmp}/l-mpv-runtime-build}"
+PREFIX="${L_MPV_RUNTIME_PREFIX:-${ROOT_DIR}/.runtime/linux}"
+JOBS="${JOBS:-$(nproc)}"
+
+MPV_REF="2c219aa822df18a1b7fd9abe3e151cd93ad67307" # v0.41.0
+NCNN_REF="c4193aadbbb56582aa87b1850dd3d98fb8fd936d" # 20250916
+VS_REF="acabf605b2205b32d65859bb2736405719d2fafd" # R79
+SHADER_REF="1e4ded59a4c5080c832badedb5a4a80d9064be6e"
+
+mkdir -p "${BUILD_DIR}" "${PREFIX}"
+
+clone_at() {
+ local url="$1" ref="$2" destination="$3"
+ if [[ ! -d "${destination}/.git" ]]; then
+ git clone --filter=blob:none "${url}" "${destination}"
+ fi
+ git -C "${destination}" fetch --depth 1 origin "${ref}"
+ git -C "${destination}" checkout --detach FETCH_HEAD
+}
+
+clone_at https://github.com/Tencent/ncnn.git "${NCNN_REF}" "${BUILD_DIR}/ncnn"
+git -C "${BUILD_DIR}/ncnn" submodule update --init --depth 1
+cmake -S "${BUILD_DIR}/ncnn" -B "${BUILD_DIR}/ncnn-build" -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${PREFIX}" \
+ -DCMAKE_INSTALL_LIBDIR=lib \
+ -DNCNN_VULKAN=ON -DNCNN_SHARED_LIB=ON -DNCNN_BUILD_TOOLS=OFF \
+ -DNCNN_BUILD_EXAMPLES=OFF -DNCNN_BUILD_BENCHMARK=OFF -DNCNN_BUILD_TESTS=OFF
+cmake --build "${BUILD_DIR}/ncnn-build" --parallel "${JOBS}"
+cmake --install "${BUILD_DIR}/ncnn-build"
+
+clone_at https://github.com/vapoursynth/vapoursynth.git "${VS_REF}" "${BUILD_DIR}/vapoursynth"
+rm -rf "${BUILD_DIR}/vapoursynth-build"
+meson setup "${BUILD_DIR}/vapoursynth-build" "${BUILD_DIR}/vapoursynth" \
+ --prefix="${PREFIX}" --libdir=lib --buildtype=release
+meson compile -C "${BUILD_DIR}/vapoursynth-build" -j "${JOBS}"
+meson install -C "${BUILD_DIR}/vapoursynth-build"
+
+VS_SITE_DIR="$(find "${PREFIX}/lib" -type d -path '*/site-packages/vapoursynth' -print -quit)"
+if [[ -z "${VS_SITE_DIR}" ]]; then
+ echo 'VapourSynth Python package was not installed' >&2
+ exit 1
+fi
+mkdir -p "${PREFIX}/include/vapoursynth"
+cp -a "${VS_SITE_DIR}/include/." "${PREFIX}/include/vapoursynth/"
+find "${VS_SITE_DIR}" -maxdepth 1 -type f \( -name 'libvapoursynth*.so*' -o -name 'libvsscript.so*' \) \
+ -exec cp -aL {} "${PREFIX}/lib/" \;
+cat > "${PREFIX}/lib/pkgconfig/vapoursynth-script.pc" < Self {
+ Self
+ }
+
+ pub fn set_active(&self, _active: bool) {}
+
+ pub fn get_spectrum(&self) -> [f32; 32] {
+ [0.0; 32]
+ }
+}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 1b884c3..fb8c459 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -11,6 +11,30 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::collections::{HashMap, HashSet};
use tauri::State;
+/// Корневая директория пользовательских данных.
+/// Windows сохраняет portable-поведение, Linux следует XDG Base Directory.
+pub fn app_data_root() -> std::path::PathBuf {
+ #[cfg(target_os = "linux")]
+ {
+ let base = std::env::var_os("XDG_CONFIG_HOME")
+ .map(std::path::PathBuf::from)
+ .or_else(|| std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config")))
+ .unwrap_or_else(|| std::path::PathBuf::from("."));
+ return base.join("l-mpv");
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ std::env::current_exe()
+ .ok()
+ .and_then(|path| path.parent().map(std::path::Path::to_path_buf))
+ .unwrap_or_else(|| std::path::PathBuf::from("."))
+}
+
+#[tauri::command]
+pub fn get_runtime_platform() -> &'static str {
+ std::env::consts::OS
+}
+
fn default_true() -> bool {
true
}
@@ -510,16 +534,9 @@ pub fn load_external_tracks_internal(
state: &PlayerState,
video_path: &std::path::Path,
) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- let (auto_load, auto_select_audio) = if let Some(ref p_dir) = exe_dir {
- let settings = AppSettings::load(p_dir);
- (settings.auto_load_tracks, settings.auto_select_external_audio)
- } else {
- (false, false)
- };
+ let settings = AppSettings::load(&app_data_root());
+ let (auto_load, auto_select_audio) =
+ (settings.auto_load_tracks, settings.auto_select_external_audio);
if !auto_load {
return Ok(());
@@ -1155,19 +1172,13 @@ pub fn set_screenshot_dir(
state: State<'_, PlayerState>,
path: String,
) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
+ let exe_dir = app_data_root();
let safe_path = path.replace("\\", "/");
let is_reset = safe_path == "screenshots" || safe_path.is_empty();
let target_path = if is_reset {
- if let Some(ref p_dir) = exe_dir {
- p_dir.join("screenshots").to_string_lossy().replace("\\", "/")
- } else {
- "screenshots".to_string()
- }
+ exe_dir.join("screenshots").to_string_lossy().replace("\\", "/")
} else {
safe_path.clone()
};
@@ -1176,15 +1187,13 @@ pub fn set_screenshot_dir(
.mpv
.set_property_string("screenshot-directory", &target_path)?;
- if let Some(p_dir) = exe_dir {
- let mut settings = AppSettings::load(&p_dir);
- if is_reset {
- settings.screenshot_directory = None;
- } else {
- settings.screenshot_directory = Some(target_path);
- }
- settings.save(&p_dir).ok();
+ let mut settings = AppSettings::load(&exe_dir);
+ if is_reset {
+ settings.screenshot_directory = None;
+ } else {
+ settings.screenshot_directory = Some(target_path);
}
+ settings.save(&exe_dir).ok();
Ok(())
}
@@ -1192,105 +1201,52 @@ pub fn set_screenshot_dir(
/// Получить настройку multi-instance.
#[tauri::command]
pub fn get_multi_instance() -> Result {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let settings = AppSettings::load(&p_dir);
- return Ok(settings.allow_multi_instance);
- }
- Ok(false)
+ Ok(AppSettings::load(&app_data_root()).allow_multi_instance)
}
/// Установить настройку multi-instance.
#[tauri::command]
pub fn set_multi_instance(allow: bool) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let mut settings = AppSettings::load(&p_dir);
- settings.allow_multi_instance = allow;
- settings.save(&p_dir).ok();
- }
-
- Ok(())
+ let root = app_data_root();
+ let mut settings = AppSettings::load(&root);
+ settings.allow_multi_instance = allow;
+ settings.save(&root)
}
/// Получить текущий статус настройки автоматического подхвата внешних дорожек.
#[tauri::command]
pub fn get_auto_load_tracks() -> Result {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let settings = AppSettings::load(&p_dir);
- return Ok(settings.auto_load_tracks);
- }
- Ok(false)
+ Ok(AppSettings::load(&app_data_root()).auto_load_tracks)
}
/// Установить статус настройки автоматического подхвата внешних дорожек с сохранением в settings.json.
#[tauri::command]
pub fn set_auto_load_tracks(enabled: bool) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let mut settings = AppSettings::load(&p_dir);
- settings.auto_load_tracks = enabled;
- settings.save(&p_dir).ok();
- return Ok(());
- }
- Err("Не удалось определить директорию приложения для сохранения настроек".to_string())
+ let root = app_data_root();
+ let mut settings = AppSettings::load(&root);
+ settings.auto_load_tracks = enabled;
+ settings.save(&root)
}
/// Получить текущий статус настройки автоматического переключения звука на внешнюю аудиодорожку.
#[tauri::command]
pub fn get_auto_select_external_audio() -> Result {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let settings = AppSettings::load(&p_dir);
- return Ok(settings.auto_select_external_audio);
- }
- Ok(false)
+ Ok(AppSettings::load(&app_data_root()).auto_select_external_audio)
}
/// Установить статус настройки автоматического переключения звука на внешнюю аудиодорожку.
#[tauri::command]
pub fn set_auto_select_external_audio(enabled: bool) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let mut settings = AppSettings::load(&p_dir);
- settings.auto_select_external_audio = enabled;
- settings.save(&p_dir).ok();
- return Ok(());
- }
- Err("Не удалось определить директорию приложения для сохранения настроек".to_string())
+ let root = app_data_root();
+ let mut settings = AppSettings::load(&root);
+ settings.auto_select_external_audio = enabled;
+ settings.save(&root)
}
/// Получить текущий статус настройки автоматического переключения на следующее видео по окончании.
#[tauri::command]
pub fn get_play_next_on_end() -> Result {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let settings = AppSettings::load(&p_dir);
- return Ok(settings.play_next_on_end);
- }
- Ok(true)
+ Ok(AppSettings::load(&app_data_root()).play_next_on_end)
}
/// Установить статус настройки автоматического переключения на следующее видео по окончании.
@@ -1299,20 +1255,12 @@ pub fn set_play_next_on_end(
state: State<'_, PlayerState>,
enabled: bool,
) -> Result<(), String> {
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
-
- if let Some(p_dir) = exe_dir {
- let mut settings = AppSettings::load(&p_dir);
- settings.play_next_on_end = enabled;
- settings.save(&p_dir).ok();
-
- let keep_open_val = if enabled { "yes" } else { "always" };
- let _ = state.mpv.set_property_string("keep-open", keep_open_val);
- return Ok(());
- }
- Err("Не удалось определить директорию приложения для сохранения настроек".to_string())
+ let root = app_data_root();
+ let mut settings = AppSettings::load(&root);
+ settings.play_next_on_end = enabled;
+ settings.save(&root)?;
+ let keep_open_val = if enabled { "yes" } else { "always" };
+ state.mpv.set_property_string("keep-open", keep_open_val)
}
/// Сканирование и загрузка внешних дорожек и субтитров для указанного медиафайла.
@@ -1358,10 +1306,7 @@ pub fn set_ambient_settings(
) -> Result<(), String> {
state.ambient_controller.apply(&settings)?;
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()))
- .ok_or_else(|| "Не удалось определить путь к директории приложения".to_string())?;
+ let exe_dir = app_data_root();
let mut current_settings = AppSettings::load(&exe_dir);
current_settings.ambient = settings;
@@ -1381,10 +1326,7 @@ pub fn toggle_ambient_mode(
current.mode = AmbientController::cycle_mode(¤t.mode);
state.ambient_controller.apply(¤t)?;
- let exe_dir = std::env::current_exe()
- .ok()
- .and_then(|p| p.parent().map(|p| p.to_path_buf()))
- .ok_or_else(|| "Не удалось определить путь к директории приложения".to_string())?;
+ let exe_dir = app_data_root();
let mut current_settings = AppSettings::load(&exe_dir);
current_settings.ambient = current.clone();
@@ -1724,7 +1666,7 @@ pub fn toggle_shuffle(state: State<'_, PlayerState>) -> Result<(), String> {
#[tauri::command]
pub async fn copy_frame_to_clipboard(state: tauri::State<'_, PlayerState>) -> Result<(), String> {
use std::time::Duration;
- let exe_dir = std::env::current_exe().map_err(|e| e.to_string())?.parent().unwrap().to_path_buf();
+ let exe_dir = app_data_root();
let data_dir = exe_dir.join("data");
let _ = std::fs::create_dir_all(&data_dir);
@@ -1800,15 +1742,11 @@ pub fn normalize_history_path(path: &str) -> String {
fn get_history_map() -> &'static Mutex> {
WATCH_HISTORY.get_or_init(|| {
let mut map = HashMap::new();
- if let Ok(exe_dir) = std::env::current_exe() {
- if let Some(parent) = exe_dir.parent() {
- let history_path = parent.join("config").join("history.json");
- if let Ok(content) = std::fs::read_to_string(&history_path) {
- if let Ok(parsed) = serde_json::from_str::>(&content) {
- for (k, v) in parsed {
- map.insert(normalize_history_path(&k), v);
- }
- }
+ let history_path = app_data_root().join("config").join("history.json");
+ if let Ok(content) = std::fs::read_to_string(&history_path) {
+ if let Ok(parsed) = serde_json::from_str::>(&content) {
+ for (k, v) in parsed {
+ map.insert(normalize_history_path(&k), v);
}
}
}
@@ -1818,15 +1756,11 @@ fn get_history_map() -> &'static Mutex> {
pub fn save_history_to_disk() {
if let Ok(map) = get_history_map().lock() {
- if let Ok(exe_dir) = std::env::current_exe() {
- if let Some(parent) = exe_dir.parent() {
- let config_dir = parent.join("config");
- let _ = std::fs::create_dir_all(&config_dir);
- let history_path = config_dir.join("history.json");
- if let Ok(json) = serde_json::to_string_pretty(&*map) {
- let _ = std::fs::write(&history_path, json);
- }
- }
+ let config_dir = app_data_root().join("config");
+ let _ = std::fs::create_dir_all(&config_dir);
+ let history_path = config_dir.join("history.json");
+ if let Ok(json) = serde_json::to_string_pretty(&*map) {
+ let _ = std::fs::write(&history_path, json);
}
}
}
@@ -2189,13 +2123,22 @@ pub async fn extract_track(
};
// Определение пути к встроенному исполняемому файлу ffmpeg
+ #[cfg(target_os = "windows")]
let exe_dir = std::env::current_exe()
.map_err(|e| e.to_string())?
.parent()
.ok_or_else(|| "Не удалось определить каталог приложения L-MPV".to_string())?
.to_path_buf();
+ #[cfg(target_os = "windows")]
let mut ffmpeg_path = exe_dir.join("ffmpeg.exe");
+ #[cfg(target_os = "linux")]
+ let ffmpeg_path = std::env::var_os("APPDIR")
+ .map(std::path::PathBuf::from)
+ .map(|dir| dir.join("usr/bin/ffmpeg"))
+ .unwrap_or_else(|| std::path::PathBuf::from("ffmpeg"));
+
+ #[cfg(target_os = "windows")]
if !ffmpeg_path.exists() {
if std::path::Path::new("ffmpeg.exe").exists() {
ffmpeg_path = std::path::PathBuf::from("ffmpeg.exe");
@@ -2361,11 +2304,7 @@ fn sanitize_filename(name: &str) -> String {
/// Получение абсолютного пути к портативной директории config/presets/.
fn get_presets_dir() -> Result {
- let exe_dir = std::env::current_exe()
- .map_err(|e| format!("Не удалось определить путь к исполняемому файлу: {e}"))?
- .parent()
- .ok_or_else(|| "Не удалось определить директорию исполняемого файла".to_string())?
- .to_path_buf();
+ let exe_dir = app_data_root();
let dir = exe_dir.join("config").join("presets");
if !dir.exists() {
let _ = std::fs::create_dir_all(&dir);
@@ -2540,5 +2479,3 @@ pub fn write_text_file(path: String, content: String) -> Result<(), String> {
pub fn read_text_file(path: String) -> Result {
std::fs::read_to_string(&path).map_err(|e| format!("Ошибка чтения файла по пути {path}: {e}"))
}
-
-
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index baca0b5..8a4e343 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -5,6 +5,10 @@
mod ambient;
pub mod upscale;
+#[cfg(target_os = "windows")]
+mod audio_capture;
+#[cfg(not(target_os = "windows"))]
+#[path = "audio_capture_stub.rs"]
mod audio_capture;
mod commands;
mod mediainfo;
@@ -35,7 +39,7 @@ pub fn parse_cli_args>(args: I) -> ParsedCliArgs
let lower = arg.to_lowercase();
if lower == "--mediainfo" || lower == "-mediainfo" || lower == "/mediainfo" {
open_mediainfo = true;
- } else if !arg.starts_with('-') && !arg.starts_with('/') && file_path.is_none() {
+ } else if !arg.starts_with('-') && file_path.is_none() {
file_path = Some(arg);
}
}
@@ -46,6 +50,28 @@ pub fn parse_cli_args>(args: I) -> ParsedCliArgs
}
}
+#[cfg(test)]
+mod cli_tests {
+ use super::parse_cli_args;
+
+ #[test]
+ fn accepts_linux_absolute_media_path() {
+ let parsed = parse_cli_args(["l-mpv".to_string(), "/tmp/video.mp4".to_string()]);
+ assert_eq!(parsed.file_path.as_deref(), Some("/tmp/video.mp4"));
+ }
+
+ #[test]
+ fn keeps_windows_mediainfo_switch() {
+ let parsed = parse_cli_args([
+ "l-mpv".to_string(),
+ "/mediainfo".to_string(),
+ "C:\\video.mp4".to_string(),
+ ]);
+ assert!(parsed.open_mediainfo);
+ assert_eq!(parsed.file_path.as_deref(), Some("C:\\video.mp4"));
+ }
+}
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let raw_args: Vec = std::env::args().collect();
@@ -74,11 +100,7 @@ pub fn run() {
println!("[L-MPV] Запуск функции run()...");
// Определяем портативную директорию приложения
- let exe_dir = std::env::current_exe()
- .expect("Не удалось определить путь к исполняемому файлу")
- .parent()
- .expect("Не удалось определить директорию исполняемого файла")
- .to_path_buf();
+ let exe_dir = commands::app_data_root();
println!("[L-MPV] Директория exe: {:?}", exe_dir);
@@ -111,10 +133,14 @@ pub fn run() {
.map(|d| d.as_secs())
.unwrap_or(0);
- let payload = panic_info.payload().downcast_ref::<&str>()
- .unwrap_or(&"Box");
+ let payload = panic_info
+ .payload()
+ .downcast_ref::<&str>()
+ .map(|value| (*value).to_string())
+ .or_else(|| panic_info.payload().downcast_ref::().cloned())
+ .unwrap_or_else(|| "Box".to_string());
let location = panic_info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_default();
-
+ eprintln!("[L-MPV] CRASH at {location}: {payload}");
let _ = writeln!(file, "[{}] CRASH (Panic) at {}: {}", timestamp, location, payload);
}));
@@ -264,6 +290,7 @@ pub fn run() {
commands::set_play_next_on_end,
commands::load_external_tracks_for_file,
commands::get_app_version,
+ commands::get_runtime_platform,
// Автообновление
updater::check_launch_and_update,
updater::check_for_updates,
@@ -293,6 +320,7 @@ pub fn run() {
upscale::switch_upscale_network_hotkey,
upscale::precompile_model_engine_1080p,
upscale::save_models_order,
+ upscale::download_curated_ncnn_model,
])
.on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
@@ -323,6 +351,7 @@ pub fn run() {
})
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
+ let cli = parse_cli_args(std::env::args());
#[cfg(target_os = "windows")]
{
@@ -361,7 +390,159 @@ pub fn run() {
}
}
- let cli = parse_cli_args(std::env::args());
+ #[cfg(target_os = "linux")]
+ {
+ use gtk::glib::{self, Cast, ControlFlow, ObjectExt, Propagation};
+ use gtk::prelude::{ContainerExt, GLAreaExt, OverlayExt, WidgetExt};
+
+ let state = app.state::();
+ let mpv = state.mpv.clone();
+ let root = window.default_vbox()
+ .map_err(|e| format!("Не удалось получить GTK-контейнер: {e}"))?;
+ let Some(overlay) = root.children().into_iter().last()
+ .and_then(|widget| widget.downcast::().ok()) else {
+ println!("[L-MPV] Нативная видеоповерхность отключена");
+ window.show().ok();
+ return Ok(());
+ };
+ let children = overlay.children();
+ let video = children.iter()
+ .find_map(|widget| widget.clone().downcast::().ok())
+ .ok_or_else(|| "GtkGLArea отсутствует в GtkOverlay".to_string())?;
+ let webview_host = children.into_iter()
+ .find(|widget| !widget.is::())
+ .ok_or_else(|| "WebView отсутствует в GtkOverlay".to_string())?;
+ let webview = webview_host.clone().downcast::().ok()
+ .and_then(|container| container.children().into_iter().next())
+ .unwrap_or_else(|| webview_host.clone());
+
+ video.set_auto_render(false);
+ video.set_has_alpha(false);
+ video.set_required_version(3, 2);
+ video.set_hexpand(true);
+ video.set_vexpand(true);
+ let display_name = gtk::gdk::Display::default()
+ .map(|display| display.type_().name().to_string())
+ .unwrap_or_else(|| "unknown".to_string());
+ if display_name == "GdkX11Display" {
+ // X11 native child windows cannot alpha-compose WebKitGTK
+ // above GLArea. Keep the GL surface between the titlebar
+ // and controls so both remain visible and interactive.
+ video.set_vexpand(false);
+ video.set_valign(gtk::Align::Start);
+ video.set_margin_top(44);
+ video.set_margin_bottom(0);
+ let weak_x11_video = video.downgrade();
+ overlay.connect_size_allocate(move |_overlay, allocation| {
+ if let Some(video) = weak_x11_video.upgrade() {
+ video.set_size_request(-1, (allocation.height() - 140).max(1));
+ }
+ });
+ overlay.set_overlay_pass_through(&video, true);
+ } else {
+ overlay.set_overlay_pass_through(&webview_host, false);
+ }
+ // The Linux config creates this window decorated so Tauri does
+ // not install its hierarchy-dependent resize handler. Once the
+ // WebView has been placed over GtkGLArea we restore the custom
+ // title bar used by the application.
+ window.set_decorations(false)
+ .map_err(|e| format!("Не удалось отключить системную рамку: {e}"))?;
+
+ let mpv_realize = mpv.clone();
+ let startup_path = if cli.open_mediainfo { None } else { cli.file_path.clone() };
+ let startup_app = app.handle().clone();
+ video.connect_realize(move |area| {
+ area.make_current();
+ if let Some(error) = area.error() {
+ eprintln!("[L-MPV] Не удалось создать OpenGL-контекст: {error}");
+ return;
+ }
+ if let Err(error) = mpv_realize.initialize_opengl_renderer() {
+ eprintln!("[L-MPV] Ошибка libmpv render API: {error}");
+ return;
+ }
+ if let Some(path) = startup_path.as_deref() {
+ let state = startup_app.state::();
+ if let Err(error) = commands::open_file_internal(&state, path) {
+ eprintln!("[L-MPV] Ошибка открытия файла при запуске: {error}");
+ } else {
+ println!("[L-MPV] Открыт файл из CLI: {path}");
+ }
+ if let Some(window) = startup_app.get_webview_window("main") {
+ window.show().ok();
+ }
+ }
+ });
+
+ let mpv_render = mpv.clone();
+ video.connect_render(move |area, _context| {
+ const GL_DRAW_FRAMEBUFFER_BINDING: u32 = 0x8CA6;
+ #[link(name = "GL")]
+ unsafe extern "C" {
+ fn glGetIntegerv(name: u32, value: *mut i32);
+ }
+
+ let mut fbo = 0i32;
+ unsafe { glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut fbo) };
+ let scale = area.scale_factor().max(1);
+ let allocation = area.allocation();
+ let width = allocation.width().max(1) * scale;
+ let height = allocation.height().max(1) * scale;
+ if let Err(error) = mpv_render.render_opengl_frame(fbo, width, height) {
+ eprintln!("[L-MPV] Ошибка отрисовки кадра: {error}");
+ }
+ Propagation::Stop
+ });
+
+ let mpv_unrealize = mpv.clone();
+ video.connect_unrealize(move |area| {
+ area.make_current();
+ mpv_unrealize.free_opengl_renderer();
+ });
+
+ let weak_video = video.downgrade();
+ let mpv_updates = mpv.clone();
+ glib::timeout_add_local(std::time::Duration::from_millis(8), move || {
+ if let Some(video) = weak_video.upgrade() {
+ if mpv_updates.take_render_request() {
+ video.queue_render();
+ }
+ ControlFlow::Continue
+ } else {
+ ControlFlow::Break
+ }
+ });
+
+ overlay.show_all();
+ // GtkGLArea and WebKitGTK both own native GDK child windows.
+ // Keep the WebView's window above the OpenGL surface; GTK's
+ // widget order alone is not sufficient on every compositor.
+ if display_name == "GdkX11Display" {
+ if let Some(video_window) = video.window() {
+ video_window.raise();
+ }
+ } else {
+ if let Some(video_window) = video.window() {
+ video_window.lower();
+ }
+ if let Some(webview_window) = webview.window() {
+ webview_window.raise();
+ }
+ webview.connect_realize(|widget| {
+ if let Some(window) = widget.window() {
+ window.raise();
+ }
+ });
+ }
+ println!("[L-MPV] libmpv render API подключён к GTK GLArea ({display_name})");
+
+ let saved_cfg = commands::AppSettings::load(&commands::app_data_root());
+ if let Err(e) = state.ambient_controller.apply(&saved_cfg.ambient) {
+ println!("[L-MPV] Ошибка инициализации Ambient Light: {e}");
+ }
+ }
+
if cli.open_mediainfo {
let target_path = cli.file_path.as_deref().unwrap_or("");
let app_handle = app.handle().clone();
@@ -370,10 +551,20 @@ pub fn run() {
}
// Окно плеера main остается скрытым
} else if let Some(ref path) = cli.file_path {
+ #[cfg(target_os = "linux")]
+ {
+ let _ = path;
+ window.show().ok();
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
let state = app.state::();
if let Err(e) = commands::open_file_internal(&state, path) {
println!("[L-MPV] Ошибка открытия файла при запуске: {}", e);
window.show().ok();
+ } else {
+ println!("[L-MPV] Открыт файл из CLI: {path}");
+ }
}
} else {
// Нет аргумента файла — показываем окно сразу со стартовой страницей
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index ac272a3..8849a48 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -2,5 +2,115 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
+ #[cfg(target_os = "linux")]
+ configure_linux_runtime();
l_mpv_lib::run()
}
+
+#[cfg(target_os = "linux")]
+fn configure_linux_runtime() {
+ use std::path::PathBuf;
+
+ // WebKitGTK's accelerated child surface cannot be alpha-composited over a
+ // GtkGLArea reliably on native Wayland (and some X11 compositors). Keep
+ // WebKit in GTK's compositing path; video itself remains GPU-rendered by
+ // libmpv in GtkGLArea.
+ std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
+ std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
+ let runtime = std::env::var_os("L_MPV_RUNTIME_DIR")
+ .map(PathBuf::from)
+ .or_else(|| std::env::var_os("APPDIR").map(|dir| PathBuf::from(dir).join("usr/lib/l-mpv")))
+ .or_else(|| {
+ std::env::current_exe().ok().and_then(|exe| {
+ let candidate = exe.parent()?.join("../lib/l-mpv");
+ candidate.exists().then_some(candidate)
+ })
+ });
+ let Some(runtime) = runtime else { return };
+ std::env::set_var("L_MPV_RUNTIME_DIR", &runtime);
+ let libmpv = runtime.join("lib/libmpv.so.2");
+ if libmpv.is_file() {
+ std::env::set_var("L_MPV_LIBMPV", libmpv);
+ }
+
+ let bundled_python = runtime.join("python/bin/python3");
+ let bundled_python_home = runtime.join("python");
+ if bundled_python.is_file() {
+ std::env::set_var("PYTHONHOME", &bundled_python_home);
+ prepend_env_path("PATH", runtime.join("bin"));
+ }
+
+ let python_root = runtime.join("lib");
+ if let Ok(entries) = std::fs::read_dir(&python_root) {
+ if let Some(site_packages) = entries.flatten()
+ .map(|entry| entry.path().join("site-packages"))
+ .find(|path| path.is_dir())
+ {
+ let mut paths = vec![site_packages];
+ if let Some(existing) = std::env::var_os("PYTHONPATH") {
+ paths.extend(std::env::split_paths(&existing));
+ }
+ if let Ok(value) = std::env::join_paths(paths) {
+ std::env::set_var("PYTHONPATH", value);
+ }
+ }
+ }
+
+ configure_vapoursynth(&runtime, &bundled_python);
+}
+
+#[cfg(target_os = "linux")]
+fn prepend_env_path(name: &str, path: std::path::PathBuf) {
+ let mut paths = vec![path];
+ if let Some(existing) = std::env::var_os(name) {
+ paths.extend(std::env::split_paths(&existing));
+ }
+ if let Ok(value) = std::env::join_paths(paths) {
+ std::env::set_var(name, value);
+ }
+}
+
+#[cfg(target_os = "linux")]
+fn configure_vapoursynth(runtime: &std::path::Path, python: &std::path::Path) {
+ use std::os::unix::fs::MetadataExt;
+
+ let vsscript = runtime.join("lib/libvsscript.so");
+ let libpython = std::fs::read_dir(runtime.join("lib")).ok().and_then(|entries| {
+ entries.flatten().map(|entry| entry.path()).find(|path| {
+ path.file_name().and_then(|name| name.to_str())
+ .is_some_and(|name| name.starts_with("libpython") && name.contains(".so"))
+ })
+ });
+ let (Ok(vsscript), Ok(python), Some(libpython)) = (
+ vsscript.canonicalize(),
+ python.canonicalize(),
+ libpython.and_then(|path| path.canonicalize().ok()),
+ ) else { return };
+ let mtime = std::fs::metadata(&python).map(|metadata| metadata.mtime()).unwrap_or(0);
+ let config_root = std::env::var_os("XDG_CONFIG_HOME")
+ .map(std::path::PathBuf::from)
+ .or_else(|| std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config")))
+ .unwrap_or_else(|| std::path::PathBuf::from("."));
+ let config_dir = config_root.join("vapoursynth");
+ let config_path = config_dir.join("vapoursynth.toml");
+ let key = toml_string(&vsscript.to_string_lossy());
+ let line = format!(
+ "{key} = [{}, {}, \"{mtime}\"]",
+ toml_string(&python.to_string_lossy()),
+ toml_string(&libpython.to_string_lossy()),
+ );
+ let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
+ let mut lines = existing.lines()
+ .filter(|existing_line| !existing_line.starts_with(&key))
+ .map(str::to_owned)
+ .collect::>();
+ lines.push(line);
+ if std::fs::create_dir_all(config_dir).is_ok() {
+ let _ = std::fs::write(config_path, format!("{}\n", lines.join("\n")));
+ }
+}
+
+#[cfg(target_os = "linux")]
+fn toml_string(value: &str) -> String {
+ format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
+}
diff --git a/src-tauri/src/mediainfo.rs b/src-tauri/src/mediainfo.rs
index c579b04..2398d31 100644
--- a/src-tauri/src/mediainfo.rs
+++ b/src-tauri/src/mediainfo.rs
@@ -4,9 +4,12 @@
//! извлечение исчерпывающего текстового и JSON-отчёта обо всех потоках и метаданных
//! медиаконтейнера (видео, аудиодорожки, субтитры, кодеки, параметры кодирования).
+#[cfg(target_os = "windows")]
use std::os::raw::c_void;
use std::path::{Path, PathBuf};
+#[cfg(target_os = "windows")]
use std::sync::OnceLock;
+#[cfg(target_os = "windows")]
use libloading::{Library, Symbol};
use serde::{Deserialize, Serialize};
use tauri::State;
@@ -21,9 +24,11 @@ pub struct DetailedMediaInfo {
pub json: String,
}
+#[cfg(target_os = "windows")]
static MEDIAINFO_API: OnceLock> = OnceLock::new();
/// Набор динамически загружаемых символов C-API MediaInfo.
+#[cfg(target_os = "windows")]
struct MediaInfoApi {
_lib: &'static Library,
new_fn: Symbol<'static, unsafe extern "C" fn() -> *mut c_void>,
@@ -35,9 +40,12 @@ struct MediaInfoApi {
option_fn: Symbol<'static, unsafe extern "C" fn(*mut c_void, *const u16, *const u16) -> *const u16>,
}
+#[cfg(target_os = "windows")]
unsafe impl Send for MediaInfoApi {}
+#[cfg(target_os = "windows")]
unsafe impl Sync for MediaInfoApi {}
+#[cfg(target_os = "windows")]
impl MediaInfoApi {
/// Получение синглтона API библиотеки (ленивая инициализация один раз).
pub fn get() -> Result<&'static Self, String> {
@@ -96,6 +104,7 @@ impl MediaInfoApi {
}
/// Поиск пути к файлу библиотеки mediainfo.dll по списку стандартных кандидатов.
+#[cfg(target_os = "windows")]
fn find_mediainfo_dll() -> Result {
let mut candidates = Vec::new();
@@ -125,11 +134,13 @@ fn find_mediainfo_dll() -> Result {
}
/// Конвертация строкового среза в нуль-терминированный UTF-16 буфер.
+#[cfg(target_os = "windows")]
fn to_wide(s: &str) -> Vec {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
/// Чтение нуль-терминированной UTF-16 строки из нативного указателя.
+#[cfg(target_os = "windows")]
unsafe fn from_wide_ptr(ptr: *const u16) -> String {
if ptr.is_null() {
return String::new();
@@ -161,6 +172,13 @@ pub fn analyze_media_file>(path: P) -> Result>(path: P) -> Result Result {
+ let binary = std::env::var_os("APPDIR")
+ .map(PathBuf::from)
+ .map(|dir| dir.join("usr/bin/mediainfo"))
+ .filter(|candidate| candidate.exists())
+ .unwrap_or_else(|| PathBuf::from("mediainfo"));
+
+ let text_output = std::process::Command::new(&binary)
+ .arg(path)
+ .output()
+ .map_err(|e| format!("Не удалось запустить MediaInfo: {e}"))?;
+ if !text_output.status.success() {
+ return Err(String::from_utf8_lossy(&text_output.stderr).trim().to_string());
+ }
+
+ let json_output = std::process::Command::new(&binary)
+ .arg("--Output=JSON")
+ .arg(path)
+ .output()
+ .map_err(|e| format!("Не удалось получить JSON MediaInfo: {e}"))?;
+
+ Ok(DetailedMediaInfo {
+ text: String::from_utf8_lossy(&text_output.stdout).into_owned(),
+ json: if json_output.status.success() {
+ String::from_utf8_lossy(&json_output.stdout).into_owned()
+ } else {
+ String::new()
+ },
+ })
}
/// Получение полного детального отчёта MediaInfo через библиотеку MediaInfo.dll.
diff --git a/src-tauri/src/mpv_manager.rs b/src-tauri/src/mpv_manager.rs
index 937cea7..0f611dc 100644
--- a/src-tauri/src/mpv_manager.rs
+++ b/src-tauri/src/mpv_manager.rs
@@ -8,6 +8,8 @@ use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_double, c_int, c_void};
use std::path::Path;
use std::sync::Mutex;
+#[cfg(target_os = "linux")]
+use std::sync::atomic::{AtomicBool, Ordering};
use libloading::{Library, Symbol};
// ─── Определения FFI для libmpv C API ───────────────────
@@ -32,6 +34,37 @@ struct MpvHandle {
_private: [u8; 0],
}
+#[repr(C)]
+struct MpvRenderContext {
+ _private: [u8; 0],
+}
+
+#[repr(C)]
+struct MpvRenderParam {
+ param_type: c_int,
+ data: *mut c_void,
+}
+
+#[repr(C)]
+struct MpvOpenGlInitParams {
+ get_proc_address: Option *mut c_void>,
+ get_proc_address_ctx: *mut c_void,
+}
+
+#[repr(C)]
+struct MpvOpenGlFbo {
+ fbo: c_int,
+ width: c_int,
+ height: c_int,
+ internal_format: c_int,
+}
+
+const MPV_RENDER_PARAM_INVALID: c_int = 0;
+const MPV_RENDER_PARAM_API_TYPE: c_int = 1;
+const MPV_RENDER_PARAM_OPENGL_INIT_PARAMS: c_int = 2;
+const MPV_RENDER_PARAM_OPENGL_FBO: c_int = 3;
+const MPV_RENDER_PARAM_FLIP_Y: c_int = 4;
+
// ─── Динамически загружаемые функции ─────────────────────
struct MpvApi {
_lib: Library,
@@ -45,6 +78,10 @@ struct MpvApi {
free: Symbol<'static, unsafe extern "C" fn(data: *mut c_void)>,
set_property: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvHandle, name: *const c_char, format: MpvFormat, data: *mut c_void) -> c_int>,
set_property_string: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvHandle, name: *const c_char, data: *const c_char) -> c_int>,
+ render_context_create: Symbol<'static, unsafe extern "C" fn(ctx: *mut *mut MpvRenderContext, handle: *mut MpvHandle, params: *mut MpvRenderParam) -> c_int>,
+ render_context_render: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext, params: *mut MpvRenderParam) -> c_int>,
+ render_context_set_update_callback: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext, callback: Option, callback_ctx: *mut c_void)>,
+ render_context_free: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext)>,
}
unsafe impl Send for MpvApi {}
@@ -65,6 +102,10 @@ impl MpvApi {
let free = std::mem::transmute(lib.get::(b"mpv_free\0").map_err(|e| e.to_string())?);
let set_property = std::mem::transmute(lib.get:: c_int>(b"mpv_set_property\0").map_err(|e| e.to_string())?);
let set_property_string = std::mem::transmute(lib.get:: c_int>(b"mpv_set_property_string\0").map_err(|e| e.to_string())?);
+ let render_context_create = std::mem::transmute(lib.get:: c_int>(b"mpv_render_context_create\0").map_err(|e| e.to_string())?);
+ let render_context_render = std::mem::transmute(lib.get:: c_int>(b"mpv_render_context_render\0").map_err(|e| e.to_string())?);
+ let render_context_set_update_callback = std::mem::transmute(lib.get::, callback_ctx: *mut c_void)>(b"mpv_render_context_set_update_callback\0").map_err(|e| e.to_string())?);
+ let render_context_free = std::mem::transmute(lib.get::(b"mpv_render_context_free\0").map_err(|e| e.to_string())?);
Ok(Self {
_lib: lib,
@@ -78,6 +119,10 @@ impl MpvApi {
free,
set_property,
set_property_string,
+ render_context_create,
+ render_context_render,
+ render_context_set_update_callback,
+ render_context_free,
})
}
}
@@ -85,6 +130,9 @@ impl MpvApi {
/// Безопасная обёртка над контекстом mpv.
pub struct MpvManager {
handle: Mutex<*mut MpvHandle>,
+ render_context: Mutex<*mut MpvRenderContext>,
+ #[cfg(target_os = "linux")]
+ render_pending: AtomicBool,
api: MpvApi,
}
@@ -94,23 +142,35 @@ unsafe impl Sync for MpvManager {}
impl MpvManager {
/// Загрузка API из первой доступной библиотеки mpv.
unsafe fn load_mpv_api() -> Result {
- let dll_names = [
+ #[cfg(target_os = "windows")]
+ let library_names = [
"libmpv-2.dll",
"mpv-2.dll",
"libmpv-1.dll",
"mpv-1.dll",
];
- for dll_name in &dll_names {
- if let Ok(loaded) = MpvApi::load(dll_name) {
+ #[cfg(target_os = "linux")]
+ let library_names = {
+ let mut names = Vec::new();
+ if let Ok(path) = std::env::var("L_MPV_LIBMPV") {
+ names.push(path);
+ }
+ if let Ok(exe) = std::env::current_exe() {
+ if let Some(dir) = exe.parent() {
+ names.push(dir.join("../lib/l-mpv/lib/libmpv.so.2").to_string_lossy().into_owned());
+ names.push(dir.join("libmpv.so.2").to_string_lossy().into_owned());
+ }
+ }
+ names.extend(["/usr/lib/l-mpv/lib/libmpv.so.2", "libmpv.so.2", "libmpv.so.1", "libmpv.so"].map(String::from));
+ names
+ };
+
+ for library_name in &library_names {
+ if let Ok(loaded) = MpvApi::load(library_name) {
return Ok(loaded);
}
}
- Err(
- "Не найдена библиотека libmpv-2.dll / \
- mpv-2.dll. Пожалуйста, скачайте её и \
- поместите рядом с исполняемым файлом."
- .to_string(),
- )
+ Err(format!("Не найдена библиотека libmpv (проверены: {}).", library_names.join(", ")))
}
/// Создание нового экземпляра менеджера mpv.
@@ -155,9 +215,17 @@ impl MpvManager {
Self::set_option(&api, handle, "screenshot-directory", &screenshots_dir);
Self::set_option(&api, handle, "screenshot-format", "png");
- // ─── Оптимизированный рендеринг: Direct3D 11 (нативный для Windows / DWM) ───
+ // ─── Оптимизированный платформенный рендеринг ───
+ #[cfg(target_os = "windows")]
Self::set_option(&api, handle, "vo", "gpu-next");
+ #[cfg(target_os = "linux")]
+ Self::set_option(&api, handle, "vo", "libmpv");
+ #[cfg(target_os = "windows")]
Self::set_option(&api, handle, "gpu-api", "d3d11,auto");
+ #[cfg(target_os = "linux")]
+ // GtkGLArea supplies an EGL/OpenGL context on both native Wayland
+ // and X11. Vulkan remains available independently to NCNN.
+ Self::set_option(&api, handle, "gpu-api", "opengl");
Self::set_option(&api, handle, "hwdec", "auto-safe");
// Отключаем лог-файл и снижаем уровень логирования для исключения дискового I/O
@@ -179,7 +247,10 @@ impl MpvManager {
Self::set_option(&api, handle, "cache-pause", "no"); // Не ставить на паузу при буферизации локальных файлов
// ─── Качественный отзывчивый звук (WASAPI) ───
+ #[cfg(target_os = "windows")]
Self::set_option(&api, handle, "ao", "wasapi"); // Высококачественный драйвер Windows WASAPI
+ #[cfg(target_os = "linux")]
+ Self::set_option(&api, handle, "ao", "pipewire,pulse,alsa");
Self::set_option(&api, handle, "audio-buffer", "0.2"); // Отзывчивый размер буфера для плавной перемотки
Self::set_option(&api, handle, "audio-channels", "auto-safe"); // Автоопределение каналов оборудования
Self::set_option(&api, handle, "audio-pitch-correction", "yes"); // Сохранение тональности при изменении скорости
@@ -220,6 +291,9 @@ impl MpvManager {
Ok(Self {
handle: Mutex::new(handle),
+ render_context: Mutex::new(std::ptr::null_mut()),
+ #[cfg(target_os = "linux")]
+ render_pending: AtomicBool::new(true),
api,
})
}
@@ -295,6 +369,22 @@ impl MpvManager {
})
}
+ pub fn get_property_i64(&self, name: &str) -> Result {
+ self.with_handle(|handle| {
+ let c_name = CString::new(name).map_err(|e| e.to_string())?;
+ let mut value = 0i64;
+ let result = unsafe {
+ (self.api.get_property)(
+ handle,
+ c_name.as_ptr(),
+ MpvFormat::Int64,
+ &mut value as *mut i64 as *mut c_void,
+ )
+ };
+ if result < 0 { Err(format!("Ошибка чтения свойства {name}: {result}")) } else { Ok(value) }
+ })
+ }
+
pub fn set_property_string(&self, name: &str, value: &str) -> Result<(), String> {
self.with_handle(|handle| {
let c_name = CString::new(name).map_err(|e| format!("Ошибка CString: {}", e))?;
@@ -330,6 +420,146 @@ impl MpvManager {
})
}
+ /// Creates the Linux OpenGL render context while GtkGLArea's context is current.
+ #[cfg(target_os = "linux")]
+ pub fn initialize_opengl_renderer(&self) -> Result<(), String> {
+ let mut render_context = self
+ .render_context
+ .lock()
+ .map_err(|_| "Ошибка блокировки render context".to_string())?;
+ if !render_context.is_null() {
+ return Ok(());
+ }
+
+ unsafe extern "C" fn get_proc_address(
+ _ctx: *mut c_void,
+ name: *const c_char,
+ ) -> *mut c_void {
+ #[link(name = "dl")]
+ unsafe extern "C" {
+ fn dlsym(handle: *mut c_void, name: *const c_char) -> *mut c_void;
+ }
+ #[link(name = "EGL")]
+ unsafe extern "C" {
+ fn eglGetProcAddress(name: *const c_char) -> *mut c_void;
+ }
+
+ unsafe {
+ let symbol = dlsym(std::ptr::null_mut(), name);
+ if symbol.is_null() {
+ eglGetProcAddress(name)
+ } else {
+ symbol
+ }
+ }
+ }
+
+ let mut init_params = MpvOpenGlInitParams {
+ get_proc_address: Some(get_proc_address),
+ get_proc_address_ctx: std::ptr::null_mut(),
+ };
+ let api_type = b"opengl\0".as_ptr() as *mut c_void;
+ let mut params = [
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_API_TYPE,
+ data: api_type,
+ },
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
+ data: &mut init_params as *mut MpvOpenGlInitParams as *mut c_void,
+ },
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_INVALID,
+ data: std::ptr::null_mut(),
+ },
+ ];
+
+ self.with_handle(|handle| {
+ let result = unsafe {
+ (self.api.render_context_create)(&mut *render_context, handle, params.as_mut_ptr())
+ };
+ if result < 0 {
+ Err(format!("mpv_render_context_create завершился с кодом {result}"))
+ } else {
+ unsafe extern "C" fn request_render(ctx: *mut c_void) {
+ let pending = unsafe { &*(ctx as *const AtomicBool) };
+ pending.store(true, Ordering::Release);
+ }
+ unsafe {
+ (self.api.render_context_set_update_callback)(
+ *render_context,
+ Some(request_render),
+ &self.render_pending as *const AtomicBool as *mut c_void,
+ );
+ }
+ self.render_pending.store(true, Ordering::Release);
+ Ok(())
+ }
+ })
+ }
+
+ #[cfg(target_os = "linux")]
+ pub fn take_render_request(&self) -> bool {
+ self.render_pending.swap(false, Ordering::AcqRel)
+ }
+
+ /// Renders the current video frame into GtkGLArea's active framebuffer.
+ #[cfg(target_os = "linux")]
+ pub fn render_opengl_frame(&self, fbo_id: i32, width: i32, height: i32) -> Result<(), String> {
+ let render_context = self
+ .render_context
+ .lock()
+ .map_err(|_| "Ошибка блокировки render context".to_string())?;
+ if render_context.is_null() {
+ return Ok(());
+ }
+
+ let mut fbo = MpvOpenGlFbo {
+ fbo: fbo_id,
+ width,
+ height,
+ internal_format: 0,
+ };
+ let mut flip_y: c_int = 1;
+ let mut params = [
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_OPENGL_FBO,
+ data: &mut fbo as *mut MpvOpenGlFbo as *mut c_void,
+ },
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_FLIP_Y,
+ data: &mut flip_y as *mut c_int as *mut c_void,
+ },
+ MpvRenderParam {
+ param_type: MPV_RENDER_PARAM_INVALID,
+ data: std::ptr::null_mut(),
+ },
+ ];
+ let result = unsafe { (self.api.render_context_render)(*render_context, params.as_mut_ptr()) };
+ if result < 0 {
+ Err(format!("mpv_render_context_render завершился с кодом {result}"))
+ } else {
+ Ok(())
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ pub fn free_opengl_renderer(&self) {
+ if let Ok(mut render_context) = self.render_context.lock() {
+ if !render_context.is_null() {
+ unsafe {
+ (self.api.render_context_set_update_callback)(
+ *render_context,
+ None,
+ std::ptr::null_mut(),
+ )
+ };
+ unsafe { (self.api.render_context_free)(*render_context) };
+ *render_context = std::ptr::null_mut();
+ }
+ }
+ }
+
pub fn get_property_bool(&self, name: &str) -> Result {
self.with_handle(|handle| {
let c_name = CString::new(name).map_err(|e| format!("Ошибка CString: {}", e))?;
diff --git a/src-tauri/src/upscale/config.rs b/src-tauri/src/upscale/config.rs
index c135c31..7234aa4 100644
--- a/src-tauri/src/upscale/config.rs
+++ b/src-tauri/src/upscale/config.rs
@@ -7,17 +7,27 @@ use std::path::PathBuf;
/// Возвращает корневой каталог приложения
pub fn get_app_root_dir() -> PathBuf {
- if let Ok(exe_path) = std::env::current_exe() {
- if let Some(exe_dir) = exe_path.parent() {
- return exe_dir.to_path_buf();
+ #[cfg(target_os = "linux")]
+ {
+ return crate::commands::app_data_root();
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ if let Ok(exe_path) = std::env::current_exe() {
+ if let Some(exe_dir) = exe_path.parent() {
+ return exe_dir.to_path_buf();
+ }
}
+ PathBuf::from(".")
}
- PathBuf::from(".")
}
/// Возвращает путь к универсальной папке моделей `models/onnx/`
pub fn get_models_dir() -> PathBuf {
let root = get_app_root_dir();
+ #[cfg(target_os = "linux")]
+ let models_dir = root.join("models").join("ncnn");
+ #[cfg(not(target_os = "linux"))]
let models_dir = root.join("models").join("onnx");
if !models_dir.exists() {
let _ = fs::create_dir_all(&models_dir);
@@ -158,6 +168,7 @@ pub fn has_compiled_engine_for_model(
}
/// Сканирует папку `models/onnx/` и формирует список моделей
+#[cfg(not(target_os = "linux"))]
pub fn scan_onnx_models_internal() -> Vec {
let models_dir = get_models_dir();
let mut items = Vec::new();
@@ -273,7 +284,56 @@ pub fn scan_onnx_models_internal() -> Vec {
items
}
+/// Linux models are distributed as preconverted NCNN `.param` + `.bin` pairs.
+#[cfg(target_os = "linux")]
+pub fn scan_onnx_models_internal() -> Vec {
+ let models_dir = get_models_dir();
+ let saved_order: Vec = fs::read_to_string(get_models_order_path())
+ .ok()
+ .and_then(|value| serde_json::from_str(&value).ok())
+ .unwrap_or_default();
+ let mut paths = fs::read_dir(&models_dir)
+ .into_iter()
+ .flatten()
+ .flatten()
+ .map(|entry| entry.path())
+ .filter(|path| {
+ path.extension().and_then(|ext| ext.to_str()) == Some("param")
+ && path.with_extension("bin").is_file()
+ })
+ .collect::>();
+ paths.sort_by(|a, b| {
+ let a = a.file_name().unwrap_or_default().to_string_lossy();
+ let b = b.file_name().unwrap_or_default().to_string_lossy();
+ match (
+ saved_order.iter().position(|item| item.eq_ignore_ascii_case(&a)),
+ saved_order.iter().position(|item| item.eq_ignore_ascii_case(&b)),
+ ) {
+ (Some(a), Some(b)) => a.cmp(&b),
+ (Some(_), None) => std::cmp::Ordering::Less,
+ (None, Some(_)) => std::cmp::Ordering::Greater,
+ (None, None) => a.cmp(&b),
+ }
+ });
+
+ paths.into_iter().enumerate().map(|(index, path)| {
+ let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
+ let stem = path.file_stem().unwrap_or_default().to_string_lossy();
+ let size_bytes = fs::metadata(&path).map(|m| m.len()).unwrap_or(0)
+ + fs::metadata(path.with_extension("bin")).map(|m| m.len()).unwrap_or(0);
+ ModelFileItem {
+ filename,
+ display_name: stem.replace(['_', '-'], " "),
+ size_bytes,
+ slot: (index + 1) as u32,
+ full_path: path.to_string_lossy().into_owned(),
+ has_engine_1080p: true,
+ }
+ }).collect()
+}
+
/// Проверяет физическое наличие всех требуемых библиотек инференса и статус подсистемы
+#[cfg(not(target_os = "linux"))]
pub fn check_upscale_status_internal() -> UpscaleStatus {
let inf_dir = get_inference_dir();
let root = get_app_root_dir();
@@ -316,6 +376,8 @@ pub fn check_upscale_status_internal() -> UpscaleStatus {
aji_present,
directml_present,
tensorrt_present,
+ ncnn_present: false,
+ platform: std::env::consts::OS.to_string(),
models_count: models.len(),
models_dir: models_dir.to_string_lossy().to_string(),
models,
@@ -323,6 +385,25 @@ pub fn check_upscale_status_internal() -> UpscaleStatus {
}
}
+#[cfg(target_os = "linux")]
+pub fn check_upscale_status_internal() -> UpscaleStatus {
+ let models_dir = get_models_dir();
+ let models = scan_onnx_models_internal();
+ let ncnn_present = super::linux::find_ncnn_plugin().is_some();
+ UpscaleStatus {
+ filter_supported: ncnn_present,
+ aji_present: false,
+ directml_present: false,
+ tensorrt_present: false,
+ ncnn_present,
+ platform: "linux".to_string(),
+ models_count: models.len(),
+ models_dir: models_dir.to_string_lossy().into_owned(),
+ models,
+ gpu_info: detect_system_gpu(),
+ }
+}
+
/// Записывает конфигурационный файл `upscale.conf` для нативного фильтра инференса
pub fn write_upscale_conf(backend: &str, default_slot: u32) -> Result {
let conf_path = get_upscale_conf_path();
diff --git a/src-tauri/src/upscale/controller.rs b/src-tauri/src/upscale/controller.rs
index 69be13b..2260574 100644
--- a/src-tauri/src/upscale/controller.rs
+++ b/src-tauri/src/upscale/controller.rs
@@ -1,13 +1,17 @@
//! Контроллер взаимодействия с медиа-движком libmpv и OSD при апскейлинге.
-use super::config::{ensure_inference_environment, get_inference_dir, get_models_dir, write_upscale_conf};
+use super::config::{get_inference_dir, get_models_dir};
+#[cfg(not(target_os = "linux"))]
+use super::config::{ensure_inference_environment, write_upscale_conf};
use super::types::UpscaleSettings;
use crate::commands::PlayerState;
use crate::mpv_manager::MpvManager;
use std::process::Command;
+#[cfg(not(target_os = "linux"))]
use std::sync::Mutex;
use tauri::State;
+#[cfg(not(target_os = "linux"))]
static LAST_APPLIED_BACKEND: Mutex> = Mutex::new(None);
/// Принудительно заставляет mpv перерисовать и отобразить апскейленный кадр,
@@ -28,6 +32,11 @@ pub fn apply_upscale_settings_impl(
state: &State<'_, PlayerState>,
settings: UpscaleSettings,
) -> Result<(), String> {
+ #[cfg(target_os = "linux")]
+ return super::linux::apply(state, &settings);
+
+ #[cfg(not(target_os = "linux"))]
+ {
let mut active_slot = settings.active_slot;
// Миграция старых CRC-слотов (2000-9999) на новые 1-9
@@ -80,6 +89,7 @@ pub fn apply_upscale_settings_impl(
}
Ok(())
+ }
}
/// Переключение видов нейросетей по горячим клавишам Shift+1..4 на лету
@@ -88,6 +98,11 @@ pub fn switch_upscale_network_hotkey_impl(
slot: u32,
backend: Option,
) -> Result<(), String> {
+ #[cfg(target_os = "linux")]
+ return super::linux::switch_model(state, slot, backend);
+
+ #[cfg(not(target_os = "linux"))]
+ {
let chosen_backend = backend.unwrap_or_else(|| {
let last = LAST_APPLIED_BACKEND.lock().unwrap();
last.clone().unwrap_or_else(|| "DirectML".to_string())
@@ -130,12 +145,17 @@ pub fn switch_upscale_network_hotkey_impl(
force_frame_refresh(&state.mpv);
Ok(())
+ }
}
/// Открытие папки моделей в системном Проводнике Windows
pub fn open_models_folder_impl() -> Result<(), String> {
let dir = get_models_dir();
- Command::new("explorer.exe")
+ #[cfg(target_os = "linux")]
+ let mut command = Command::new("xdg-open");
+ #[cfg(not(target_os = "linux"))]
+ let mut command = Command::new("explorer.exe");
+ command
.arg(&dir)
.spawn()
.map_err(|e| format!("Не удалось открыть Проводник: {}", e))?;
@@ -145,7 +165,11 @@ pub fn open_models_folder_impl() -> Result<(), String> {
/// Открытие каталога библиотек инференса в Проводнике Windows
pub fn open_inference_folder_impl() -> Result<(), String> {
let dir = get_inference_dir();
- Command::new("explorer.exe")
+ #[cfg(target_os = "linux")]
+ let mut command = Command::new("xdg-open");
+ #[cfg(not(target_os = "linux"))]
+ let mut command = Command::new("explorer.exe");
+ command
.arg(&dir)
.spawn()
.map_err(|e| format!("Не удалось открыть Проводник: {}", e))?;
diff --git a/src-tauri/src/upscale/hardware.rs b/src-tauri/src/upscale/hardware.rs
index de5a1d0..f900c85 100644
--- a/src-tauri/src/upscale/hardware.rs
+++ b/src-tauri/src/upscale/hardware.rs
@@ -212,6 +212,54 @@ pub fn detect_system_gpu() -> GpuHardwareInfo {
}
fn detect_system_gpu_uncached() -> GpuHardwareInfo {
+ #[cfg(target_os = "linux")]
+ {
+ let summary = std::process::Command::new("vulkaninfo")
+ .arg("--summary")
+ .output()
+ .ok()
+ .filter(|output| output.status.success())
+ .map(|output| String::from_utf8_lossy(&output.stdout).into_owned())
+ .unwrap_or_default();
+ let name = summary.lines()
+ .filter_map(|line| line.split_once("deviceName").map(|(_, value)| value.trim_start_matches([' ', '=']).trim().to_string()))
+ .max_by_key(|value| {
+ let lower = value.to_lowercase();
+ if lower.contains("nvidia") || lower.contains("radeon") || lower.contains("arc") { 2 }
+ else if lower.contains("intel") || lower.contains("amd") { 1 }
+ else { 0 }
+ })
+ .filter(|value| !value.is_empty())
+ .or_else(|| {
+ std::process::Command::new("lspci").output().ok().and_then(|output| {
+ String::from_utf8_lossy(&output.stdout).lines()
+ .find(|line| line.contains("VGA compatible controller") || line.contains("3D controller"))
+ .and_then(|line| line.split_once(": ").map(|(_, name)| name.to_string()))
+ })
+ })
+ .unwrap_or_else(|| "Vulkan GPU".to_string());
+ let lower = name.to_lowercase();
+ let (vendor, vendor_id) = if lower.contains("nvidia") {
+ ("NVIDIA", 0x10DE)
+ } else if lower.contains("amd") || lower.contains("radeon") {
+ ("AMD", 0x1002)
+ } else if lower.contains("intel") {
+ ("Intel", 0x8086)
+ } else {
+ ("Unknown", 0)
+ };
+ return GpuHardwareInfo {
+ name,
+ vendor: vendor.to_string(),
+ vendor_id,
+ device_id: 0,
+ recommended_backend: "NCNN Vulkan".to_string(),
+ supports_tensorrt: false,
+ sm_architecture: "vulkan".to_string(),
+ vram_bytes: 0,
+ };
+ }
+
#[cfg(windows)]
{
use windows::Win32::Graphics::Dxgi::{
@@ -305,7 +353,8 @@ fn detect_system_gpu_uncached() -> GpuHardwareInfo {
}
}
- GpuHardwareInfo {
+ #[cfg(not(target_os = "linux"))]
+ GpuHardwareInfo {
name: "Универсальный GPU".to_string(),
vendor: "Unknown".to_string(),
vendor_id: 0,
diff --git a/src-tauri/src/upscale/linux.rs b/src-tauri/src/upscale/linux.rs
new file mode 100644
index 0000000..de2f136
--- /dev/null
+++ b/src-tauri/src/upscale/linux.rs
@@ -0,0 +1,198 @@
+//! Linux NCNN/Vulkan integration through mpv's VapourSynth bridge.
+
+use super::config::{get_app_root_dir, scan_onnx_models_internal};
+use super::types::UpscaleSettings;
+use crate::commands::PlayerState;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicU64, Ordering};
+use tauri::State;
+
+const CURATED_MODEL_URL: &str = "https://github.com/TNTwise/Universal-NCNN-Upscaler/releases/download/Animation/2x_AnimeJaNai_V2.zip";
+const CURATED_MODEL_SHA256: &str = "8a627d2697249d64c1e461c3c4f21f8fee500ba77012d7d19936972df00202cf";
+
+static WATCHDOG_GENERATION: AtomicU64 = AtomicU64::new(0);
+
+fn runtime_roots() -> Vec {
+ let mut roots = Vec::new();
+ if let Some(path) = std::env::var_os("L_MPV_RUNTIME_DIR") {
+ roots.push(path.into());
+ }
+ if let Some(appdir) = std::env::var_os("APPDIR") {
+ roots.push(PathBuf::from(appdir).join("usr/lib/l-mpv"));
+ }
+ if let Ok(exe) = std::env::current_exe() {
+ if let Some(bin) = exe.parent() {
+ roots.push(bin.join("../lib/l-mpv"));
+ roots.push(bin.to_path_buf());
+ }
+ }
+ roots.push(PathBuf::from("/usr/lib/l-mpv"));
+ roots.push(get_app_root_dir().join("runtime"));
+ roots
+}
+
+fn find_runtime_file(relative: &str) -> Option {
+ runtime_roots()
+ .into_iter()
+ .map(|root| root.join(relative))
+ .find(|path| path.is_file())
+}
+
+pub fn find_ncnn_plugin() -> Option {
+ find_runtime_file("vapoursynth/liblmpv_ncnn.so")
+}
+
+fn mpv_quote(value: &str) -> String {
+ format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
+}
+
+fn python_string(path: &Path) -> String {
+ format!("{:?}", path.to_string_lossy())
+}
+
+fn write_vapoursynth_script(param: &Path, plugin: &Path) -> Result {
+ let script_dir = get_app_root_dir().join("config");
+ std::fs::create_dir_all(&script_dir).map_err(|e| e.to_string())?;
+ let script_path = script_dir.join("linux-ncnn.vpy");
+ let bin = param.with_extension("bin");
+ let content = format!(
+ "import vapoursynth as vs\n\
+ core = vs.core\n\
+ core.std.LoadPlugin(path={plugin})\n\
+ rgb = core.resize.Bicubic(video_in, format=vs.RGBS, matrix_in_s='709')\n\
+ video_out = core.lmpvncnn.Model(rgb, param_path={param}, model_path={bin}, scale=2, tile_size=256, overlap=16, fp16=1)\n",
+ plugin = python_string(plugin),
+ param = python_string(param),
+ bin = python_string(&bin),
+ );
+ std::fs::write(&script_path, content).map_err(|e| e.to_string())?;
+ Ok(script_path)
+}
+
+fn disable_all(state: &State<'_, PlayerState>) {
+ WATCHDOG_GENERATION.fetch_add(1, Ordering::AcqRel);
+ let _ = state.mpv.command("vf remove @lmpv-ai");
+ let _ = state.mpv.set_property_string("glsl-shaders", "");
+ let _ = state.mpv.set_property_string("hwdec", "auto-safe");
+}
+
+fn enable_shader_fallback(mpv: &crate::mpv_manager::MpvManager) -> Result<(), String> {
+ let shaders = [
+ "shaders/FSRCNNX_x2_8-0-4-1.glsl",
+ "shaders/Anime4K_Upscale_CNN_x2_S.glsl",
+ ]
+ .into_iter()
+ .filter_map(find_runtime_file)
+ .collect::>();
+ if shaders.is_empty() {
+ mpv.set_property_string("scale", "ewa_lanczossharp")?;
+ return Ok(());
+ }
+ let list = shaders
+ .iter()
+ .map(|path| path.to_string_lossy())
+ .collect::>()
+ .join(":");
+ mpv.set_property_string("glsl-shaders", &list)
+}
+
+fn start_performance_watchdog(state: &State<'_, PlayerState>) {
+ let generation = WATCHDOG_GENERATION.fetch_add(1, Ordering::AcqRel) + 1;
+ let mpv = state.mpv.clone();
+ std::thread::spawn(move || {
+ std::thread::sleep(std::time::Duration::from_secs(4));
+ let initial = mpv.get_property_i64("vo-drop-frame-count").unwrap_or(0)
+ + mpv.get_property_i64("decoder-frame-drop-count").unwrap_or(0);
+ std::thread::sleep(std::time::Duration::from_secs(3));
+ if WATCHDOG_GENERATION.load(Ordering::Acquire) != generation {
+ return;
+ }
+ let current = mpv.get_property_i64("vo-drop-frame-count").unwrap_or(initial)
+ + mpv.get_property_i64("decoder-frame-drop-count").unwrap_or(0);
+ // More than five dropped frames in the sample means the filter cannot
+ // sustain smooth 24 FPS playback on this GPU/model combination.
+ if current.saturating_sub(initial) > 5 {
+ let _ = mpv.command("vf remove @lmpv-ai");
+ let _ = enable_shader_fallback(&mpv);
+ let _ = mpv.command("show-text \"NCNN не успевает: включён шейдерный fallback\" 4000");
+ eprintln!("[L-MPV][Upscale] NCNN is too slow; switched to packaged GLSL fallback");
+ }
+ });
+}
+
+pub fn apply(state: &State<'_, PlayerState>, settings: &UpscaleSettings) -> Result<(), String> {
+ disable_all(state);
+ if settings.mode != "ai" {
+ return Ok(());
+ }
+ let plugin = find_ncnn_plugin().ok_or_else(|| {
+ "NCNN/Vulkan runtime не найден. Установите полный Linux-пакет L-MPV.".to_string()
+ })?;
+ let models = scan_onnx_models_internal();
+ let model = models
+ .iter()
+ .find(|model| model.filename == settings.selected_model)
+ .or_else(|| models.iter().find(|model| model.slot == settings.active_slot))
+ .ok_or_else(|| "Не выбрана совместимая пара модели NCNN (.param + .bin).".to_string())?;
+ let script = write_vapoursynth_script(Path::new(&model.full_path), &plugin)?;
+
+ // VapourSynth currently maps frames through system memory, so hardware
+ // decoding is disabled while the NCNN filter is active.
+ state.mpv.set_property_string("hwdec", "no")?;
+ let filter = format!(
+ "@lmpv-ai:vapoursynth=file={}:buffered-frames=2:concurrent-frames=1",
+ script.to_string_lossy()
+ );
+ state.mpv.command(&format!("vf add {}", mpv_quote(&filter)))?;
+ start_performance_watchdog(state);
+ Ok(())
+}
+
+pub fn switch_model(
+ state: &State<'_, PlayerState>,
+ slot: u32,
+ backend: Option,
+) -> Result<(), String> {
+ if slot == 0 {
+ disable_all(state);
+ return Ok(());
+ }
+ apply(state, &UpscaleSettings {
+ mode: "ai".to_string(),
+ active_slot: slot,
+ backend: backend.unwrap_or_else(|| "NCNN Vulkan".to_string()),
+ selected_model: String::new(),
+ })
+}
+
+pub async fn download_curated_model() -> Result {
+ use sha2::{Digest, Sha256};
+ use std::io::{Cursor, Read};
+
+ let bytes = reqwest::get(CURATED_MODEL_URL).await
+ .map_err(|e| format!("Не удалось скачать модель: {e}"))?
+ .error_for_status().map_err(|e| format!("Сервер модели вернул ошибку: {e}"))?
+ .bytes().await.map_err(|e| e.to_string())?;
+ let digest = format!("{:x}", Sha256::digest(&bytes));
+ if digest != CURATED_MODEL_SHA256 {
+ return Err("Контрольная сумма NCNN-модели не совпала.".to_string());
+ }
+
+ let models_dir = super::config::get_models_dir();
+ let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| e.to_string())?;
+ let mut installed = 0;
+ for index in 0..archive.len() {
+ let mut entry = archive.by_index(index).map_err(|e| e.to_string())?;
+ let Some(name) = Path::new(entry.name()).file_name().map(|name| name.to_owned()) else { continue };
+ let extension = Path::new(&name).extension().and_then(|ext| ext.to_str()).unwrap_or_default();
+ if extension != "param" && extension != "bin" { continue; }
+ let mut data = Vec::new();
+ entry.read_to_end(&mut data).map_err(|e| e.to_string())?;
+ std::fs::write(models_dir.join(name), data).map_err(|e| e.to_string())?;
+ installed += 1;
+ }
+ if installed != 2 {
+ return Err("Архив не содержит полную пару .param/.bin.".to_string());
+ }
+ Ok("AnimeJaNai V2 NCNN установлена".to_string())
+}
diff --git a/src-tauri/src/upscale/mod.rs b/src-tauri/src/upscale/mod.rs
index 8bc8e65..6f9f35e 100644
--- a/src-tauri/src/upscale/mod.rs
+++ b/src-tauri/src/upscale/mod.rs
@@ -6,6 +6,8 @@ pub mod controller;
pub mod downloader;
pub mod engine_builder;
pub mod hardware;
+#[cfg(target_os = "linux")]
+pub mod linux;
pub mod types;
// Реэкспорт моделей данных для использования внешними модулями
@@ -32,6 +34,8 @@ pub fn get_upscale_status() -> UpscaleStatus {
aji_present: false,
directml_present: false,
tensorrt_present: false,
+ ncnn_present: false,
+ platform: std::env::consts::OS.to_string(),
models_count: 0,
models_dir: String::new(),
models: Vec::new(),
@@ -114,6 +118,14 @@ pub fn save_models_order(order: Vec) -> Result<(), String> {
config::save_models_order_internal(&order)
}
+#[tauri::command]
+pub async fn download_curated_ncnn_model() -> Result {
+ #[cfg(target_os = "linux")]
+ return linux::download_curated_model().await;
+ #[cfg(not(target_os = "linux"))]
+ Err("Каталог NCNN-моделей доступен только в Linux.".to_string())
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src-tauri/src/upscale/types.rs b/src-tauri/src/upscale/types.rs
index 43cfdd2..65a0396 100644
--- a/src-tauri/src/upscale/types.rs
+++ b/src-tauri/src/upscale/types.rs
@@ -75,6 +75,10 @@ pub struct UpscaleStatus {
pub directml_present: bool,
/// Наличие полного набора библиотек NVIDIA TensorRT (aji_trt.dll, nvinfer_11.dll)
pub tensorrt_present: bool,
+ /// Linux NCNN/Vulkan runtime and VapourSynth bridge are available.
+ pub ncnn_present: bool,
+ /// Runtime platform used to adapt the settings UI.
+ pub platform: String,
/// Количество обнаруженных ONNX моделей в папке models/onnx/
pub models_count: usize,
/// Путь к каталогу моделей
@@ -120,4 +124,3 @@ pub struct UpscaleCompileProgress {
/// Описание ошибки сборки (если возникла)
pub error: Option,
}
-
diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json
new file mode 100644
index 0000000..6949139
--- /dev/null
+++ b/src-tauri/tauri.linux.conf.json
@@ -0,0 +1,37 @@
+{
+ "app": {
+ "windows": [
+ {
+ "label": "main",
+ "title": "L-MPV",
+ "width": 1280,
+ "height": 720,
+ "minWidth": 560,
+ "minHeight": 180,
+ "decorations": true,
+ "transparent": true,
+ "resizable": true,
+ "center": true,
+ "visible": false
+ },
+ {
+ "label": "mediainfo",
+ "title": "L-MPV MediaInfo",
+ "width": 565,
+ "height": 685,
+ "minWidth": 460,
+ "minHeight": 360,
+ "decorations": false,
+ "transparent": false,
+ "resizable": true,
+ "alwaysOnTop": true,
+ "center": true,
+ "visible": false
+ }
+ ]
+ },
+ "bundle": {
+ "targets": ["appimage"],
+ "resources": []
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
index 8aebd0f..472d369 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -148,17 +148,20 @@ function App() {
// При каждом запуске плеера апскейлинг всегда гарантированно отключен по умолчанию
localStorage.setItem("l-mpv-upscale-mode", "off");
- // Фоновая проверка обновлений (показываем ненавязчивое уведомление в правом углу)
- invoke("check_launch_and_update")
- .then((info) => {
- if (info && info.has_update) {
- setPendingUpdate(info);
- setShowUpdateToast(true);
- }
+ invoke("get_runtime_platform")
+ .then((platform) => {
+ document.documentElement.dataset.platform = platform;
+ if (platform !== "windows") return;
+
+ // Portable-обновление заменяет exe/dll и поэтому доступно только в Windows.
+ return invoke("check_launch_and_update").then((info) => {
+ if (info && info.has_update) {
+ setPendingUpdate(info);
+ setShowUpdateToast(true);
+ }
+ });
})
- .catch((err) => {
- console.warn("Фоновая проверка обновлений пропущена:", err);
- });
+ .catch((err) => console.warn("Определение платформы/проверка обновлений пропущены:", err));
}, []);
// Синхронизация настройки автоскрытия панели в верхней половине окна
@@ -360,7 +363,7 @@ function App() {
const mode = localStorage.getItem("l-mpv-upscale-mode") || "off";
if (mode === "ai") {
const slot = parseInt(localStorage.getItem("l-mpv-upscale-slot") || "1001", 10);
- const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML";
+ const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML");
const selectedModel = localStorage.getItem("l-mpv-upscale-selected-model") || "";
invoke("apply_upscale_settings", {
settings: { mode, active_slot: slot, backend, selected_model: selectedModel }
@@ -701,7 +704,7 @@ function App() {
case "upscaleStats": {
try {
const mode = localStorage.getItem("l-mpv-upscale-mode") || "off";
- const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML";
+ const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML");
const slot = parseInt(localStorage.getItem("l-mpv-upscale-slot") || "1001", 10);
const selectedModel = localStorage.getItem("l-mpv-upscale-selected-model") || "";
@@ -761,7 +764,7 @@ function App() {
}
case "upscaleOff":
try {
- const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML";
+ const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML");
await invoke("switch_upscale_network_hotkey", { slot: 0, backend });
localStorage.setItem("l-mpv-upscale-mode", "off");
setOsdText("4K AI Апскейлинг: Выключен");
@@ -789,7 +792,7 @@ function App() {
if (targetModel) {
const slot = targetModel.slot;
- const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML";
+ const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML");
// При выборе любой модели по хоткею гарантированно активируем режим "ai"
localStorage.setItem("l-mpv-upscale-mode", "ai");
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index ce0a04d..1a736ba 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -103,6 +103,9 @@ export { AccordionSection } from "./settings/AccordionSection";
export type { AccordionSectionProps } from "./settings/AccordionSection";
export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
+ const [runtimePlatform, setRuntimePlatform] = useState(
+ () => document.documentElement.dataset.platform || "windows",
+ );
const [screenshotDir, setScreenshotDir] = useState("");
const [uiOpacity, setUiOpacity] = useState(() => getSavedUiOpacity());
const [activeColor, setActiveColor] = useState(() => {
@@ -136,6 +139,10 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
const [controlBarStyle, setControlBarStyle] = useState(() => getSavedControlBarStyle());
const [activeTab, setActiveTab] = useState<"general" | "appearance" | "presets" | "upscaling" | "hotkeys" | "integration">("general");
+ useEffect(() => {
+ invoke("get_runtime_platform").then(setRuntimePlatform).catch(() => {});
+ }, []);
+
const [isClosing, setIsClosing] = useState(false);
const closeTimerRef = useRef | null>(null);
@@ -190,8 +197,8 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
"presets",
"upscaling",
"hotkeys",
- "integration",
];
+ if (runtimePlatform === "windows") tabs.push("integration");
const currentIndex = tabs.indexOf(activeTab);
if (e.key === "ArrowRight") {
@@ -213,7 +220,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
return () => {
window.removeEventListener("keydown", handleKeyDown, true);
};
- }, [activeTab, isRecordingHotkey, handleClose]);
+ }, [activeTab, isRecordingHotkey, handleClose, runtimePlatform]);
// Синхронизация локальных состояний SettingsModal при применении любого пресета
const handlePresetApplied = useCallback((preset: SettingsPreset) => {
@@ -589,7 +596,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
{ id: "upscaling", label: "Апскейлинг", icon: Sparkles },
{ id: "hotkeys", label: "Хоткей", icon: Keyboard },
{ id: "integration", label: "Интеграция", icon: Link },
- ].map((tab) => {
+ ].filter((tab) => runtimePlatform === "windows" || tab.id !== "integration").map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
@@ -732,7 +739,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) {
v{appVersion}
- Проверить обновления
>
)}
-
+ }
{updateStatus && (
)}
- Портативная редакция
+
+ {runtimePlatform === "windows" ? "Портативная редакция" : "Linux · Wayland/X11"}
+
diff --git a/src/components/settings/AppearanceSettingsTab.tsx b/src/components/settings/AppearanceSettingsTab.tsx
index 24819d2..5b1c4d3 100644
--- a/src/components/settings/AppearanceSettingsTab.tsx
+++ b/src/components/settings/AppearanceSettingsTab.tsx
@@ -820,10 +820,10 @@ export function AppearanceSettingsTab(props: AppearanceSettingsTabProps) {
{/* 5. Аудио-визуалайзер на панели управления */}
- toggleSection("app_visualizer")}
- />
+ />}
{/* 6. Видимость кнопок панели управления */}
{
+ ].filter(btn => document.documentElement.dataset.platform !== "linux" || btn.id !== "visualizer").map(btn => {
const isChecked = visibleButtons[btn.id] !== undefined
? visibleButtons[btn.id]
: btn.defaultChecked;
diff --git a/src/components/settings/UpscalingSettingsSection.tsx b/src/components/settings/UpscalingSettingsSection.tsx
index d160429..a481d75 100644
--- a/src/components/settings/UpscalingSettingsSection.tsx
+++ b/src/components/settings/UpscalingSettingsSection.tsx
@@ -7,6 +7,7 @@ import {
Layers,
Eye,
EyeOff,
+ Download,
} from "lucide-react";
import {
getCustomHotkeys,
@@ -50,6 +51,7 @@ export const UpscalingSettingsSection: React.FC =
const [errorMessage, setErrorMessage] = useState(null);
const [downloadProgressText, setDownloadProgressText] = useState(null);
const [downloadProgress, setDownloadProgress] = useState(null);
+ const [downloadingNcnnModel, setDownloadingNcnnModel] = useState(false);
// Хоткеи
const [customHotkeys, setCustomHotkeys] = useState>(() => getCustomHotkeys());
@@ -127,7 +129,7 @@ export const UpscalingSettingsSection: React.FC =
const [settings, setSettings] = useState(() => {
try {
const savedMode = (localStorage.getItem("l-mpv-upscale-mode") as "off" | "ai") || "off";
- const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as "DirectML" | "TensorRT") || "DirectML";
+ const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as UpscaleSettings["backend"]) || "DirectML";
const savedSlot = Number(localStorage.getItem("l-mpv-upscale-slot") || 1001);
const savedModel = localStorage.getItem("l-mpv-upscale-selected-model") || "";
return {
@@ -166,6 +168,10 @@ export const UpscalingSettingsSection: React.FC =
const currentStatus = await invoke("get_upscale_status");
if (isMountedRef.current) {
setStatus(currentStatus);
+ if (currentStatus.platform === "linux") {
+ setSettings((previous) => ({ ...previous, backend: "NCNN Vulkan" }));
+ localStorage.setItem("l-mpv-upscale-backend", "NCNN Vulkan");
+ }
// Очищаем ошибку загрузки статуса при успешном получении
setErrorMessage((prev) =>
prev === "Не удалось получить статус компонентов апскейлинга" ? null : prev
@@ -268,7 +274,7 @@ export const UpscalingSettingsSection: React.FC =
if (isMountedRef.current) {
setCustomHotkeys(getCustomHotkeys());
const savedMode = (localStorage.getItem("l-mpv-upscale-mode") as "off" | "ai") || "off";
- const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as "DirectML" | "TensorRT") || "DirectML";
+ const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as UpscaleSettings["backend"]) || "DirectML";
const savedSlot = Number(localStorage.getItem("l-mpv-upscale-slot") || 1001);
const savedModel = localStorage.getItem("l-mpv-upscale-selected-model") || "";
setSettings({
@@ -306,7 +312,9 @@ export const UpscalingSettingsSection: React.FC =
// Проверяем, установлен ли движок при попытке включить режим "ai"
const isDmlInstalled = !!(status?.directml_present && status?.aji_present);
const isTrtInstalled = !!(status?.tensorrt_present && status?.aji_present);
- const isInstalled = updated.backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
+ const isInstalled = updated.backend === "NCNN Vulkan"
+ ? !!status?.ncnn_present
+ : updated.backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
if (updated.mode === "ai" && !isInstalled) {
setErrorMessage(`Для включения апскейлинга необходимо сначала скачать библиотеки движка ${updated.backend}.`);
@@ -325,7 +333,7 @@ export const UpscalingSettingsSection: React.FC =
};
// Переключение выбранного бэкенда без лишних перерисовок кадра, если апскейл выключен
- const handleSelectBackend = async (backend: "DirectML" | "TensorRT") => {
+ const handleSelectBackend = async (backend: UpscaleSettings["backend"]) => {
const updated: UpscaleSettings = { ...settings, backend };
setSettings(updated);
setErrorMessage(null);
@@ -335,7 +343,9 @@ export const UpscalingSettingsSection: React.FC =
if (updated.mode === "ai") {
const isDmlInstalled = !!(status?.directml_present && status?.aji_present);
const isTrtInstalled = !!(status?.tensorrt_present && status?.aji_present);
- const isInstalled = backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
+ const isInstalled = backend === "NCNN Vulkan"
+ ? !!status?.ncnn_present
+ : backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
if (isInstalled) {
try {
@@ -368,6 +378,20 @@ export const UpscalingSettingsSection: React.FC =
}
};
+ const handleDownloadNcnnModel = async () => {
+ setDownloadingNcnnModel(true);
+ setErrorMessage(null);
+ try {
+ const message = await invoke("download_curated_ncnn_model");
+ setEngineSuccessMessage(message);
+ await refreshStatus();
+ } catch (error) {
+ setErrorMessage(String(error));
+ } finally {
+ setDownloadingNcnnModel(false);
+ }
+ };
+
// Фоновое скачивание библиотек инференса с прогресс-баром
const handleDownloadEngine = async () => {
if (downloadTimerRef.current) {
@@ -553,14 +577,25 @@ export const UpscalingSettingsSection: React.FC =
- Папка нейросетей (models/onnx/)
+ Папка нейросетей ({status?.platform === "linux" ? "models/ncnn/" : "models/onnx/"})
- Обнаружено файлов ONNX-моделей: {status?.models_count || 0}
+ Обнаружено моделей {status?.platform === "linux" ? "NCNN" : "ONNX"}: {status?.models_count || 0}
+ {status?.platform === "linux" && (
+
+ {downloadingNcnnModel ? "Загрузка…" : "AnimeJaNai V2"}
+
+ )}
=
})
) : (
- В папке models/onnx/ не найдено совместимых моделей .onnx.
+ В папке {status?.platform === "linux" ? "models/ncnn/ не найдено пар .param + .bin" : "models/onnx/ не найдено совместимых моделей .onnx"}.
Нажмите значок папки справа вверху и скопируйте файлы моделей.
diff --git a/src/components/upscale/BackendSelector.tsx b/src/components/upscale/BackendSelector.tsx
index 04eaaf3..b020687 100644
--- a/src/components/upscale/BackendSelector.tsx
+++ b/src/components/upscale/BackendSelector.tsx
@@ -20,7 +20,7 @@ interface BackendSelectorProps {
downloadProgress: DownloadProgressPayload | null;
engineSuccessMessage: string | null;
errorMessage: string | null;
- onSelectBackend: (backend: "DirectML" | "TensorRT") => void;
+ onSelectBackend: (backend: "DirectML" | "TensorRT" | "NCNN Vulkan") => void;
onOpenInferenceFolder: () => void;
onDownloadEngine: () => void;
onDeleteEngine: () => void;
@@ -44,9 +44,11 @@ export const BackendSelector: React.FC = ({
}) => {
const isDmlInstalled = !!(status?.directml_present && status?.aji_present);
const isTrtInstalled = !!(status?.tensorrt_present && status?.aji_present);
+ const isLinux = status?.platform === "linux";
+ const isNcnnInstalled = !!status?.ncnn_present;
const isCurrentBackendInstalled =
- settings.backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
- const isAnyEnginePresent = isDmlInstalled || isTrtInstalled;
+ settings.backend === "NCNN Vulkan" ? isNcnnInstalled : settings.backend === "DirectML" ? isDmlInstalled : isTrtInstalled;
+ const isAnyEnginePresent = isDmlInstalled || isTrtInstalled || isNcnnInstalled;
const isFinished = !!(downloadProgress && downloadProgress.is_finished && !downloadProgress.error);
const isError = !!(downloadProgress && downloadProgress.error);
@@ -91,7 +93,7 @@ export const BackendSelector: React.FC = ({
textOverflow: "ellipsis",
}}
>
- Библиотеки выполнения нейросетей (aji.dll, DirectML, OnnxRuntime, TensorRT)
+ {isLinux ? "NCNN с Vulkan для NVIDIA, AMD и Intel" : "Библиотеки выполнения нейросетей (aji.dll, DirectML, OnnxRuntime, TensorRT)"}
@@ -125,6 +127,10 @@ export const BackendSelector: React.FC = ({
Установлен
+ ) : isLinux ? (
+
+ NCNN runtime отсутствует
+
) : (
= ({
)}
- {isAnyEnginePresent && (
+ {!isLinux && isAnyEnginePresent && (
= ({
)}
- {/* Выбор между DirectML и TensorRT */}
+ {isLinux ? (
+
+
+
+
+ NCNN Vulkan
+
+
+ {isNcnnInstalled ? "Установлен" : "Не установлен"}
+
+ Рекомендуется
+
+
+
+
+ Кросс-вендорный Vulkan-инференс для NVIDIA, AMD и Intel. При недостаточной скорости плеер автоматически включает FSRCNNX/Anime4K.
+
+
+ ) : (
+ /* Выбор между DirectML и TensorRT */
{/* Карточка DirectML */}
= ({
+ )}
);
};
diff --git a/src/components/upscale/types.ts b/src/components/upscale/types.ts
index 8a125ae..cbe28d5 100644
--- a/src/components/upscale/types.ts
+++ b/src/components/upscale/types.ts
@@ -12,7 +12,7 @@ export interface GpuHardwareInfo {
vendor: string;
vendor_id: number;
device_id: number;
- recommended_backend: "TensorRT" | "DirectML";
+ recommended_backend: "TensorRT" | "DirectML" | "NCNN Vulkan";
supports_tensorrt: boolean;
sm_architecture: string;
vram_bytes: number;
@@ -23,6 +23,8 @@ export interface UpscaleStatus {
aji_present: boolean;
directml_present: boolean;
tensorrt_present: boolean;
+ ncnn_present: boolean;
+ platform: string;
models_count: number;
models_dir: string;
models: ModelFileItem[];
@@ -32,7 +34,7 @@ export interface UpscaleStatus {
export interface UpscaleSettings {
mode: "off" | "ai";
active_slot: number;
- backend: "DirectML" | "TensorRT";
+ backend: "DirectML" | "TensorRT" | "NCNN Vulkan";
selected_model: string;
}
@@ -54,4 +56,3 @@ export interface UpscaleCompileProgress {
is_finished: boolean;
error?: string | null;
}
-
diff --git a/src/index.css b/src/index.css
index 3c9585c..c5d9bb3 100644
--- a/src/index.css
+++ b/src/index.css
@@ -44,3 +44,27 @@
/* 13. Пресеты настроек */
@import './styles/presets.css';
+/* WASAPI visualizer is intentionally unavailable in the Linux build. */
+html[data-platform='linux'] #btn-visualizer {
+ display: none !important;
+}
+
+html[data-platform='linux'] .color-circle--windows {
+ display: none !important;
+}
+
+/* X11 native child windows do not forward pointer motion through GtkGLArea,
+ so the DOM idle timer cannot reliably reveal controls again. Keep Linux
+ controls available; Windows retains the cinematic auto-hide behavior. */
+html[data-platform='linux'] .app-container--idle .titlebar,
+html[data-platform='linux'] .app-container--hide-controls .titlebar,
+html[data-platform='linux'] .app-container--idle .player-controls-wrapper,
+html[data-platform='linux'] .app-container--hide-controls .player-controls-wrapper {
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+}
+
+html[data-platform='linux'] .app-container--idle {
+ cursor: default;
+}
diff --git a/vendor/tauri-runtime-wry/.cargo-ok b/vendor/tauri-runtime-wry/.cargo-ok
new file mode 100644
index 0000000..5f8b795
--- /dev/null
+++ b/vendor/tauri-runtime-wry/.cargo-ok
@@ -0,0 +1 @@
+{"v":1}
\ No newline at end of file
diff --git a/vendor/tauri-runtime-wry/.cargo_vcs_info.json b/vendor/tauri-runtime-wry/.cargo_vcs_info.json
new file mode 100644
index 0000000..e517113
--- /dev/null
+++ b/vendor/tauri-runtime-wry/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "ca90b46b2e2cbbc981dae1b809f4af4343fe0558"
+ },
+ "path_in_vcs": "crates/tauri-runtime-wry"
+}
\ No newline at end of file
diff --git a/vendor/tauri-runtime-wry/Cargo.lock b/vendor/tauri-runtime-wry/Cargo.lock
new file mode 100644
index 0000000..30c6e0e
--- /dev/null
+++ b/vendor/tauri-runtime-wry/Cargo.lock
@@ -0,0 +1,3593 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytemuck"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.7.0",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
+dependencies = [
+ "bitflags 2.7.0",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.7.0",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.7.0",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ctor"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
+dependencies = [
+ "ctor-proc-macro",
+ "dtor",
+]
+
+[[package]]
+name = "ctor-proc-macro"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dtor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.7.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core 0.52.0",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.178"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.7.0",
+ "libc",
+]
+
+[[package]]
+name = "litemap"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.7.0",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
+dependencies = [
+ "proc-macro-crate 3.2.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
+dependencies = [
+ "bitflags 2.7.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation 0.3.0",
+ "objc2-quartz-core 0.3.0",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.0",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+
+[[package]]
+name = "plist"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
+dependencies = [
+ "base64",
+ "indexmap 2.11.4",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
+dependencies = [
+ "toml_edit 0.20.7",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+dependencies = [
+ "toml_edit 0.22.24",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+dependencies = [
+ "bitflags 2.7.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror 2.0.12",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+dependencies = [
+ "bitflags 2.7.0",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
+dependencies = [
+ "base64",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.11.4",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "softbuffer"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
+dependencies = [
+ "bytemuck",
+ "cfg_aliases",
+ "core-graphics 0.24.0",
+ "foreign-types",
+ "js-sys",
+ "log",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
+ "raw-window-handle",
+ "redox_syscall",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.20",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159"
+dependencies = [
+ "bitflags 2.7.0",
+ "block2 0.6.2",
+ "core-foundation",
+ "core-graphics 0.25.0",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-sys",
+ "objc2 0.6.4",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.0",
+ "objc2-ui-kit",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core 0.61.0",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri-runtime"
+version = "2.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2 0.6.4",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.12",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.11.4"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2 0.6.4",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "tracing",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
+dependencies = [
+ "anyhow",
+ "ctor",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "regex",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "thiserror 2.0.12",
+ "toml 0.9.12+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
+dependencies = [
+ "new_debug_unreachable",
+ "utf-8",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "time"
+version = "0.3.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.8",
+ "toml_datetime 0.6.8",
+ "toml_edit 0.22.24",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.11.4",
+ "serde_core",
+ "serde_spanned 1.0.4",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.11.4",
+ "toml_datetime 0.6.8",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
+dependencies = [
+ "indexmap 2.11.4",
+ "toml_datetime 0.6.8",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+dependencies = [
+ "indexmap 2.11.4",
+ "serde",
+ "serde_spanned 0.6.8",
+ "toml_datetime 0.6.8",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
+
+[[package]]
+name = "typenum"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web_atoms"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core 0.61.0",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
+dependencies = [
+ "thiserror 2.0.12",
+ "windows",
+ "windows-core 0.61.0",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.61.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.0",
+ "windows-future",
+ "windows-link",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.0",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
+dependencies = [
+ "windows-core 0.61.0",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.0",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "wry"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429"
+dependencies = [
+ "base64",
+ "block2 0.6.2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2 0.6.4",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.0",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.12",
+ "tracing",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.0",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
diff --git a/vendor/tauri-runtime-wry/Cargo.toml b/vendor/tauri-runtime-wry/Cargo.toml
new file mode 100644
index 0000000..0b2a1b4
--- /dev/null
+++ b/vendor/tauri-runtime-wry/Cargo.toml
@@ -0,0 +1,151 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.77.2"
+name = "tauri-runtime-wry"
+version = "2.11.4"
+authors = ["Tauri Programme within The Commons Conservancy"]
+build = "build.rs"
+exclude = [
+ "CHANGELOG.md",
+ "/target",
+]
+autolib = false
+autobins = false
+autoexamples = false
+autotests = false
+autobenches = false
+description = "Wry bindings to the Tauri runtime"
+homepage = "https://tauri.app/"
+readme = "README.md"
+categories = [
+ "gui",
+ "web-programming",
+]
+license = "Apache-2.0 OR MIT"
+repository = "https://github.com/tauri-apps/tauri"
+
+[features]
+common-controls-v6 = []
+dbus = ["tao/dbus"]
+default = [
+ "x11",
+ "dbus",
+]
+devtools = [
+ "wry/devtools",
+ "tauri-runtime/devtools",
+]
+macos-private-api = [
+ "wry/fullscreen",
+ "wry/transparent",
+ "tauri-runtime/macos-private-api",
+]
+macos-proxy = ["wry/mac-proxy"]
+objc-exception = []
+tracing = [
+ "dep:tracing",
+ "wry/tracing",
+]
+unstable = []
+x11 = [
+ "tao/x11",
+ "wry/x11",
+]
+
+[lib]
+name = "tauri_runtime_wry"
+path = "src/lib.rs"
+
+[dependencies.http]
+version = "1"
+
+[dependencies.log]
+version = "0.4.21"
+
+[dependencies.raw-window-handle]
+version = "0.6"
+
+[dependencies.tao]
+version = "0.35.0"
+features = ["rwh_06"]
+default-features = false
+
+[dependencies.tauri-runtime]
+version = "2.11.3"
+
+[dependencies.tauri-utils]
+version = "2.9.3"
+
+[dependencies.tracing]
+version = "0.1"
+optional = true
+
+[dependencies.url]
+version = "2"
+
+[dependencies.wry]
+version = "0.55.0"
+features = [
+ "protocol",
+ "os-webview",
+ "linux-body",
+]
+default-features = false
+
+[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk]
+version = "0.18"
+features = ["v3_24"]
+
+[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.percent-encoding]
+version = "2"
+
+[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk]
+version = "=2.0"
+features = ["v2_40"]
+
+[target.'cfg(target_os = "android")'.dependencies.jni]
+version = "0.21"
+
+[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit]
+version = "0.3"
+features = [
+ "block2",
+ "NSApplication",
+ "NSResponder",
+ "NSView",
+ "NSWindow",
+ "NSScreen",
+ "NSGraphics",
+]
+default-features = false
+
+[target.'cfg(target_vendor = "apple")'.dependencies.objc2]
+version = "0.6"
+
+[target."cfg(windows)".dependencies.once_cell]
+version = "1.20"
+
+[target."cfg(windows)".dependencies.softbuffer]
+version = "0.4"
+default-features = false
+
+[target."cfg(windows)".dependencies.webview2-com]
+version = "0.38"
+
+[target."cfg(windows)".dependencies.windows]
+version = "0.61"
+features = [
+ "Win32_Foundation",
+ "Win32_Graphics_Dwm",
+]
diff --git a/vendor/tauri-runtime-wry/Cargo.toml.orig b/vendor/tauri-runtime-wry/Cargo.toml.orig
new file mode 100644
index 0000000..7c576de
--- /dev/null
+++ b/vendor/tauri-runtime-wry/Cargo.toml.orig
@@ -0,0 +1,76 @@
+[package]
+name = "tauri-runtime-wry"
+version = "2.11.4"
+description = "Wry bindings to the Tauri runtime"
+exclude = ["CHANGELOG.md", "/target"]
+readme = "README.md"
+authors.workspace = true
+homepage.workspace = true
+repository.workspace = true
+categories.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+wry = { version = "0.55.0", default-features = false, features = [
+ "protocol",
+ "os-webview",
+ "linux-body",
+] }
+tao = { version = "0.35.0", default-features = false, features = ["rwh_06"] }
+tauri-runtime = { version = "2.11.3", path = "../tauri-runtime" }
+tauri-utils = { version = "2.9.3", path = "../tauri-utils" }
+raw-window-handle = "0.6"
+http = "1"
+url = "2"
+tracing = { version = "0.1", optional = true }
+log = "0.4.21"
+
+[target."cfg(windows)".dependencies]
+webview2-com = "0.38"
+softbuffer = { version = "0.4", default-features = false }
+once_cell = "1.20"
+
+[target."cfg(windows)".dependencies.windows]
+version = "0.61"
+features = ["Win32_Foundation", "Win32_Graphics_Dwm"]
+
+[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
+gtk = { version = "0.18", features = ["v3_24"] }
+webkit2gtk = { version = "=2.0", features = ["v2_40"] }
+percent-encoding = "2"
+
+[target.'cfg(target_vendor = "apple")'.dependencies]
+objc2 = "0.6"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+objc2-app-kit = { version = "0.3", default-features = false, features = [
+ "block2",
+ "NSApplication",
+ "NSResponder",
+ "NSView",
+ "NSWindow",
+ "NSScreen",
+ "NSGraphics",
+] }
+
+[target."cfg(target_os = \"android\")".dependencies]
+jni = "0.21"
+
+[features]
+default = ["x11", "dbus"]
+devtools = ["wry/devtools", "tauri-runtime/devtools"]
+x11 = ["tao/x11", "wry/x11"]
+macos-private-api = [
+ "wry/fullscreen",
+ "wry/transparent",
+ "tauri-runtime/macos-private-api",
+]
+# TODO: Remove in v3 - wry does not have this feature anymore
+objc-exception = []
+tracing = ["dep:tracing", "wry/tracing"]
+macos-proxy = ["wry/mac-proxy"]
+unstable = []
+common-controls-v6 = []
+dbus = ["tao/dbus"]
diff --git a/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0 b/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/vendor/tauri-runtime-wry/LICENSE_MIT b/vendor/tauri-runtime-wry/LICENSE_MIT
new file mode 100644
index 0000000..b08530d
--- /dev/null
+++ b/vendor/tauri-runtime-wry/LICENSE_MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 - Present Tauri Apps Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/tauri-runtime-wry/README.md b/vendor/tauri-runtime-wry/README.md
new file mode 100644
index 0000000..a452527
--- /dev/null
+++ b/vendor/tauri-runtime-wry/README.md
@@ -0,0 +1,44 @@
+# tauri-runtime-wry
+
+
+
+[](https://github.com/tauri-apps/tauri)
+[](https://discord.gg/SpmNs4S)
+
+[](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
+[](https://tauri.app)
+
+[](https://good-labs.github.io/greater-good-affirmation)
+[](https://opencollective.com/tauri)
+
+| Component | Version |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| tauri-runtime-wry | [](https://crates.io/crates/tauri-runtime-wry) |
+
+## About Tauri
+
+Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
+
+Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
+
+## This module
+
+This crate opens up direct systems-level interactions specifically for WRY, such as printing, monitor detection, and other windowing related tasks. `tauri-runtime` implementation for WRY.
+None of the exposed API of this crate is stable, and it may break semver
+compatibility in the future. The major version only signifies the intended Tauri version.
+
+To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
+
+## Semver
+
+**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
+
+## Licenses
+
+Code: (c) 2021 - The Tauri Programme within The Commons Conservancy.
+
+MIT or MIT/Apache 2.0 where applicable.
+
+Logo: CC-BY-NC-ND
+
+- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)
diff --git a/vendor/tauri-runtime-wry/build.rs b/vendor/tauri-runtime-wry/build.rs
new file mode 100644
index 0000000..9974c50
--- /dev/null
+++ b/vendor/tauri-runtime-wry/build.rs
@@ -0,0 +1,19 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+// creates a cfg alias if `has_feature` is true.
+// `alias` must be a snake case string.
+fn alias(alias: &str, has_feature: bool) {
+ println!("cargo:rustc-check-cfg=cfg({alias})");
+ if has_feature {
+ println!("cargo:rustc-cfg={alias}");
+ }
+}
+
+fn main() {
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
+ let mobile = target_os == "ios" || target_os == "android";
+ alias("desktop", !mobile);
+ alias("mobile", mobile);
+}
diff --git a/vendor/tauri-runtime-wry/src/dialog/mod.rs b/vendor/tauri-runtime-wry/src/dialog/mod.rs
new file mode 100644
index 0000000..83d8922
--- /dev/null
+++ b/vendor/tauri-runtime-wry/src/dialog/mod.rs
@@ -0,0 +1,19 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#[cfg(windows)]
+mod windows;
+
+// Takes a `&'static str` here since we convert clickable hyperlinks,
+// DO NOT pass in untrusted input
+#[cfg_attr(not(windows), allow(unused))]
+pub fn error(err: &'static str) {
+ #[cfg(windows)]
+ windows::error(err);
+
+ #[cfg(not(windows))]
+ {
+ unimplemented!("Error dialog is not implemented for this platform");
+ }
+}
diff --git a/vendor/tauri-runtime-wry/src/dialog/windows.rs b/vendor/tauri-runtime-wry/src/dialog/windows.rs
new file mode 100644
index 0000000..0f9b2c2
--- /dev/null
+++ b/vendor/tauri-runtime-wry/src/dialog/windows.rs
@@ -0,0 +1,124 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use windows::core::{w, HSTRING};
+
+enum Level {
+ Error,
+ #[allow(unused)]
+ Warning,
+ #[allow(unused)]
+ Info,
+}
+
+pub fn error(err: &'static str) {
+ dialog_inner(err, Level::Error);
+}
+
+fn dialog_inner(err: &str, level: Level) {
+ let title = match level {
+ Level::Warning => w!("Warning"),
+ Level::Error => w!("Error"),
+ Level::Info => w!("Info"),
+ };
+
+ #[cfg(not(feature = "common-controls-v6"))]
+ {
+ use windows::Win32::UI::WindowsAndMessaging::*;
+
+ let err = remove_hyperlink(err);
+ let err = HSTRING::from(err);
+
+ unsafe {
+ MessageBoxW(
+ None,
+ &err,
+ title,
+ match level {
+ Level::Warning => MB_ICONWARNING,
+ Level::Error => MB_ICONERROR,
+ Level::Info => MB_ICONINFORMATION,
+ },
+ )
+ };
+ }
+
+ #[cfg(feature = "common-controls-v6")]
+ {
+ use windows::core::{HRESULT, PCWSTR};
+ use windows::Win32::Foundation::*;
+ use windows::Win32::UI::Controls::*;
+ use windows::Win32::UI::Shell::*;
+ use windows::Win32::UI::WindowsAndMessaging::*;
+
+ extern "system" fn task_dialog_callback(
+ _hwnd: HWND,
+ msg: TASKDIALOG_NOTIFICATIONS,
+ _wparam: WPARAM,
+ lparam: LPARAM,
+ _data: isize,
+ ) -> HRESULT {
+ if msg == TDN_HYPERLINK_CLICKED {
+ let link = PCWSTR(lparam.0 as _);
+ let _ = unsafe { ShellExecuteW(None, None, link, None, None, SW_SHOWNORMAL) };
+ }
+
+ S_OK
+ }
+
+ let err = HSTRING::from(err);
+ let err = PCWSTR(err.as_ptr());
+
+ let task_dialog_config = TASKDIALOGCONFIG {
+ cbSize: std::mem::size_of::() as u32,
+ dwFlags: TDF_ALLOW_DIALOG_CANCELLATION | TDF_ENABLE_HYPERLINKS,
+ pszWindowTitle: title,
+ pszContent: err,
+ Anonymous1: TASKDIALOGCONFIG_0 {
+ pszMainIcon: match level {
+ Level::Warning => TD_WARNING_ICON,
+ Level::Error => TD_ERROR_ICON,
+ Level::Info => TD_INFORMATION_ICON,
+ },
+ },
+ dwCommonButtons: TDCBF_OK_BUTTON,
+ pfCallback: Some(task_dialog_callback),
+ ..Default::default()
+ };
+
+ let _ = unsafe { TaskDialogIndirect(&task_dialog_config, None, None, None) };
+ }
+}
+
+#[cfg(not(feature = "common-controls-v6"))]
+fn remove_hyperlink(str: &str) -> String {
+ let mut result = String::new();
+ let mut in_hyperlink = false;
+
+ for c in str.chars() {
+ if c == '<' {
+ in_hyperlink = true;
+ } else if c == '>' {
+ in_hyperlink = false;
+ } else if !in_hyperlink {
+ result.push(c);
+ }
+ }
+
+ result
+}
+
+#[cfg(test)]
+#[cfg(not(feature = "common-controls-v6"))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_remove_hyperlink() {
+ let input = "This is a test string.";
+ let expected = "This is a test string.";
+ let result = remove_hyperlink(input);
+ assert_eq!(result, expected);
+ }
+}
diff --git a/vendor/tauri-runtime-wry/src/lib.rs b/vendor/tauri-runtime-wry/src/lib.rs
new file mode 100644
index 0000000..3a08bc4
--- /dev/null
+++ b/vendor/tauri-runtime-wry/src/lib.rs
@@ -0,0 +1,5510 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+//! The [`wry`] Tauri [`Runtime`].
+//!
+//! None of the exposed API of this crate is stable, and it may break semver
+//! compatibility in the future. The major version only signifies the intended Tauri version.
+
+#![doc(
+ html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
+ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
+)]
+
+use self::monitor::MonitorExt;
+use http::Request;
+#[cfg(target_os = "macos")]
+use objc2::ClassType;
+use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
+
+#[cfg(windows)]
+use tauri_runtime::webview::ScrollBarStyle;
+use tauri_runtime::{
+ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
+ monitor::Monitor,
+ webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler},
+ window::{
+ CursorIcon, DetachedWindow, DetachedWindowWebview, DragDropEvent, PendingWindow, RawWindow,
+ WebviewEvent, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints,
+ },
+ Cookie, DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon,
+ ProgressBarState, ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs,
+ UserAttentionType, UserEvent, WebviewDispatch, WebviewEventId, WindowDispatch, WindowEventId,
+};
+
+#[cfg(target_vendor = "apple")]
+use objc2::rc::Retained;
+#[cfg(target_os = "android")]
+use tao::platform::android::{WindowBuilderExtAndroid, WindowExtAndroid};
+#[cfg(target_os = "macos")]
+use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS};
+#[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+use tao::platform::unix::{WindowBuilderExtUnix, WindowExtUnix};
+#[cfg(windows)]
+use tao::platform::windows::{WindowBuilderExtWindows, WindowExtWindows};
+#[cfg(windows)]
+use webview2_com::{ContainsFullScreenElementChangedEventHandler, FocusChangedEventHandler};
+#[cfg(windows)]
+use windows::Win32::Foundation::HWND;
+#[cfg(target_os = "ios")]
+use wry::WebViewBuilderExtIos;
+#[cfg(target_os = "macos")]
+use wry::WebViewBuilderExtMacos;
+#[cfg(windows)]
+use wry::WebViewBuilderExtWindows;
+#[cfg(target_vendor = "apple")]
+use wry::{WebViewBuilderExtDarwin, WebViewExtDarwin};
+
+use tao::{
+ dpi::{
+ LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize,
+ PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize,
+ Position as TaoPosition, Size as TaoSize,
+ },
+ event::{Event, StartCause, WindowEvent as TaoWindowEvent},
+ event_loop::{
+ ControlFlow, DeviceEventFilter as TaoDeviceEventFilter, EventLoop, EventLoopBuilder,
+ EventLoopProxy as TaoEventLoopProxy, EventLoopWindowTarget,
+ },
+ monitor::MonitorHandle,
+ window::{
+ CursorIcon as TaoCursorIcon, Fullscreen, Icon as TaoWindowIcon,
+ ProgressBarState as TaoProgressBarState, ProgressState as TaoProgressState, Theme as TaoTheme,
+ UserAttentionType as TaoUserAttentionType,
+ },
+};
+use tauri_utils::config::PreventOverflowConfig;
+#[cfg(target_os = "macos")]
+use tauri_utils::TitleBarStyle;
+use tauri_utils::{
+ config::{Color, WindowConfig},
+ Theme,
+};
+use url::Url;
+#[cfg(windows)]
+use wry::ScrollBarStyle as WryScrollBarStyle;
+use wry::{
+ DragDropEvent as WryDragDropEvent, ProxyConfig, ProxyEndpoint, WebContext as WryWebContext,
+ WebView, WebViewBuilder,
+};
+
+pub use tao;
+pub use tao::window::{Window, WindowBuilder as TaoWindowBuilder, WindowId as TaoWindowId};
+pub use wry;
+pub use wry::webview_version;
+
+#[cfg(windows)]
+use wry::WebViewExtWindows;
+#[cfg(target_os = "android")]
+use wry::{
+ prelude::{dispatch, find_class},
+ WebViewBuilderExtAndroid, WebViewExtAndroid,
+};
+#[cfg(not(any(
+ target_os = "windows",
+ target_os = "macos",
+ target_os = "ios",
+ target_os = "android"
+)))]
+use wry::{WebViewBuilderExtUnix, WebViewExtUnix};
+
+#[cfg(target_os = "ios")]
+pub use tao::platform::ios::{WindowBuilderExtIOS, WindowExtIOS};
+#[cfg(target_os = "macos")]
+pub use tao::platform::macos::{
+ ActivationPolicy as TaoActivationPolicy, EventLoopExtMacOS, WindowExtMacOS,
+};
+#[cfg(target_os = "macos")]
+use tauri_runtime::ActivationPolicy;
+
+use std::{
+ cell::RefCell,
+ collections::{
+ hash_map::Entry::{Occupied, Vacant},
+ BTreeMap, HashMap, HashSet,
+ },
+ fmt,
+ ops::Deref,
+ path::PathBuf,
+ rc::Rc,
+ sync::{
+ atomic::{AtomicBool, AtomicU32, Ordering},
+ mpsc::{channel, Sender},
+ Arc, Mutex, Weak,
+ },
+ thread::{current as current_thread, ThreadId},
+};
+
+pub type WebviewId = u32;
+type IpcHandler = dyn Fn(Request) + 'static;
+
+#[cfg(not(debug_assertions))]
+mod dialog;
+mod monitor;
+#[cfg(any(
+ windows,
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+mod undecorated_resizing;
+mod util;
+mod webview;
+mod window;
+
+pub use webview::Webview;
+use window::WindowExt as _;
+
+#[derive(Debug)]
+pub struct WebContext {
+ pub inner: WryWebContext,
+ pub referenced_by_webviews: HashSet,
+ // on Linux the custom protocols are associated with the context
+ // and you cannot register a URI scheme more than once
+ pub registered_custom_protocols: HashSet,
+}
+
+pub type WebContextStore = Arc, WebContext>>>;
+// window
+pub type WindowEventHandler = Box;
+pub type WindowEventListeners = Arc>>;
+pub type WebviewEventHandler = Box;
+pub type WebviewEventListeners = Arc>>;
+
+#[derive(Debug, Clone, Default)]
+pub struct WindowIdStore(Arc>>);
+
+impl WindowIdStore {
+ pub fn insert(&self, w: TaoWindowId, id: WindowId) {
+ self.0.lock().unwrap().insert(w, id);
+ }
+
+ pub fn get(&self, w: &TaoWindowId) -> Option {
+ self.0.lock().unwrap().get(w).copied()
+ }
+}
+
+#[macro_export]
+macro_rules! getter {
+ ($self: ident, $rx: expr, $message: expr) => {{
+ $crate::send_user_message(&$self.context, $message)?;
+ $rx
+ .recv()
+ .map_err(|_| $crate::Error::FailedToReceiveMessage)
+ }};
+}
+
+macro_rules! window_getter {
+ ($self: ident, $message: expr) => {{
+ let (tx, rx) = channel();
+ getter!($self, rx, Message::Window($self.window_id, $message(tx)))
+ }};
+}
+
+macro_rules! event_loop_window_getter {
+ ($self: ident, $message: expr) => {{
+ let (tx, rx) = channel();
+ getter!($self, rx, Message::EventLoopWindowTarget($message(tx)))
+ }};
+}
+
+macro_rules! webview_getter {
+ ($self: ident, $message: expr) => {{
+ let (tx, rx) = channel();
+ getter!(
+ $self,
+ rx,
+ Message::Webview(
+ *$self.window_id.lock().unwrap(),
+ $self.webview_id,
+ $message(tx)
+ )
+ )
+ }};
+}
+
+pub(crate) fn send_user_message(
+ context: &Context,
+ message: Message,
+) -> Result<()> {
+ if current_thread().id() == context.main_thread_id {
+ handle_user_message(
+ &context.main_thread.window_target,
+ message,
+ UserMessageContext {
+ window_id_map: context.window_id_map.clone(),
+ windows: context.main_thread.windows.clone(),
+ },
+ );
+ Ok(())
+ } else {
+ context
+ .proxy
+ .send_event(message)
+ .map_err(|_| Error::FailedToSendMessage)
+ }
+}
+
+#[derive(Clone)]
+pub struct Context {
+ pub window_id_map: WindowIdStore,
+ main_thread_id: ThreadId,
+ pub proxy: TaoEventLoopProxy>,
+ main_thread: DispatcherMainThreadContext,
+ plugins: Arc + Send>>>>,
+ next_window_id: Arc,
+ next_webview_id: Arc,
+ next_window_event_id: Arc,
+ next_webview_event_id: Arc,
+ webview_runtime_installed: bool,
+}
+
+impl Context {
+ pub fn run_threaded(&self, f: F) -> R
+ where
+ F: FnOnce(Option<&DispatcherMainThreadContext>) -> R,
+ {
+ f(if current_thread().id() == self.main_thread_id {
+ Some(&self.main_thread)
+ } else {
+ None
+ })
+ }
+
+ fn next_window_id(&self) -> WindowId {
+ self.next_window_id.fetch_add(1, Ordering::Relaxed).into()
+ }
+
+ fn next_webview_id(&self) -> WebviewId {
+ self.next_webview_id.fetch_add(1, Ordering::Relaxed)
+ }
+
+ fn next_window_event_id(&self) -> u32 {
+ self.next_window_event_id.fetch_add(1, Ordering::Relaxed)
+ }
+
+ fn next_webview_event_id(&self) -> u32 {
+ self.next_webview_event_id.fetch_add(1, Ordering::Relaxed)
+ }
+}
+
+impl Context {
+ fn create_window(
+ &self,
+ pending: PendingWindow>,
+ after_window_creation: Option,
+ ) -> Result>> {
+ let label = pending.label.clone();
+ let context = self.clone();
+ let window_id = self.next_window_id();
+ let (webview_id, use_https_scheme) = pending
+ .webview
+ .as_ref()
+ .map(|w| {
+ (
+ Some(context.next_webview_id()),
+ w.webview_attributes.use_https_scheme,
+ )
+ })
+ .unwrap_or((None, false));
+
+ send_user_message(
+ self,
+ Message::CreateWindow(
+ window_id,
+ Box::new(move |event_loop| {
+ create_window(
+ window_id,
+ webview_id.unwrap_or_default(),
+ event_loop,
+ &context,
+ pending,
+ after_window_creation,
+ )
+ }),
+ ),
+ )?;
+
+ let dispatcher = WryWindowDispatcher {
+ window_id,
+ context: self.clone(),
+ };
+
+ let detached_webview = webview_id.map(|id| {
+ let webview = DetachedWebview {
+ label: label.clone(),
+ dispatcher: WryWebviewDispatcher {
+ window_id: Arc::new(Mutex::new(window_id)),
+ webview_id: id,
+ context: self.clone(),
+ },
+ };
+ DetachedWindowWebview {
+ webview,
+ use_https_scheme,
+ }
+ });
+
+ Ok(DetachedWindow {
+ id: window_id,
+ label,
+ dispatcher,
+ webview: detached_webview,
+ })
+ }
+
+ fn create_webview(
+ &self,
+ window_id: WindowId,
+ pending: PendingWebview>,
+ ) -> Result>> {
+ let label = pending.label.clone();
+ let context = self.clone();
+
+ let webview_id = self.next_webview_id();
+
+ let window_id_wrapper = Arc::new(Mutex::new(window_id));
+ let window_id_wrapper_ = window_id_wrapper.clone();
+
+ send_user_message(
+ self,
+ Message::CreateWebview(
+ window_id,
+ Box::new(move |window, options| {
+ create_webview(
+ WebviewKind::WindowChild,
+ window,
+ window_id_wrapper_,
+ webview_id,
+ &context,
+ pending,
+ options.focused_webview,
+ )
+ }),
+ ),
+ )?;
+
+ let dispatcher = WryWebviewDispatcher {
+ window_id: window_id_wrapper,
+ webview_id,
+ context: self.clone(),
+ };
+
+ Ok(DetachedWebview { label, dispatcher })
+ }
+}
+
+#[cfg(feature = "tracing")]
+#[derive(Debug, Clone, Default)]
+pub struct ActiveTraceSpanStore(Rc>>);
+
+#[cfg(feature = "tracing")]
+impl ActiveTraceSpanStore {
+ pub fn remove_window_draw(&self) {
+ self
+ .0
+ .borrow_mut()
+ .retain(|t| !matches!(t, ActiveTracingSpan::WindowDraw { id: _, span: _ }));
+ }
+}
+
+#[cfg(feature = "tracing")]
+#[derive(Debug)]
+pub enum ActiveTracingSpan {
+ WindowDraw {
+ id: TaoWindowId,
+ span: tracing::span::EnteredSpan,
+ },
+}
+
+#[derive(Debug)]
+pub struct WindowsStore(pub RefCell>);
+
+// SAFETY: we ensure this type is only used on the main thread.
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for WindowsStore {}
+
+// SAFETY: we ensure this type is only used on the main thread.
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Sync for WindowsStore {}
+
+#[derive(Debug, Clone)]
+pub struct DispatcherMainThreadContext {
+ pub window_target: EventLoopWindowTarget>,
+ pub web_context: WebContextStore,
+ // changing this to an Rc will cause frequent app crashes.
+ pub windows: Arc,
+ #[cfg(feature = "tracing")]
+ pub active_tracing_spans: ActiveTraceSpanStore,
+}
+
+// SAFETY: we ensure this type is only used on the main thread.
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for DispatcherMainThreadContext {}
+
+// SAFETY: we ensure this type is only used on the main thread.
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Sync for DispatcherMainThreadContext {}
+
+impl fmt::Debug for Context {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Context")
+ .field("main_thread_id", &self.main_thread_id)
+ .field("proxy", &self.proxy)
+ .field("main_thread", &self.main_thread)
+ .finish()
+ }
+}
+
+pub struct DeviceEventFilterWrapper(pub TaoDeviceEventFilter);
+
+impl From for DeviceEventFilterWrapper {
+ fn from(item: DeviceEventFilter) -> Self {
+ match item {
+ DeviceEventFilter::Always => Self(TaoDeviceEventFilter::Always),
+ DeviceEventFilter::Never => Self(TaoDeviceEventFilter::Never),
+ DeviceEventFilter::Unfocused => Self(TaoDeviceEventFilter::Unfocused),
+ }
+ }
+}
+
+pub struct RectWrapper(pub wry::Rect);
+impl From for RectWrapper {
+ fn from(value: tauri_runtime::dpi::Rect) -> Self {
+ RectWrapper(wry::Rect {
+ position: value.position,
+ size: value.size,
+ })
+ }
+}
+
+/// Wrapper around a [`tao::window::Icon`] that can be created from an [`Icon`].
+pub struct TaoIcon(pub TaoWindowIcon);
+
+impl TryFrom> for TaoIcon {
+ type Error = Error;
+ fn try_from(icon: Icon<'_>) -> std::result::Result {
+ TaoWindowIcon::from_rgba(icon.rgba.to_vec(), icon.width, icon.height)
+ .map(Self)
+ .map_err(|e| Error::InvalidIcon(Box::new(e)))
+ }
+}
+
+pub struct WindowEventWrapper(pub Option);
+
+impl WindowEventWrapper {
+ fn map_from_tao(
+ event: &TaoWindowEvent<'_>,
+ #[allow(unused_variables)] window: &WindowWrapper,
+ ) -> Self {
+ let event = match event {
+ TaoWindowEvent::Resized(size) => WindowEvent::Resized(PhysicalSizeWrapper(*size).into()),
+ TaoWindowEvent::Moved(position) => {
+ WindowEvent::Moved(PhysicalPositionWrapper(*position).into())
+ }
+ TaoWindowEvent::Destroyed => WindowEvent::Destroyed,
+ TaoWindowEvent::ScaleFactorChanged {
+ scale_factor,
+ new_inner_size,
+ } => WindowEvent::ScaleFactorChanged {
+ scale_factor: *scale_factor,
+ new_inner_size: PhysicalSizeWrapper(**new_inner_size).into(),
+ },
+ TaoWindowEvent::Focused(focused) => {
+ #[cfg(not(windows))]
+ return Self(Some(WindowEvent::Focused(*focused)));
+ // on multiwebview mode, if there's no focused webview, it means we're receiving a direct window focus change
+ // (without receiving a webview focus, such as when clicking the taskbar app icon or using Alt + Tab)
+ // in this case we must send the focus change event here
+ #[cfg(windows)]
+ #[allow(clippy::collapsible_match)]
+ if window.has_children.load(Ordering::Relaxed) {
+ const FOCUSED_WEBVIEW_MARKER: &str = "__tauriWindow?";
+ let mut focused_webview = window.focused_webview.lock().unwrap();
+ // when we focus a webview and the window was previously focused, we get a blur event here
+ // so on blur we should only send events if the current focus is owned by the window
+ if !*focused
+ && focused_webview
+ .as_deref()
+ .is_some_and(|w| w != FOCUSED_WEBVIEW_MARKER)
+ {
+ return Self(None);
+ }
+
+ // reset focused_webview on blur, or set to a dummy value on focus
+ // (to prevent double focus event when we click a webview after focusing a window)
+ *focused_webview = if *focused {
+ Some(FOCUSED_WEBVIEW_MARKER.to_owned())
+ } else {
+ None
+ };
+
+ WindowEvent::Focused(*focused)
+ } else {
+ // when not on multiwebview mode, we handle focus change events on the webview (add_GotFocus and add_LostFocus)
+ return Self(None);
+ }
+ }
+ TaoWindowEvent::ThemeChanged(theme) => WindowEvent::ThemeChanged(map_theme(theme)),
+ _ => return Self(None),
+ };
+ Self(Some(event))
+ }
+
+ fn parse(window: &WindowWrapper, event: &TaoWindowEvent<'_>) -> Self {
+ match event {
+ // resized event from tao doesn't include a reliable size on macOS
+ // because wry replaces the NSView
+ TaoWindowEvent::Resized(_) => {
+ if let Some(w) = &window.inner {
+ let size = inner_size(
+ w,
+ &window.webviews,
+ window.has_children.load(Ordering::Relaxed),
+ );
+ Self(Some(WindowEvent::Resized(PhysicalSizeWrapper(size).into())))
+ } else {
+ Self(None)
+ }
+ }
+ e => Self::map_from_tao(e, window),
+ }
+ }
+}
+
+pub fn map_theme(theme: &TaoTheme) -> Theme {
+ match theme {
+ TaoTheme::Light => Theme::Light,
+ TaoTheme::Dark => Theme::Dark,
+ _ => Theme::Light,
+ }
+}
+
+#[cfg(target_os = "macos")]
+fn tao_activation_policy(activation_policy: ActivationPolicy) -> TaoActivationPolicy {
+ match activation_policy {
+ ActivationPolicy::Regular => TaoActivationPolicy::Regular,
+ ActivationPolicy::Accessory => TaoActivationPolicy::Accessory,
+ ActivationPolicy::Prohibited => TaoActivationPolicy::Prohibited,
+ _ => unimplemented!(),
+ }
+}
+
+pub struct MonitorHandleWrapper(pub MonitorHandle);
+
+impl From for Monitor {
+ fn from(monitor: MonitorHandleWrapper) -> Monitor {
+ Self {
+ name: monitor.0.name(),
+ position: PhysicalPositionWrapper(monitor.0.position()).into(),
+ size: PhysicalSizeWrapper(monitor.0.size()).into(),
+ work_area: monitor.0.work_area(),
+ scale_factor: monitor.0.scale_factor(),
+ }
+ }
+}
+
+pub struct PhysicalPositionWrapper(pub TaoPhysicalPosition);
+
+impl From> for PhysicalPosition {
+ fn from(position: PhysicalPositionWrapper) -> Self {
+ Self {
+ x: position.0.x,
+ y: position.0.y,
+ }
+ }
+}
+
+impl From> for PhysicalPositionWrapper {
+ fn from(position: PhysicalPosition) -> Self {
+ Self(TaoPhysicalPosition {
+ x: position.x,
+ y: position.y,
+ })
+ }
+}
+
+struct LogicalPositionWrapper(TaoLogicalPosition);
+
+impl From> for LogicalPositionWrapper {
+ fn from(position: LogicalPosition) -> Self {
+ Self(TaoLogicalPosition {
+ x: position.x,
+ y: position.y,
+ })
+ }
+}
+
+pub struct PhysicalSizeWrapper(pub TaoPhysicalSize);
+
+impl From> for PhysicalSize {
+ fn from(size: PhysicalSizeWrapper) -> Self {
+ Self {
+ width: size.0.width,
+ height: size.0.height,
+ }
+ }
+}
+
+impl From> for PhysicalSizeWrapper {
+ fn from(size: PhysicalSize) -> Self {
+ Self(TaoPhysicalSize {
+ width: size.width,
+ height: size.height,
+ })
+ }
+}
+
+struct LogicalSizeWrapper(TaoLogicalSize);
+
+impl From> for LogicalSizeWrapper {
+ fn from(size: LogicalSize) -> Self {
+ Self(TaoLogicalSize {
+ width: size.width,
+ height: size.height,
+ })
+ }
+}
+
+pub struct SizeWrapper(pub TaoSize);
+
+impl From for SizeWrapper {
+ fn from(size: Size) -> Self {
+ match size {
+ Size::Logical(s) => Self(TaoSize::Logical(LogicalSizeWrapper::from(s).0)),
+ Size::Physical(s) => Self(TaoSize::Physical(PhysicalSizeWrapper::from(s).0)),
+ }
+ }
+}
+
+pub struct PositionWrapper(pub TaoPosition);
+
+impl From for PositionWrapper {
+ fn from(position: Position) -> Self {
+ match position {
+ Position::Logical(s) => Self(TaoPosition::Logical(LogicalPositionWrapper::from(s).0)),
+ Position::Physical(s) => Self(TaoPosition::Physical(PhysicalPositionWrapper::from(s).0)),
+ }
+ }
+}
+
+#[cfg(desktop)]
+fn find_monitor_for_position(
+ monitors: impl Iterator- ,
+ window_position: TaoPosition,
+) -> Option
{
+ monitors.into_iter().find(|m| {
+ let monitor_pos = m.position();
+ let monitor_size = m.size();
+
+ // type annotations required for 32bit targets.
+ let window_position = window_position.to_physical::(m.scale_factor());
+
+ monitor_pos.x <= window_position.x
+ && window_position.x < monitor_pos.x + monitor_size.width as i32
+ && monitor_pos.y <= window_position.y
+ && window_position.y < monitor_pos.y + monitor_size.height as i32
+ })
+}
+
+#[derive(Debug, Clone)]
+pub struct UserAttentionTypeWrapper(pub TaoUserAttentionType);
+
+impl From for UserAttentionTypeWrapper {
+ fn from(request_type: UserAttentionType) -> Self {
+ let o = match request_type {
+ UserAttentionType::Critical => TaoUserAttentionType::Critical,
+ UserAttentionType::Informational => TaoUserAttentionType::Informational,
+ };
+ Self(o)
+ }
+}
+
+#[derive(Debug)]
+pub struct CursorIconWrapper(pub TaoCursorIcon);
+
+impl From for CursorIconWrapper {
+ fn from(icon: CursorIcon) -> Self {
+ use CursorIcon::*;
+ let i = match icon {
+ Default => TaoCursorIcon::Default,
+ Crosshair => TaoCursorIcon::Crosshair,
+ Hand => TaoCursorIcon::Hand,
+ Arrow => TaoCursorIcon::Arrow,
+ Move => TaoCursorIcon::Move,
+ Text => TaoCursorIcon::Text,
+ Wait => TaoCursorIcon::Wait,
+ Help => TaoCursorIcon::Help,
+ Progress => TaoCursorIcon::Progress,
+ NotAllowed => TaoCursorIcon::NotAllowed,
+ ContextMenu => TaoCursorIcon::ContextMenu,
+ Cell => TaoCursorIcon::Cell,
+ VerticalText => TaoCursorIcon::VerticalText,
+ Alias => TaoCursorIcon::Alias,
+ Copy => TaoCursorIcon::Copy,
+ NoDrop => TaoCursorIcon::NoDrop,
+ Grab => TaoCursorIcon::Grab,
+ Grabbing => TaoCursorIcon::Grabbing,
+ AllScroll => TaoCursorIcon::AllScroll,
+ ZoomIn => TaoCursorIcon::ZoomIn,
+ ZoomOut => TaoCursorIcon::ZoomOut,
+ EResize => TaoCursorIcon::EResize,
+ NResize => TaoCursorIcon::NResize,
+ NeResize => TaoCursorIcon::NeResize,
+ NwResize => TaoCursorIcon::NwResize,
+ SResize => TaoCursorIcon::SResize,
+ SeResize => TaoCursorIcon::SeResize,
+ SwResize => TaoCursorIcon::SwResize,
+ WResize => TaoCursorIcon::WResize,
+ EwResize => TaoCursorIcon::EwResize,
+ NsResize => TaoCursorIcon::NsResize,
+ NeswResize => TaoCursorIcon::NeswResize,
+ NwseResize => TaoCursorIcon::NwseResize,
+ ColResize => TaoCursorIcon::ColResize,
+ RowResize => TaoCursorIcon::RowResize,
+ _ => TaoCursorIcon::Default,
+ };
+ Self(i)
+ }
+}
+
+pub struct ProgressStateWrapper(pub TaoProgressState);
+
+impl From for ProgressStateWrapper {
+ fn from(status: ProgressBarStatus) -> Self {
+ let state = match status {
+ ProgressBarStatus::None => TaoProgressState::None,
+ ProgressBarStatus::Normal => TaoProgressState::Normal,
+ ProgressBarStatus::Indeterminate => TaoProgressState::Indeterminate,
+ ProgressBarStatus::Paused => TaoProgressState::Paused,
+ ProgressBarStatus::Error => TaoProgressState::Error,
+ };
+ Self(state)
+ }
+}
+
+pub struct ProgressBarStateWrapper(pub TaoProgressBarState);
+
+impl From for ProgressBarStateWrapper {
+ fn from(progress_state: ProgressBarState) -> Self {
+ Self(TaoProgressBarState {
+ progress: progress_state.progress,
+ state: progress_state
+ .status
+ .map(|state| ProgressStateWrapper::from(state).0),
+ desktop_filename: progress_state.desktop_filename,
+ })
+ }
+}
+
+#[derive(Clone, Default)]
+pub struct WindowBuilderWrapper {
+ inner: TaoWindowBuilder,
+ center: bool,
+ prevent_overflow: Option,
+ #[cfg(target_os = "macos")]
+ tabbing_identifier: Option,
+}
+
+impl std::fmt::Debug for WindowBuilderWrapper {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut s = f.debug_struct("WindowBuilderWrapper");
+ s.field("inner", &self.inner)
+ .field("center", &self.center)
+ .field("prevent_overflow", &self.prevent_overflow);
+ #[cfg(target_os = "macos")]
+ {
+ s.field("tabbing_identifier", &self.tabbing_identifier);
+ }
+ s.finish()
+ }
+}
+
+// SAFETY: this type is `Send` since `menu_items` are read only here
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for WindowBuilderWrapper {}
+
+impl WindowBuilderBase for WindowBuilderWrapper {}
+impl WindowBuilder for WindowBuilderWrapper {
+ fn new() -> Self {
+ #[allow(unused_mut)]
+ let mut builder = Self::default().focused(true);
+
+ #[cfg(target_os = "macos")]
+ {
+ // TODO: find a proper way to prevent webview being pushed out of the window.
+ // Workaround for issue: https://github.com/tauri-apps/tauri/issues/10225
+ // The window requires `NSFullSizeContentViewWindowMask` flag to prevent devtools
+ // pushing the content view out of the window.
+ // By setting the default style to `TitleBarStyle::Visible` should fix the issue for most of the users.
+ builder = builder.title_bar_style(TitleBarStyle::Visible);
+ }
+
+ builder = builder.title("Tauri App");
+
+ #[cfg(windows)]
+ {
+ builder = builder.window_classname("Tauri Window");
+ }
+
+ builder
+ }
+
+ fn with_config(config: &WindowConfig) -> Self {
+ let mut window = WindowBuilderWrapper::new();
+
+ #[cfg(target_os = "macos")]
+ {
+ window = window
+ .hidden_title(config.hidden_title)
+ .title_bar_style(config.title_bar_style);
+ if let Some(identifier) = &config.tabbing_identifier {
+ window = window.tabbing_identifier(identifier);
+ }
+ if let Some(position) = &config.traffic_light_position {
+ window = window.traffic_light_position(tauri_runtime::dpi::LogicalPosition::new(
+ position.x, position.y,
+ ));
+ }
+ }
+
+ #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
+ {
+ window = window.transparent(config.transparent);
+ }
+ #[cfg(all(
+ target_os = "macos",
+ not(feature = "macos-private-api"),
+ debug_assertions
+ ))]
+ if config.transparent {
+ eprintln!(
+ "The window is set to be transparent but the `macos-private-api` is not enabled.
+ This can be enabled via the `tauri.macOSPrivateApi` configuration property
+ ");
+ }
+
+ #[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ {
+ // Mouse event is disabled on Linux since sudden event bursts could block event loop.
+ window.inner = window.inner.with_cursor_moved_event(false);
+ }
+
+ #[cfg(target_os = "android")]
+ {
+ if let Some(activity_name) = &config.activity_name {
+ window.inner = window.inner.with_activity_name(activity_name.clone());
+ }
+ if let Some(activity_name) = &config.created_by_activity_name {
+ window.inner = window
+ .inner
+ .with_created_by_activity_name(activity_name.clone());
+ }
+ }
+
+ #[cfg(target_os = "ios")]
+ {
+ if let Some(scene_identifier) = &config.requested_by_scene_identifier {
+ window.inner = window
+ .inner
+ .with_requesting_scene_identifier(scene_identifier.clone());
+ }
+ }
+
+ // ignore size from config for mobile for backward compatibility
+ #[cfg(not(any(target_os = "ios", target_os = "android")))]
+ {
+ window = window.inner_size(config.width, config.height);
+ }
+
+ window = window
+ .title(config.title.to_string())
+ .focused(config.focus)
+ .focusable(config.focusable)
+ .visible(config.visible)
+ .resizable(config.resizable)
+ .fullscreen(config.fullscreen)
+ .decorations(config.decorations)
+ .maximized(config.maximized)
+ .always_on_bottom(config.always_on_bottom)
+ .always_on_top(config.always_on_top)
+ .visible_on_all_workspaces(config.visible_on_all_workspaces)
+ .content_protected(config.content_protected)
+ .skip_taskbar(config.skip_taskbar)
+ .theme(config.theme)
+ .closable(config.closable)
+ .maximizable(config.maximizable)
+ .minimizable(config.minimizable)
+ .shadow(config.shadow);
+
+ let mut constraints = WindowSizeConstraints::default();
+
+ if let Some(min_width) = config.min_width {
+ constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into());
+ }
+ if let Some(min_height) = config.min_height {
+ constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into());
+ }
+ if let Some(max_width) = config.max_width {
+ constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into());
+ }
+ if let Some(max_height) = config.max_height {
+ constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into());
+ }
+ if let Some(color) = config.background_color {
+ window = window.background_color(color);
+ }
+ window = window.inner_size_constraints(constraints);
+
+ if let (Some(x), Some(y)) = (config.x, config.y) {
+ window = window.position(x, y);
+ }
+
+ if config.center {
+ window = window.center();
+ }
+
+ if let Some(window_classname) = &config.window_classname {
+ window = window.window_classname(window_classname);
+ }
+
+ if let Some(prevent_overflow) = &config.prevent_overflow {
+ window = match prevent_overflow {
+ PreventOverflowConfig::Enable(true) => window.prevent_overflow(),
+ PreventOverflowConfig::Margin(margin) => window
+ .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()),
+ _ => window,
+ };
+ }
+
+ window
+ }
+
+ fn center(mut self) -> Self {
+ self.center = true;
+ self
+ }
+
+ fn position(mut self, x: f64, y: f64) -> Self {
+ self.inner = self.inner.with_position(TaoLogicalPosition::new(x, y));
+ self
+ }
+
+ fn inner_size(mut self, width: f64, height: f64) -> Self {
+ self.inner = self
+ .inner
+ .with_inner_size(TaoLogicalSize::new(width, height));
+ self
+ }
+
+ fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self {
+ self.inner = self
+ .inner
+ .with_min_inner_size(TaoLogicalSize::new(min_width, min_height));
+ self
+ }
+
+ fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self {
+ self.inner = self
+ .inner
+ .with_max_inner_size(TaoLogicalSize::new(max_width, max_height));
+ self
+ }
+
+ fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self {
+ self.inner.window.inner_size_constraints = tao::window::WindowSizeConstraints {
+ min_width: constraints.min_width,
+ min_height: constraints.min_height,
+ max_width: constraints.max_width,
+ max_height: constraints.max_height,
+ };
+ self
+ }
+
+ /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation
+ ///
+ /// ## Platform-specific
+ ///
+ /// - **iOS / Android:** Unsupported.
+ fn prevent_overflow(mut self) -> Self {
+ self
+ .prevent_overflow
+ .replace(PhysicalSize::new(0, 0).into());
+ self
+ }
+
+ /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size)
+ /// on creation with a margin
+ ///
+ /// ## Platform-specific
+ ///
+ /// - **iOS / Android:** Unsupported.
+ fn prevent_overflow_with_margin(mut self, margin: Size) -> Self {
+ self.prevent_overflow.replace(margin);
+ self
+ }
+
+ fn resizable(mut self, resizable: bool) -> Self {
+ self.inner = self.inner.with_resizable(resizable);
+ self
+ }
+
+ fn maximizable(mut self, maximizable: bool) -> Self {
+ self.inner = self.inner.with_maximizable(maximizable);
+ self
+ }
+
+ fn minimizable(mut self, minimizable: bool) -> Self {
+ self.inner = self.inner.with_minimizable(minimizable);
+ self
+ }
+
+ fn closable(mut self, closable: bool) -> Self {
+ self.inner = self.inner.with_closable(closable);
+ self
+ }
+
+ fn title>(mut self, title: S) -> Self {
+ self.inner = self.inner.with_title(title.into());
+ self
+ }
+
+ fn fullscreen(mut self, fullscreen: bool) -> Self {
+ self.inner = if fullscreen {
+ self
+ .inner
+ .with_fullscreen(Some(Fullscreen::Borderless(None)))
+ } else {
+ self.inner.with_fullscreen(None)
+ };
+ self
+ }
+
+ fn focused(mut self, focused: bool) -> Self {
+ self.inner = self.inner.with_focused(focused);
+ self
+ }
+
+ fn focusable(mut self, focusable: bool) -> Self {
+ self.inner = self.inner.with_focusable(focusable);
+ self
+ }
+
+ fn maximized(mut self, maximized: bool) -> Self {
+ self.inner = self.inner.with_maximized(maximized);
+ self
+ }
+
+ fn visible(mut self, visible: bool) -> Self {
+ self.inner = self.inner.with_visible(visible);
+ self
+ }
+
+ #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
+ fn transparent(mut self, transparent: bool) -> Self {
+ self.inner = self.inner.with_transparent(transparent);
+ self
+ }
+
+ fn decorations(mut self, decorations: bool) -> Self {
+ self.inner = self.inner.with_decorations(decorations);
+ self
+ }
+
+ fn always_on_bottom(mut self, always_on_bottom: bool) -> Self {
+ self.inner = self.inner.with_always_on_bottom(always_on_bottom);
+ self
+ }
+
+ fn always_on_top(mut self, always_on_top: bool) -> Self {
+ self.inner = self.inner.with_always_on_top(always_on_top);
+ self
+ }
+
+ fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self {
+ self.inner = self
+ .inner
+ .with_visible_on_all_workspaces(visible_on_all_workspaces);
+ self
+ }
+
+ fn content_protected(mut self, protected: bool) -> Self {
+ self.inner = self.inner.with_content_protection(protected);
+ self
+ }
+
+ fn shadow(#[allow(unused_mut)] mut self, _enable: bool) -> Self {
+ #[cfg(windows)]
+ {
+ self.inner = self.inner.with_undecorated_shadow(_enable);
+ }
+ #[cfg(target_os = "macos")]
+ {
+ self.inner = self.inner.with_has_shadow(_enable);
+ }
+ self
+ }
+
+ #[cfg(windows)]
+ fn owner(mut self, owner: HWND) -> Self {
+ self.inner = self.inner.with_owner_window(owner.0 as _);
+ self
+ }
+
+ #[cfg(windows)]
+ fn parent(mut self, parent: HWND) -> Self {
+ self.inner = self.inner.with_parent_window(parent.0 as _);
+ self
+ }
+
+ #[cfg(target_os = "macos")]
+ fn parent(mut self, parent: *mut std::ffi::c_void) -> Self {
+ self.inner = self.inner.with_parent_window(parent);
+ self
+ }
+
+ #[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ fn transient_for(mut self, parent: &impl gtk::glib::IsA) -> Self {
+ self.inner = self.inner.with_transient_for(parent);
+ self
+ }
+
+ #[cfg(windows)]
+ fn drag_and_drop(mut self, enabled: bool) -> Self {
+ self.inner = self.inner.with_drag_and_drop(enabled);
+ self
+ }
+
+ #[cfg(target_os = "macos")]
+ fn title_bar_style(mut self, style: TitleBarStyle) -> Self {
+ match style {
+ TitleBarStyle::Visible => {
+ self.inner = self.inner.with_titlebar_transparent(false);
+ // Fixes rendering issue when resizing window with devtools open (https://github.com/tauri-apps/tauri/issues/3914)
+ self.inner = self.inner.with_fullsize_content_view(true);
+ }
+ TitleBarStyle::Transparent => {
+ self.inner = self.inner.with_titlebar_transparent(true);
+ self.inner = self.inner.with_fullsize_content_view(false);
+ }
+ TitleBarStyle::Overlay => {
+ self.inner = self.inner.with_titlebar_transparent(true);
+ self.inner = self.inner.with_fullsize_content_view(true);
+ }
+ unknown => {
+ #[cfg(feature = "tracing")]
+ tracing::warn!("unknown title bar style applied: {unknown}");
+
+ #[cfg(not(feature = "tracing"))]
+ eprintln!("unknown title bar style applied: {unknown}");
+ }
+ }
+ self
+ }
+
+ #[cfg(target_os = "macos")]
+ fn traffic_light_position>(mut self, position: P) -> Self {
+ self.inner = self.inner.with_traffic_light_inset(position.into());
+ self
+ }
+
+ #[cfg(target_os = "macos")]
+ fn hidden_title(mut self, hidden: bool) -> Self {
+ self.inner = self.inner.with_title_hidden(hidden);
+ self
+ }
+
+ #[cfg(target_os = "macos")]
+ fn tabbing_identifier(mut self, identifier: &str) -> Self {
+ self.inner = self.inner.with_tabbing_identifier(identifier);
+ self.tabbing_identifier.replace(identifier.into());
+ self
+ }
+
+ fn icon(mut self, icon: Icon) -> Result {
+ self.inner = self
+ .inner
+ .with_window_icon(Some(TaoIcon::try_from(icon)?.0));
+ Ok(self)
+ }
+
+ fn background_color(mut self, color: Color) -> Self {
+ self.inner = self.inner.with_background_color(color.into());
+ self
+ }
+
+ #[cfg(any(
+ windows,
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ fn skip_taskbar(mut self, skip: bool) -> Self {
+ self.inner = self.inner.with_skip_taskbar(skip);
+ self
+ }
+
+ #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
+ fn skip_taskbar(self, _skip: bool) -> Self {
+ self
+ }
+
+ fn theme(mut self, theme: Option) -> Self {
+ self.inner = self.inner.with_theme(if let Some(t) = theme {
+ match t {
+ Theme::Dark => Some(TaoTheme::Dark),
+ _ => Some(TaoTheme::Light),
+ }
+ } else {
+ None
+ });
+
+ self
+ }
+
+ fn has_icon(&self) -> bool {
+ self.inner.window.window_icon.is_some()
+ }
+
+ fn get_theme(&self) -> Option {
+ self.inner.window.preferred_theme.map(|theme| match theme {
+ TaoTheme::Dark => Theme::Dark,
+ _ => Theme::Light,
+ })
+ }
+
+ #[cfg(windows)]
+ fn window_classname>(mut self, window_classname: S) -> Self {
+ self.inner = self.inner.with_window_classname(window_classname);
+ self
+ }
+ #[cfg(not(windows))]
+ fn window_classname>(self, _window_classname: S) -> Self {
+ self
+ }
+
+ #[cfg(target_os = "android")]
+ fn activity_name>(mut self, class_name: S) -> Self {
+ self.inner = self.inner.with_activity_name(class_name.into());
+ self
+ }
+
+ #[cfg(target_os = "android")]
+ fn created_by_activity_name>(mut self, class_name: S) -> Self {
+ self.inner = self.inner.with_created_by_activity_name(class_name.into());
+ self
+ }
+
+ #[cfg(target_os = "ios")]
+ fn requested_by_scene_identifier>(mut self, identifier: S) -> Self {
+ self.inner = self
+ .inner
+ .with_requesting_scene_identifier(identifier.into());
+ self
+ }
+}
+
+#[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+pub struct GtkWindow(pub gtk::ApplicationWindow);
+#[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for GtkWindow {}
+
+#[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+pub struct GtkBox(pub gtk::Box);
+#[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+))]
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for GtkBox {}
+
+pub struct SendRawWindowHandle(pub raw_window_handle::RawWindowHandle);
+unsafe impl Send for SendRawWindowHandle {}
+
+pub enum ApplicationMessage {
+ #[cfg(target_os = "macos")]
+ Show,
+ #[cfg(target_os = "macos")]
+ Hide,
+ #[cfg(any(target_os = "macos", target_os = "ios"))]
+ FetchDataStoreIdentifiers(Box) + Send + 'static>),
+ #[cfg(any(target_os = "macos", target_os = "ios"))]
+ RemoveDataStore([u8; 16], Box) + Send + 'static>),
+}
+
+pub enum WindowMessage {
+ AddEventListener(WindowEventId, Box),
+ // Getters
+ ScaleFactor(Sender),
+ InnerPosition(Sender>>),
+ OuterPosition(Sender>>),
+ InnerSize(Sender>),
+ OuterSize(Sender>),
+ IsFullscreen(Sender),
+ IsMinimized(Sender),
+ IsMaximized(Sender),
+ IsFocused(Sender),
+ IsDecorated(Sender),
+ IsResizable(Sender),
+ IsMaximizable(Sender),
+ IsMinimizable(Sender),
+ IsClosable(Sender),
+ IsVisible(Sender),
+ Title(Sender),
+ CurrentMonitor(Sender>),
+ PrimaryMonitor(Sender >),
+ MonitorFromPoint(Sender >, (f64, f64)),
+ AvailableMonitors(Sender>),
+ #[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ GtkWindow(Sender),
+ #[cfg(any(
+ target_os = "linux",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ GtkBox(Sender),
+ #[cfg(target_os = "android")]
+ ActivityName(Sender),
+ #[cfg(target_os = "ios")]
+ SceneIdentifier(Sender),
+ RawWindowHandle(Sender>),
+ Theme(Sender),
+ IsEnabled(Sender),
+ IsAlwaysOnTop(Sender),
+ // Setters
+ Center,
+ RequestUserAttention(Option),
+ SetEnabled(bool),
+ SetResizable(bool),
+ SetMaximizable(bool),
+ SetMinimizable(bool),
+ SetClosable(bool),
+ SetTitle(String),
+ Maximize,
+ Unmaximize,
+ Minimize,
+ Unminimize,
+ Show,
+ Hide,
+ Close,
+ Destroy,
+ SetDecorations(bool),
+ SetShadow(bool),
+ SetAlwaysOnBottom(bool),
+ SetAlwaysOnTop(bool),
+ SetVisibleOnAllWorkspaces(bool),
+ SetContentProtected(bool),
+ SetSize(Size),
+ SetMinSize(Option),
+ SetMaxSize(Option),
+ SetSizeConstraints(WindowSizeConstraints),
+ SetPosition(Position),
+ SetFullscreen(bool),
+ #[cfg(target_os = "macos")]
+ SetSimpleFullscreen(bool),
+ SetFocus,
+ SetFocusable(bool),
+ SetIcon(TaoWindowIcon),
+ SetSkipTaskbar(bool),
+ SetCursorGrab(bool),
+ SetCursorVisible(bool),
+ SetCursorIcon(CursorIcon),
+ SetCursorPosition(Position),
+ SetIgnoreCursorEvents(bool),
+ SetBadgeCount(Option, Option),
+ SetBadgeLabel(Option