diff --git a/.env.example b/.env.example index 3dd78566a..65abc19de 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,11 @@ CAP_AWS_ENDPOINT=${NEXT_PUBLIC_CAP_AWS_ENDPOINT} # Necessary for authentication, genearte by running `openssl rand -base64 32` NEXTAUTH_SECRET= +# Restrict signup to specific email domains (comma-separated) +# If empty or not set, signup is open to all email addresses +# Example: CAP_ALLOWED_SIGNUP_DOMAINS=company.com,partner.org +# CAP_ALLOWED_SIGNUP_DOMAINS= + # Provide if you want to use Google authentication # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= diff --git a/.eslintrc.ts b/.eslintrc.ts deleted file mode 100644 index 5b999efa4..000000000 --- a/.eslintrc.ts +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - root: true, - // This tells ESLint to load the config from the package `eslint-config-custom` - extends: ["custom"], - settings: { - next: { - rootDir: ["apps/*/"], - }, - }, -}; diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..6c36c92c5 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +d3b3a0bb176ac922bc623271fc64a60cf7f5bd8b diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a2183488..a53ec93ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,32 @@ concurrency: cancel-in-progress: true jobs: + changes: + name: Detect Changes + runs-on: ubuntu-latest + outputs: + rust: ${{ steps.filter.outputs.rust }} + tauri-plugins: ${{ steps.filter.outputs.tauri-plugins }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for changes + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + rust: + - '.cargo/**' + - '.github/**' + - 'crates/**' + - 'desktop/src-tauri/**' + - 'Cargo.toml' + - 'Cargo.lock' + tauri-plugins: + - 'Cargo.lock' + - 'pnpm-lock.yaml' + typecheck: name: Typecheck runs-on: ubuntu-latest @@ -23,6 +49,21 @@ jobs: - name: Typecheck run: pnpm typecheck + format-biome: + name: Format (Biome) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Run Biome + run: biome ci . --linter-enabled=false + format-rust: name: Format (Cargo) runs-on: ubuntu-latest @@ -38,6 +79,8 @@ jobs: clippy: name: Clippy runs-on: macos-latest + needs: changes + if: needs.changes.outputs.rust == 'true' permissions: contents: read steps: @@ -54,8 +97,6 @@ jobs: with: shared-key: ${{ matrix.settings.target }} - - uses: ./.github/actions/setup-js - - name: Create .env file in root run: | echo "VITE_ENVIRONMENT=production" >> .env @@ -73,14 +114,29 @@ jobs: cat .env >> $GITHUB_ENV - name: Run setup - run: | - pnpm cap-setup + shell: bash + run: node scripts/setup.js - name: Run Clippy uses: actions-rs-plus/clippy-check@v2 with: args: --workspace --all-features --locked + lint-biome: + name: Lint (Biome) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Run Biome + run: biome ci . || true + build-desktop: name: Build Desktop strategy: @@ -92,6 +148,9 @@ jobs: - target: x86_64-pc-windows-msvc runner: windows-latest runs-on: ${{ matrix.settings.runner }} + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -135,3 +194,21 @@ jobs: pnpm tauri build --debug --target ${{ matrix.settings.target }} --no-bundle env: RUST_TARGET_TRIPLE: ${{ matrix.settings.target }} + + tauri-plugins: + name: Verify Tauri plugin versions + runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.tauri-plugins == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Run verify + shell: bash + run: node scripts/check-tauri-plugin-versions.js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d1d21f524..f4e7c3409 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -137,11 +137,14 @@ jobs: matrix: settings: - target: x86_64-apple-darwin - runner: macos-latest + runner: macos-latest-large - target: aarch64-apple-darwin - runner: macos-latest + runner: macos-latest-large - target: x86_64-pc-windows-msvc runner: windows-latest-l + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} runs-on: ${{ matrix.settings.runner }} steps: - name: Checkout repository @@ -151,13 +154,13 @@ jobs: run: echo "${{ secrets.APPLE_API_KEY_FILE }}" > api.p8 - uses: apple-actions/import-codesign-certs@v2 - if: ${{ matrix.settings.runner == 'macos-latest' }} + if: ${{ matrix.settings.runner == 'macos-latest-large' }} with: p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - name: Verify certificate - if: ${{ matrix.settings.runner == 'macos-latest' }} + if: ${{ matrix.settings.runner == 'macos-latest-large' }} run: security find-identity -v -p codesigning ${{ runner.temp }}/build.keychain - name: Rust setup diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 24d7cc6de..9f5433281 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] } diff --git a/Cargo.lock b/Cargo.lock index 6362f5186..bc354bfc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" dependencies = [ "clipboard-win", - "image", + "image 0.25.6", "log", "objc2 0.6.1", "objc2-app-kit", @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "cap-desktop" -version = "0.3.63" +version = "0.3.66" dependencies = [ "anyhow", "axum", @@ -980,6 +980,7 @@ dependencies = [ "bytemuck", "cap-audio", "cap-camera", + "cap-displays", "cap-editor", "cap-export", "cap-fail", @@ -1004,7 +1005,7 @@ dependencies = [ "futures", "futures-intrusive", "global-hotkey", - "image", + "image 0.25.6", "keyed_priority_queue", "lazy_static", "log", @@ -1064,7 +1065,13 @@ dependencies = [ name = "cap-displays" version = "0.1.0" dependencies = [ + "cocoa 0.26.1", + "core-foundation 0.10.1", "core-graphics 0.24.0", + "image 0.24.9", + "objc", + "serde", + "specta", "windows 0.60.0", "windows-sys 0.59.0", ] @@ -1106,7 +1113,7 @@ dependencies = [ "clap", "ffmpeg-next", "futures", - "image", + "image 0.25.6", "inquire", "mp4", "serde", @@ -1164,7 +1171,7 @@ dependencies = [ "flume", "futures", "gif", - "image", + "image 0.25.6", "indexmap 2.10.0", "inquire", "kameo", @@ -1248,7 +1255,7 @@ dependencies = [ "flume", "futures", "hex", - "image", + "image 0.25.6", "objc", "objc2-app-kit", "relative-path", @@ -1286,7 +1293,7 @@ dependencies = [ "futures", "futures-intrusive", "glyphon", - "image", + "image 0.25.6", "log", "pretty_assertions", "reactive_graph", @@ -1538,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" dependencies = [ "clipboard-win", - "image", + "image 0.25.6", "objc2 0.6.1", "objc2-app-kit", "objc2-foundation 0.3.1", @@ -3810,6 +3817,24 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "image" version = "0.25.6" @@ -4071,6 +4096,9 @@ name = "jpeg-decoder" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] [[package]] name = "js-sys" @@ -6761,6 +6789,39 @@ dependencies = [ "windows-capture", ] +[[package]] +name = "scap-direct3d" +version = "0.1.0" +dependencies = [ + "windows 0.60.0", +] + +[[package]] +name = "scap-ffmpeg" +version = "0.1.0" +dependencies = [ + "cidre", + "ffmpeg-next", + "futures", + "scap-direct3d", + "scap-screencapturekit", + "windows 0.60.0", +] + +[[package]] +name = "scap-screencapturekit" +version = "0.1.0" +dependencies = [ + "cidre", + "clap", + "futures", + "inquire", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "tracing", +] + [[package]] name = "schannel" version = "0.1.27" @@ -7896,7 +7957,7 @@ dependencies = [ "heck 0.5.0", "http", "http-range", - "image", + "image 0.25.6", "jni", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index 477df9c32..3959865e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ sentry = { version = "0.34.0", features = [ "debug-images", ] } tracing = "0.1.41" +futures = "0.3.31" cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438", features = [ "macos_13_0", diff --git a/apps/desktop/.vscode/extensions.json b/apps/desktop/.vscode/extensions.json index 24d7cc6de..9f5433281 100644 --- a/apps/desktop/.vscode/extensions.json +++ b/apps/desktop/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] } diff --git a/apps/desktop/app.config.ts b/apps/desktop/app.config.ts index b50119d99..4a96df522 100644 --- a/apps/desktop/app.config.ts +++ b/apps/desktop/app.config.ts @@ -3,33 +3,33 @@ import { defineConfig } from "@solidjs/start/config"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - ssr: false, - server: { preset: "static" }, - // https://vitejs.dev/config - vite: () => ({ - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // 1. tauri expects a fixed port, fail if that port is not available - server: { - port: 3001, - strictPort: true, - watch: { - // 2. tell vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], - }, - }, - // 3. to make use of `TAURI_DEBUG` and other env variables - // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand - envPrefix: ["VITE_", "TAURI_"], - assetsInclude: ["**/*.riv"], - plugins: [ - capUIPlugin, - tsconfigPaths({ - // If this isn't set Vinxi hangs on startup - root: ".", - }), - ], - define: { - "import.meta.vitest": "undefined", - }, - }), + ssr: false, + server: { preset: "static" }, + // https://vitejs.dev/config + vite: () => ({ + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // 1. tauri expects a fixed port, fail if that port is not available + server: { + port: 3001, + strictPort: true, + watch: { + // 2. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, + // 3. to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ["VITE_", "TAURI_"], + assetsInclude: ["**/*.riv"], + plugins: [ + capUIPlugin, + tsconfigPaths({ + // If this isn't set Vinxi hangs on startup + root: ".", + }), + ], + define: { + "import.meta.vitest": "undefined", + }, + }), }); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 625c6cab0..126dd75d4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,84 +1,85 @@ { - "name": "@cap/desktop", - "type": "module", - "scripts": { - "dev": "pnpm -w cap-setup && dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri dev", - "build:tauri": "dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri build", - "preparescript": "node scripts/prepare.js", - "localdev": "dotenv -e ../../.env -- vinxi dev --port 3001", - "build": "vinxi build", - "tauri": "tauri" - }, - "dependencies": { - "@aerofoil/rive-solid-canvas": "^2.1.4", - "@cap/database": "workspace:*", - "@cap/ui-solid": "workspace:*", - "@cap/utils": "workspace:*", - "@cap/web-api-contract": "workspace:*", - "@corvu/tooltip": "^0.2.1", - "@intercom/messenger-js-sdk": "^0.0.14", - "@kobalte/core": "^0.13.7", - "@radix-ui/colors": "^3.0.0", - "@rive-app/canvas": "^2.26.7", - "@solid-primitives/bounds": "^0.0.122", - "@solid-primitives/context": "^0.2.3", - "@solid-primitives/date": "^2.0.23", - "@solid-primitives/deep": "^0.2.9", - "@solid-primitives/event-bus": "^1.1.1", - "@solid-primitives/event-listener": "^2.3.3", - "@solid-primitives/history": "^0.1.5", - "@solid-primitives/memo": "^1.4.2", - "@solid-primitives/refs": "^1.0.8", - "@solid-primitives/resize-observer": "^2.0.26", - "@solid-primitives/scheduled": "^1.4.3", - "@solid-primitives/storage": "^4.0.0", - "@solid-primitives/timer": "^1.3.9", - "@solid-primitives/websocket": "^1.2.2", - "@solidjs/router": "^0.14.2", - "@solidjs/start": "^1.1.3", - "@tanstack/solid-query": "^5.51.21", - "@tauri-apps/api": "2.5.0", - "@tauri-apps/plugin-clipboard-manager": "^2.2.1", - "@tauri-apps/plugin-deep-link": "^2.2.0", - "@tauri-apps/plugin-dialog": "2.0.1", - "@tauri-apps/plugin-fs": "2.2.0", - "@tauri-apps/plugin-http": "^2.4.4", - "@tauri-apps/plugin-notification": "2.0.0", - "@tauri-apps/plugin-opener": "^2.2.6", - "@tauri-apps/plugin-os": "2.0.0", - "@tauri-apps/plugin-process": "2.0.0", - "@tauri-apps/plugin-shell": ">=2.0.1", - "@tauri-apps/plugin-store": "2.1.0", - "@tauri-apps/plugin-updater": "2.0.0", - "@ts-rest/core": "^3.52.1", - "@types/react-tooltip": "^4.2.4", - "cva": "npm:class-variance-authority@^0.7.0", - "effect": "^3.7.2", - "mp4box": "^0.5.2", - "posthog-js": "^1.215.3", - "solid-js": "^1.9.3", - "solid-markdown": "^2.0.13", - "solid-presence": "^0.1.8", - "solid-toast": "^0.5.0", - "solid-transition-group": "^0.2.3", - "unplugin-auto-import": "^0.18.2", - "unplugin-icons": "^0.19.2", - "uuid": "^9.0.1", - "vinxi": "^0.5.6", - "webcodecs": "^0.1.0", - "zod": "^3.25.76" - }, - "devDependencies": { - "@fontsource/geist-sans": "^5.0.3", - "@iconify/json": "^2.2.239", - "@tauri-apps/cli": ">=2.1.0", - "@total-typescript/ts-reset": "^0.6.1", - "@types/dom-webcodecs": "^0.1.11", - "@types/uuid": "^9.0.8", - "cross-env": "^7.0.3", - "typescript": "^5.8.3", - "vite": "^6.3.5", - "vite-tsconfig-paths": "^5.0.1", - "vitest": "~2.1.9" - } + "name": "@cap/desktop", + "type": "module", + "scripts": { + "dev": "pnpm -w cap-setup && dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri dev", + "build:tauri": "dotenv -e ../../.env -- pnpm run preparescript && dotenv -e ../../.env -- pnpm tauri build", + "preparescript": "node scripts/prepare.js", + "localdev": "dotenv -e ../../.env -- vinxi dev --port 3001", + "build": "vinxi build", + "tauri": "tauri" + }, + "dependencies": { + "@aerofoil/rive-solid-canvas": "^2.1.4", + "@cap/database": "workspace:*", + "@cap/ui-solid": "workspace:*", + "@cap/utils": "workspace:*", + "@cap/web-api-contract": "workspace:*", + "@corvu/tooltip": "^0.2.1", + "@intercom/messenger-js-sdk": "^0.0.14", + "@kobalte/core": "^0.13.7", + "@radix-ui/colors": "^3.0.0", + "@rive-app/canvas": "^2.26.7", + "@solid-primitives/bounds": "^0.0.122", + "@solid-primitives/context": "^0.2.3", + "@solid-primitives/date": "^2.0.23", + "@solid-primitives/deep": "^0.2.9", + "@solid-primitives/event-bus": "^1.1.1", + "@solid-primitives/event-listener": "^2.3.3", + "@solid-primitives/history": "^0.1.5", + "@solid-primitives/memo": "^1.4.2", + "@solid-primitives/mouse": "^2.1.2", + "@solid-primitives/refs": "^1.0.8", + "@solid-primitives/resize-observer": "^2.0.26", + "@solid-primitives/scheduled": "^1.4.3", + "@solid-primitives/storage": "^4.0.0", + "@solid-primitives/timer": "^1.3.9", + "@solid-primitives/websocket": "^1.2.2", + "@solidjs/router": "^0.14.2", + "@solidjs/start": "^1.1.3", + "@tanstack/solid-query": "^5.51.21", + "@tauri-apps/api": "2.5.0", + "@tauri-apps/plugin-clipboard-manager": "^2.3.0", + "@tauri-apps/plugin-deep-link": "^2.4.1", + "@tauri-apps/plugin-dialog": "^2.3.2", + "@tauri-apps/plugin-fs": "^2.4.1", + "@tauri-apps/plugin-http": "^2.5.1", + "@tauri-apps/plugin-notification": "^2.3.0", + "@tauri-apps/plugin-opener": "^2.4.0", + "@tauri-apps/plugin-os": "^2.3.0", + "@tauri-apps/plugin-process": "2.3.0", + "@tauri-apps/plugin-shell": "^2.3.0", + "@tauri-apps/plugin-store": "^2.3.0", + "@tauri-apps/plugin-updater": "^2.9.0", + "@ts-rest/core": "^3.52.1", + "@types/react-tooltip": "^4.2.4", + "cva": "npm:class-variance-authority@^0.7.0", + "effect": "^3.17.7", + "mp4box": "^0.5.2", + "posthog-js": "^1.215.3", + "solid-js": "^1.9.3", + "solid-markdown": "^2.0.13", + "solid-presence": "^0.1.8", + "solid-toast": "^0.5.0", + "solid-transition-group": "^0.2.3", + "unplugin-auto-import": "^0.18.2", + "unplugin-icons": "^0.19.2", + "uuid": "^9.0.1", + "vinxi": "^0.5.6", + "webcodecs": "^0.1.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@fontsource/geist-sans": "^5.0.3", + "@iconify/json": "^2.2.239", + "@tauri-apps/cli": ">=2.1.0", + "@total-typescript/ts-reset": "^0.6.1", + "@types/dom-webcodecs": "^0.1.11", + "@types/uuid": "^9.0.8", + "cross-env": "^7.0.3", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "~2.1.9" + } } diff --git a/apps/desktop/scripts/prepare.js b/apps/desktop/scripts/prepare.js index 8b5459e06..bd1c88666 100644 --- a/apps/desktop/scripts/prepare.js +++ b/apps/desktop/scripts/prepare.js @@ -16,28 +16,29 @@ const __dirname = path.dirname(__filename); * @returns {Promise} */ async function semverToWIXCompatibleVersion(cargoFilePath) { - const config = await fs.readFile(cargoFilePath, "utf-8"); - const match = /version\s*=\s*"([\w.-]+)"/.exec(config); - if (!match) - throw new Error( - 'Failed to extract version from "Cargo.toml". Have you removed the main crate version by accident?' - ); + const config = await fs.readFile(cargoFilePath, "utf-8"); + const match = /version\s*=\s*"([\w.-]+)"/.exec(config); + if (!match) + throw new Error( + 'Failed to extract version from "Cargo.toml". Have you removed the main crate version by accident?', + ); - const ver = match[1]; - const [core, buildOrPrerelease] = ver.includes("+") - ? ver.split("+") - : ver.split("-"); - const [major, minor, patch] = core.split("."); - let build = 0; - if (buildOrPrerelease) { - const numMatch = buildOrPrerelease.match(/\d+$/); - build = numMatch ? parseInt(numMatch[0]) : 0; - } - const wixVersion = `${major}.${minor}.${patch}${build === 0 ? "" : `.${build}` - }`; - if (wixVersion !== ver) - console.log(`Using wix-compatible version ${ver} --> ${wixVersion}`); - return wixVersion; + const ver = match[1]; + const [core, buildOrPrerelease] = ver.includes("+") + ? ver.split("+") + : ver.split("-"); + const [major, minor, patch] = core.split("."); + let build = 0; + if (buildOrPrerelease) { + const numMatch = buildOrPrerelease.match(/\d+$/); + build = numMatch ? parseInt(numMatch[0]) : 0; + } + const wixVersion = `${major}.${minor}.${patch}${ + build === 0 ? "" : `.${build}` + }`; + if (wixVersion !== ver) + console.log(`Using wix-compatible version ${ver} --> ${wixVersion}`); + return wixVersion; } /** * Deeply merges two objects @@ -47,16 +48,16 @@ async function semverToWIXCompatibleVersion(cargoFilePath) { * @returns {Object} */ function deepMerge(target, source) { - for (const key of Object.keys(source)) { - if ( - source[key] instanceof Object && - key in target && - target[key] instanceof Object - ) { - Object.assign(source[key], deepMerge(target[key], source[key])); - } - } - return { ...target, ...source }; + for (const key of Object.keys(source)) { + if ( + source[key] instanceof Object && + key in target && + target[key] instanceof Object + ) { + Object.assign(source[key], deepMerge(target[key], source[key])); + } + } + return { ...target, ...source }; } /** @@ -66,52 +67,52 @@ function deepMerge(target, source) { * @param {{} | undefined} configOptions */ export async function createTauriPlatformConfigs( - platform, - configOptions = undefined + platform, + configOptions = undefined, ) { - const srcTauri = path.join(__dirname, "../src-tauri/"); - let baseConfig = {}; - let configFileName = null; + const srcTauri = path.join(__dirname, "../src-tauri/"); + let baseConfig = {}; + let configFileName = null; - console.log(`Updating Platform (${platform}) Tauri config...`); - if (platform === "win32") { - configFileName = "tauri.windows.conf.json"; - baseConfig = { - ...baseConfig, - bundle: { - resources: { - "../../../target/ffmpeg/bin/*.dll": "./", - }, - windows: { - wix: { - version: await semverToWIXCompatibleVersion( - path.join(srcTauri, "Cargo.toml") - ), - }, - }, - }, - }; - } + console.log(`Updating Platform (${platform}) Tauri config...`); + if (platform === "win32") { + configFileName = "tauri.windows.conf.json"; + baseConfig = { + ...baseConfig, + bundle: { + resources: { + "../../../target/ffmpeg/bin/*.dll": "./", + }, + windows: { + wix: { + version: await semverToWIXCompatibleVersion( + path.join(srcTauri, "Cargo.toml"), + ), + }, + }, + }, + }; + } - if (!configFileName) return; + if (!configFileName) return; - const mergedConfig = configOptions - ? deepMerge(baseConfig, configOptions) - : baseConfig; - await fs.writeFile( - `${srcTauri}/${configFileName}`, - JSON.stringify(mergedConfig, null, 2) - ); + const mergedConfig = configOptions + ? deepMerge(baseConfig, configOptions) + : baseConfig; + await fs.writeFile( + `${srcTauri}/${configFileName}`, + JSON.stringify(mergedConfig, null, 2), + ); } async function main() { - console.log("--- Preparing sidecars and configs..."); - await createTauriPlatformConfigs(process.platform); - console.log("--- Preparation finished"); + console.log("--- Preparing sidecars and configs..."); + await createTauriPlatformConfigs(process.platform); + console.log("--- Preparation finished"); } main().catch((err) => { - console.error("\n--- Preparation Failed"); - console.error(err); - console.error("---"); + console.error("\n--- Preparation Failed"); + console.error(err); + console.error("---"); }); diff --git a/apps/desktop/scripts/prodBeforeBundle.js b/apps/desktop/scripts/prodBeforeBundle.js index 2ae2dc351..09a81f312 100644 --- a/apps/desktop/scripts/prodBeforeBundle.js +++ b/apps/desktop/scripts/prodBeforeBundle.js @@ -1,9 +1,9 @@ // @ts-check +import { exec as execCb } from "node:child_process"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { exec as execCb } from "node:child_process"; import { promisify } from "node:util"; const exec = promisify(execCb); @@ -14,39 +14,39 @@ const __dirname = path.dirname(__filename); const targetDir = path.join(__dirname, "../../../target"); async function main() { - if (process.platform === "darwin") { - const dirs = []; - let releaseDir = path.join(targetDir, "release"); - if (!(await fileExists(releaseDir))) return; - const releaseFiles = await fs.readdir(releaseDir); - let releaseFile = releaseFiles.find((f) => f.startsWith("Cap")); - dirs.push(releaseDir); - - if (!releaseFile) { - releaseDir = path.join( - targetDir, - `${process.env.TAURI_ENV_TARGET_TRIPLE}/release` - ); - dirs.push(releaseDir); - const releaseFiles = await fs.readdir(releaseDir); - releaseFile = releaseFiles.find((f) => f.startsWith("Cap")); - } - - if (!releaseFile) throw new Error(`No binary found at ${dirs.join(", ")}`); - - const binaryPath = path.join(releaseDir, releaseFile); - - await exec( - `dsymutil "${binaryPath}" -o "${path.join(targetDir, releaseFile)}.dSYM"` - ); - } + if (process.platform === "darwin") { + const dirs = []; + let releaseDir = path.join(targetDir, "release"); + if (!(await fileExists(releaseDir))) return; + const releaseFiles = await fs.readdir(releaseDir); + let releaseFile = releaseFiles.find((f) => f.startsWith("Cap")); + dirs.push(releaseDir); + + if (!releaseFile) { + releaseDir = path.join( + targetDir, + `${process.env.TAURI_ENV_TARGET_TRIPLE}/release`, + ); + dirs.push(releaseDir); + const releaseFiles = await fs.readdir(releaseDir); + releaseFile = releaseFiles.find((f) => f.startsWith("Cap")); + } + + if (!releaseFile) throw new Error(`No binary found at ${dirs.join(", ")}`); + + const binaryPath = path.join(releaseDir, releaseFile); + + await exec( + `dsymutil "${binaryPath}" -o "${path.join(targetDir, releaseFile)}.dSYM"`, + ); + } } main(); async function fileExists(path) { - return await fs - .access(path) - .then(() => true) - .catch(() => false); + return await fs + .access(path) + .then(() => true) + .catch(() => false); } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index d6b39d985..af38d91fb 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cap-desktop" -version = "0.3.63" +version = "0.3.66" description = "Beautiful screen recordings, owned by you." authors = ["you"] edition = "2024" @@ -37,7 +37,7 @@ tauri-plugin-process = "2.2.0" tauri-plugin-shell = "2.2.0" tauri-plugin-single-instance = { version = "2.2.0", features = ["deep-link"] } tauri-plugin-store = "2.2.0" -tauri-plugin-updater = "2.3.1" +tauri-plugin-updater = "2.9.0" tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" } tauri-plugin-window-state = "2.2.0" tauri-plugin-positioner = "2.2.0" @@ -56,7 +56,7 @@ image = "0.25.2" mp4 = "0.14.0" futures-intrusive = "0.5.0" anyhow.workspace = true -futures = "0.3" +futures = { workspace = true } axum = { version = "0.7.5", features = ["ws"] } tracing.workspace = true tempfile = "3.9.0" @@ -88,6 +88,8 @@ cap-media = { path = "../../../crates/media" } cap-flags = { path = "../../../crates/flags" } cap-recording = { path = "../../../crates/recording" } cap-export = { path = "../../../crates/export" } +cap-displays = { path = "../../../crates/displays" } + flume.workspace = true tracing-subscriber = "0.3.19" tracing-appender = "0.2.3" diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 67fd6bf87..187182981 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -1,67 +1,67 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["*"], - "permissions": [ - "fs:default", - "fs:allow-resource-read", - "fs:allow-resource-read-recursive", - "fs:write-all", - "fs:read-all", - "fs:allow-appdata-read", - "fs:allow-appdata-write", - "fs:allow-picture-read-recursive", - { - "identifier": "fs:scope", - "allow": [ - { "path": "$APPDATA/**" }, - { "path": "$HOME/**" }, - { "path": "$RESOURCE/**" } - ] - }, - "core:path:allow-resolve-directory", - "core:path:default", - "core:event:default", - "core:menu:default", - "core:window:default", - "core:window:allow-close", - "core:window:allow-destroy", - "core:window:allow-hide", - "core:window:allow-show", - "core:window:allow-center", - "core:window:allow-minimize", - "core:window:allow-unminimize", - "core:window:allow-maximize", - "core:window:allow-unmaximize", - "core:window:allow-set-size", - "core:window:allow-set-focus", - "core:window:allow-start-dragging", - "core:window:allow-set-position", - "core:window:allow-set-theme", - "core:window:allow-set-progress-bar", - "core:window:allow-set-effects", - "core:webview:default", - "core:webview:allow-create-webview-window", - "core:app:allow-version", - "shell:default", - "core:image:default", - "dialog:default", - "store:default", - "process:default", - "oauth:allow-start", - "updater:default", - "notification:default", - "deep-link:default", - { - "identifier": "http:default", - "allow": [ - { "url": "http://*" }, - { "url": "https://*" }, - { "url": "http://localhost:*" } - ] - }, - "clipboard-manager:allow-write-text", - "opener:allow-reveal-item-in-dir" - ] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["*"], + "permissions": [ + "fs:default", + "fs:allow-resource-read", + "fs:allow-resource-read-recursive", + "fs:write-all", + "fs:read-all", + "fs:allow-appdata-read", + "fs:allow-appdata-write", + "fs:allow-picture-read-recursive", + { + "identifier": "fs:scope", + "allow": [ + { "path": "$APPDATA/**" }, + { "path": "$HOME/**" }, + { "path": "$RESOURCE/**" } + ] + }, + "core:path:allow-resolve-directory", + "core:path:default", + "core:event:default", + "core:menu:default", + "core:window:default", + "core:window:allow-close", + "core:window:allow-destroy", + "core:window:allow-hide", + "core:window:allow-show", + "core:window:allow-center", + "core:window:allow-minimize", + "core:window:allow-unminimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-set-size", + "core:window:allow-set-focus", + "core:window:allow-start-dragging", + "core:window:allow-set-position", + "core:window:allow-set-theme", + "core:window:allow-set-progress-bar", + "core:window:allow-set-effects", + "core:webview:default", + "core:webview:allow-create-webview-window", + "core:app:allow-version", + "shell:default", + "core:image:default", + "dialog:default", + "store:default", + "process:default", + "oauth:allow-start", + "updater:default", + "notification:default", + "deep-link:default", + { + "identifier": "http:default", + "allow": [ + { "url": "http://*" }, + { "url": "https://*" }, + { "url": "http://localhost:*" } + ] + }, + "clipboard-manager:allow-write-text", + "opener:allow-reveal-item-in-dir" + ] } diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index fb0762053..c71e4d1c2 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,8 +1,9 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use cap_recording::RecordingMode; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager, Url}; +use tracing::trace; use crate::{ App, ArcLock, camera::CameraPreview, recording::StartRecordingInputs, windows::ShowCapWindow, @@ -27,7 +28,7 @@ pub enum DeepLinkAction { }, StopRecording, OpenEditor { - project_path: String, + project_path: PathBuf, }, OpenSettings { page: Option, @@ -35,8 +36,7 @@ pub enum DeepLinkAction { } pub fn handle(app_handle: &AppHandle, urls: Vec) { - #[cfg(debug_assertions)] - println!("Handling deep actions for: {:?}", &urls); + trace!("Handling deep actions for: {:?}", &urls); let actions: Vec<_> = urls .into_iter() @@ -81,6 +81,13 @@ impl TryFrom<&Url> for DeepLinkAction { type Error = ActionParseFromUrlError; fn try_from(url: &Url) -> Result { + #[cfg(target_os = "macos")] + if url.scheme() == "file" { + return Ok(Self::OpenEditor { + project_path: url.to_file_path().unwrap(), + }); + } + match url.domain() { Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), _ => Err(ActionParseFromUrlError::Invalid), diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 62252e295..cd178ae39 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -80,18 +80,39 @@ pub struct GeneralSettingsStore { pub server_url: String, #[serde(default)] pub recording_countdown: Option, - #[serde(default, alias = "open_editor_after_recording")] - #[deprecated] - _open_editor_after_recording: bool, - #[deprecated = "can be removed when native camera preview is ready"] - #[serde(default)] + // #[deprecated = "can be removed when native camera preview is ready"] + #[serde( + default = "default_enable_native_camera_preview", + skip_serializing_if = "no" + )] pub enable_native_camera_preview: bool, #[serde(default)] pub auto_zoom_on_clicks: bool, + // #[deprecated = "can be removed when new recording flow is the default"] + #[serde( + default = "default_enable_new_recording_flow", + skip_serializing_if = "no" + )] + pub enable_new_recording_flow: bool, #[serde(default)] pub post_deletion_behaviour: PostDeletionBehaviour, } +fn default_enable_native_camera_preview() -> bool { + // TODO: + // cfg!(target_os = "macos") + false +} + +fn default_enable_new_recording_flow() -> bool { + false + // cfg!(debug_assertions) +} + +fn no(_: &bool) -> bool { + false +} + fn default_server_url() -> String { std::option_env!("VITE_SERVER_URL") .unwrap_or("https://cap.so") @@ -127,9 +148,9 @@ impl Default for GeneralSettingsStore { custom_cursor_capture: false, server_url: default_server_url(), recording_countdown: Some(3), - _open_editor_after_recording: false, - enable_native_camera_preview: false, + enable_native_camera_preview: default_enable_native_camera_preview(), auto_zoom_on_clicks: false, + enable_new_recording_flow: default_enable_new_recording_flow(), post_deletion_behaviour: PostDeletionBehaviour::DoNothing, } } diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs index d4f5a99ac..0247fdd58 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-tauri/src/hotkeys.rs @@ -65,6 +65,9 @@ impl HotkeysStore { } } +#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] +pub struct OnEscapePress; + pub type HotkeysState = Mutex; pub fn init(app: &AppHandle) { app.plugin( @@ -74,6 +77,10 @@ pub fn init(app: &AppHandle) { return; } + if shortcut.key == Code::Escape { + OnEscapePress.emit(app).ok(); + } + let state = app.state::(); let store = state.lock().unwrap(); @@ -90,7 +97,6 @@ pub fn init(app: &AppHandle) { let store = HotkeysStore::get(app).unwrap().unwrap_or_default(); let global_shortcut = app.global_shortcut(); - for hotkey in store.hotkeys.values() { global_shortcut.register(Shortcut::from(*hotkey)).ok(); } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index fb38f62dd..833728eb7 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ mod permissions; mod platform; mod presets; mod recording; +mod target_select_overlay; mod tray; mod upload; mod web_api; @@ -69,6 +70,7 @@ use tauri::Window; use tauri::{AppHandle, Manager, State, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::DialogExt; +use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; use tauri_plugin_opener::OpenerExt; use tauri_plugin_shell::ShellExt; @@ -77,6 +79,7 @@ use tokio::sync::mpsc; use tokio::sync::{Mutex, RwLock}; use tracing::debug; use tracing::error; +use tracing::trace; use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video}; use web_api::ManagerExt as WebManagerExt; use windows::EditorWindowIds; @@ -1758,7 +1761,7 @@ async fn get_system_audio_waveforms( #[tauri::command] #[specta::specta] async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { - window.show(&app).await.unwrap(); + let _ = window.show(&app).await; Ok(()) } @@ -1951,7 +1954,9 @@ pub async fn run(recording_logging_handle: LoggingHandle) { captions::download_whisper_model, captions::check_model_exists, captions::delete_whisper_model, - captions::export_captions_srt + captions::export_captions_srt, + target_select_overlay::open_target_select_overlays, + target_select_overlay::close_target_select_overlays, ]) .events(tauri_specta::collect_events![ RecordingOptionsChanged, @@ -1971,7 +1976,9 @@ pub async fn run(recording_logging_handle: LoggingHandle) { UploadProgress, captions::DownloadProgress, recording::RecordingEvent, - RecordingDeleted + RecordingDeleted, + target_select_overlay::TargetUnderCursor, + hotkeys::OnEscapePress ]) .error_handling(tauri_specta::ErrorHandlingMode::Throw) .typ::() @@ -1981,13 +1988,13 @@ pub async fn run(recording_logging_handle: LoggingHandle) { .typ::() .typ::(); - #[cfg(debug_assertions)] - specta_builder - .export( - specta_typescript::Typescript::default(), - "../src/utils/tauri.ts", - ) - .expect("Failed to export typescript bindings"); + // #[cfg(debug_assertions)] + // specta_builder + // .export( + // specta_typescript::Typescript::default(), + // "../src/utils/tauri.ts", + // ) + // .expect("Failed to export typescript bindings"); let (camera_tx, camera_ws_port, _shutdown) = camera_legacy::create_camera_preview_ws().await; @@ -1998,6 +2005,9 @@ pub async fn run(recording_logging_handle: LoggingHandle) { #[allow(unused_mut)] let mut builder = tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + trace!("Single instance invoked with args {args:?}"); + + // This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions let Some(cap_file) = args .iter() .find(|arg| arg.ends_with(".cap")) @@ -2042,6 +2052,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { .with_denylist(&[ CapWindowId::Setup.label().as_str(), "window-capture-occluder", + "target-select-overlay", CapWindowId::CaptureArea.label().as_str(), CapWindowId::Camera.label().as_str(), CapWindowId::RecordingsOverlay.label().as_str(), @@ -2053,6 +2064,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { label if label.starts_with("window-capture-occluder-") => { "window-capture-occluder" } + label if label.starts_with("target-select-overlay") => "target-select-overlay", _ => label, }) .build(), @@ -2064,6 +2076,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { hotkeys::init(&app); general_settings::init(&app); fake_window::init(&app); + app.manage(target_select_overlay::WindowFocusManager::default()); app.manage(EditorWindowIds::default()); if let Ok(Some(auth)) = AuthStore::load(&app) { @@ -2197,6 +2210,10 @@ pub async fn run(recording_logging_handle: LoggingHandle) { } return; } + CapWindowId::TargetSelectOverlay { display_id } => { + app.state::() + .destroy(&display_id, app.global_shortcut()); + } _ => {} }; } @@ -2256,7 +2273,9 @@ pub async fn run(recording_logging_handle: LoggingHandle) { } } else { let handle = handle.clone(); - tokio::spawn(async move { ShowCapWindow::Main.show(&handle).await }); + tokio::spawn(async move { + let _ = ShowCapWindow::Main.show(&handle).await; + }); } } tauri::RunEvent::ExitRequested { code, api, .. } => { diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs new file mode 100644 index 000000000..a9bf72810 --- /dev/null +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -0,0 +1,206 @@ +use std::{ + collections::HashMap, + str::FromStr, + sync::{Mutex, PoisonError}, + time::Duration, +}; + +use base64::prelude::*; + +use crate::windows::{CapWindowId, ShowCapWindow}; +use cap_displays::{ + DisplayId, WindowId, + bounds::{LogicalBounds, PhysicalSize}, +}; +use serde::Serialize; +use specta::Type; +use tauri::{AppHandle, Manager, WebviewWindow}; +use tauri_plugin_global_shortcut::{GlobalShortcut, GlobalShortcutExt}; +use tauri_specta::Event; +use tokio::task::JoinHandle; +use tracing::error; + +#[derive(tauri_specta::Event, Serialize, Type, Clone)] +pub struct TargetUnderCursor { + display_id: Option, + window: Option, + screen: Option, +} + +#[derive(Serialize, Type, Clone)] +pub struct WindowUnderCursor { + id: WindowId, + app_name: String, + bounds: LogicalBounds, + icon: Option, +} + +#[derive(Serialize, Type, Clone)] +pub struct ScreenUnderCursor { + name: String, + physical_size: PhysicalSize, + refresh_rate: String, +} + +#[specta::specta] +#[tauri::command] +pub async fn open_target_select_overlays( + app: AppHandle, + state: tauri::State<'_, WindowFocusManager>, +) -> Result<(), String> { + let displays = cap_displays::Display::list() + .into_iter() + .map(|d| d.id()) + .collect::>(); + for display_id in displays { + let _ = ShowCapWindow::TargetSelectOverlay { display_id } + .show(&app) + .await; + } + + let handle = tokio::spawn({ + let app = app.clone(); + async move { + loop { + let display = cap_displays::Display::get_containing_cursor(); + let window = cap_displays::Window::get_topmost_at_cursor(); + + let _ = TargetUnderCursor { + display_id: display.map(|d| d.id()), + window: window.and_then(|w| { + Some(WindowUnderCursor { + id: w.id(), + bounds: w.bounds()?, + app_name: w.owner_name()?, + icon: w.app_icon().map(|bytes| { + format!("data:image/png;base64,{}", BASE64_STANDARD.encode(&bytes)) + }), + }) + }), + screen: display.map(|d| ScreenUnderCursor { + name: d.name(), + physical_size: d.physical_size(), + refresh_rate: d.refresh_rate().to_string(), + }), + } + .emit(&app); + + tokio::time::sleep(Duration::from_millis(50)).await; + } + } + }); + + if let Some(task) = state + .task + .lock() + .unwrap_or_else(PoisonError::into_inner) + .replace(handle) + { + task.abort(); + } else { + // If task is already set we know we have already registered this. + app.global_shortcut() + .register("Escape") + .map_err(|err| error!("Error registering global keyboard shortcut for Escape: {err}")) + .ok(); + } + + Ok(()) +} + +#[specta::specta] +#[tauri::command] +pub async fn close_target_select_overlays( + app: AppHandle, + // state: tauri::State<'_, WindowFocusManager>, +) -> Result<(), String> { + for (id, window) in app.webview_windows() { + if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { + let _ = window.close(); + } + } + + Ok(()) +} + +// Windows doesn't have a proper concept of window z-index's so we implement them in userspace :( +#[derive(Default)] +pub struct WindowFocusManager { + task: Mutex>>, + tasks: Mutex>>, +} + +impl WindowFocusManager { + /// Called when a window is created to spawn it's task + pub fn spawn(&self, id: &DisplayId, window: WebviewWindow) { + let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); + tasks.insert( + id.to_string(), + tokio::spawn(async move { + let app = window.app_handle(); + loop { + let Some(cap_main) = CapWindowId::Main.get(app) else { + window.close().ok(); + break; + }; + + // If the main window is minimized or not visible, close the overlay + // + // This is a workaround for the fact that the Cap main window + // is minimized when opening settings, etc instead of it being + // closed. + if cap_main.is_minimized().ok().unwrap_or_default() + || cap_main.is_visible().map(|v| !v).ok().unwrap_or_default() + { + window.close().ok(); + break; + } + + #[cfg(windows)] + { + let should_refocus = cap_main.is_focused().ok().unwrap_or_default() + || window.is_focused().unwrap_or_default(); + + // If a Cap window is not focused we know something is trying to steal the focus. + // We need to move the overlay above it. We don't use `always_on_top` on the overlay because we need the Cap window to stay above it. + if !should_refocus { + window.set_focus().ok(); + } + } + + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + } + }), + ); + } + + /// Called when a specific overlay window is destroyed to cleanup it's resources + pub fn destroy(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { + let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(task) = tasks.remove(&id.to_string()) { + let _ = task.abort(); + } + + // When all overlay windows are closed cleanup shared resources. + if tasks.is_empty() { + // Unregister keyboard shortcut + // This messes with other applications if we don't remove it. + global_shortcut + .unregister("Escape") + .map_err(|err| { + error!("Error unregistering global keyboard shortcut for Escape: {err}") + }) + .ok(); + + // Shutdown the cursor tracking task + if let Some(task) = self + .task + .lock() + .unwrap_or_else(PoisonError::into_inner) + .take() + { + task.abort(); + } + } + } +} diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index 3d76112f8..dced37786 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -3,8 +3,10 @@ use crate::{ RecordingStarted, RecordingStopped, RequestNewScreenshot, RequestOpenSettings, recording, }; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; use tauri::Manager; use tauri::menu::{MenuId, PredefinedMenuItem}; use tauri::{ @@ -99,7 +101,9 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { move |app: &AppHandle, event| match TrayItem::try_from(event.id) { Ok(TrayItem::OpenCap) => { let app = app.clone(); - tokio::spawn(async move { ShowCapWindow::Main.show(&app).await }); + tokio::spawn(async move { + let _ = ShowCapWindow::Main.show(&app).await; + }); } Ok(TrayItem::TakeScreenshot) => { let _ = RequestNewScreenshot.emit(&app_handle); diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index f9d47b996..41b4ad79c 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -1,8 +1,13 @@ #![allow(unused_mut)] #![allow(unused_imports)] -use crate::{App, ArcLock, fake_window, general_settings::AppTheme, permissions}; -use cap_flags::FLAGS; +use crate::{ + App, ArcLock, fake_window, + general_settings::{AppTheme, GeneralSettingsStore}, + permissions, + target_select_overlay::WindowFocusManager, +}; +use cap_displays::DisplayId; use cap_media::{platform::logical_monitor_bounds, sources::CaptureScreen}; use futures::pin_mut; use serde::Deserialize; @@ -23,7 +28,7 @@ use tracing::debug; #[cfg(target_os = "macos")] const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 12.0); -#[derive(Clone)] +#[derive(Clone, Deserialize, Type)] pub enum CapWindowId { // Contains onboarding + permissions Setup, @@ -32,6 +37,7 @@ pub enum CapWindowId { Editor { id: u32 }, RecordingsOverlay, WindowCaptureOccluder { screen_id: u32 }, + TargetSelectOverlay { display_id: DisplayId }, CaptureArea, Camera, InProgressRecording, @@ -67,6 +73,12 @@ impl FromStr for CapWindowId { .parse::() .map_err(|e| e.to_string())?, }, + s if s.starts_with("target-select-overlay-") => Self::TargetSelectOverlay { + display_id: s + .replace("target-select-overlay-", "") + .parse::() + .map_err(|e| e.to_string())?, + }, _ => return Err(format!("unknown window label: {s}")), }) } @@ -83,6 +95,9 @@ impl std::fmt::Display for CapWindowId { write!(f, "window-capture-occluder-{screen_id}") } Self::CaptureArea => write!(f, "capture-area"), + Self::TargetSelectOverlay { display_id } => { + write!(f, "target-select-overlay-{display_id}") + } Self::InProgressRecording => write!(f, "in-progress-recording"), Self::RecordingsOverlay => write!(f, "recordings-overlay"), Self::Upgrade => write!(f, "upgrade"), @@ -138,7 +153,8 @@ impl CapWindowId { Self::Camera | Self::WindowCaptureOccluder { .. } | Self::CaptureArea - | Self::RecordingsOverlay => None, + | Self::RecordingsOverlay + | Self::TargetSelectOverlay { .. } => None, _ => Some(None), } } @@ -165,6 +181,7 @@ pub enum ShowCapWindow { Editor { project_path: PathBuf }, RecordingsOverlay, WindowCaptureOccluder { screen_id: u32 }, + TargetSelectOverlay { display_id: DisplayId }, CaptureArea { screen_id: u32 }, Camera, InProgressRecording { countdown: Option }, @@ -174,7 +191,7 @@ pub enum ShowCapWindow { impl ShowCapWindow { pub async fn show(&self, app: &AppHandle) -> tauri::Result { - if let Self::Editor { project_path } = self { + if let Self::Editor { project_path } = &self { let state = app.state::(); let mut s = state.ids.lock().unwrap(); if !s.iter().any(|(path, _)| path == project_path) { @@ -207,18 +224,68 @@ impl ShowCapWindow { .build()?, Self::Main => { if permissions::do_permissions_check(false).necessary_granted() { - self.window_builder(app, "/") + let new_recording_flow = GeneralSettingsStore::get(&app) + .ok() + .flatten() + .map(|s| s.enable_new_recording_flow) + .unwrap_or_default(); + + let window = self + .window_builder(app, if new_recording_flow { "/new-main" } else { "/" }) .resizable(false) .maximized(false) .maximizable(false) .always_on_top(true) .visible_on_all_workspaces(true) .center() - .build()? + .build()?; + + if new_recording_flow { + #[cfg(target_os = "macos")] + crate::platform::set_window_level(window.as_ref().window(), 50); + } + + window } else { Box::pin(Self::Setup.show(app)).await? } } + Self::TargetSelectOverlay { display_id } => { + let Some(display) = cap_displays::Display::from_id(display_id.clone()) else { + return Err(tauri::Error::WindowNotFound); + }; + + let size = display.raw_handle().logical_size(); + let position = display.raw_handle().logical_position(); + + let mut window_builder = self + .window_builder( + app, + format!("/target-select-overlay?displayId={display_id}"), + ) + .maximized(false) + .resizable(false) + .fullscreen(false) + .shadow(false) + .always_on_top(cfg!(target_os = "macos")) + .visible_on_all_workspaces(true) + .skip_taskbar(true) + .inner_size(size.width(), size.height()) + .position(position.x(), position.y()) + .transparent(true); + + let window = window_builder.build()?; + + app.state::() + .spawn(display_id, window.clone()); + + #[cfg(target_os = "macos")] + { + crate::platform::set_window_level(window.as_ref().window(), 45); + } + + window + } Self::Settings { page } => { // Hide main window when settings window opens if let Some(main) = CapWindowId::Main.get(app) { @@ -554,6 +621,9 @@ impl ShowCapWindow { CapWindowId::Editor { id } } ShowCapWindow::RecordingsOverlay => CapWindowId::RecordingsOverlay, + ShowCapWindow::TargetSelectOverlay { display_id } => CapWindowId::TargetSelectOverlay { + display_id: display_id.clone(), + }, ShowCapWindow::WindowCaptureOccluder { screen_id } => { CapWindowId::WindowCaptureOccluder { screen_id: *screen_id, diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 448fd064c..214c68a74 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,84 +1,91 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "Cap - Development", - "identifier": "so.cap.desktop.dev", - "mainBinaryName": "Cap - Development", - "build": { - "beforeDevCommand": "pnpm localdev", - "devUrl": "http://localhost:3001", - "beforeBuildCommand": "pnpm turbo build --filter @cap/desktop", - "frontendDist": "../.output/public" - }, - "app": { - "macOSPrivateApi": true, - "security": { - "csp": "default-src 'self' ws: ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost data:; media-src 'self' asset: http://asset.localhost; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'", - "assetProtocol": { - "enable": true, - "scope": ["$APPDATA/**", "**/*.jpg", "**/*.webp", "$RESOURCE/**", "**/*.riv", "$HOME/**"] - } - } - }, - "plugins": { - "updater": { "active": false, "pubkey": "" }, - "deep-link": { - "desktop": { - "schemes": ["cap-desktop"] - } - } - }, - "bundle": { - "active": true, - "createUpdaterArtifacts": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/macos/icon.icns", - "icons/icon.ico" - ], - "resources": { - "assets/backgrounds/macOS/*": "assets/backgrounds/macOS/", - "assets/backgrounds/blue/*": "assets/backgrounds/blue/", - "assets/backgrounds/dark/*": "assets/backgrounds/dark/", - "assets/backgrounds/orange/*": "assets/backgrounds/orange/", - "assets/backgrounds/purple/*": "assets/backgrounds/purple/", - "../src/assets/rive/*.riv": "assets/rive/" - }, - "macOS": { - "dmg": { - "background": "assets/dmg-background.png", - "appPosition": { - "x": 180, - "y": 140 - }, - "applicationFolderPosition": { - "x": 480, - "y": 140 - } - }, - "frameworks": ["../../../target/native-deps/Spacedrive.framework"] - }, - "windows": { - "nsis": { - "headerImage": "assets/nsis-header.bmp", - "sidebarImage": "assets/nsis-sidebar.bmp", - "installerIcon": "icons/icon.ico" - }, - "wix": { - "upgradeCode": "79f4309d-ca23-54df-b6f9-826a1d783676", - "bannerPath": "assets/wix-banner.bmp", - "dialogImagePath": "assets/wix-dialog.bmp" - } - }, - "fileAssociations": [ - { - "ext": ["cap"], - "name": "Cap Project", - "mimeType": "application/x-cap-project", - "role": "Editor" - } - ] - } + "$schema": "https://schema.tauri.app/config/2", + "productName": "Cap - Development", + "identifier": "so.cap.desktop.dev", + "mainBinaryName": "Cap - Development", + "build": { + "beforeDevCommand": "pnpm localdev", + "devUrl": "http://localhost:3001", + "beforeBuildCommand": "pnpm turbo build --filter @cap/desktop", + "frontendDist": "../.output/public" + }, + "app": { + "macOSPrivateApi": true, + "security": { + "csp": "default-src 'self' ws: ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost data:; media-src 'self' asset: http://asset.localhost; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'", + "assetProtocol": { + "enable": true, + "scope": [ + "$APPDATA/**", + "**/*.jpg", + "**/*.webp", + "$RESOURCE/**", + "**/*.riv", + "$HOME/**" + ] + } + } + }, + "plugins": { + "updater": { "active": false, "pubkey": "" }, + "deep-link": { + "desktop": { + "schemes": ["cap-desktop"] + } + } + }, + "bundle": { + "active": true, + "createUpdaterArtifacts": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/macos/icon.icns", + "icons/icon.ico" + ], + "resources": { + "assets/backgrounds/macOS/*": "assets/backgrounds/macOS/", + "assets/backgrounds/blue/*": "assets/backgrounds/blue/", + "assets/backgrounds/dark/*": "assets/backgrounds/dark/", + "assets/backgrounds/orange/*": "assets/backgrounds/orange/", + "assets/backgrounds/purple/*": "assets/backgrounds/purple/", + "../src/assets/rive/*.riv": "assets/rive/" + }, + "macOS": { + "dmg": { + "background": "assets/dmg-background.png", + "appPosition": { + "x": 180, + "y": 140 + }, + "applicationFolderPosition": { + "x": 480, + "y": 140 + } + }, + "frameworks": ["../../../target/native-deps/Spacedrive.framework"] + }, + "windows": { + "nsis": { + "headerImage": "assets/nsis-header.bmp", + "sidebarImage": "assets/nsis-sidebar.bmp", + "installerIcon": "icons/icon.ico" + }, + "wix": { + "upgradeCode": "79f4309d-ca23-54df-b6f9-826a1d783676", + "bannerPath": "assets/wix-banner.bmp", + "dialogImagePath": "assets/wix-dialog.bmp" + } + }, + "fileAssociations": [ + { + "ext": ["cap"], + "name": "Cap Project", + "mimeType": "application/x-cap-project", + "role": "Editor" + } + ] + } } diff --git a/apps/desktop/src-tauri/tauri.prod.conf.json b/apps/desktop/src-tauri/tauri.prod.conf.json index 6f71ae604..6218ec5d2 100644 --- a/apps/desktop/src-tauri/tauri.prod.conf.json +++ b/apps/desktop/src-tauri/tauri.prod.conf.json @@ -1,29 +1,29 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "Cap", - "mainBinaryName": "Cap", - "identifier": "so.cap.desktop", - "build": { - "beforeBundleCommand": "node scripts/prodBeforeBundle.js" - }, - "plugins": { - "updater": { - "active": true, - "endpoints": [ - "https://cdn.crabnebula.app/update/cap/cap/{{target}}-{{arch}}/{{current_version}}" - ], - "dialog": true, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUyOTAzOTdFNzJFQkRFOTMKUldTVDN1dHlmam1RNHFXb1VYTXlrQk1iMFFkcjN0YitqZlA5WnZNY0ZtQ1dvM1dxK211M3VIYUQK" - } - }, - "bundle": { - "macOS": { - "entitlements": "Entitlements.plist" - }, - "windows": { - "wix": { - "upgradeCode": "a765d9de-0ecc-55d0-b8a0-61e9d3276664" - } - } - } + "$schema": "https://schema.tauri.app/config/2", + "productName": "Cap", + "mainBinaryName": "Cap", + "identifier": "so.cap.desktop", + "build": { + "beforeBundleCommand": "node scripts/prodBeforeBundle.js" + }, + "plugins": { + "updater": { + "active": true, + "endpoints": [ + "https://cdn.crabnebula.app/update/cap/cap/{{target}}-{{arch}}/{{current_version}}" + ], + "dialog": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUyOTAzOTdFNzJFQkRFOTMKUldTVDN1dHlmam1RNHFXb1VYTXlrQk1iMFFkcjN0YitqZlA5WnZNY0ZtQ1dvM1dxK211M3VIYUQK" + } + }, + "bundle": { + "macOS": { + "entitlements": "Entitlements.plist" + }, + "windows": { + "wix": { + "upgradeCode": "a765d9de-0ecc-55d0-b8a0-61e9d3276664" + } + } + } } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 37010abad..aad1752d7 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,136 +1,139 @@ import { Router, useCurrentMatches } from "@solidjs/router"; import { FileRoutes } from "@solidjs/start/router"; import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; +import { + getCurrentWebviewWindow, + type WebviewWindow, +} from "@tauri-apps/api/webviewWindow"; import { message } from "@tauri-apps/plugin-dialog"; import { - createEffect, - ErrorBoundary, - onCleanup, - onMount, - Suspense, + createEffect, + ErrorBoundary, + onCleanup, + onMount, + Suspense, } from "solid-js"; -import { - getCurrentWebviewWindow, - WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; import { Toaster } from "solid-toast"; import "@cap/ui-solid/main.css"; import "unfonts.css"; import "./styles/theme.css"; +import { CapErrorBoundary } from "./components/CapErrorBoundary"; import { generalSettingsStore } from "./store"; import { initAnonymousUser } from "./utils/analytics"; -import { commands, type AppTheme } from "./utils/tauri"; +import { type AppTheme, commands } from "./utils/tauri"; import titlebar from "./utils/titlebar-state"; -import { CapErrorBoundary } from "./components/CapErrorBoundary"; const queryClient = new QueryClient({ - defaultOptions: { - mutations: { - onError: (e) => { - message(`Error\n${e}`); - }, - }, - }, + defaultOptions: { + queries: { + experimental_prefetchInRender: true, + }, + mutations: { + onError: (e) => { + message(`Error\n${e}`); + }, + }, + }, }); export default function App() { - return ( - - - - - - ); + return ( + + + + + + ); } function Inner() { - const currentWindow = getCurrentWebviewWindow(); - createThemeListener(currentWindow); - - onMount(() => { - initAnonymousUser(); - }); - - return ( - <> - - - { - const matches = useCurrentMatches(); - - onMount(() => { - for (const match of matches()) { - if (match.route.info?.AUTO_SHOW_WINDOW === false) return; - } - - if (location.pathname !== "/camera") currentWindow.show(); - }); - - return ( - { - console.log("Root suspense fallback showing"); - }) as any - } - > - {props.children} - - ); - }} - > - - - - - ); + const currentWindow = getCurrentWebviewWindow(); + createThemeListener(currentWindow); + + onMount(() => { + initAnonymousUser(); + }); + + return ( + <> + + + { + const matches = useCurrentMatches(); + + onMount(() => { + for (const match of matches()) { + if (match.route.info?.AUTO_SHOW_WINDOW === false) return; + } + + if (location.pathname !== "/camera") currentWindow.show(); + }); + + return ( + { + console.log("Root suspense fallback showing"); + }) as any + } + > + {props.children} + + ); + }} + > + + + + + ); } function createThemeListener(currentWindow: WebviewWindow) { - const generalSettings = generalSettingsStore.createQuery(); - - createEffect(() => { - update(generalSettings.data?.theme ?? null); - }); - - onMount(async () => { - const unlisten = await currentWindow.onThemeChanged((_) => - update(generalSettings.data?.theme) - ); - onCleanup(() => unlisten?.()); - }); - - function update(appTheme: AppTheme | null | undefined) { - if (location.pathname === "/camera") return; - - if (appTheme === undefined || appTheme === null) return; - - commands.setTheme(appTheme).then(() => { - document.documentElement.classList.toggle( - "dark", - appTheme === "dark" || - window.matchMedia("(prefers-color-scheme: dark)").matches - ); - }); - } + const generalSettings = generalSettingsStore.createQuery(); + + createEffect(() => { + update(generalSettings.data?.theme ?? null); + }); + + onMount(async () => { + const unlisten = await currentWindow.onThemeChanged((_) => + update(generalSettings.data?.theme), + ); + onCleanup(() => unlisten?.()); + }); + + function update(appTheme: AppTheme | null | undefined) { + if (location.pathname === "/camera") return; + + if (appTheme === undefined || appTheme === null) return; + + commands.setTheme(appTheme).then(() => { + document.documentElement.classList.toggle( + "dark", + appTheme === "dark" || + window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + }); + } } diff --git a/apps/desktop/src/components/CapErrorBoundary.tsx b/apps/desktop/src/components/CapErrorBoundary.tsx index a4385d8c5..86cd41c20 100644 --- a/apps/desktop/src/components/CapErrorBoundary.tsx +++ b/apps/desktop/src/components/CapErrorBoundary.tsx @@ -1,53 +1,53 @@ import { Button } from "@cap/ui-solid"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { ErrorBoundary, ParentProps } from "solid-js"; +import { ErrorBoundary, type ParentProps } from "solid-js"; export function CapErrorBoundary(props: ParentProps) { - return ( - { - console.error(e); - return ( -
- -

- An Error Occured -

-

- We're very sorry, but something has gone wrong. -

-
- - -
+ return ( + { + console.error(e); + return ( +
+ +

+ An Error Occured +

+

+ We're very sorry, but something has gone wrong. +

+
+ + +
- {import.meta.env.DEV && ( - - )} -
- ); - }} - > - {props.children} -
- ); + {import.meta.env.DEV && ( + + )} +
+ ); + }} + > + {props.children} +
+ ); } diff --git a/apps/desktop/src/components/CropAreaRenderer.tsx b/apps/desktop/src/components/CropAreaRenderer.tsx index 3de37c0e4..13481af8f 100644 --- a/apps/desktop/src/components/CropAreaRenderer.tsx +++ b/apps/desktop/src/components/CropAreaRenderer.tsx @@ -1,290 +1,290 @@ -import type { Bounds } from "~/utils/tauri"; import { - onMount, - onCleanup, - createEffect, - type ParentProps, - createSignal, + createEffect, + createSignal, + onCleanup, + onMount, + type ParentProps, } from "solid-js"; import { createHiDPICanvasContext } from "~/utils/canvas"; +import type { Bounds } from "~/utils/tauri"; type DrawContext = { - ctx: CanvasRenderingContext2D; - bounds: Bounds; - radius: number; - prefersDark: boolean; - highlighted: boolean; - selected: boolean; + ctx: CanvasRenderingContext2D; + bounds: Bounds; + radius: number; + prefersDark: boolean; + highlighted: boolean; + selected: boolean; }; function drawHandles({ - ctx, - bounds, - radius, - highlighted, - selected, + ctx, + bounds, + radius, + highlighted, + selected, }: DrawContext) { - const { x, y, width, height } = bounds; - const minSizeForSideHandles = 100; - - ctx.strokeStyle = selected - ? "rgba(255, 255, 255, 1)" - : highlighted - ? "rgba(60, 150, 280, 1)" - : "rgba(255, 255, 255, 0.8)"; - - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.roundRect(x, y, width, height, radius); - ctx.stroke(); - - ctx.lineWidth = 5; - ctx.lineCap = "round"; - ctx.setLineDash([]); - - const cornerHandleLength = radius === 0 ? 20 : 10; - - // Corner handles - const adjustedRadius = Math.min(radius, width / 2, height / 2); - - const x2 = x + width; - const y2 = y + height; - - // top left - ctx.beginPath(); - - ctx.moveTo(x, y + adjustedRadius + cornerHandleLength); - ctx.arcTo(x, y, x2, y, adjustedRadius); - ctx.lineTo(x + adjustedRadius + cornerHandleLength, y); - - // top right - ctx.moveTo(x2 - adjustedRadius - cornerHandleLength, y); - ctx.arcTo(x2, y, x2, y2, adjustedRadius); - ctx.lineTo(x2, y + adjustedRadius + cornerHandleLength); - - // bottom left - ctx.moveTo(x + adjustedRadius + cornerHandleLength, y2); - ctx.arcTo(x, y2, x, y, adjustedRadius); - ctx.lineTo(x, y2 - adjustedRadius - cornerHandleLength); - - // bottom right - ctx.moveTo(x2, y2 - adjustedRadius - cornerHandleLength); - ctx.arcTo(x2, y2, x, y2, adjustedRadius); - ctx.lineTo(x2 - adjustedRadius - cornerHandleLength, y2); - - ctx.stroke(); - - // Only draw side handles if there's enough space. - if (!(width > minSizeForSideHandles && height > minSizeForSideHandles)) { - return; - } - - // Center handles - const handleLength = 35; - const sideHandleDistance = 0; - const centerX = bounds.x + bounds.width / 2; - const centerY = bounds.y + bounds.height / 2; - - ctx.beginPath(); - - // top center - ctx.moveTo(centerX - handleLength / 2, bounds.y - sideHandleDistance); - ctx.lineTo(centerX + handleLength / 2, bounds.y - sideHandleDistance); - - // bottom center - ctx.moveTo( - centerX - handleLength / 2, - bounds.y + bounds.height + sideHandleDistance - ); - ctx.lineTo( - centerX + handleLength / 2, - bounds.y + bounds.height + sideHandleDistance - ); - - // left center - ctx.moveTo(bounds.x - sideHandleDistance, centerY - handleLength / 2); - ctx.lineTo(bounds.x - sideHandleDistance, centerY + handleLength / 2); - - // right center - ctx.moveTo( - bounds.x + bounds.width + sideHandleDistance, - centerY - handleLength / 2 - ); - ctx.lineTo( - bounds.x + bounds.width + sideHandleDistance, - centerY + handleLength / 2 - ); - - ctx.stroke(); + const { x, y, width, height } = bounds; + const minSizeForSideHandles = 100; + + ctx.strokeStyle = selected + ? "rgba(255, 255, 255, 1)" + : highlighted + ? "rgba(60, 150, 280, 1)" + : "rgba(255, 255, 255, 0.8)"; + + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.stroke(); + + ctx.lineWidth = 5; + ctx.lineCap = "round"; + ctx.setLineDash([]); + + const cornerHandleLength = radius === 0 ? 20 : 10; + + // Corner handles + const adjustedRadius = Math.min(radius, width / 2, height / 2); + + const x2 = x + width; + const y2 = y + height; + + // top left + ctx.beginPath(); + + ctx.moveTo(x, y + adjustedRadius + cornerHandleLength); + ctx.arcTo(x, y, x2, y, adjustedRadius); + ctx.lineTo(x + adjustedRadius + cornerHandleLength, y); + + // top right + ctx.moveTo(x2 - adjustedRadius - cornerHandleLength, y); + ctx.arcTo(x2, y, x2, y2, adjustedRadius); + ctx.lineTo(x2, y + adjustedRadius + cornerHandleLength); + + // bottom left + ctx.moveTo(x + adjustedRadius + cornerHandleLength, y2); + ctx.arcTo(x, y2, x, y, adjustedRadius); + ctx.lineTo(x, y2 - adjustedRadius - cornerHandleLength); + + // bottom right + ctx.moveTo(x2, y2 - adjustedRadius - cornerHandleLength); + ctx.arcTo(x2, y2, x, y2, adjustedRadius); + ctx.lineTo(x2 - adjustedRadius - cornerHandleLength, y2); + + ctx.stroke(); + + // Only draw side handles if there's enough space. + if (!(width > minSizeForSideHandles && height > minSizeForSideHandles)) { + return; + } + + // Center handles + const handleLength = 35; + const sideHandleDistance = 0; + const centerX = bounds.x + bounds.width / 2; + const centerY = bounds.y + bounds.height / 2; + + ctx.beginPath(); + + // top center + ctx.moveTo(centerX - handleLength / 2, bounds.y - sideHandleDistance); + ctx.lineTo(centerX + handleLength / 2, bounds.y - sideHandleDistance); + + // bottom center + ctx.moveTo( + centerX - handleLength / 2, + bounds.y + bounds.height + sideHandleDistance, + ); + ctx.lineTo( + centerX + handleLength / 2, + bounds.y + bounds.height + sideHandleDistance, + ); + + // left center + ctx.moveTo(bounds.x - sideHandleDistance, centerY - handleLength / 2); + ctx.lineTo(bounds.x - sideHandleDistance, centerY + handleLength / 2); + + // right center + ctx.moveTo( + bounds.x + bounds.width + sideHandleDistance, + centerY - handleLength / 2, + ); + ctx.lineTo( + bounds.x + bounds.width + sideHandleDistance, + centerY + handleLength / 2, + ); + + ctx.stroke(); } // Rule of thirds guide lines and center crosshair function drawGuideLines({ ctx, bounds, prefersDark }: DrawContext) { - ctx.strokeStyle = prefersDark - ? "rgba(255, 255, 255, 0.5)" - : "rgba(0, 0, 0, 0.5)"; - ctx.lineWidth = 1; - ctx.setLineDash([5, 2]); - - // Rule of thirds - ctx.beginPath(); - for (let i = 1; i < 3; i++) { - const x = bounds.x + (bounds.width * i) / 3; - ctx.moveTo(x, bounds.y); - ctx.lineTo(x, bounds.y + bounds.height); - } - ctx.stroke(); - - ctx.beginPath(); - for (let i = 1; i < 3; i++) { - const y = bounds.y + (bounds.height * i) / 3; - ctx.moveTo(bounds.x, y); - ctx.lineTo(bounds.x + bounds.width, y); - } - ctx.stroke(); - - // Center crosshair - const centerX = Math.round(bounds.x + bounds.width / 2); - const centerY = Math.round(bounds.y + bounds.height / 2); - - ctx.setLineDash([]); - ctx.lineWidth = 2; - const crosshairLength = 7; - - ctx.beginPath(); - ctx.moveTo(centerX - crosshairLength, centerY); - ctx.lineTo(centerX + crosshairLength, centerY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(centerX, centerY - crosshairLength); - ctx.lineTo(centerX, centerY + crosshairLength); - ctx.stroke(); + ctx.strokeStyle = prefersDark + ? "rgba(255, 255, 255, 0.5)" + : "rgba(0, 0, 0, 0.5)"; + ctx.lineWidth = 1; + ctx.setLineDash([5, 2]); + + // Rule of thirds + ctx.beginPath(); + for (let i = 1; i < 3; i++) { + const x = bounds.x + (bounds.width * i) / 3; + ctx.moveTo(x, bounds.y); + ctx.lineTo(x, bounds.y + bounds.height); + } + ctx.stroke(); + + ctx.beginPath(); + for (let i = 1; i < 3; i++) { + const y = bounds.y + (bounds.height * i) / 3; + ctx.moveTo(bounds.x, y); + ctx.lineTo(bounds.x + bounds.width, y); + } + ctx.stroke(); + + // Center crosshair + const centerX = Math.round(bounds.x + bounds.width / 2); + const centerY = Math.round(bounds.y + bounds.height / 2); + + ctx.setLineDash([]); + ctx.lineWidth = 2; + const crosshairLength = 7; + + ctx.beginPath(); + ctx.moveTo(centerX - crosshairLength, centerY); + ctx.lineTo(centerX + crosshairLength, centerY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(centerX, centerY - crosshairLength); + ctx.lineTo(centerX, centerY + crosshairLength); + ctx.stroke(); } // Main draw function function draw( - ctx: CanvasRenderingContext2D, - bounds: Bounds, - radius: number, - guideLines: boolean, - showHandles: boolean, - highlighted: boolean, - selected: boolean, - prefersDark: boolean + ctx: CanvasRenderingContext2D, + bounds: Bounds, + radius: number, + guideLines: boolean, + showHandles: boolean, + highlighted: boolean, + selected: boolean, + prefersDark: boolean, ) { - if (bounds.width <= 0 || bounds.height <= 0) return; - const drawContext: DrawContext = { - ctx, - bounds, - radius, - prefersDark, - highlighted, - selected, - }; - - ctx.save(); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - ctx.fillStyle = "rgba(0, 0, 0, 0.65)"; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - // Shadow - ctx.save(); - ctx.shadowColor = "rgba(0, 0, 0, 0.8)"; - ctx.shadowBlur = 200; - ctx.shadowOffsetY = 25; - ctx.beginPath(); - ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius); - ctx.fill(); - ctx.restore(); - - if (showHandles) drawHandles(drawContext); - - ctx.beginPath(); - ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius); - ctx.clip(); - ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height); - - if (guideLines) drawGuideLines(drawContext); - - ctx.restore(); + if (bounds.width <= 0 || bounds.height <= 0) return; + const drawContext: DrawContext = { + ctx, + bounds, + radius, + prefersDark, + highlighted, + selected, + }; + + ctx.save(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + ctx.fillStyle = "rgba(0, 0, 0, 0.65)"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // Shadow + ctx.save(); + ctx.shadowColor = "rgba(0, 0, 0, 0.8)"; + ctx.shadowBlur = 200; + ctx.shadowOffsetY = 25; + ctx.beginPath(); + ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius); + ctx.fill(); + ctx.restore(); + + if (showHandles) drawHandles(drawContext); + + ctx.beginPath(); + ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius); + ctx.clip(); + ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height); + + if (guideLines) drawGuideLines(drawContext); + + ctx.restore(); } export default function CropAreaRenderer( - props: ParentProps<{ - bounds: Bounds; - guideLines?: boolean; - handles?: boolean; - borderRadius?: number; - highlighted?: boolean; - selected?: boolean; - }> + props: ParentProps<{ + bounds: Bounds; + guideLines?: boolean; + handles?: boolean; + borderRadius?: number; + highlighted?: boolean; + selected?: boolean; + }>, ) { - let canvasRef: HTMLCanvasElement | undefined; - const [prefersDarkScheme, setPrefersDarkScheme] = createSignal(false); - - onMount(() => { - if (!canvasRef) { - console.error("Canvas ref was not setup"); - return; - } - - const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)"); - setPrefersDarkScheme(colorSchemeQuery.matches); - const handleChange = (e: MediaQueryListEvent) => - setPrefersDarkScheme(e.matches); - colorSchemeQuery.addEventListener("change", handleChange); - - const hidpiCanvas = createHiDPICanvasContext(canvasRef, (ctx) => - draw( - ctx, - props.bounds, - props.borderRadius || 0, - props.guideLines || false, - props.handles || false, - props.highlighted || false, - props.selected || false, - prefersDarkScheme() - ) - ); - const ctx = hidpiCanvas?.ctx; - if (!ctx) return; - - let lastAnimationFrameId: number | undefined; - createEffect(() => { - if (lastAnimationFrameId) cancelAnimationFrame(lastAnimationFrameId); - - const { x, y, width, height } = props.bounds; - const { guideLines, handles, borderRadius, highlighted, selected } = - props; - - const prefersDark = prefersDarkScheme(); - lastAnimationFrameId = requestAnimationFrame(() => - draw( - ctx, - { x, y, width, height }, - borderRadius || 0, - guideLines || false, - handles || false, - highlighted || false, - selected || false, - prefersDark - ) - ); - }); - - onCleanup(() => { - if (lastAnimationFrameId) cancelAnimationFrame(lastAnimationFrameId); - hidpiCanvas.cleanup(); - colorSchemeQuery.removeEventListener("change", handleChange); - }); - }); - - return ( -
- -
{props.children}
-
- ); + let canvasRef: HTMLCanvasElement | undefined; + const [prefersDarkScheme, setPrefersDarkScheme] = createSignal(false); + + onMount(() => { + if (!canvasRef) { + console.error("Canvas ref was not setup"); + return; + } + + const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + setPrefersDarkScheme(colorSchemeQuery.matches); + const handleChange = (e: MediaQueryListEvent) => + setPrefersDarkScheme(e.matches); + colorSchemeQuery.addEventListener("change", handleChange); + + const hidpiCanvas = createHiDPICanvasContext(canvasRef, (ctx) => + draw( + ctx, + props.bounds, + props.borderRadius || 0, + props.guideLines || false, + props.handles || false, + props.highlighted || false, + props.selected || false, + prefersDarkScheme(), + ), + ); + const ctx = hidpiCanvas?.ctx; + if (!ctx) return; + + let lastAnimationFrameId: number | undefined; + createEffect(() => { + if (lastAnimationFrameId) cancelAnimationFrame(lastAnimationFrameId); + + const { x, y, width, height } = props.bounds; + const { guideLines, handles, borderRadius, highlighted, selected } = + props; + + const prefersDark = prefersDarkScheme(); + lastAnimationFrameId = requestAnimationFrame(() => + draw( + ctx, + { x, y, width, height }, + borderRadius || 0, + guideLines || false, + handles || false, + highlighted || false, + selected || false, + prefersDark, + ), + ); + }); + + onCleanup(() => { + if (lastAnimationFrameId) cancelAnimationFrame(lastAnimationFrameId); + hidpiCanvas.cleanup(); + colorSchemeQuery.removeEventListener("change", handleChange); + }); + }); + + return ( +
+ +
{props.children}
+
+ ); } diff --git a/apps/desktop/src/components/Cropper.tsx b/apps/desktop/src/components/Cropper.tsx index b5ef17c05..bcc0e9ae4 100644 --- a/apps/desktop/src/components/Cropper.tsx +++ b/apps/desktop/src/components/Cropper.tsx @@ -3,765 +3,765 @@ import { makePersisted } from "@solid-primitives/storage"; import { type CheckMenuItemOptions, Menu } from "@tauri-apps/api/menu"; import { type as ostype } from "@tauri-apps/plugin-os"; import { - type ParentProps, - batch, - createEffect, - createMemo, - createResource, - createRoot, - createSignal, - For, - on, - onCleanup, - onMount, - Show, + batch, + createEffect, + createMemo, + createResource, + createRoot, + createSignal, + For, + on, + onCleanup, + onMount, + type ParentProps, + Show, } from "solid-js"; import { createStore } from "solid-js/store"; import { Transition } from "solid-transition-group"; import { generalSettingsStore } from "~/store"; import Box from "~/utils/box"; -import { type Crop, type XY, commands } from "~/utils/tauri"; +import { type Crop, commands, type XY } from "~/utils/tauri"; import CropAreaRenderer from "./CropAreaRenderer"; type Direction = "n" | "e" | "s" | "w" | "nw" | "ne" | "se" | "sw"; type HandleSide = { - x: "l" | "r" | "c"; - y: "t" | "b" | "c"; - direction: Direction; - cursor: "ew" | "ns" | "nesw" | "nwse"; + x: "l" | "r" | "c"; + y: "t" | "b" | "c"; + direction: Direction; + cursor: "ew" | "ns" | "nesw" | "nwse"; }; const HANDLES: HandleSide[] = [ - { x: "l", y: "t", direction: "nw", cursor: "nwse" }, - { x: "r", y: "t", direction: "ne", cursor: "nesw" }, - { x: "l", y: "b", direction: "sw", cursor: "nesw" }, - { x: "r", y: "b", direction: "se", cursor: "nwse" }, - { x: "c", y: "t", direction: "n", cursor: "ns" }, - { x: "c", y: "b", direction: "s", cursor: "ns" }, - { x: "l", y: "c", direction: "w", cursor: "ew" }, - { x: "r", y: "c", direction: "e", cursor: "ew" }, + { x: "l", y: "t", direction: "nw", cursor: "nwse" }, + { x: "r", y: "t", direction: "ne", cursor: "nesw" }, + { x: "l", y: "b", direction: "sw", cursor: "nesw" }, + { x: "r", y: "b", direction: "se", cursor: "nwse" }, + { x: "c", y: "t", direction: "n", cursor: "ns" }, + { x: "c", y: "b", direction: "s", cursor: "ns" }, + { x: "l", y: "c", direction: "w", cursor: "ew" }, + { x: "r", y: "c", direction: "e", cursor: "ew" }, ]; type Ratio = [number, number]; const COMMON_RATIOS: Ratio[] = [ - [1, 1], - [4, 3], - [3, 2], - [16, 9], - [2, 1], - [21, 9], + [1, 1], + [4, 3], + [3, 2], + [16, 9], + [2, 1], + [21, 9], ]; const KEY_MAPPINGS = new Map([ - ["ArrowRight", "e"], - ["ArrowDown", "s"], - ["ArrowLeft", "w"], - ["ArrowUp", "n"], + ["ArrowRight", "e"], + ["ArrowDown", "s"], + ["ArrowLeft", "w"], + ["ArrowUp", "n"], ]); const ORIGIN_CENTER: XY = { x: 0.5, y: 0.5 }; function clamp(n: number, min = 0, max = 1) { - return Math.max(min, Math.min(max, n)); + return Math.max(min, Math.min(max, n)); } function distanceOf(firstPoint: Touch, secondPoint: Touch): number { - const dx = firstPoint.clientX - secondPoint.clientX; - const dy = firstPoint.clientY - secondPoint.clientY; - return Math.sqrt(dx * dx + dy * dy); + const dx = firstPoint.clientX - secondPoint.clientX; + const dy = firstPoint.clientY - secondPoint.clientY; + return Math.sqrt(dx * dx + dy * dy); } export function cropToFloor(value: Crop): Crop { - return { - size: { - x: Math.floor(value.size.x), - y: Math.floor(value.size.y), - }, - position: { - x: Math.floor(value.position.x), - y: Math.floor(value.position.y), - }, - }; + return { + size: { + x: Math.floor(value.size.x), + y: Math.floor(value.size.y), + }, + position: { + x: Math.floor(value.position.x), + y: Math.floor(value.position.y), + }, + }; } export default function Cropper( - props: ParentProps<{ - class?: string; - onCropChange: (value: Crop) => void; - value: Crop; - mappedSize?: XY; - minSize?: XY; - initialSize?: XY; - aspectRatio?: number; - showGuideLines?: boolean; - }> + props: ParentProps<{ + class?: string; + onCropChange: (value: Crop) => void; + value: Crop; + mappedSize?: XY; + minSize?: XY; + initialSize?: XY; + aspectRatio?: number; + showGuideLines?: boolean; + }>, ) { - const crop = props.value; - - const [containerSize, setContainerSize] = createSignal({ x: 0, y: 0 }); - const mappedSize = createMemo(() => props.mappedSize || containerSize()); - const minSize = createMemo(() => { - const mapped = mappedSize(); - return { - x: Math.min(100, mapped.x * 0.1), - y: Math.min(100, mapped.y * 0.1), - }; - }); - - const containerToMappedSizeScale = createMemo(() => { - const container = containerSize(); - const mapped = mappedSize(); - return { - x: container.x / mapped.x, - y: container.y / mapped.y, - }; - }); - - const displayScaledCrop = createMemo(() => { - const mapped = mappedSize(); - const container = containerSize(); - return { - x: (crop.position.x / mapped.x) * container.x, - y: (crop.position.y / mapped.y) * container.y, - width: (crop.size.x / mapped.x) * container.x, - height: (crop.size.y / mapped.y) * container.y, - }; - }); - - let containerRef: HTMLDivElement | undefined; - onMount(() => { - if (!containerRef) return; - - const updateContainerSize = () => { - setContainerSize({ - x: containerRef!.clientWidth, - y: containerRef!.clientHeight, - }); - }; - - updateContainerSize(); - const resizeObserver = new ResizeObserver(updateContainerSize); - resizeObserver.observe(containerRef); - onCleanup(() => resizeObserver.disconnect()); - - const mapped = mappedSize(); - const initial = props.initialSize || { - x: mapped.x / 2, - y: mapped.y / 2, - }; - - let width = clamp(initial.x, minSize().x, mapped.x); - let height = clamp(initial.y, minSize().y, mapped.y); - - const box = Box.from( - { x: (mapped.x - width) / 2, y: (mapped.y - height) / 2 }, - { x: width, y: height } - ); - box.constrainAll(box, containerSize(), ORIGIN_CENTER, props.aspectRatio); - - setCrop({ - size: { x: width, y: height }, - position: { - x: (mapped.x - width) / 2, - y: (mapped.y - height) / 2, - }, - }); - }); - - createEffect( - on( - () => props.aspectRatio, - () => { - if (!props.aspectRatio) return; - const box = Box.from(crop.position, crop.size); - box.constrainToRatio(props.aspectRatio, ORIGIN_CENTER); - box.constrainToBoundary(mappedSize().x, mappedSize().y, ORIGIN_CENTER); - setCrop(box.toBounds()); - } - ) - ); - - const [snapToRatioEnabled, setSnapToRatioEnabled] = makePersisted( - createSignal(true), - { name: "cropSnapsToRatio" } - ); - const [snappedRatio, setSnappedRatio] = createSignal(null); - const [dragging, setDragging] = createSignal(false); - const [gestureState, setGestureState] = createStore<{ - isTrackpadGesture: boolean; - lastTouchCenter: XY | null; - initialPinchDistance: number; - initialSize: { width: number; height: number }; - }>({ - isTrackpadGesture: false, - lastTouchCenter: null, - initialPinchDistance: 0, - initialSize: { width: 0, height: 0 }, - }); - - function handleDragStart(event: MouseEvent) { - if (gestureState.isTrackpadGesture) return; // Don't start drag if we're in a trackpad gesture - event.stopPropagation(); - setDragging(true); - let lastValidPos = { x: event.clientX, y: event.clientY }; - const box = Box.from(crop.position, crop.size); - const scaleFactors = containerToMappedSizeScale(); - - createRoot((dispose) => { - const mapped = mappedSize(); - createEventListenerMap(window, { - mouseup: () => { - setDragging(false); - dispose(); - }, - mousemove: (e) => { - requestAnimationFrame(() => { - const dx = (e.clientX - lastValidPos.x) / scaleFactors.x; - const dy = (e.clientY - lastValidPos.y) / scaleFactors.y; - - box.move( - clamp(box.x + dx, 0, mapped.x - box.width), - clamp(box.y + dy, 0, mapped.y - box.height) - ); - - const newBox = box; - if (newBox.x !== crop.position.x || newBox.y !== crop.position.y) { - lastValidPos = { x: e.clientX, y: e.clientY }; - setCrop(newBox.toBounds()); - } - }); - }, - }); - }); - } - - function handleWheel(event: WheelEvent) { - event.preventDefault(); - const box = Box.from(crop.position, crop.size); - const mapped = mappedSize(); - - if (event.ctrlKey) { - setGestureState("isTrackpadGesture", true); - - const velocity = Math.max(0.001, Math.abs(event.deltaY) * 0.001); - const scale = 1 - event.deltaY * velocity; - - box.resize( - clamp(box.width * scale, minSize().x, mapped.x), - clamp(box.height * scale, minSize().y, mapped.y), - ORIGIN_CENTER - ); - box.constrainAll(box, mapped, ORIGIN_CENTER, props.aspectRatio); - setTimeout(() => setGestureState("isTrackpadGesture", false), 100); - setSnappedRatio(null); - } else { - const velocity = Math.max(1, Math.abs(event.deltaY) * 0.01); - const scaleFactors = containerToMappedSizeScale(); - const dx = (-event.deltaX * velocity) / scaleFactors.x; - const dy = (-event.deltaY * velocity) / scaleFactors.y; - - box.move( - clamp(box.x + dx, 0, mapped.x - box.width), - clamp(box.y + dy, 0, mapped.y - box.height) - ); - } - - setCrop(box.toBounds()); - } - - function handleTouchStart(event: TouchEvent) { - if (event.touches.length === 2) { - // Initialize pinch zoom - const distance = distanceOf(event.touches[0], event.touches[1]); - - // Initialize touch center - const centerX = (event.touches[0].clientX + event.touches[1].clientX) / 2; - const centerY = (event.touches[0].clientY + event.touches[1].clientY) / 2; - - batch(() => { - setGestureState("initialPinchDistance", distance); - setGestureState("initialSize", { - width: crop.size.x, - height: crop.size.y, - }); - setGestureState("lastTouchCenter", { x: centerX, y: centerY }); - }); - } else if (event.touches.length === 1) { - // Handle single touch as drag - batch(() => { - setDragging(true); - setGestureState("lastTouchCenter", { - x: event.touches[0].clientX, - y: event.touches[0].clientY, - }); - }); - } - } - - function handleTouchMove(event: TouchEvent) { - if (event.touches.length === 2) { - // Handle pinch zoom - const currentDistance = distanceOf(event.touches[0], event.touches[1]); - const scale = currentDistance / gestureState.initialPinchDistance; - - const box = Box.from(crop.position, crop.size); - const mapped = mappedSize(); - - // Calculate new dimensions while maintaining aspect ratio - const currentRatio = crop.size.x / crop.size.y; - let newWidth = clamp( - gestureState.initialSize.width * scale, - minSize().x, - mapped.x - ); - let newHeight = newWidth / currentRatio; - - // Adjust if height exceeds bounds - if (newHeight < minSize().y || newHeight > mapped.y) { - newHeight = clamp(newHeight, minSize().y, mapped.y); - newWidth = newHeight * currentRatio; - } - - // Resize from center - box.resize(newWidth, newHeight, ORIGIN_CENTER); - - // Handle two-finger pan - const centerX = (event.touches[0].clientX + event.touches[1].clientX) / 2; - const centerY = (event.touches[0].clientY + event.touches[1].clientY) / 2; - - if (gestureState.lastTouchCenter) { - const scaleFactors = containerToMappedSizeScale(); - const dx = (centerX - gestureState.lastTouchCenter.x) / scaleFactors.x; - const dy = (centerY - gestureState.lastTouchCenter.y) / scaleFactors.y; - - box.move( - clamp(box.x + dx, 0, mapped.x - box.width), - clamp(box.y + dy, 0, mapped.y - box.height) - ); - } - - setGestureState("lastTouchCenter", { x: centerX, y: centerY }); - setCrop(box.toBounds()); - } else if (event.touches.length === 1 && dragging()) { - // Handle single touch drag - const box = Box.from(crop.position, crop.size); - const scaleFactors = containerToMappedSizeScale(); - const mapped = mappedSize(); - - if (gestureState.lastTouchCenter) { - const dx = - (event.touches[0].clientX - gestureState.lastTouchCenter.x) / - scaleFactors.x; - const dy = - (event.touches[0].clientY - gestureState.lastTouchCenter.y) / - scaleFactors.y; - - box.move( - clamp(box.x + dx, 0, mapped.x - box.width), - clamp(box.y + dy, 0, mapped.y - box.height) - ); - } - - setGestureState("lastTouchCenter", { - x: event.touches[0].clientX, - y: event.touches[0].clientY, - }); - setCrop(box.toBounds()); - } - } - - function handleTouchEnd(event: TouchEvent) { - if (event.touches.length === 0) { - setDragging(false); - setGestureState("lastTouchCenter", null); - } else if (event.touches.length === 1) { - setGestureState("lastTouchCenter", { - x: event.touches[0].clientX, - y: event.touches[0].clientY, - }); - } - } - - function handleResizeStartTouch(event: TouchEvent, dir: Direction) { - if (event.touches.length !== 1) return; - event.stopPropagation(); - const touch = event.touches[0]; - handleResizeStart(touch.clientX, touch.clientY, dir); - } - - function findClosestRatio( - width: number, - height: number, - threshold = 0.01 - ): Ratio | null { - if (props.aspectRatio) return null; - const currentRatio = width / height; - for (const ratio of COMMON_RATIOS) { - if (Math.abs(currentRatio - ratio[0] / ratio[1]) < threshold) { - return [ratio[0], ratio[1]]; - } - if (Math.abs(currentRatio - ratio[1] / ratio[0]) < threshold) { - return [ratio[1], ratio[0]]; - } - } - return null; - } - - function handleResizeStart(clientX: number, clientY: number, dir: Direction) { - const origin: XY = { - x: dir.includes("w") ? 1 : 0, - y: dir.includes("n") ? 1 : 0, - }; - - let lastValidPos = { x: clientX, y: clientY }; - const box = Box.from(crop.position, crop.size); - const scaleFactors = containerToMappedSizeScale(); - const mapped = mappedSize(); - - createRoot((dispose) => { - createEventListenerMap(window, { - mouseup: dispose, - touchend: dispose, - touchmove: (e) => - requestAnimationFrame(() => { - if (e.touches.length !== 1) return; - handleResizeMove(e.touches[0].clientX, e.touches[0].clientY); - }), - mousemove: (e) => - requestAnimationFrame(() => - handleResizeMove(e.clientX, e.clientY, e.altKey) - ), - }); - }); - - const [hapticsEnabled, hapticsEnabledOptions] = createResource( - async () => - (await generalSettingsStore.get())?.hapticsEnabled && - ostype() === "macos" - ); - generalSettingsStore.listen(() => hapticsEnabledOptions.refetch()); - - function handleResizeMove( - moveX: number, - moveY: number, - centerOrigin = false - ) { - const dx = (moveX - lastValidPos.x) / scaleFactors.x; - const dy = (moveY - lastValidPos.y) / scaleFactors.y; - - const scaleMultiplier = centerOrigin ? 2 : 1; - const currentBox = box.toBounds(); - - let newWidth = - dir.includes("e") || dir.includes("w") - ? clamp( - dir.includes("w") - ? currentBox.size.x - dx * scaleMultiplier - : currentBox.size.x + dx * scaleMultiplier, - minSize().x, - mapped.x - ) - : currentBox.size.x; - - let newHeight = - dir.includes("n") || dir.includes("s") - ? clamp( - dir.includes("n") - ? currentBox.size.y - dy * scaleMultiplier - : currentBox.size.y + dy * scaleMultiplier, - minSize().y, - mapped.y - ) - : currentBox.size.y; - - const closest = findClosestRatio(newWidth, newHeight); - if (dir.length === 2 && snapToRatioEnabled() && closest) { - const ratio = closest[0] / closest[1]; - if (dir.includes("n") || dir.includes("s")) { - newWidth = newHeight * ratio; - } else { - newHeight = newWidth / ratio; - } - if (!snappedRatio() && hapticsEnabled()) { - commands.performHapticFeedback("Alignment", "Now"); - } - setSnappedRatio(closest); - } else { - setSnappedRatio(null); - } - - const newOrigin = centerOrigin ? ORIGIN_CENTER : origin; - box.resize(newWidth, newHeight, newOrigin); - - if (props.aspectRatio) { - box.constrainToRatio( - props.aspectRatio, - newOrigin, - dir.includes("n") || dir.includes("s") ? "width" : "height" - ); - } - box.constrainToBoundary(mapped.x, mapped.y, newOrigin); - - const newBox = box.toBounds(); - if ( - newBox.size.x !== crop.size.x || - newBox.size.y !== crop.size.y || - newBox.position.x !== crop.position.x || - newBox.position.y !== crop.position.y - ) { - lastValidPos = { x: moveX, y: moveY }; - props.onCropChange(newBox); - } - } - } - - function setCrop(value: Crop) { - props.onCropChange(value); - } - - let pressedKeys = new Set([]); - let lastKeyHandleFrame: number | null = null; - function handleKeyDown(event: KeyboardEvent) { - if (dragging()) return; - const dir = KEY_MAPPINGS.get(event.key); - if (!dir) return; - event.preventDefault(); - pressedKeys.add(event.key); - - if (lastKeyHandleFrame) return; - lastKeyHandleFrame = requestAnimationFrame(() => { - const box = Box.from(crop.position, crop.size); - const mapped = mappedSize(); - const scaleFactors = containerToMappedSizeScale(); - - const moveDelta = event.shiftKey ? 20 : 5; - const origin = event.altKey ? ORIGIN_CENTER : { x: 0, y: 0 }; - - for (const key of pressedKeys) { - const dir = KEY_MAPPINGS.get(key); - if (!dir) continue; - - const isUpKey = dir === "n"; - const isLeftKey = dir === "w"; - const isDownKey = dir === "s"; - const isRightKey = dir === "e"; - - if (event.metaKey || event.ctrlKey) { - const scaleMultiplier = event.altKey ? 2 : 1; - const currentBox = box.toBounds(); - - let newWidth = currentBox.size.x; - let newHeight = currentBox.size.y; - - if (isLeftKey || isRightKey) { - newWidth = clamp( - isLeftKey - ? currentBox.size.x - moveDelta * scaleMultiplier - : currentBox.size.x + moveDelta * scaleMultiplier, - minSize().x, - mapped.x - ); - } - - if (isUpKey || isDownKey) { - newHeight = clamp( - isUpKey - ? currentBox.size.y - moveDelta * scaleMultiplier - : currentBox.size.y + moveDelta * scaleMultiplier, - minSize().y, - mapped.y - ); - } - - box.resize(newWidth, newHeight, origin); - } else { - const dx = - (isRightKey ? moveDelta : isLeftKey ? -moveDelta : 0) / - scaleFactors.x; - const dy = - (isDownKey ? moveDelta : isUpKey ? -moveDelta : 0) / scaleFactors.y; - - box.move( - clamp(box.x + dx, 0, mapped.x - box.width), - clamp(box.y + dy, 0, mapped.y - box.height) - ); - } - } - - if (props.aspectRatio) box.constrainToRatio(props.aspectRatio, origin); - box.constrainToBoundary(mapped.x, mapped.y, origin); - setCrop(box.toBounds()); - - pressedKeys.clear(); - lastKeyHandleFrame = null; - }); - } - - return ( -
{ - e.preventDefault(); - const menu = await Menu.new({ - id: "crop-options", - items: [ - { - id: "enableRatioSnap", - text: "Snap to aspect ratios", - checked: snapToRatioEnabled(), - action: () => { - setSnapToRatioEnabled((v) => !v); - }, - } satisfies CheckMenuItemOptions, - ], - }); - menu.popup(); - }} - > - - {props.children} - -
-
- { - const animation = el.animate( - [ - { opacity: 0, transform: "translateY(-8px)" }, - { opacity: 0.65, transform: "translateY(0)" }, - ], - { - duration: 100, - easing: "ease-out", - } - ); - animation.finished.then(done); - }} - onExit={(el, done) => { - const animation = el.animate( - [ - { opacity: 0.65, transform: "translateY(0)" }, - { opacity: 0, transform: "translateY(-8px)" }, - ], - { - duration: 100, - easing: "ease-in", - } - ); - animation.finished.then(done); - }} - > - -
- {snappedRatio()![0]}:{snappedRatio()![1]} -
-
-
-
- - {(handle) => { - const isCorner = handle.x !== "c" && handle.y !== "c"; - - return isCorner ? ( -
{ - e.stopPropagation(); - handleResizeStart(e.clientX, e.clientY, handle.direction); - }} - onTouchStart={(e) => - handleResizeStartTouch(e, handle.direction) - } - /> - ) : ( -
{ - e.stopPropagation(); - handleResizeStart(e.clientX, e.clientY, handle.direction); - }} - onTouchStart={(e) => - handleResizeStartTouch(e, handle.direction) - } - /> - ); - }} - -
-
- ); + const crop = props.value; + + const [containerSize, setContainerSize] = createSignal({ x: 0, y: 0 }); + const mappedSize = createMemo(() => props.mappedSize || containerSize()); + const minSize = createMemo(() => { + const mapped = mappedSize(); + return { + x: Math.min(100, mapped.x * 0.1), + y: Math.min(100, mapped.y * 0.1), + }; + }); + + const containerToMappedSizeScale = createMemo(() => { + const container = containerSize(); + const mapped = mappedSize(); + return { + x: container.x / mapped.x, + y: container.y / mapped.y, + }; + }); + + const displayScaledCrop = createMemo(() => { + const mapped = mappedSize(); + const container = containerSize(); + return { + x: (crop.position.x / mapped.x) * container.x, + y: (crop.position.y / mapped.y) * container.y, + width: (crop.size.x / mapped.x) * container.x, + height: (crop.size.y / mapped.y) * container.y, + }; + }); + + let containerRef: HTMLDivElement | undefined; + onMount(() => { + if (!containerRef) return; + + const updateContainerSize = () => { + setContainerSize({ + x: containerRef!.clientWidth, + y: containerRef!.clientHeight, + }); + }; + + updateContainerSize(); + const resizeObserver = new ResizeObserver(updateContainerSize); + resizeObserver.observe(containerRef); + onCleanup(() => resizeObserver.disconnect()); + + const mapped = mappedSize(); + const initial = props.initialSize || { + x: mapped.x / 2, + y: mapped.y / 2, + }; + + const width = clamp(initial.x, minSize().x, mapped.x); + const height = clamp(initial.y, minSize().y, mapped.y); + + const box = Box.from( + { x: (mapped.x - width) / 2, y: (mapped.y - height) / 2 }, + { x: width, y: height }, + ); + box.constrainAll(box, containerSize(), ORIGIN_CENTER, props.aspectRatio); + + setCrop({ + size: { x: width, y: height }, + position: { + x: (mapped.x - width) / 2, + y: (mapped.y - height) / 2, + }, + }); + }); + + createEffect( + on( + () => props.aspectRatio, + () => { + if (!props.aspectRatio) return; + const box = Box.from(crop.position, crop.size); + box.constrainToRatio(props.aspectRatio, ORIGIN_CENTER); + box.constrainToBoundary(mappedSize().x, mappedSize().y, ORIGIN_CENTER); + setCrop(box.toBounds()); + }, + ), + ); + + const [snapToRatioEnabled, setSnapToRatioEnabled] = makePersisted( + createSignal(true), + { name: "cropSnapsToRatio" }, + ); + const [snappedRatio, setSnappedRatio] = createSignal(null); + const [dragging, setDragging] = createSignal(false); + const [gestureState, setGestureState] = createStore<{ + isTrackpadGesture: boolean; + lastTouchCenter: XY | null; + initialPinchDistance: number; + initialSize: { width: number; height: number }; + }>({ + isTrackpadGesture: false, + lastTouchCenter: null, + initialPinchDistance: 0, + initialSize: { width: 0, height: 0 }, + }); + + function handleDragStart(event: MouseEvent) { + if (gestureState.isTrackpadGesture) return; // Don't start drag if we're in a trackpad gesture + event.stopPropagation(); + setDragging(true); + let lastValidPos = { x: event.clientX, y: event.clientY }; + const box = Box.from(crop.position, crop.size); + const scaleFactors = containerToMappedSizeScale(); + + createRoot((dispose) => { + const mapped = mappedSize(); + createEventListenerMap(window, { + mouseup: () => { + setDragging(false); + dispose(); + }, + mousemove: (e) => { + requestAnimationFrame(() => { + const dx = (e.clientX - lastValidPos.x) / scaleFactors.x; + const dy = (e.clientY - lastValidPos.y) / scaleFactors.y; + + box.move( + clamp(box.x + dx, 0, mapped.x - box.width), + clamp(box.y + dy, 0, mapped.y - box.height), + ); + + const newBox = box; + if (newBox.x !== crop.position.x || newBox.y !== crop.position.y) { + lastValidPos = { x: e.clientX, y: e.clientY }; + setCrop(newBox.toBounds()); + } + }); + }, + }); + }); + } + + function handleWheel(event: WheelEvent) { + event.preventDefault(); + const box = Box.from(crop.position, crop.size); + const mapped = mappedSize(); + + if (event.ctrlKey) { + setGestureState("isTrackpadGesture", true); + + const velocity = Math.max(0.001, Math.abs(event.deltaY) * 0.001); + const scale = 1 - event.deltaY * velocity; + + box.resize( + clamp(box.width * scale, minSize().x, mapped.x), + clamp(box.height * scale, minSize().y, mapped.y), + ORIGIN_CENTER, + ); + box.constrainAll(box, mapped, ORIGIN_CENTER, props.aspectRatio); + setTimeout(() => setGestureState("isTrackpadGesture", false), 100); + setSnappedRatio(null); + } else { + const velocity = Math.max(1, Math.abs(event.deltaY) * 0.01); + const scaleFactors = containerToMappedSizeScale(); + const dx = (-event.deltaX * velocity) / scaleFactors.x; + const dy = (-event.deltaY * velocity) / scaleFactors.y; + + box.move( + clamp(box.x + dx, 0, mapped.x - box.width), + clamp(box.y + dy, 0, mapped.y - box.height), + ); + } + + setCrop(box.toBounds()); + } + + function handleTouchStart(event: TouchEvent) { + if (event.touches.length === 2) { + // Initialize pinch zoom + const distance = distanceOf(event.touches[0], event.touches[1]); + + // Initialize touch center + const centerX = (event.touches[0].clientX + event.touches[1].clientX) / 2; + const centerY = (event.touches[0].clientY + event.touches[1].clientY) / 2; + + batch(() => { + setGestureState("initialPinchDistance", distance); + setGestureState("initialSize", { + width: crop.size.x, + height: crop.size.y, + }); + setGestureState("lastTouchCenter", { x: centerX, y: centerY }); + }); + } else if (event.touches.length === 1) { + // Handle single touch as drag + batch(() => { + setDragging(true); + setGestureState("lastTouchCenter", { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + }); + }); + } + } + + function handleTouchMove(event: TouchEvent) { + if (event.touches.length === 2) { + // Handle pinch zoom + const currentDistance = distanceOf(event.touches[0], event.touches[1]); + const scale = currentDistance / gestureState.initialPinchDistance; + + const box = Box.from(crop.position, crop.size); + const mapped = mappedSize(); + + // Calculate new dimensions while maintaining aspect ratio + const currentRatio = crop.size.x / crop.size.y; + let newWidth = clamp( + gestureState.initialSize.width * scale, + minSize().x, + mapped.x, + ); + let newHeight = newWidth / currentRatio; + + // Adjust if height exceeds bounds + if (newHeight < minSize().y || newHeight > mapped.y) { + newHeight = clamp(newHeight, minSize().y, mapped.y); + newWidth = newHeight * currentRatio; + } + + // Resize from center + box.resize(newWidth, newHeight, ORIGIN_CENTER); + + // Handle two-finger pan + const centerX = (event.touches[0].clientX + event.touches[1].clientX) / 2; + const centerY = (event.touches[0].clientY + event.touches[1].clientY) / 2; + + if (gestureState.lastTouchCenter) { + const scaleFactors = containerToMappedSizeScale(); + const dx = (centerX - gestureState.lastTouchCenter.x) / scaleFactors.x; + const dy = (centerY - gestureState.lastTouchCenter.y) / scaleFactors.y; + + box.move( + clamp(box.x + dx, 0, mapped.x - box.width), + clamp(box.y + dy, 0, mapped.y - box.height), + ); + } + + setGestureState("lastTouchCenter", { x: centerX, y: centerY }); + setCrop(box.toBounds()); + } else if (event.touches.length === 1 && dragging()) { + // Handle single touch drag + const box = Box.from(crop.position, crop.size); + const scaleFactors = containerToMappedSizeScale(); + const mapped = mappedSize(); + + if (gestureState.lastTouchCenter) { + const dx = + (event.touches[0].clientX - gestureState.lastTouchCenter.x) / + scaleFactors.x; + const dy = + (event.touches[0].clientY - gestureState.lastTouchCenter.y) / + scaleFactors.y; + + box.move( + clamp(box.x + dx, 0, mapped.x - box.width), + clamp(box.y + dy, 0, mapped.y - box.height), + ); + } + + setGestureState("lastTouchCenter", { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + }); + setCrop(box.toBounds()); + } + } + + function handleTouchEnd(event: TouchEvent) { + if (event.touches.length === 0) { + setDragging(false); + setGestureState("lastTouchCenter", null); + } else if (event.touches.length === 1) { + setGestureState("lastTouchCenter", { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + }); + } + } + + function handleResizeStartTouch(event: TouchEvent, dir: Direction) { + if (event.touches.length !== 1) return; + event.stopPropagation(); + const touch = event.touches[0]; + handleResizeStart(touch.clientX, touch.clientY, dir); + } + + function findClosestRatio( + width: number, + height: number, + threshold = 0.01, + ): Ratio | null { + if (props.aspectRatio) return null; + const currentRatio = width / height; + for (const ratio of COMMON_RATIOS) { + if (Math.abs(currentRatio - ratio[0] / ratio[1]) < threshold) { + return [ratio[0], ratio[1]]; + } + if (Math.abs(currentRatio - ratio[1] / ratio[0]) < threshold) { + return [ratio[1], ratio[0]]; + } + } + return null; + } + + function handleResizeStart(clientX: number, clientY: number, dir: Direction) { + const origin: XY = { + x: dir.includes("w") ? 1 : 0, + y: dir.includes("n") ? 1 : 0, + }; + + let lastValidPos = { x: clientX, y: clientY }; + const box = Box.from(crop.position, crop.size); + const scaleFactors = containerToMappedSizeScale(); + const mapped = mappedSize(); + + createRoot((dispose) => { + createEventListenerMap(window, { + mouseup: dispose, + touchend: dispose, + touchmove: (e) => + requestAnimationFrame(() => { + if (e.touches.length !== 1) return; + handleResizeMove(e.touches[0].clientX, e.touches[0].clientY); + }), + mousemove: (e) => + requestAnimationFrame(() => + handleResizeMove(e.clientX, e.clientY, e.altKey), + ), + }); + }); + + const [hapticsEnabled, hapticsEnabledOptions] = createResource( + async () => + (await generalSettingsStore.get())?.hapticsEnabled && + ostype() === "macos", + ); + generalSettingsStore.listen(() => hapticsEnabledOptions.refetch()); + + function handleResizeMove( + moveX: number, + moveY: number, + centerOrigin = false, + ) { + const dx = (moveX - lastValidPos.x) / scaleFactors.x; + const dy = (moveY - lastValidPos.y) / scaleFactors.y; + + const scaleMultiplier = centerOrigin ? 2 : 1; + const currentBox = box.toBounds(); + + let newWidth = + dir.includes("e") || dir.includes("w") + ? clamp( + dir.includes("w") + ? currentBox.size.x - dx * scaleMultiplier + : currentBox.size.x + dx * scaleMultiplier, + minSize().x, + mapped.x, + ) + : currentBox.size.x; + + let newHeight = + dir.includes("n") || dir.includes("s") + ? clamp( + dir.includes("n") + ? currentBox.size.y - dy * scaleMultiplier + : currentBox.size.y + dy * scaleMultiplier, + minSize().y, + mapped.y, + ) + : currentBox.size.y; + + const closest = findClosestRatio(newWidth, newHeight); + if (dir.length === 2 && snapToRatioEnabled() && closest) { + const ratio = closest[0] / closest[1]; + if (dir.includes("n") || dir.includes("s")) { + newWidth = newHeight * ratio; + } else { + newHeight = newWidth / ratio; + } + if (!snappedRatio() && hapticsEnabled()) { + commands.performHapticFeedback("Alignment", "Now"); + } + setSnappedRatio(closest); + } else { + setSnappedRatio(null); + } + + const newOrigin = centerOrigin ? ORIGIN_CENTER : origin; + box.resize(newWidth, newHeight, newOrigin); + + if (props.aspectRatio) { + box.constrainToRatio( + props.aspectRatio, + newOrigin, + dir.includes("n") || dir.includes("s") ? "width" : "height", + ); + } + box.constrainToBoundary(mapped.x, mapped.y, newOrigin); + + const newBox = box.toBounds(); + if ( + newBox.size.x !== crop.size.x || + newBox.size.y !== crop.size.y || + newBox.position.x !== crop.position.x || + newBox.position.y !== crop.position.y + ) { + lastValidPos = { x: moveX, y: moveY }; + props.onCropChange(newBox); + } + } + } + + function setCrop(value: Crop) { + props.onCropChange(value); + } + + const pressedKeys = new Set([]); + let lastKeyHandleFrame: number | null = null; + function handleKeyDown(event: KeyboardEvent) { + if (dragging()) return; + const dir = KEY_MAPPINGS.get(event.key); + if (!dir) return; + event.preventDefault(); + pressedKeys.add(event.key); + + if (lastKeyHandleFrame) return; + lastKeyHandleFrame = requestAnimationFrame(() => { + const box = Box.from(crop.position, crop.size); + const mapped = mappedSize(); + const scaleFactors = containerToMappedSizeScale(); + + const moveDelta = event.shiftKey ? 20 : 5; + const origin = event.altKey ? ORIGIN_CENTER : { x: 0, y: 0 }; + + for (const key of pressedKeys) { + const dir = KEY_MAPPINGS.get(key); + if (!dir) continue; + + const isUpKey = dir === "n"; + const isLeftKey = dir === "w"; + const isDownKey = dir === "s"; + const isRightKey = dir === "e"; + + if (event.metaKey || event.ctrlKey) { + const scaleMultiplier = event.altKey ? 2 : 1; + const currentBox = box.toBounds(); + + let newWidth = currentBox.size.x; + let newHeight = currentBox.size.y; + + if (isLeftKey || isRightKey) { + newWidth = clamp( + isLeftKey + ? currentBox.size.x - moveDelta * scaleMultiplier + : currentBox.size.x + moveDelta * scaleMultiplier, + minSize().x, + mapped.x, + ); + } + + if (isUpKey || isDownKey) { + newHeight = clamp( + isUpKey + ? currentBox.size.y - moveDelta * scaleMultiplier + : currentBox.size.y + moveDelta * scaleMultiplier, + minSize().y, + mapped.y, + ); + } + + box.resize(newWidth, newHeight, origin); + } else { + const dx = + (isRightKey ? moveDelta : isLeftKey ? -moveDelta : 0) / + scaleFactors.x; + const dy = + (isDownKey ? moveDelta : isUpKey ? -moveDelta : 0) / scaleFactors.y; + + box.move( + clamp(box.x + dx, 0, mapped.x - box.width), + clamp(box.y + dy, 0, mapped.y - box.height), + ); + } + } + + if (props.aspectRatio) box.constrainToRatio(props.aspectRatio, origin); + box.constrainToBoundary(mapped.x, mapped.y, origin); + setCrop(box.toBounds()); + + pressedKeys.clear(); + lastKeyHandleFrame = null; + }); + } + + return ( +
{ + e.preventDefault(); + const menu = await Menu.new({ + id: "crop-options", + items: [ + { + id: "enableRatioSnap", + text: "Snap to aspect ratios", + checked: snapToRatioEnabled(), + action: () => { + setSnapToRatioEnabled((v) => !v); + }, + } satisfies CheckMenuItemOptions, + ], + }); + menu.popup(); + }} + > + + {props.children} + +
+
+ { + const animation = el.animate( + [ + { opacity: 0, transform: "translateY(-8px)" }, + { opacity: 0.65, transform: "translateY(0)" }, + ], + { + duration: 100, + easing: "ease-out", + }, + ); + animation.finished.then(done); + }} + onExit={(el, done) => { + const animation = el.animate( + [ + { opacity: 0.65, transform: "translateY(0)" }, + { opacity: 0, transform: "translateY(-8px)" }, + ], + { + duration: 100, + easing: "ease-in", + }, + ); + animation.finished.then(done); + }} + > + +
+ {snappedRatio()![0]}:{snappedRatio()![1]} +
+
+
+
+ + {(handle) => { + const isCorner = handle.x !== "c" && handle.y !== "c"; + + return isCorner ? ( +
{ + e.stopPropagation(); + handleResizeStart(e.clientX, e.clientY, handle.direction); + }} + onTouchStart={(e) => + handleResizeStartTouch(e, handle.direction) + } + /> + ) : ( +
{ + e.stopPropagation(); + handleResizeStart(e.clientX, e.clientY, handle.direction); + }} + onTouchStart={(e) => + handleResizeStartTouch(e, handle.direction) + } + /> + ); + }} + +
+
+ ); } diff --git a/apps/desktop/src/components/Loader.tsx b/apps/desktop/src/components/Loader.tsx index 593848525..f88661a0e 100644 --- a/apps/desktop/src/components/Loader.tsx +++ b/apps/desktop/src/components/Loader.tsx @@ -1,9 +1,9 @@ export function AbsoluteInsetLoader() { - return ( -
-
- -
-
- ); + return ( +
+
+ +
+
+ ); } diff --git a/apps/desktop/src/components/Mode.tsx b/apps/desktop/src/components/Mode.tsx index be0454610..398929dc2 100644 --- a/apps/desktop/src/components/Mode.tsx +++ b/apps/desktop/src/components/Mode.tsx @@ -5,95 +5,95 @@ import { useRecordingOptions } from "~/routes/(window-chrome)/OptionsContext"; import { commands } from "~/utils/tauri"; const Mode = () => { - const { rawOptions, setOptions } = useRecordingOptions(); - const [isInfoHovered, setIsInfoHovered] = createSignal(false); + const { rawOptions, setOptions } = useRecordingOptions(); + const [isInfoHovered, setIsInfoHovered] = createSignal(false); - return ( -
-
commands.showWindow("ModeSelect")} - onMouseEnter={() => setIsInfoHovered(true)} - onMouseLeave={() => setIsInfoHovered(false)} - > - -
+ return ( +
+
commands.showWindow("ModeSelect")} + onMouseEnter={() => setIsInfoHovered(true)} + onMouseLeave={() => setIsInfoHovered(false)} + > + +
- {!isInfoHovered() && ( - -
{ - setOptions({ mode: "instant" }); - }} - class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ - rawOptions.mode === "instant" - ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-7 hover:bg-gray-7 ring-blue-10" - : "bg-gray-3 hover:bg-gray-7" - }`} - > - -
-
- )} + {!isInfoHovered() && ( + +
{ + setOptions({ mode: "instant" }); + }} + class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ + rawOptions.mode === "instant" + ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-7 hover:bg-gray-7 ring-blue-10" + : "bg-gray-3 hover:bg-gray-7" + }`} + > + +
+
+ )} - {!isInfoHovered() && ( - -
{ - setOptions({ mode: "studio" }); - }} - class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ - rawOptions.mode === "studio" - ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-7 hover:bg-gray-7 ring-blue-10" - : "bg-gray-3 hover:bg-gray-7" - }`} - > - -
-
- )} + {!isInfoHovered() && ( + +
{ + setOptions({ mode: "studio" }); + }} + class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ + rawOptions.mode === "studio" + ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-7 hover:bg-gray-7 ring-blue-10" + : "bg-gray-3 hover:bg-gray-7" + }`} + > + +
+
+ )} - {isInfoHovered() && ( - <> -
{ - setOptions({ mode: "instant" }); - }} - class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ - rawOptions.mode === "instant" - ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-5 hover:bg-gray-7 ring-blue-10" - : "bg-gray-3 hover:bg-gray-7" - }`} - > - -
+ {isInfoHovered() && ( + <> +
{ + setOptions({ mode: "instant" }); + }} + class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ + rawOptions.mode === "instant" + ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-5 hover:bg-gray-7 ring-blue-10" + : "bg-gray-3 hover:bg-gray-7" + }`} + > + +
-
{ - setOptions({ mode: "studio" }); - }} - class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ - rawOptions.mode === "studio" - ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-5 hover:bg-gray-7 ring-blue-10" - : "bg-gray-3 hover:bg-gray-7" - }`} - > - -
- - )} -
- ); +
{ + setOptions({ mode: "studio" }); + }} + class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${ + rawOptions.mode === "studio" + ? "ring-2 ring-offset-1 ring-offset-gray-1 bg-gray-5 hover:bg-gray-7 ring-blue-10" + : "bg-gray-3 hover:bg-gray-7" + }`} + > + +
+ + )} +
+ ); }; export default Mode; diff --git a/apps/desktop/src/components/ModeSelect.tsx b/apps/desktop/src/components/ModeSelect.tsx index 0380a6b5f..0c94b64c0 100644 --- a/apps/desktop/src/components/ModeSelect.tsx +++ b/apps/desktop/src/components/ModeSelect.tsx @@ -1,93 +1,93 @@ import { cx } from "cva"; -import { JSX } from "solid-js"; +import type { JSX } from "solid-js"; import { createOptionsQuery } from "~/utils/queries"; -import { RecordingMode } from "~/utils/tauri"; +import type { RecordingMode } from "~/utils/tauri"; import InstantModeDark from "../assets/illustrations/instant-mode-dark.png"; import InstantModeLight from "../assets/illustrations/instant-mode-light.png"; import StudioModeDark from "../assets/illustrations/studio-mode-dark.png"; import StudioModeLight from "../assets/illustrations/studio-mode-light.png"; interface ModeOptionProps { - mode: RecordingMode; - title: string; - description: string; - icon: (props: { class: string }) => JSX.Element; - isSelected: boolean; - onSelect: (mode: RecordingMode) => void; - darkimg: string; - lightimg: string; + mode: RecordingMode; + title: string; + description: string; + icon: (props: { class: string }) => JSX.Element; + isSelected: boolean; + onSelect: (mode: RecordingMode) => void; + darkimg: string; + lightimg: string; } const ModeOption = (props: ModeOptionProps) => { - return ( -
props.onSelect(props.mode)} - class={cx(`p-4 rounded-lg bg-gray-2 transition-all duration-200`, { - "ring-2 ring-offset-2 hover:bg-gray-2 cursor-default ring-blue-9 ring-offset-gray-100": - props.isSelected, - "ring-2 ring-transparent ring-offset-transparent hover:bg-gray-3 cursor-pointer": - !props.isSelected, - })} - > -
- -

{props.title}

-
+ return ( +
props.onSelect(props.mode)} + class={cx(`p-4 rounded-lg bg-gray-2 transition-all duration-200`, { + "ring-2 ring-offset-2 hover:bg-gray-2 cursor-default ring-blue-9 ring-offset-gray-100": + props.isSelected, + "ring-2 ring-transparent ring-offset-transparent hover:bg-gray-3 cursor-pointer": + !props.isSelected, + })} + > +
+ +

{props.title}

+
-

- {props.description} -

-
- ); +

+ {props.description} +

+
+ ); }; const ModeSelect = () => { - const { rawOptions, setOptions } = createOptionsQuery(); + const { rawOptions, setOptions } = createOptionsQuery(); - const handleModeChange = (mode: RecordingMode) => { - setOptions({ mode }); - }; + const handleModeChange = (mode: RecordingMode) => { + setOptions({ mode }); + }; - const modeOptions = [ - { - mode: "instant" as const, - title: "Instant Mode", - description: - "Share your screen instantly with a magic link — no waiting for rendering, just capture and share in seconds.", - icon: IconCapInstant, - darkimg: InstantModeDark, - lightimg: InstantModeLight, - }, - { - mode: "studio" as const, - title: "Studio Mode", - description: - "Records at the highest quality/framerate. Captures both your screen and camera separately for editing later.", - icon: IconCapFilmCut, - darkimg: StudioModeDark, - lightimg: StudioModeLight, - }, - ]; + const modeOptions = [ + { + mode: "instant" as const, + title: "Instant Mode", + description: + "Share your screen instantly with a magic link — no waiting for rendering, just capture and share in seconds.", + icon: IconCapInstant, + darkimg: InstantModeDark, + lightimg: InstantModeLight, + }, + { + mode: "studio" as const, + title: "Studio Mode", + description: + "Records at the highest quality/framerate. Captures both your screen and camera separately for editing later.", + icon: IconCapFilmCut, + darkimg: StudioModeDark, + lightimg: StudioModeLight, + }, + ]; - return ( -
- {modeOptions.map((option) => ( - - ))} -
- ); + return ( +
+ {modeOptions.map((option) => ( + + ))} +
+ ); }; export default ModeSelect; diff --git a/apps/desktop/src/components/SignInButton.tsx b/apps/desktop/src/components/SignInButton.tsx index f72559b70..49548a45e 100644 --- a/apps/desktop/src/components/SignInButton.tsx +++ b/apps/desktop/src/components/SignInButton.tsx @@ -1,29 +1,29 @@ import { Button } from "@cap/ui-solid"; -import { ComponentProps } from "solid-js"; +import type { ComponentProps } from "solid-js"; import { createSignInMutation } from "~/utils/auth"; export function SignInButton( - props: Omit, "onClick"> + props: Omit, "onClick">, ) { - const signIn = createSignInMutation(); + const signIn = createSignInMutation(); - return ( - - ); + return ( + + ); } diff --git a/apps/desktop/src/components/Toggle.tsx b/apps/desktop/src/components/Toggle.tsx index 85a6523e5..60f29d664 100644 --- a/apps/desktop/src/components/Toggle.tsx +++ b/apps/desktop/src/components/Toggle.tsx @@ -1,50 +1,50 @@ import { Switch as KSwitch } from "@kobalte/core/switch"; import { cva } from "cva"; -import { splitProps, type ComponentProps } from "solid-js"; +import { type ComponentProps, splitProps } from "solid-js"; const toggleControlStyles = cva( - "rounded-full bg-gray-6 ui-disabled:bg-gray-3 ui-checked:bg-blue-500 transition-colors outline-2 outline-offset-2 outline-blue-300", - { - variants: { - size: { - sm: "w-9 h-[1.25rem] p-[0.125rem]", - md: "w-11 h-[1.5rem] p-[0.125rem]", - lg: "w-14 h-[1.75rem] p-[0.1875rem]", - }, - }, - defaultVariants: { - size: "md", - }, - } + "rounded-full bg-gray-6 ui-disabled:bg-gray-3 ui-checked:bg-blue-500 transition-colors outline-2 outline-offset-2 outline-blue-300", + { + variants: { + size: { + sm: "w-9 h-[1.25rem] p-[0.125rem]", + md: "w-11 h-[1.5rem] p-[0.125rem]", + lg: "w-14 h-[1.75rem] p-[0.1875rem]", + }, + }, + defaultVariants: { + size: "md", + }, + }, ); const toggleThumbStyles = cva( - "bg-white rounded-full transition-transform ui-checked:translate-x-[calc(100%)]", - { - variants: { - size: { - sm: "size-[1rem]", - md: "size-[1.25rem]", - lg: "size-[1.5rem]", - }, - }, - defaultVariants: { - size: "md", - }, - } + "bg-white rounded-full transition-transform ui-checked:translate-x-[calc(100%)]", + { + variants: { + size: { + sm: "size-[1rem]", + md: "size-[1.25rem]", + lg: "size-[1.5rem]", + }, + }, + defaultVariants: { + size: "md", + }, + }, ); export function Toggle( - props: ComponentProps & { size?: "sm" | "md" | "lg" } + props: ComponentProps & { size?: "sm" | "md" | "lg" }, ) { - const [local, others] = splitProps(props, ["size"]); + const [local, others] = splitProps(props, ["size"]); - return ( - - - - - - - ); + return ( + + + + + + + ); } diff --git a/apps/desktop/src/components/Tooltip.tsx b/apps/desktop/src/components/Tooltip.tsx index 4184e7129..df6e0c92f 100644 --- a/apps/desktop/src/components/Tooltip.tsx +++ b/apps/desktop/src/components/Tooltip.tsx @@ -1,24 +1,24 @@ import { Tooltip as KTooltip } from "@kobalte/core"; import { cx } from "cva"; -import { ComponentProps, JSX } from "solid-js"; +import type { ComponentProps, JSX } from "solid-js"; interface Props extends ComponentProps { - content: JSX.Element; - childClass?: string; + content: JSX.Element; + childClass?: string; } export default function Tooltip(props: Props) { - return ( - - - {props.children} - - - - {props.content} - - - - - ); + return ( + + + {props.children} + + + + {props.content} + + + + + ); } diff --git a/apps/desktop/src/components/titlebar/Titlebar.tsx b/apps/desktop/src/components/titlebar/Titlebar.tsx index 5b3129fcf..4e89ac088 100644 --- a/apps/desktop/src/components/titlebar/Titlebar.tsx +++ b/apps/desktop/src/components/titlebar/Titlebar.tsx @@ -1,62 +1,62 @@ // Credits: tauri-controls import { type } from "@tauri-apps/plugin-os"; import { cx } from "cva"; -import { type ComponentProps, Match, splitProps, Switch } from "solid-js"; +import { type ComponentProps, Match, Switch, splitProps } from "solid-js"; import titlebarState from "~/utils/titlebar-state"; import CaptionControlsWindows11 from "./controls/CaptionControlsWindows11"; export default function Titlebar() { - function left() { - if (titlebarState.order === "platform") return type() === "macos"; - return titlebarState.order === "left"; - } + function left() { + if (titlebarState.order === "platform") return type() === "macos"; + return titlebarState.order === "left"; + } - return ( -
- {left() ? ( - <> - -
{titlebarState.items}
- - ) : ( - <> - {titlebarState.items} - - - )} -
- ); + return ( +
+ {left() ? ( + <> + +
{titlebarState.items}
+ + ) : ( + <> + {titlebarState.items} + + + )} +
+ ); } export function WindowControls(props: ComponentProps<"div">) { - const [local, otherProps] = splitProps(props, ["class"]); - const ostype = type(); + const [local, otherProps] = splitProps(props, ["class"]); + const ostype = type(); - return ( - - - - - -
- - - ); + return ( + + + + + +
+ + + ); } diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx index f3b66dadb..85ec943c8 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx @@ -1,163 +1,163 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { cx } from "cva"; import { - type ComponentProps, - createSignal, - type JSX, - onCleanup, - onMount, - Show, - splitProps, + type ComponentProps, + createSignal, + type JSX, + onCleanup, + onMount, + Show, + splitProps, } from "solid-js"; import titlebarState from "~/utils/titlebar-state"; import { WindowControlButton as ControlButton } from "./WindowControlButton"; export default function ( - props: ComponentProps<"div"> & { maximizable?: boolean } + props: ComponentProps<"div"> & { maximizable?: boolean }, ) { - const [local, otherProps] = splitProps(props, ["class"]); - const currentWindow = getCurrentWindow(); - const [focused, setFocus] = createSignal(true); + const [local, otherProps] = splitProps(props, ["class"]); + const currentWindow = getCurrentWindow(); + const [focused, setFocus] = createSignal(true); - let unlisten: () => void | undefined; - onMount(async () => { - unlisten = await currentWindow.onFocusChanged(({ payload: focused }) => - setFocus(focused) - ); - }); - onCleanup(() => unlisten?.()); + let unlisten: () => void | undefined; + onMount(async () => { + unlisten = await currentWindow.onFocusChanged(({ payload: focused }) => + setFocus(focused), + ); + }); + onCleanup(() => unlisten?.()); - return ( -
- - - - - - {titlebarState.maximized ? ( - - ) : ( - - )} - - - - - -
- ); + return ( +
+ + + + + + {titlebarState.maximized ? ( + + ) : ( + + )} + + + + + +
+ ); } const icons = { - minimizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( - - - - ), - maximizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( - - - - ), - maximizeRestoreWin: ( - props: JSX.IntrinsicAttributes & ComponentProps<"svg"> - ) => ( - - - - ), - closeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( - - - - ), + minimizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), + maximizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), + maximizeRestoreWin: ( + props: JSX.IntrinsicAttributes & ComponentProps<"svg">, + ) => ( + + + + ), + closeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), }; diff --git a/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx b/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx index 1809a51cf..7f4b5b59f 100644 --- a/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx +++ b/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx @@ -1,14 +1,14 @@ -import { splitProps, type ComponentProps } from "solid-js"; +import { type ComponentProps, splitProps } from "solid-js"; export function WindowControlButton(props: ComponentProps<"button">) { - const [local, otherProps] = splitProps(props, ["class", "children"]); + const [local, otherProps] = splitProps(props, ["class", "children"]); - return ( - - ); + return ( + + ); } diff --git a/apps/desktop/src/declaration.d.ts b/apps/desktop/src/declaration.d.ts index 67704bd9c..742bcc3f0 100644 --- a/apps/desktop/src/declaration.d.ts +++ b/apps/desktop/src/declaration.d.ts @@ -1,4 +1,4 @@ declare module "*.riv" { - const src: string; - export default src; + const src: string; + export default src; } diff --git a/apps/desktop/src/entry-server.tsx b/apps/desktop/src/entry-server.tsx index 7e46f5107..2d450c023 100644 --- a/apps/desktop/src/entry-server.tsx +++ b/apps/desktop/src/entry-server.tsx @@ -2,22 +2,22 @@ import { createHandler, StartServer } from "@solidjs/start/server"; export default createHandler(() => ( - ( - - - - - - {assets} - - -
- {children} -
- {scripts} - - - )} - /> + ( + + + + + + {assets} + + +
+ {children} +
+ {scripts} + + + )} + /> )); diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index dc4eb1edc..1b9dd6672 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -4,1776 +4,1776 @@ import { Flags } from "./utils/tauri"; declare module "mp4box"; export interface MP4MediaTrack { - id: number; - created: Date; - modified: Date; - movie_duration: number; - layer: number; - alternate_group: number; - volume: number; - track_width: number; - track_height: number; - timescale: number; - duration: number; - bitrate: number; - codec: string; - language: string; - nb_samples: number; + id: number; + created: Date; + modified: Date; + movie_duration: number; + layer: number; + alternate_group: number; + volume: number; + track_width: number; + track_height: number; + timescale: number; + duration: number; + bitrate: number; + codec: string; + language: string; + nb_samples: number; } export interface MP4VideoData { - width: number; - height: number; + width: number; + height: number; } export interface MP4VideoTrack extends MP4MediaTrack { - video: MP4VideoData; + video: MP4VideoData; } export interface MP4AudioData { - sample_rate: number; - channel_count: number; - sample_size: number; + sample_rate: number; + channel_count: number; + sample_size: number; } export interface MP4AudioTrack extends MP4MediaTrack { - audio: MP4AudioData; + audio: MP4AudioData; } export type MP4Track = MP4VideoTrack | MP4AudioTrack; export interface MP4Info { - duration: number; - timescale: number; - fragment_duration: number; - isFragmented: boolean; - isProgressive: boolean; - hasIOD: boolean; - brands: string[]; - created: Date; - modified: Date; - tracks: MP4Track[]; - mime: string; - audioTracks: MP4AudioTrack[]; - videoTracks: MP4VideoTrack[]; + duration: number; + timescale: number; + fragment_duration: number; + isFragmented: boolean; + isProgressive: boolean; + hasIOD: boolean; + brands: string[]; + created: Date; + modified: Date; + tracks: MP4Track[]; + mime: string; + audioTracks: MP4AudioTrack[]; + videoTracks: MP4VideoTrack[]; } export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number }; export interface MP4File { - onMoovStart?: () => void; - onReady?: (info: MP4Info) => void; - onError?: (e: string) => void; - onSamples?: (id: number, user: any, samples: Sample[]) => void; + onMoovStart?: () => void; + onReady?: (info: MP4Info) => void; + onError?: (e: string) => void; + onSamples?: (id: number, user: any, samples: Sample[]) => void; - appendBuffer(data: MP4ArrayBuffer): number; - start(): void; - stop(): void; - flush(): void; + appendBuffer(data: MP4ArrayBuffer): number; + start(): void; + stop(): void; + flush(): void; - setExtractionOptions(id: number, user: any, options: ExtractionOptions): void; + setExtractionOptions(id: number, user: any, options: ExtractionOptions): void; } export function createFile(): MP4File; export interface Sample { - number: number; - track_id: number; - timescale: number; - description_index: number; - description: { - avcC?: BoxParser.avcCBox; // h.264 - hvcC?: BoxParser.hvcCBox; // hevc - vpcC?: BoxParser.vpcCBox; // vp9 - av1C?: BoxParser.av1CBox; // av1 - }; - data: ArrayBuffer; - size: number; - alreadyRead?: number; - duration: number; - cts: number; - dts: number; - is_sync: boolean; - is_leading?: number; - depends_on?: number; - is_depended_on?: number; - has_redundancy?: number; - degradation_priority?: number; - offset?: number; - subsamples?: any; + number: number; + track_id: number; + timescale: number; + description_index: number; + description: { + avcC?: BoxParser.avcCBox; // h.264 + hvcC?: BoxParser.hvcCBox; // hevc + vpcC?: BoxParser.vpcCBox; // vp9 + av1C?: BoxParser.av1CBox; // av1 + }; + data: ArrayBuffer; + size: number; + alreadyRead?: number; + duration: number; + cts: number; + dts: number; + is_sync: boolean; + is_leading?: number; + depends_on?: number; + is_depended_on?: number; + has_redundancy?: number; + degradation_priority?: number; + offset?: number; + subsamples?: any; } export interface ExtractionOptions { - nbSamples: number; + nbSamples: number; } export class DataStream { - // WARNING, the default is little endian, which is not what MP4 uses. - constructor(buffer?: ArrayBuffer, byteOffset?: number, endianness?: boolean); - getPosition(): number; - - get byteLength(): number; - get buffer(): ArrayBuffer; - set buffer(v: ArrayBuffer); - get byteOffset(): number; - set byteOffset(v: number); - get dataView(): DataView; - set dataView(v: DataView); - - seek(pos: number): void; - isEof(): boolean; - - mapFloat32Array(length: number, e?: boolean): any; - mapFloat64Array(length: number, e?: boolean): any; - mapInt16Array(length: number, e?: boolean): any; - mapInt32Array(length: number, e?: boolean): any; - mapInt8Array(length: number): any; - mapUint16Array(length: number, e?: boolean): any; - mapUint32Array(length: number, e?: boolean): any; - mapUint8Array(length: number): any; - - readInt32Array(length: number, endianness?: boolean): Int32Array; - readInt16Array(length: number, endianness?: boolean): Int16Array; - readInt8Array(length: number): Int8Array; - readUint32Array(length: number, endianness?: boolean): Uint32Array; - readUint16Array(length: number, endianness?: boolean): Uint16Array; - readUint8Array(length: number): Uint8Array; - readFloat64Array(length: number, endianness?: boolean): Float64Array; - readFloat32Array(length: number, endianness?: boolean): Float32Array; - - readInt32(endianness?: boolean): number; - readInt16(endianness?: boolean): number; - readInt8(): number; - readUint32(endianness?: boolean): number; - //readUint32Array(length: any, e: any): any - readUint24(): number; - readUint16(endianness?: boolean): number; - readUint8(): number; - //readUint64(): any - readFloat32(endianness?: boolean): number; - readFloat64(endianness?: boolean): number; - //readCString(length: number): any - //readString(length: number, encoding: any): any - - static endianness: boolean; - - memcpy( - dst: ArrayBufferLike, - dstOffset: number, - src: ArrayBufferLike, - srcOffset: number, - byteLength: number - ): void; - - // TODO I got bored porting all functions - - save(filename: string): void; - shift(offset: number): void; - - writeInt32Array(arr: Int32Array, endianness?: boolean): void; - writeInt16Array(arr: Int16Array, endianness?: boolean): void; - writeInt8Array(arr: Int8Array): void; - writeUint32Array(arr: Uint32Array, endianness?: boolean): void; - writeUint16Array(arr: Uint16Array, endianness?: boolean): void; - writeUint8Array(arr: Uint8Array): void; - writeFloat64Array(arr: Float64Array, endianness?: boolean): void; - writeFloat32Array(arr: Float32Array, endianness?: boolean): void; - writeInt32(v: number, endianness?: boolean): void; - writeInt16(v: number, endianness?: boolean): void; - writeInt8(v: number): void; - writeUint32(v: number, endianness?: boolean): void; - writeUint16(v: number, endianness?: boolean): void; - writeUint8(v: number): void; - writeFloat32(v: number, endianness?: boolean): void; - writeFloat64(v: number, endianness?: boolean): void; - writeUCS2String(s: string, endianness?: boolean, length?: number): void; - writeString(s: string, encoding?: string, length?: number): void; - writeCString(s: string, length?: number): void; - writeUint64(v: number): void; - writeUint24(v: number): void; - adjustUint32(pos: number, v: number): void; - - static LITTLE_ENDIAN: boolean; - static BIG_ENDIAN: boolean; - - // TODO add correct types; these are exported by dts-gen - readCString(length: any): any; - readInt64(): any; - readString(length: any, encoding: any): any; - readUint64(): any; - writeStruct(structDefinition: any, struct: any): void; - writeType(t: any, v: any, struct: any): any; - - static arrayToNative(array: any, arrayIsLittleEndian: any): any; - static flipArrayEndianness(array: any): any; - static memcpy( - dst: any, - dstOffset: any, - src: any, - srcOffset: any, - byteLength: any - ): void; - static nativeToEndian(array: any, littleEndian: any): any; + // WARNING, the default is little endian, which is not what MP4 uses. + constructor(buffer?: ArrayBuffer, byteOffset?: number, endianness?: boolean); + getPosition(): number; + + get byteLength(): number; + get buffer(): ArrayBuffer; + set buffer(v: ArrayBuffer); + get byteOffset(): number; + set byteOffset(v: number); + get dataView(): DataView; + set dataView(v: DataView); + + seek(pos: number): void; + isEof(): boolean; + + mapFloat32Array(length: number, e?: boolean): any; + mapFloat64Array(length: number, e?: boolean): any; + mapInt16Array(length: number, e?: boolean): any; + mapInt32Array(length: number, e?: boolean): any; + mapInt8Array(length: number): any; + mapUint16Array(length: number, e?: boolean): any; + mapUint32Array(length: number, e?: boolean): any; + mapUint8Array(length: number): any; + + readInt32Array(length: number, endianness?: boolean): Int32Array; + readInt16Array(length: number, endianness?: boolean): Int16Array; + readInt8Array(length: number): Int8Array; + readUint32Array(length: number, endianness?: boolean): Uint32Array; + readUint16Array(length: number, endianness?: boolean): Uint16Array; + readUint8Array(length: number): Uint8Array; + readFloat64Array(length: number, endianness?: boolean): Float64Array; + readFloat32Array(length: number, endianness?: boolean): Float32Array; + + readInt32(endianness?: boolean): number; + readInt16(endianness?: boolean): number; + readInt8(): number; + readUint32(endianness?: boolean): number; + //readUint32Array(length: any, e: any): any + readUint24(): number; + readUint16(endianness?: boolean): number; + readUint8(): number; + //readUint64(): any + readFloat32(endianness?: boolean): number; + readFloat64(endianness?: boolean): number; + //readCString(length: number): any + //readString(length: number, encoding: any): any + + static endianness: boolean; + + memcpy( + dst: ArrayBufferLike, + dstOffset: number, + src: ArrayBufferLike, + srcOffset: number, + byteLength: number, + ): void; + + // TODO I got bored porting all functions + + save(filename: string): void; + shift(offset: number): void; + + writeInt32Array(arr: Int32Array, endianness?: boolean): void; + writeInt16Array(arr: Int16Array, endianness?: boolean): void; + writeInt8Array(arr: Int8Array): void; + writeUint32Array(arr: Uint32Array, endianness?: boolean): void; + writeUint16Array(arr: Uint16Array, endianness?: boolean): void; + writeUint8Array(arr: Uint8Array): void; + writeFloat64Array(arr: Float64Array, endianness?: boolean): void; + writeFloat32Array(arr: Float32Array, endianness?: boolean): void; + writeInt32(v: number, endianness?: boolean): void; + writeInt16(v: number, endianness?: boolean): void; + writeInt8(v: number): void; + writeUint32(v: number, endianness?: boolean): void; + writeUint16(v: number, endianness?: boolean): void; + writeUint8(v: number): void; + writeFloat32(v: number, endianness?: boolean): void; + writeFloat64(v: number, endianness?: boolean): void; + writeUCS2String(s: string, endianness?: boolean, length?: number): void; + writeString(s: string, encoding?: string, length?: number): void; + writeCString(s: string, length?: number): void; + writeUint64(v: number): void; + writeUint24(v: number): void; + adjustUint32(pos: number, v: number): void; + + static LITTLE_ENDIAN: boolean; + static BIG_ENDIAN: boolean; + + // TODO add correct types; these are exported by dts-gen + readCString(length: any): any; + readInt64(): any; + readString(length: any, encoding: any): any; + readUint64(): any; + writeStruct(structDefinition: any, struct: any): void; + writeType(t: any, v: any, struct: any): any; + + static arrayToNative(array: any, arrayIsLittleEndian: any): any; + static flipArrayEndianness(array: any): any; + static memcpy( + dst: any, + dstOffset: any, + src: any, + srcOffset: any, + byteLength: any, + ): void; + static nativeToEndian(array: any, littleEndian: any): any; } export interface TrackOptions { - id?: number; - type?: string; - width?: number; - height?: number; - duration?: number; - layer?: number; - timescale?: number; - media_duration?: number; - language?: string; - hdlr?: string; - - // video - avcDecoderConfigRecord?: any; - hevcDecoderConfigRecord?: any; - - // audio - balance?: number; - channel_count?: number; - samplesize?: number; - samplerate?: number; - - //captions - namespace?: string; - schema_location?: string; - auxiliary_mime_types?: string; - - description?: BoxParser.Box; - description_boxes?: BoxParser.Box[]; - - default_sample_description_index_id?: number; - default_sample_duration?: number; - default_sample_size?: number; - default_sample_flags?: number; + id?: number; + type?: string; + width?: number; + height?: number; + duration?: number; + layer?: number; + timescale?: number; + media_duration?: number; + language?: string; + hdlr?: string; + + // video + avcDecoderConfigRecord?: any; + hevcDecoderConfigRecord?: any; + + // audio + balance?: number; + channel_count?: number; + samplesize?: number; + samplerate?: number; + + //captions + namespace?: string; + schema_location?: string; + auxiliary_mime_types?: string; + + description?: BoxParser.Box; + description_boxes?: BoxParser.Box[]; + + default_sample_description_index_id?: number; + default_sample_duration?: number; + default_sample_size?: number; + default_sample_flags?: number; } export interface FileOptions { - brands?: string[]; - timescale?: number; - rate?: number; - duration?: number; - width?: number; + brands?: string[]; + timescale?: number; + rate?: number; + duration?: number; + width?: number; } export interface SampleOptions { - sample_description_index?: number; - duration?: number; - cts?: number; - dts?: number; - is_sync?: boolean; - is_leading?: number; - depends_on?: number; - is_depended_on?: number; - has_redundancy?: number; - degradation_priority?: number; - subsamples?: any; + sample_description_index?: number; + duration?: number; + cts?: number; + dts?: number; + is_sync?: boolean; + is_leading?: number; + depends_on?: number; + is_depended_on?: number; + has_redundancy?: number; + degradation_priority?: number; + subsamples?: any; } // TODO add the remaining functions // TODO move to another module export class ISOFile { - constructor(stream?: DataStream); - - init(options?: FileOptions): ISOFile; - addTrack(options?: TrackOptions): number; - addSample(track: number, data: ArrayBuffer, options?: SampleOptions): Sample; - - createSingleSampleMoof(sample: Sample): BoxParser.moofBox; - - // helpers - getTrackById(id: number): BoxParser.trakBox | undefined; - getTrexById(id: number): BoxParser.trexBox | undefined; - - // boxes that are added to the root - boxes: BoxParser.Box[]; - mdats: BoxParser.mdatBox[]; - moofs: BoxParser.moofBox[]; - - ftyp?: BoxParser.ftypBox; - moov?: BoxParser.moovBox; - - static writeInitializationSegment( - ftyp: BoxParser.ftypBox, - moov: BoxParser.moovBox, - total_duration: number, - sample_duration: number - ): ArrayBuffer; - - // TODO add correct types; these are exported by dts-gen - add(name: any): any; - addBox(box: any): any; - appendBuffer(ab: any, last: any): any; - buildSampleLists(): void; - buildTrakSampleLists(trak: any): void; - checkBuffer(ab: any): any; - createFragment(track_id: any, sampleNumber: any, stream_: any): any; - equal(b: any): any; - flattenItemInfo(): void; - flush(): void; - getAllocatedSampleDataSize(): any; - getBox(type: any): any; - getBoxes(type: any, returnEarly: any): any; - getBuffer(): any; - getCodecs(): any; - getInfo(): any; - getItem(item_id: any): any; - getMetaHandler(): any; - getPrimaryItem(): any; - getSample(trak: any, sampleNum: any): any; - getTrackSample(track_id: any, number: any): any; - getTrackSamplesInfo(track_id: any): any; - hasIncompleteMdat(): any; - hasItem(name: any): any; - initializeSegmentation(): any; - itemToFragmentedTrackFile(_options: any): any; - parse(): void; - print(output: any): void; - processIncompleteBox(ret: any): any; - processIncompleteMdat(): any; - processItems(callback: any): void; - processSamples(last: any): void; - releaseItem(item_id: any): any; - releaseSample(trak: any, sampleNum: any): any; - releaseUsedSamples(id: any, sampleNum: any): void; - resetTables(): void; - restoreParsePosition(): any; - save(name: any): void; - saveParsePosition(): void; - seek(time: any, useRap: any): any; - seekTrack(time: any, useRap: any, trak: any): any; - setExtractionOptions(id: any, user: any, options: any): void; - setSegmentOptions(id: any, user: any, options: any): void; - start(): void; - stop(): void; - unsetExtractionOptions(id: any): void; - unsetSegmentOptions(id: any): void; - updateSampleLists(): void; - updateUsedBytes(box: any, ret: any): void; - write(outstream: any): void; - - static initSampleGroups( - trak: any, - traf: any, - sbgps: any, - trak_sgpds: any, - traf_sgpds: any - ): void; - static process_sdtp(sdtp: any, sample: any, number: any): void; - static setSampleGroupProperties( - trak: any, - sample: any, - sample_number: any, - sample_groups_info: any - ): void; + constructor(stream?: DataStream); + + init(options?: FileOptions): ISOFile; + addTrack(options?: TrackOptions): number; + addSample(track: number, data: ArrayBuffer, options?: SampleOptions): Sample; + + createSingleSampleMoof(sample: Sample): BoxParser.moofBox; + + // helpers + getTrackById(id: number): BoxParser.trakBox | undefined; + getTrexById(id: number): BoxParser.trexBox | undefined; + + // boxes that are added to the root + boxes: BoxParser.Box[]; + mdats: BoxParser.mdatBox[]; + moofs: BoxParser.moofBox[]; + + ftyp?: BoxParser.ftypBox; + moov?: BoxParser.moovBox; + + static writeInitializationSegment( + ftyp: BoxParser.ftypBox, + moov: BoxParser.moovBox, + total_duration: number, + sample_duration: number, + ): ArrayBuffer; + + // TODO add correct types; these are exported by dts-gen + add(name: any): any; + addBox(box: any): any; + appendBuffer(ab: any, last: any): any; + buildSampleLists(): void; + buildTrakSampleLists(trak: any): void; + checkBuffer(ab: any): any; + createFragment(track_id: any, sampleNumber: any, stream_: any): any; + equal(b: any): any; + flattenItemInfo(): void; + flush(): void; + getAllocatedSampleDataSize(): any; + getBox(type: any): any; + getBoxes(type: any, returnEarly: any): any; + getBuffer(): any; + getCodecs(): any; + getInfo(): any; + getItem(item_id: any): any; + getMetaHandler(): any; + getPrimaryItem(): any; + getSample(trak: any, sampleNum: any): any; + getTrackSample(track_id: any, number: any): any; + getTrackSamplesInfo(track_id: any): any; + hasIncompleteMdat(): any; + hasItem(name: any): any; + initializeSegmentation(): any; + itemToFragmentedTrackFile(_options: any): any; + parse(): void; + print(output: any): void; + processIncompleteBox(ret: any): any; + processIncompleteMdat(): any; + processItems(callback: any): void; + processSamples(last: any): void; + releaseItem(item_id: any): any; + releaseSample(trak: any, sampleNum: any): any; + releaseUsedSamples(id: any, sampleNum: any): void; + resetTables(): void; + restoreParsePosition(): any; + save(name: any): void; + saveParsePosition(): void; + seek(time: any, useRap: any): any; + seekTrack(time: any, useRap: any, trak: any): any; + setExtractionOptions(id: any, user: any, options: any): void; + setSegmentOptions(id: any, user: any, options: any): void; + start(): void; + stop(): void; + unsetExtractionOptions(id: any): void; + unsetSegmentOptions(id: any): void; + updateSampleLists(): void; + updateUsedBytes(box: any, ret: any): void; + write(outstream: any): void; + + static initSampleGroups( + trak: any, + traf: any, + sbgps: any, + trak_sgpds: any, + traf_sgpds: any, + ): void; + static process_sdtp(sdtp: any, sample: any, number: any): void; + static setSampleGroupProperties( + trak: any, + sample: any, + sample_number: any, + sample_groups_info: any, + ): void; } export namespace BoxParser { - export class Box { - size?: number; - data?: Uint8Array; - - constructor(type?: string, size?: number); - - add(name: string): Box; - addBox(box: Box): Box; - set(name: string, value: any): void; - addEntry(value: string, prop?: string): void; - printHeader(output: any): void; - write(stream: DataStream): void; - writeHeader(stream: DataStream, msg?: string): void; - computeSize(): void; - - // TODO add types for these - parse(stream: any): void; - parseDataAndRewind(stream: any): void; - parseLanguage(stream: any): void; - print(output: any): void; - } - - // TODO finish add types for these classes - export class AudioSampleEntry extends SampleEntry { - constructor(type: any, size: any); - - getChannelCount(): any; - getSampleRate(): any; - getSampleSize(): any; - isAudio(): any; - parse(stream: any): void; - write(stream: any): void; - } - - export class CoLLBox extends ContainerBox { - constructor(size: any); - - parse(stream: any): void; - } - - export class ContainerBox extends Box { - constructor(type: any, size: any, uuid: any); - - parse(stream: any): void; - print(output: any): void; - write(stream: any): void; - } - - export class FullBox extends Box { - constructor(type: any, size: any, uuid: any); - - parse(stream: any): void; - parseDataAndRewind(stream: any): void; - parseFullHeader(stream: any): void; - printHeader(output: any): void; - writeHeader(stream: any): void; - } - - export class HintSampleEntry extends SampleEntry { - constructor(type: any, size: any); - } - - export class MetadataSampleEntry extends SampleEntry { - constructor(type: any, size: any); - - isMetadata(): any; - } - - export class OpusSampleEntry extends SampleEntry { - constructor(size: any); - } - - export class SampleEntry extends Box { - constructor(type: any, size: any, hdr_size: any, start: any); - - getChannelCount(): any; - getCodec(): any; - getHeight(): any; - getSampleRate(): any; - getSampleSize(): any; - getWidth(): any; - isAudio(): any; - isHint(): any; - isMetadata(): any; - isSubtitle(): any; - isVideo(): any; - parse(stream: any): void; - parseDataAndRewind(stream: any): void; - parseFooter(stream: any): void; - parseHeader(stream: any): void; - write(stream: any): void; - writeFooter(stream: any): void; - writeHeader(stream: any): void; - } + export class Box { + size?: number; + data?: Uint8Array; + + constructor(type?: string, size?: number); + + add(name: string): Box; + addBox(box: Box): Box; + set(name: string, value: any): void; + addEntry(value: string, prop?: string): void; + printHeader(output: any): void; + write(stream: DataStream): void; + writeHeader(stream: DataStream, msg?: string): void; + computeSize(): void; + + // TODO add types for these + parse(stream: any): void; + parseDataAndRewind(stream: any): void; + parseLanguage(stream: any): void; + print(output: any): void; + } - export class SampleGroupEntry { - constructor(type: any); + // TODO finish add types for these classes + export class AudioSampleEntry extends SampleEntry { + constructor(type: any, size: any); - parse(stream: any): void; - write(stream: any): void; - } + getChannelCount(): any; + getSampleRate(): any; + getSampleSize(): any; + isAudio(): any; + parse(stream: any): void; + write(stream: any): void; + } - export class SingleItemTypeReferenceBox extends ContainerBox { - constructor(type: any, size: any, hdr_size: any, start: any); + export class CoLLBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class SingleItemTypeReferenceBoxLarge { - constructor(type: any, size: any, hdr_size: any, start: any); + export class ContainerBox extends Box { + constructor(type: any, size: any, uuid: any); - parse(stream: any): void; - } + parse(stream: any): void; + print(output: any): void; + write(stream: any): void; + } - export class SmDmBox extends ContainerBox { - constructor(size: any); + export class FullBox extends Box { + constructor(type: any, size: any, uuid: any); - parse(stream: any): void; - } + parse(stream: any): void; + parseDataAndRewind(stream: any): void; + parseFullHeader(stream: any): void; + printHeader(output: any): void; + writeHeader(stream: any): void; + } - export class SubtitleSampleEntry extends SampleEntry { - constructor(type: any, size: any); + export class HintSampleEntry extends SampleEntry { + constructor(type: any, size: any); + } - isSubtitle(): any; - } + export class MetadataSampleEntry extends SampleEntry { + constructor(type: any, size: any); - export class SystemSampleEntry extends SampleEntry { - constructor(type: any, size: any); - } + isMetadata(): any; + } - export class TextSampleEntry extends SampleEntry { - constructor(type: any, size: any); - } + export class OpusSampleEntry extends SampleEntry { + constructor(size: any); + } - export class TrackGroupTypeBox extends FullBox { - constructor(type: any, size: any); + export class SampleEntry extends Box { + constructor(type: any, size: any, hdr_size: any, start: any); + + getChannelCount(): any; + getCodec(): any; + getHeight(): any; + getSampleRate(): any; + getSampleSize(): any; + getWidth(): any; + isAudio(): any; + isHint(): any; + isMetadata(): any; + isSubtitle(): any; + isVideo(): any; + parse(stream: any): void; + parseDataAndRewind(stream: any): void; + parseFooter(stream: any): void; + parseHeader(stream: any): void; + write(stream: any): void; + writeFooter(stream: any): void; + writeHeader(stream: any): void; + } - parse(stream: any): void; - } + export class SampleGroupEntry { + constructor(type: any); - export class TrackReferenceTypeBox extends ContainerBox { - constructor(type: any, size: any, hdr_size: any, start: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; + export class SingleItemTypeReferenceBox extends ContainerBox { + constructor(type: any, size: any, hdr_size: any, start: any); - write(stream: any): void; - } + parse(stream: any): void; + } - export class VisualSampleEntry extends SampleEntry { - constructor(type: any, size: any); + export class SingleItemTypeReferenceBoxLarge { + constructor(type: any, size: any, hdr_size: any, start: any); - getHeight(): any; - getWidth(): any; - isVideo(): any; - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class a1lxBox extends ContainerBox { - constructor(size: any); + export class SmDmBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class a1opBox extends ContainerBox { - constructor(size: any); + export class SubtitleSampleEntry extends SampleEntry { + constructor(type: any, size: any); - parse(stream: any): void; - } + isSubtitle(): any; + } - export class alstSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class SystemSampleEntry extends SampleEntry { + constructor(type: any, size: any); + } - parse(stream: any): void; - } + export class TextSampleEntry extends SampleEntry { + constructor(type: any, size: any); + } - export class auxCBox extends ContainerBox { - constructor(size: any); + export class TrackGroupTypeBox extends FullBox { + constructor(type: any, size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class av01SampleEntry extends SampleEntry { - constructor(size: any); + export class TrackReferenceTypeBox extends ContainerBox { + constructor(type: any, size: any, hdr_size: any, start: any); - getCodec(): any; - } + parse(stream: any): void; - export class av1CBox extends ContainerBox { - constructor(size: any); + write(stream: any): void; + } - parse(stream: any): void; - } + export class VisualSampleEntry extends SampleEntry { + constructor(type: any, size: any); - export class avc1SampleEntry extends SampleEntry { - constructor(size: any); + getHeight(): any; + getWidth(): any; + isVideo(): any; + parse(stream: any): void; + write(stream: any): void; + } - getCodec(): any; - } + export class a1lxBox extends ContainerBox { + constructor(size: any); - export class avc2SampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + } - getCodec(): any; - } + export class a1opBox extends ContainerBox { + constructor(size: any); - export class avc3SampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + } - getCodec(): any; - } + export class alstSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class avc4SampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + } - getCodec(): any; - } + export class auxCBox extends ContainerBox { + constructor(size: any); - export class avcCBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class av01SampleEntry extends SampleEntry { + constructor(size: any); - export class avllSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + getCodec(): any; + } - parse(stream: any): void; - } + export class av1CBox extends ContainerBox { + constructor(size: any); - export class avssSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class avc1SampleEntry extends SampleEntry { + constructor(size: any); - export class btrtBox extends ContainerBox { - constructor(size: any); + getCodec(): any; + } - parse(stream: any): void; - } + export class avc2SampleEntry extends SampleEntry { + constructor(size: any); - export class bxmlBox extends FullBox { - constructor(size: any); + getCodec(): any; + } - parse(stream: any): void; - } + export class avc3SampleEntry extends SampleEntry { + constructor(size: any); - export class clapBox extends ContainerBox { - constructor(size: any); + getCodec(): any; + } - parse(stream: any): void; - } + export class avc4SampleEntry extends SampleEntry { + constructor(size: any); - export class clefBox extends ContainerBox { - constructor(size: any); + getCodec(): any; + } - parse(stream: any): void; - } + export class avcCBox extends ContainerBox { + constructor(size: any); - export class clliBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class avllSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class co64Box extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class avssSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class colrBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class btrtBox extends ContainerBox { + constructor(size: any); - export class cprtBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class bxmlBox extends FullBox { + constructor(size: any); - export class cslgBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class clapBox extends ContainerBox { + constructor(size: any); - export class cttsBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - unpack(samples: any): void; - write(stream: any): void; - } + export class clefBox extends ContainerBox { + constructor(size: any); - export class dOpsBox extends ContainerBox { - constructor(size?: number); + parse(stream: any): void; + } - parse(stream: DataStream): void; + export class clliBox extends ContainerBox { + constructor(size: any); - Version: number; - OutputChannelCount: number; - PreSkip: number; - InputSampleRate: number; - OutputGain: number; - ChannelMappingFamily: number; + parse(stream: any): void; + } - // When channelMappingFamily != 0 - StreamCount?: number; - CoupledCount?: number; - ChannelMapping?: number[]; - } + export class co64Box extends ContainerBox { + constructor(size: any); - export class dac3Box extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class colrBox extends ContainerBox { + constructor(size: any); - export class dec3Box extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class cprtBox extends ContainerBox { + constructor(size: any); - export class dfLaBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class cslgBox extends ContainerBox { + constructor(size: any); - export class dimmBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class cttsBox extends ContainerBox { + constructor(size: any); - export class dinfBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + unpack(samples: any): void; + write(stream: any): void; + } - export class dmaxBox extends ContainerBox { - constructor(size: any); + export class dOpsBox extends ContainerBox { + constructor(size?: number); - parse(stream: any): void; - } + parse(stream: DataStream): void; - export class dmedBox extends ContainerBox { - constructor(size: any); + Version: number; + OutputChannelCount: number; + PreSkip: number; + InputSampleRate: number; + OutputGain: number; + ChannelMappingFamily: number; - parse(stream: any): void; - } + // When channelMappingFamily != 0 + StreamCount?: number; + CoupledCount?: number; + ChannelMapping?: number[]; + } - export class drefBox extends ContainerBox { - constructor(size: any); + export class dac3Box extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class drepBox extends ContainerBox { - constructor(size: any); + export class dec3Box extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class dtrtSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class dfLaBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class edtsBox extends ContainerBox { - constructor(size: any); - } + export class dimmBox extends ContainerBox { + constructor(size: any); - export class elngBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class dinfBox extends ContainerBox { + constructor(size: any); + } - export class elstBox extends ContainerBox { - constructor(size: any); + export class dmaxBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class emsgBox extends ContainerBox { - constructor(size: any); + export class dmedBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class encaSampleEntry extends SampleEntry { - constructor(size: any); - } + export class drefBox extends ContainerBox { + constructor(size: any); - export class encmSampleEntry extends SampleEntry { - constructor(size: any); - } + parse(stream: any): void; + write(stream: any): void; + } - export class encsSampleEntry extends SampleEntry { - constructor(size: any); - } + export class drepBox extends ContainerBox { + constructor(size: any); - export class enctSampleEntry extends SampleEntry { - constructor(size: any); - } + parse(stream: any): void; + } - export class encuSampleEntry extends SampleEntry { - constructor(size: any); - } + export class dtrtSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class encvSampleEntry extends SampleEntry { - constructor(size: any); - } + parse(stream: any): void; + } - export class enofBox extends ContainerBox { - constructor(size: any); + export class edtsBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - } + export class elngBox extends ContainerBox { + constructor(size: any); - export class esdsBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class elstBox extends ContainerBox { + constructor(size: any); - export class fielBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class emsgBox extends ContainerBox { + constructor(size: any); - export class freeBox extends Box { - constructor(size: any); - } + parse(stream: any): void; + write(stream: any): void; + } - export class frmaBox extends ContainerBox { - constructor(size: any); + export class encaSampleEntry extends SampleEntry { + constructor(size: any); + } - parse(stream: any): void; - } + export class encmSampleEntry extends SampleEntry { + constructor(size: any); + } - export class ftypBox extends ContainerBox { - constructor(size: any); + export class encsSampleEntry extends SampleEntry { + constructor(size: any); + } - parse(stream: any): void; - write(stream: any): void; - } + export class enctSampleEntry extends SampleEntry { + constructor(size: any); + } - export class hdlrBox extends ContainerBox { - constructor(size: any); + export class encuSampleEntry extends SampleEntry { + constructor(size: any); + } - parse(stream: any): void; - write(stream: any): void; - } + export class encvSampleEntry extends SampleEntry { + constructor(size: any); + } - export class hev1SampleEntry extends SampleEntry { - constructor(size: any); + export class enofBox extends ContainerBox { + constructor(size: any); - getCodec(): any; - } + parse(stream: any): void; + } - export class hinfBox extends ContainerBox { - constructor(size: any); - } + export class esdsBox extends ContainerBox { + constructor(size: any); - export class hmhdBox extends FullBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class fielBox extends ContainerBox { + constructor(size: any); - export class hntiBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } - export class hvc1SampleEntry extends SampleEntry { - constructor(size: any); + export class freeBox extends Box { + constructor(size: any); + } - getCodec(): any; - } + export class frmaBox extends ContainerBox { + constructor(size: any); - export class hvcCBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class ftypBox extends ContainerBox { + constructor(size: any); - export class idatBox extends Box { - constructor(size: any); - } + parse(stream: any): void; + write(stream: any): void; + } - export class iinfBox extends ContainerBox { - constructor(size: any); + export class hdlrBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + write(stream: any): void; + } - export class ilocBox extends ContainerBox { - constructor(size: any); + export class hev1SampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + getCodec(): any; + } - export class imirBox extends ContainerBox { - constructor(size: any); + export class hinfBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - } + export class hmhdBox extends FullBox { + constructor(size: any); - export class infeBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class hntiBox extends ContainerBox { + constructor(size: any); + } - export class iodsBox extends FullBox { - constructor(size: any); + export class hvc1SampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + getCodec(): any; + } - export class ipcoBox extends ContainerBox { - constructor(size: any); - } + export class hvcCBox extends ContainerBox { + constructor(size: any); - export class ipmaBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class idatBox extends Box { + constructor(size: any); + } - export class iproBox extends FullBox { - constructor(size: any); + export class iinfBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class iprpBox extends ContainerBox { - constructor(size: any); - ipmas: ipmaBox[]; - } + export class ilocBox extends ContainerBox { + constructor(size: any); - export class irefBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class imirBox extends ContainerBox { + constructor(size: any); - export class irotBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class infeBox extends ContainerBox { + constructor(size: any); - export class ispeBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class iodsBox extends FullBox { + constructor(size: any); - export class kindBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; + export class ipcoBox extends ContainerBox { + constructor(size: any); + } - write(stream: any): void; - } + export class ipmaBox extends ContainerBox { + constructor(size: any); - export class levaBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class iproBox extends FullBox { + constructor(size: any); - export class lselBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class iprpBox extends ContainerBox { + constructor(size: any); + ipmas: ipmaBox[]; + } - export class maxrBox extends ContainerBox { - constructor(size: any); + export class irefBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class mdatBox extends Box { - constructor(size: any); - } + export class irotBox extends ContainerBox { + constructor(size: any); - export class mdcvBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class ispeBox extends ContainerBox { + constructor(size: any); - export class mdhdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; + export class kindBox extends ContainerBox { + constructor(size: any); - write(stream: any): void; - } + parse(stream: any): void; - export class mdiaBox extends ContainerBox { - constructor(size: any); - } + write(stream: any): void; + } - export class mecoBox extends Box { - constructor(size: any); - } + export class levaBox extends ContainerBox { + constructor(size: any); - export class mehdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; + export class lselBox extends ContainerBox { + constructor(size: any); - write(stream: any): void; - } + parse(stream: any): void; + } - export class mereBox extends FullBox { - constructor(size: any); + export class maxrBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class metaBox extends ContainerBox { - constructor(size: any); + export class mdatBox extends Box { + constructor(size: any); + } - parse(stream: any): void; - } + export class mdcvBox extends ContainerBox { + constructor(size: any); - export class mettSampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class mdhdBox extends ContainerBox { + constructor(size: any); - export class metxSampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; - parse(stream: any): void; - } + write(stream: any): void; + } - export class mfhdBox extends ContainerBox { - constructor(size: any); + export class mdiaBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; + export class mecoBox extends Box { + constructor(size: any); + } - write(stream: any): void; - } + export class mehdBox extends ContainerBox { + constructor(size: any); - export class mfraBox extends ContainerBox { - constructor(size: any); - tfras: tfraBox[]; - } + parse(stream: any): void; - export class mfroBox extends ContainerBox { - constructor(size: any); + write(stream: any): void; + } - parse(stream: any): void; - } + export class mereBox extends FullBox { + constructor(size: any); - export class minfBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } - export class moofBox extends ContainerBox { - constructor(size: any); - trafs: trafBox[]; - } + export class metaBox extends ContainerBox { + constructor(size: any); - export class moovBox extends ContainerBox { - constructor(size: any); - traks: trakBox[]; - psshs: psshBox[]; - } + parse(stream: any): void; + } - export class mp4aSampleEntry extends SampleEntry { - constructor(size: any); + export class mettSampleEntry extends SampleEntry { + constructor(size: any); - getCodec(): any; - } + parse(stream: any): void; + } - export class msrcTrackGroupTypeBox extends ContainerBox { - constructor(size: any); - } + export class metxSampleEntry extends SampleEntry { + constructor(size: any); - export class mvexBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - trexs: trexBox[]; - } + export class mfhdBox extends ContainerBox { + constructor(size: any); - export class mvhdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; - parse(stream: any): void; - print(output: any): void; - write(stream: any): void; - } + write(stream: any): void; + } - export class mvifSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class mfraBox extends ContainerBox { + constructor(size: any); + tfras: tfraBox[]; + } - parse(stream: any): void; - } + export class mfroBox extends ContainerBox { + constructor(size: any); - export class nmhdBox extends FullBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class minfBox extends ContainerBox { + constructor(size: any); + } - export class npckBox extends ContainerBox { - constructor(size: any); + export class moofBox extends ContainerBox { + constructor(size: any); + trafs: trafBox[]; + } - parse(stream: any): void; - } + export class moovBox extends ContainerBox { + constructor(size: any); + traks: trakBox[]; + psshs: psshBox[]; + } - export class numpBox extends ContainerBox { - constructor(size: any); + export class mp4aSampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + getCodec(): any; + } - export class padbBox extends ContainerBox { - constructor(size: any); + export class msrcTrackGroupTypeBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - } + export class mvexBox extends ContainerBox { + constructor(size: any); - export class paspBox extends ContainerBox { - constructor(size: any); + trexs: trexBox[]; + } - parse(stream: any): void; - } + export class mvhdBox extends ContainerBox { + constructor(size: any); - export class paylBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + print(output: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class mvifSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class paytBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class nmhdBox extends FullBox { + constructor(size: any); - export class pdinBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class npckBox extends ContainerBox { + constructor(size: any); - export class pitmBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class numpBox extends ContainerBox { + constructor(size: any); - export class pixiBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class padbBox extends ContainerBox { + constructor(size: any); - export class pmaxBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class paspBox extends ContainerBox { + constructor(size: any); - export class prftBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class paylBox extends ContainerBox { + constructor(size: any); - export class profBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class paytBox extends ContainerBox { + constructor(size: any); - export class prolSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class pdinBox extends ContainerBox { + constructor(size: any); - export class psshBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class pitmBox extends ContainerBox { + constructor(size: any); - export class rashSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class pixiBox extends ContainerBox { + constructor(size: any); - export class rinfBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } - export class rollSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class pmaxBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class saioBox extends ContainerBox { - constructor(size: any); + export class prftBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class saizBox extends ContainerBox { - constructor(size: any); + export class profBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class sbgpBox extends ContainerBox { - constructor(size: any); + export class prolSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - parse(stream: any): void; + parse(stream: any): void; + } - write(stream: any): void; - } + export class psshBox extends ContainerBox { + constructor(size: any); - export class sbttSampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class rashSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class schiBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } - export class schmBox extends ContainerBox { - constructor(size: any); + export class rinfBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - } + export class rollSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class scifSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class saioBox extends ContainerBox { + constructor(size: any); - export class scnmSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class saizBox extends ContainerBox { + constructor(size: any); - export class sdtpBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class sbgpBox extends ContainerBox { + constructor(size: any); - export class seigSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; - parse(stream: any): void; - } + write(stream: any): void; + } - export class sencBox extends ContainerBox { - constructor(size: any); + export class sbttSampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class sgpdBox extends ContainerBox { - constructor(size: any); + export class schiBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - write(stream: any): void; - } + export class schmBox extends ContainerBox { + constructor(size: any); - export class sidxBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class scifSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class sinfBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } - export class skipBox extends Box { - constructor(size: any); - } + export class scnmSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class smhdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class sdtpBox extends ContainerBox { + constructor(size: any); - export class ssixBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class seigSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class stblBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - sgpds: sgpdBox[]; - sbgps: sbgpBox[]; - } + export class sencBox extends ContainerBox { + constructor(size: any); - export class stcoBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - unpack(samples: any): void; - write(stream: any): void; - } + export class sgpdBox extends ContainerBox { + constructor(size: any); - export class stdpBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class sidxBox extends ContainerBox { + constructor(size: any); - export class sthdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class sinfBox extends ContainerBox { + constructor(size: any); + } - export class stppSampleEntry extends SampleEntry { - constructor(size: any); + export class skipBox extends Box { + constructor(size: any); + } - parse(stream: any): void; - write(stream: any): void; - } + export class smhdBox extends ContainerBox { + constructor(size: any); - export class strdBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + write(stream: any): void; + } - export class striBox extends ContainerBox { - constructor(size: any); + export class ssixBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class strkBox extends Box { - constructor(size: any); - } + export class stblBox extends ContainerBox { + constructor(size: any); - export class stsaSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + sgpds: sgpdBox[]; + sbgps: sbgpBox[]; + } - parse(stream: any): void; - } + export class stcoBox extends ContainerBox { + constructor(size: any); - export class stscBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + unpack(samples: any): void; + write(stream: any): void; + } - parse(stream: any): void; - unpack(samples: any): void; - write(stream: any): void; - } + export class stdpBox extends ContainerBox { + constructor(size: any); - export class stsdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class sthdBox extends ContainerBox { + constructor(size: any); - export class stsgBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class stppSampleEntry extends SampleEntry { + constructor(size: any); - export class stshBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class strdBox extends ContainerBox { + constructor(size: any); + } - export class stssBox extends ContainerBox { - constructor(size: any); + export class striBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class stszBox extends ContainerBox { - constructor(size: any); + export class strkBox extends Box { + constructor(size: any); + } - parse(stream: any): void; - unpack(samples: any): void; - write(stream: any): void; - } + export class stsaSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class sttsBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - unpack(samples: any): void; - write(stream: any): void; - } + export class stscBox extends ContainerBox { + constructor(size: any); - export class stviBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + unpack(samples: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class stsdBox extends ContainerBox { + constructor(size: any); - export class stxtSampleEntry extends SampleEntry { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - getCodec(): any; - parse(stream: any): void; - } + export class stsgBox extends ContainerBox { + constructor(size: any); - export class stypBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class stshBox extends ContainerBox { + constructor(size: any); - export class stz2Box extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class stssBox extends ContainerBox { + constructor(size: any); - export class subsBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class stszBox extends ContainerBox { + constructor(size: any); - export class syncSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + unpack(samples: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class sttsBox extends ContainerBox { + constructor(size: any); - export class taptBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + unpack(samples: any): void; + write(stream: any): void; + } - export class teleSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class stviBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class tencBox extends ContainerBox { - constructor(size: any); + export class stxtSampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + getCodec(): any; + parse(stream: any): void; + } - export class tfdtBox extends ContainerBox { - constructor(size: any); + export class stypBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class tfhdBox extends ContainerBox { - constructor(size: any); + export class stz2Box extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class tfraBox extends ContainerBox { - constructor(size: any); + export class subsBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class tkhdBox extends ContainerBox { - constructor(size: any); + export class syncSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - parse(stream: any): void; - print(output: any): void; - write(stream: any): void; - } + parse(stream: any): void; + } - export class tmaxBox extends ContainerBox { - constructor(size: any); + export class taptBox extends ContainerBox { + constructor(size: any); + } - parse(stream: any): void; - } + export class teleSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - export class tminBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class tencBox extends ContainerBox { + constructor(size: any); - export class totlBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class tfdtBox extends ContainerBox { + constructor(size: any); - export class tpayBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class tfhdBox extends ContainerBox { + constructor(size: any); - export class tpylBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class tfraBox extends ContainerBox { + constructor(size: any); - export class trafBox extends ContainerBox { - constructor(size: any); - truns: trunBox[]; - sgpd: sgpdBox[]; - sbgp: sbgpBox[]; - } + parse(stream: any): void; + } - export class trakBox extends ContainerBox { - constructor(size: any); - } + export class tkhdBox extends ContainerBox { + constructor(size: any); - export class trefBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + print(output: any): void; + write(stream: any): void; + } - parse(stream: any): void; - } + export class tmaxBox extends ContainerBox { + constructor(size: any); - export class trepBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class tminBox extends ContainerBox { + constructor(size: any); - export class trexBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class totlBox extends ContainerBox { + constructor(size: any); - export class trgrBox extends ContainerBox { - constructor(size: any); - } + parse(stream: any): void; + } + + export class tpayBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class tpylBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class trafBox extends ContainerBox { + constructor(size: any); + truns: trunBox[]; + sgpd: sgpdBox[]; + sbgp: sbgpBox[]; + } + + export class trakBox extends ContainerBox { + constructor(size: any); + } + + export class trefBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class trepBox extends ContainerBox { + constructor(size: any); - export class trpyBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class trexBox extends ContainerBox { + constructor(size: any); - export class trunBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + write(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class trgrBox extends ContainerBox { + constructor(size: any); + } - export class tsasSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class trpyBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class tsclSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + export class trunBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + write(stream: any): void; + } - export class tselBox extends ContainerBox { - constructor(size: any); + export class tsasSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class tx3gSampleEntry extends SampleEntry { - constructor(size: any); + export class tsclSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class txtCBox extends ContainerBox { - constructor(size: any); + export class tselBox extends ContainerBox { + constructor(size: any); - parse(stream: any): void; - } + parse(stream: any): void; + } - export class udtaBox extends ContainerBox { - constructor(size: any); - kinds: kindBox[]; - } + export class tx3gSampleEntry extends SampleEntry { + constructor(size: any); - export class viprSampleGroupEntry extends SampleGroupEntry { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - } + export class txtCBox extends ContainerBox { + constructor(size: any); - export class vmhdBox extends ContainerBox { - constructor(size: any); + parse(stream: any): void; + } - parse(stream: any): void; - write(stream: any): void; - } + export class udtaBox extends ContainerBox { + constructor(size: any); + kinds: kindBox[]; + } - export class vp08SampleEntry extends SampleEntry { - constructor(size: any); + export class viprSampleGroupEntry extends SampleGroupEntry { + constructor(size: any); - getCodec(): any; - } + parse(stream: any): void; + } - export class vp09SampleEntry extends SampleEntry { - constructor(size: any); + export class vmhdBox extends ContainerBox { + constructor(size: any); - getCodec(): any; - } + parse(stream: any): void; + write(stream: any): void; + } - export class vpcCBox extends ContainerBox { - constructor(size: any); + export class vp08SampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } + getCodec(): any; + } - export class vttCBox extends ContainerBox { - constructor(size: any); + export class vp09SampleEntry extends SampleEntry { + constructor(size: any); - parse(stream: any): void; - } - - export class vttcBox extends ContainerBox { - constructor(size: any); - } - - export class vvc1SampleEntry extends SampleEntry { - constructor(size: any); - - getCodec(): any; - } - - export class vvcCBox extends ContainerBox { - constructor(size: any); - - parse(stream: any): void; - } - - export class vvcNSampleEntry extends SampleEntry { - constructor(size: any); - } - - export class vvi1SampleEntry extends SampleEntry { - constructor(size: any); - - getCodec(): any; - } - - export class vvnCBox extends ContainerBox { - constructor(size: any); - - parse(stream: any): void; - } - - export class vvs1SampleEntry extends SampleEntry { - constructor(size: any); - } - - export class wvttSampleEntry extends SampleEntry { - constructor(size: any); - - parse(stream: any): void; - } - - export const BASIC_BOXES: string[]; - export const CONTAINER_BOXES: string[][]; - export const DIFF_BOXES_PROP_NAMES: string[]; - export const DIFF_PRIMITIVE_ARRAY_PROP_NAMES: string[]; - export const ERR_INVALID_DATA: number; - export const ERR_NOT_ENOUGH_DATA: number; - export const FULL_BOXES: string[]; - export const OK: number; - export const SAMPLE_ENTRY_TYPE_AUDIO: string; - export const SAMPLE_ENTRY_TYPE_HINT: string; - export const SAMPLE_ENTRY_TYPE_METADATA: string; - export const SAMPLE_ENTRY_TYPE_SUBTITLE: string; - export const SAMPLE_ENTRY_TYPE_SYSTEM: string; - export const SAMPLE_ENTRY_TYPE_TEXT: string; - export const SAMPLE_ENTRY_TYPE_VISUAL: string; - export const TFHD_FLAG_BASE_DATA_OFFSET: number; - export const TFHD_FLAG_DEFAULT_BASE_IS_MOOF: number; - export const TFHD_FLAG_DUR_EMPTY: number; - export const TFHD_FLAG_SAMPLE_DESC: number; - export const TFHD_FLAG_SAMPLE_DUR: number; - export const TFHD_FLAG_SAMPLE_FLAGS: number; - export const TFHD_FLAG_SAMPLE_SIZE: number; - export const TKHD_FLAG_ENABLED: number; - export const TKHD_FLAG_IN_MOVIE: number; - export const TKHD_FLAG_IN_PREVIEW: number; - export const TRUN_FLAGS_CTS_OFFSET: number; - export const TRUN_FLAGS_DATA_OFFSET: number; - export const TRUN_FLAGS_DURATION: number; - export const TRUN_FLAGS_FIRST_FLAG: number; - export const TRUN_FLAGS_FLAGS: number; - export const TRUN_FLAGS_SIZE: number; - export const UUIDs: string[]; - export const boxCodes: string[]; - export const containerBoxCodes: any[]; - export const fullBoxCodes: any[]; - - export const sampleEntryCodes: { - Audio: string[]; - Hint: any[]; - Metadata: string[]; - Subtitle: string[]; - System: string[]; - Text: string[]; - Visual: string[]; - }; - - export const sampleGroupEntryCodes: any[]; - - export const trackGroupTypes: any[]; - - export function addSubBoxArrays(subBoxNames: any): void; - export function boxEqual(box_a: any, box_b: any): any; - export function boxEqualFields(box_a: any, box_b: any): any; - export function createBoxCtor(type: any, parseMethod: any): void; - export function createContainerBoxCtor( - type: any, - parseMethod: any, - subBoxNames: any - ): void; - export function createEncryptedSampleEntryCtor( - mediaType: any, - type: any, - parseMethod: any - ): void; - export function createFullBoxCtor(type: any, parseMethod: any): void; - export function createMediaSampleEntryCtor( - mediaType: any, - parseMethod: any, - subBoxNames: any - ): void; - export function createSampleEntryCtor( - mediaType: any, - type: any, - parseMethod: any, - subBoxNames: any - ): void; - export function createSampleGroupCtor(type: any, parseMethod: any): void; - export function createTrackGroupCtor(type: any, parseMethod: any): void; - export function createUUIDBox( - uuid: any, - isFullBox: any, - isContainerBox: any, - parseMethod: any - ): void; - export function decimalToHex(d: any, padding: any): any; - export function initialize(): void; - export function parseHex16(stream: any): any; - export function parseOneBox( - stream: any, - headerOnly: any, - parentSize: any - ): any; - export function parseUUID(stream: any): any; - - /* ??? + getCodec(): any; + } + + export class vpcCBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class vttCBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class vttcBox extends ContainerBox { + constructor(size: any); + } + + export class vvc1SampleEntry extends SampleEntry { + constructor(size: any); + + getCodec(): any; + } + + export class vvcCBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class vvcNSampleEntry extends SampleEntry { + constructor(size: any); + } + + export class vvi1SampleEntry extends SampleEntry { + constructor(size: any); + + getCodec(): any; + } + + export class vvnCBox extends ContainerBox { + constructor(size: any); + + parse(stream: any): void; + } + + export class vvs1SampleEntry extends SampleEntry { + constructor(size: any); + } + + export class wvttSampleEntry extends SampleEntry { + constructor(size: any); + + parse(stream: any): void; + } + + export const BASIC_BOXES: string[]; + export const CONTAINER_BOXES: string[][]; + export const DIFF_BOXES_PROP_NAMES: string[]; + export const DIFF_PRIMITIVE_ARRAY_PROP_NAMES: string[]; + export const ERR_INVALID_DATA: number; + export const ERR_NOT_ENOUGH_DATA: number; + export const FULL_BOXES: string[]; + export const OK: number; + export const SAMPLE_ENTRY_TYPE_AUDIO: string; + export const SAMPLE_ENTRY_TYPE_HINT: string; + export const SAMPLE_ENTRY_TYPE_METADATA: string; + export const SAMPLE_ENTRY_TYPE_SUBTITLE: string; + export const SAMPLE_ENTRY_TYPE_SYSTEM: string; + export const SAMPLE_ENTRY_TYPE_TEXT: string; + export const SAMPLE_ENTRY_TYPE_VISUAL: string; + export const TFHD_FLAG_BASE_DATA_OFFSET: number; + export const TFHD_FLAG_DEFAULT_BASE_IS_MOOF: number; + export const TFHD_FLAG_DUR_EMPTY: number; + export const TFHD_FLAG_SAMPLE_DESC: number; + export const TFHD_FLAG_SAMPLE_DUR: number; + export const TFHD_FLAG_SAMPLE_FLAGS: number; + export const TFHD_FLAG_SAMPLE_SIZE: number; + export const TKHD_FLAG_ENABLED: number; + export const TKHD_FLAG_IN_MOVIE: number; + export const TKHD_FLAG_IN_PREVIEW: number; + export const TRUN_FLAGS_CTS_OFFSET: number; + export const TRUN_FLAGS_DATA_OFFSET: number; + export const TRUN_FLAGS_DURATION: number; + export const TRUN_FLAGS_FIRST_FLAG: number; + export const TRUN_FLAGS_FLAGS: number; + export const TRUN_FLAGS_SIZE: number; + export const UUIDs: string[]; + export const boxCodes: string[]; + export const containerBoxCodes: any[]; + export const fullBoxCodes: any[]; + + export const sampleEntryCodes: { + Audio: string[]; + Hint: any[]; + Metadata: string[]; + Subtitle: string[]; + System: string[]; + Text: string[]; + Visual: string[]; + }; + + export const sampleGroupEntryCodes: any[]; + + export const trackGroupTypes: any[]; + + export function addSubBoxArrays(subBoxNames: any): void; + export function boxEqual(box_a: any, box_b: any): any; + export function boxEqualFields(box_a: any, box_b: any): any; + export function createBoxCtor(type: any, parseMethod: any): void; + export function createContainerBoxCtor( + type: any, + parseMethod: any, + subBoxNames: any, + ): void; + export function createEncryptedSampleEntryCtor( + mediaType: any, + type: any, + parseMethod: any, + ): void; + export function createFullBoxCtor(type: any, parseMethod: any): void; + export function createMediaSampleEntryCtor( + mediaType: any, + parseMethod: any, + subBoxNames: any, + ): void; + export function createSampleEntryCtor( + mediaType: any, + type: any, + parseMethod: any, + subBoxNames: any, + ): void; + export function createSampleGroupCtor(type: any, parseMethod: any): void; + export function createTrackGroupCtor(type: any, parseMethod: any): void; + export function createUUIDBox( + uuid: any, + isFullBox: any, + isContainerBox: any, + parseMethod: any, + ): void; + export function decimalToHex(d: any, padding: any): any; + export function initialize(): void; + export function parseHex16(stream: any): any; + export function parseOneBox( + stream: any, + headerOnly: any, + parentSize: any, + ): any; + export function parseUUID(stream: any): any; + + /* ??? namespace UUIDBoxes { export class a2394f525a9b4f14a2446c427c648df4 { constructor(size: any) @@ -1802,63 +1802,63 @@ export namespace BoxParser { // TODO Add types for the remaining classes found via dts-gen export class MP4BoxStream { - constructor(arrayBuffer: any); - - getEndPosition(): any; - getLength(): any; - getPosition(): any; - isEos(): any; - readAnyInt(size: any, signed: any): any; - readCString(): any; - readInt16(): any; - readInt16Array(length: any): any; - readInt32(): any; - readInt32Array(length: any): any; - readInt64(): any; - readInt8(): any; - readString(length: any): any; - readUint16(): any; - readUint16Array(length: any): any; - readUint24(): any; - readUint32(): any; - readUint32Array(length: any): any; - readUint64(): any; - readUint8(): any; - readUint8Array(length: any): any; - seek(pos: any): any; + constructor(arrayBuffer: any); + + getEndPosition(): any; + getLength(): any; + getPosition(): any; + isEos(): any; + readAnyInt(size: any, signed: any): any; + readCString(): any; + readInt16(): any; + readInt16Array(length: any): any; + readInt32(): any; + readInt32Array(length: any): any; + readInt64(): any; + readInt8(): any; + readString(length: any): any; + readUint16(): any; + readUint16Array(length: any): any; + readUint24(): any; + readUint32(): any; + readUint32Array(length: any): any; + readUint64(): any; + readUint8(): any; + readUint8Array(length: any): any; + seek(pos: any): any; } export class MultiBufferStream { - constructor(buffer: any); - - addUsedBytes(nbBytes: any): void; - cleanBuffers(): void; - findEndContiguousBuf(inputindex: any): any; - findPosition(fromStart: any, filePosition: any, markAsUsed: any): any; - getEndFilePositionAfter(pos: any): any; - getEndPosition(): any; - getLength(): any; - getPosition(): any; - initialized(): any; - insertBuffer(ab: any): void; - logBufferLevel(info: any): void; - mergeNextBuffer(): any; - reduceBuffer(buffer: any, offset: any, newLength: any): any; - seek(filePosition: any, fromStart: any, markAsUsed: any): any; - setAllUsedBytes(): void; + constructor(buffer: any); + + addUsedBytes(nbBytes: any): void; + cleanBuffers(): void; + findEndContiguousBuf(inputindex: any): any; + findPosition(fromStart: any, filePosition: any, markAsUsed: any): any; + getEndFilePositionAfter(pos: any): any; + getEndPosition(): any; + getLength(): any; + getPosition(): any; + initialized(): any; + insertBuffer(ab: any): void; + logBufferLevel(info: any): void; + mergeNextBuffer(): any; + reduceBuffer(buffer: any, offset: any, newLength: any): any; + seek(filePosition: any, fromStart: any, markAsUsed: any): any; + setAllUsedBytes(): void; } export class Textin4Parser { - constructor(); + constructor(); - parseConfig(data: any): any; - parseSample(sample: any): any; + parseConfig(data: any): any; + parseSample(sample: any): any; } export class XMLSubtitlein4Parser { - constructor(); + constructor(); - parseSample(sample: any): any; + parseSample(sample: any): any; } export function MPEG4DescriptorParser(): any; @@ -1866,24 +1866,24 @@ export function MPEG4DescriptorParser(): any; export namespace BoxParser {} export namespace Log { - export const LOG_LEVEL_ERROR = 4; - export const LOG_LEVEL_WARNING = 3; - export const LOG_LEVEL_INFO = 2; - export const LOG_LEVEL_DEBUG = 1; - - export function debug(module: any, msg: any): void; - export function error(module: any, msg: any): void; - export function getDurationString(duration: any, _timescale: any): any; - export function info(module: any, msg: any): void; - export function log(module: any, msg: any): void; - export function printRanges(ranges: any): any; - export function setLogLevel(level: any): void; - export function warn(module: any, msg: any): void; + export const LOG_LEVEL_ERROR = 4; + export const LOG_LEVEL_WARNING = 3; + export const LOG_LEVEL_INFO = 2; + export const LOG_LEVEL_DEBUG = 1; + + export function debug(module: any, msg: any): void; + export function error(module: any, msg: any): void; + export function getDurationString(duration: any, _timescale: any): any; + export function info(module: any, msg: any): void; + export function log(module: any, msg: any): void; + export function printRanges(ranges: any): any; + export function setLogLevel(level: any): void; + export function warn(module: any, msg: any): void; } declare var FLAGS: Flags; declare global { - interface Window { - FLAGS: Flags; - } + interface Window { + FLAGS: Flags; + } } diff --git a/apps/desktop/src/icons.tsx b/apps/desktop/src/icons.tsx index 8e6b5d2b4..08cbfb816 100644 --- a/apps/desktop/src/icons.tsx +++ b/apps/desktop/src/icons.tsx @@ -1,90 +1,90 @@ export const CloseX = (props: { class: string }) => { - return ( - - - - ); + return ( + + + + ); }; export const Expand = (props: { class: string }) => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const Minimize = (props: { class: string }) => { - return ( - - - - - - - ); + return ( + + + + + + + ); }; export const Squircle = (props: { class: string }) => { - return ( - - - - ); + return ( + + + + ); }; export const Flip = (props: { class: string }) => { - return ( - - - - ); + return ( + + + + ); }; diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index c17e1b0e7..19a831b15 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -1,104 +1,104 @@ import type { RouteSectionProps } from "@solidjs/router"; -import { UnlistenFn } from "@tauri-apps/api/event"; +import type { UnlistenFn } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { type as ostype } from "@tauri-apps/plugin-os"; import { cx } from "cva"; -import { onCleanup, onMount, ParentProps, Suspense } from "solid-js"; +import { onCleanup, onMount, type ParentProps, Suspense } from "solid-js"; import { AbsoluteInsetLoader } from "~/components/Loader"; import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11"; import { initializeTitlebar } from "~/utils/titlebar-state"; import { - useWindowChromeContext, - WindowChromeContext, + useWindowChromeContext, + WindowChromeContext, } from "./(window-chrome)/Context"; export const route = { - info: { - AUTO_SHOW_WINDOW: false, - }, + info: { + AUTO_SHOW_WINDOW: false, + }, }; export default function (props: RouteSectionProps) { - let unlistenResize: UnlistenFn | undefined; + let unlistenResize: UnlistenFn | undefined; - onMount(async () => { - console.log("window chrome mounted"); - unlistenResize = await initializeTitlebar(); - if (location.pathname === "/") getCurrentWindow().show(); - }); + onMount(async () => { + console.log("window chrome mounted"); + unlistenResize = await initializeTitlebar(); + if (location.pathname === "/") getCurrentWindow().show(); + }); - onCleanup(() => { - unlistenResize?.(); - }); + onCleanup(() => { + unlistenResize?.(); + }); - return ( - -
-
+ return ( + +
+
- {/* breaks sometimes */} - {/* */} - { - console.log("Outer window chrome suspense fallback"); - return ; - }) as any - } - > - - {/* prevents flicker idk */} - { - console.log("Inner window chrome suspense fallback"); - }) as any - } - > - {props.children} - - - - {/* */} -
-
- ); + { + console.log("Outer window chrome suspense fallback"); + return ; + }) as any + } + > + + {/* prevents flicker idk */} + { + console.log("Inner window chrome suspense fallback"); + }) as any + } + > + {props.children} + + + + {/* */} +
+
+ ); } function Header() { - const ctx = useWindowChromeContext()!; + const ctx = useWindowChromeContext()!; - const isWindows = ostype() === "windows"; + const isWindows = ostype() === "windows"; - return ( -
- {ctx.state()?.items} - {isWindows && } -
- ); + return ( +
+ {ctx.state()?.items} + {isWindows && } +
+ ); } function Inner(props: ParentProps) { - onMount(() => { - if (location.pathname !== "/") getCurrentWindow().show(); - }); + onMount(() => { + if (location.pathname !== "/") getCurrentWindow().show(); + }); - return ( -
- {props.children} -
- ); + return ( +
+ {props.children} +
+ ); } diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index d446c8bc4..188a93f7e 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -1,23 +1,23 @@ import { Button } from "@cap/ui-solid"; import { useNavigate } from "@solidjs/router"; import { - createMutation, - createQuery, - useQueryClient, + createMutation, + createQuery, + useQueryClient, } from "@tanstack/solid-query"; import { getVersion } from "@tauri-apps/api/app"; import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; import { cx } from "cva"; import { - ComponentProps, - createEffect, - createResource, - createSignal, - ErrorBoundary, - onCleanup, - onMount, - Show, - Suspense, + type ComponentProps, + createEffect, + createResource, + createSignal, + ErrorBoundary, + onCleanup, + onMount, + Show, + Suspense, } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; @@ -25,1067 +25,1103 @@ import Mode from "~/components/Mode"; import Tooltip from "~/components/Tooltip"; import { identifyUser, trackEvent } from "~/utils/analytics"; import { - createCameraMutation, - createCurrentRecordingQuery, - createLicenseQuery, - createVideoDevicesQuery, - getPermissions, - listAudioDevices, - listScreens, - listWindows, + createCameraMutation, + createCurrentRecordingQuery, + createLicenseQuery, + createVideoDevicesQuery, + getPermissions, + listAudioDevices, + listScreens, + listWindows, } from "~/utils/queries"; import { - CameraInfo, - type CaptureScreen, - type CaptureWindow, - commands, - events, - ScreenCaptureTarget, + type CameraInfo, + type CaptureScreen, + type CaptureWindow, + commands, + events, + type ScreenCaptureTarget, } from "~/utils/tauri"; function getWindowSize() { - return { - width: 300, - height: 340, - }; + return { + width: 300, + height: 340, + }; } export default function () { - return ( - - - - ); + return ( + + + + ); } function Page() { - const { rawOptions, setOptions } = useRecordingOptions(); - - const currentRecording = createCurrentRecordingQuery(); - const generalSettings = generalSettingsStore.createQuery(); - - const isRecording = () => !!currentRecording.data; - - const license = createLicenseQuery(); - - createUpdateCheck(); - - const auth = authStore.createQuery(); - - onMount(async () => { - const auth = await authStore.get(); - const userId = auth?.user_id; - if (!userId) return; - - const trackedSession = localStorage.getItem("tracked_signin_session"); - - if (trackedSession !== userId) { - console.log("New auth session detected, tracking sign in event"); - identifyUser(userId); - trackEvent("user_signed_in", { platform: "desktop" }); - localStorage.setItem("tracked_signin_session", userId); - } else { - console.log("Auth session already tracked, skipping sign in event"); - } - }); - - onMount(() => { - // Enforce window size with multiple safeguards - const currentWindow = getCurrentWindow(); - - // Check size when app regains focus - const unlistenFocus = currentWindow.onFocusChanged( - ({ payload: focused }) => { - if (focused) { - const size = getWindowSize(); - - currentWindow.setSize(new LogicalSize(size.width, size.height)); - } - } - ); - - // Listen for resize events - const unlistenResize = currentWindow.onResized(() => { - const size = getWindowSize(); - - currentWindow.setSize(new LogicalSize(size.width, size.height)); - }); - - onCleanup(async () => { - (await unlistenFocus)?.(); - (await unlistenResize)?.(); - }); - }); - - createEffect(() => { - const size = getWindowSize(); - getCurrentWindow().setSize(new LogicalSize(size.width, size.height)); - }); - - const screens = createQuery(() => listScreens); - const windows = createQuery(() => listWindows); - const cameras = createVideoDevicesQuery(); - const mics = createQuery(() => listAudioDevices); - - // these all avoid suspending - const _screens = () => (screens.isPending ? [] : screens.data); - const _windows = () => (windows.isPending ? [] : windows.data); - const _mics = () => (mics.isPending ? [] : mics.data); - - // these options take the raw config values and combine them with the available options, - // allowing us to define fallbacks if the selected options aren't actually available - const options = { - screen: () => { - let screen; - - if (rawOptions.captureTarget.variant === "screen") { - const screenId = rawOptions.captureTarget.id; - screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; - } else if (rawOptions.captureTarget.variant === "area") { - const screenId = rawOptions.captureTarget.screen; - screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; - } - - return screen; - }, - window: () => { - let win; - - if (rawOptions.captureTarget.variant === "window") { - const windowId = rawOptions.captureTarget.id; - win = _windows()?.find((s) => s.id === windowId) ?? _windows()?.[0]; - } - - return win; - }, - cameraID: () => - cameras.find((c) => { - const { cameraID } = rawOptions; - if (!cameraID) return; - if ("ModelID" in cameraID && c.model_id === cameraID.ModelID) return c; - if ("DeviceID" in cameraID && c.device_id == cameraID.DeviceID) - return c; - }), - micName: () => mics.data?.find((name) => name === rawOptions.micName), - target: (): ScreenCaptureTarget => { - switch (rawOptions.captureTarget.variant) { - case "screen": - return { variant: "screen", id: options.screen()?.id ?? -1 }; - case "window": - return { variant: "window", id: options.window()?.id ?? -1 }; - case "area": - return { - variant: "area", - bounds: rawOptions.captureTarget.bounds, - screen: options.screen()?.id ?? -1, - }; - } - }, - }; - - // if target is window and no windows are available, switch to screen capture - createEffect(() => { - if (options.target().variant === "window" && _windows()?.length === 0) { - setOptions( - "captureTarget", - reconcile({ - variant: "screen", - id: options.screen()?.id ?? -1, - }) - ); - } - }); - - const toggleRecording = createMutation(() => ({ - mutationFn: async () => { - if (!isRecording()) { - await commands.startRecording({ - capture_target: options.target(), - mode: rawOptions.mode, - capture_system_audio: rawOptions.captureSystemAudio, - }); - } else await commands.stopRecording(); - }, - })); - - const setMicInput = createMutation(() => ({ - mutationFn: async (name: string | null) => { - await commands.setMicInput(name); - setOptions("micName", name); - }, - })); - - const setCamera = createCameraMutation(); - - onMount(() => { - if (rawOptions.cameraID) setCamera.mutate(rawOptions.cameraID); - }); - - return ( -
- -
- Settings}> - - - Previous Recordings}> - - - - - - - - - - {import.meta.env.DEV && ( - - )} -
-
-
-
- - - }> - - { - if (license.data?.type !== "pro") { - await commands.showWindow("Upgrade"); - } - }} - class={cx( - "text-[0.6rem] rounded-lg px-1 py-0.5", - license.data?.type === "pro" - ? "bg-[--blue-400] text-gray-1 dark:text-gray-12" - : "bg-gray-3 cursor-pointer hover:bg-gray-5" - )} - > - {license.data?.type === "commercial" - ? "Commercial" - : license.data?.type === "pro" - ? "Pro" - : "Personal"} - - - -
- -
-
- { - if (!area) - setOptions( - "captureTarget", - reconcile({ - variant: "screen", - id: options.screen()?.id ?? -1, - }) - ); - }} - /> -
-
-
-
- - options={_screens() ?? []} - onChange={(value) => { - if (!value) return; - - trackEvent("display_selected", { - display_id: value.id, - display_name: value.name, - refresh_rate: value.refresh_rate, - }); - - setOptions( - "captureTarget", - reconcile({ variant: "screen", id: value.id }) - ); - }} - value={options.screen() ?? null} - placeholder="Screen" - optionsEmptyText="No screens found" - selected={ - rawOptions.captureTarget.variant === "screen" || - rawOptions.captureTarget.variant === "area" - } - /> - - options={_windows() ?? []} - onChange={(value) => { - if (!value) return; - - trackEvent("window_selected", { - window_id: value.id, - window_name: value.name, - owner_name: value.owner_name, - refresh_rate: value.refresh_rate, - }); - - setOptions( - "captureTarget", - reconcile({ variant: "window", id: value.id }) - ); - }} - value={options.window() ?? null} - placeholder="Window" - optionsEmptyText="No windows found" - selected={rawOptions.captureTarget.variant === "window"} - getName={(value) => - platform() === "windows" - ? value.name - : `${value.owner_name} | ${value.name}` - } - disabled={_windows()?.length === 0} - /> -
-
- { - if (!v) setCamera.mutate(null); - else if (v.model_id) setCamera.mutate({ ModelID: v.model_id }); - else setCamera.mutate({ DeviceID: v.device_id }); - }} - /> - setMicInput.mutate(v)} - /> - -
- {rawOptions.mode === "instant" && !auth.data ? ( - <> - - Sign In for{" "} - - Instant Mode - - - ) : ( - - )} -
-
- ); + const { rawOptions, setOptions } = useRecordingOptions(); + + const currentRecording = createCurrentRecordingQuery(); + const generalSettings = generalSettingsStore.createQuery(); + + // We do this on focus so the window doesn't get revealed when toggling the setting + const navigate = useNavigate(); + createEventListener(window, "focus", () => { + if (generalSettings.data?.enableNewRecordingFlow === true) + navigate("/new-main"); + }); + + const isRecording = () => !!currentRecording.data; + + const license = createLicenseQuery(); + + createUpdateCheck(); + + const auth = authStore.createQuery(); + + onMount(async () => { + const auth = await authStore.get(); + const userId = auth?.user_id; + if (!userId) return; + + const trackedSession = localStorage.getItem("tracked_signin_session"); + + if (trackedSession !== userId) { + console.log("New auth session detected, tracking sign in event"); + identifyUser(userId); + trackEvent("user_signed_in", { platform: "desktop" }); + localStorage.setItem("tracked_signin_session", userId); + } else { + console.log("Auth session already tracked, skipping sign in event"); + } + }); + + onMount(() => { + // Enforce window size with multiple safeguards + const currentWindow = getCurrentWindow(); + + // Check size when app regains focus + const unlistenFocus = currentWindow.onFocusChanged( + ({ payload: focused }) => { + if (focused) { + const size = getWindowSize(); + + currentWindow.setSize(new LogicalSize(size.width, size.height)); + } + }, + ); + + // Listen for resize events + const unlistenResize = currentWindow.onResized(() => { + const size = getWindowSize(); + + currentWindow.setSize(new LogicalSize(size.width, size.height)); + }); + + onCleanup(async () => { + (await unlistenFocus)?.(); + (await unlistenResize)?.(); + }); + }); + + createEffect(() => { + const size = getWindowSize(); + getCurrentWindow().setSize(new LogicalSize(size.width, size.height)); + }); + + const screens = createQuery(() => listScreens); + const windows = createQuery(() => listWindows); + const cameras = createVideoDevicesQuery(); + const mics = createQuery(() => listAudioDevices); + + // these all avoid suspending + const _screens = () => (screens.isPending ? [] : screens.data); + const _windows = () => (windows.isPending ? [] : windows.data); + const _mics = () => (mics.isPending ? [] : mics.data); + + // these options take the raw config values and combine them with the available options, + // allowing us to define fallbacks if the selected options aren't actually available + const options = { + screen: () => { + let screen; + + if (rawOptions.captureTarget.variant === "screen") { + const screenId = rawOptions.captureTarget.id; + screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; + } else if (rawOptions.captureTarget.variant === "area") { + const screenId = rawOptions.captureTarget.screen; + screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; + } + + return screen; + }, + window: () => { + let win; + + if (rawOptions.captureTarget.variant === "window") { + const windowId = rawOptions.captureTarget.id; + win = _windows()?.find((s) => s.id === windowId) ?? _windows()?.[0]; + } + + return win; + }, + cameraID: () => + cameras.find((c) => { + const { cameraID } = rawOptions; + if (!cameraID) return; + if ("ModelID" in cameraID && c.model_id === cameraID.ModelID) return c; + if ("DeviceID" in cameraID && c.device_id == cameraID.DeviceID) + return c; + }), + micName: () => mics.data?.find((name) => name === rawOptions.micName), + }; + + // if target is window and no windows are available, switch to screen capture + createEffect(() => { + const screen = _screens()?.[0]; + if ( + rawOptions.captureTarget.variant === "window" && + !windows.isPending && + _windows()?.length === 0 && + screen + ) { + setOptions( + "captureTarget", + reconcile({ variant: "screen", id: screen.id }), + ); + } + }); + + const toggleRecording = createMutation(() => ({ + mutationFn: async () => { + if (!isRecording()) { + const capture_target = ((): ScreenCaptureTarget => { + switch (rawOptions.captureTarget.variant) { + case "screen": { + const screen = options.screen(); + if (!screen) + throw new Error( + `No screen found. Number of available screens: ${ + _screens()?.length + }`, + ); + return { variant: "screen", id: screen.id }; + } + case "window": { + const win = options.window(); + if (!win) + throw new Error( + `No window found. Number of available windows: ${ + _windows()?.length + }`, + ); + return { variant: "window", id: win.id }; + } + case "area": { + const screen = options.screen(); + if (!screen) + throw new Error( + `No screen found. Number of available screens: ${ + _screens()?.length + }`, + ); + return { + variant: "area", + bounds: rawOptions.captureTarget.bounds, + screen: screen.id, + }; + } + } + })(); + + await commands.startRecording({ + capture_target, + mode: rawOptions.mode, + capture_system_audio: rawOptions.captureSystemAudio, + }); + } else await commands.stopRecording(); + }, + })); + + const setMicInput = createMutation(() => ({ + mutationFn: async (name: string | null) => { + await commands.setMicInput(name); + setOptions("micName", name); + }, + })); + + const setCamera = createCameraMutation(); + + onMount(() => { + if (rawOptions.cameraID) setCamera.mutate(rawOptions.cameraID); + }); + + return ( +
+ +
+ Settings}> + + + Previous Recordings}> + + + + + + + + + + {import.meta.env.DEV && ( + + )} +
+
+
+
+ + + }> + + { + if (license.data?.type !== "pro") { + await commands.showWindow("Upgrade"); + } + }} + class={cx( + "text-[0.6rem] rounded-lg px-1 py-0.5", + license.data?.type === "pro" + ? "bg-[--blue-400] text-gray-1 dark:text-gray-12" + : "bg-gray-3 cursor-pointer hover:bg-gray-5", + )} + > + {license.data?.type === "commercial" + ? "Commercial" + : license.data?.type === "pro" + ? "Pro" + : "Personal"} + + + +
+ +
+
+ { + if (!area) + setOptions( + "captureTarget", + reconcile({ + variant: "screen", + id: options.screen()?.id ?? -1, + }), + ); + }} + /> +
+
+
+
+ + options={_screens() ?? []} + onChange={(value) => { + if (!value) return; + + trackEvent("display_selected", { + display_id: value.id, + display_name: value.name, + refresh_rate: value.refresh_rate, + }); + + setOptions( + "captureTarget", + reconcile({ variant: "screen", id: value.id }), + ); + }} + value={options.screen() ?? null} + placeholder="Screen" + optionsEmptyText="No screens found" + selected={ + rawOptions.captureTarget.variant === "screen" || + rawOptions.captureTarget.variant === "area" + } + /> + + options={_windows() ?? []} + onChange={(value) => { + if (!value) return; + + trackEvent("window_selected", { + window_id: value.id, + window_name: value.name, + owner_name: value.owner_name, + refresh_rate: value.refresh_rate, + }); + + setOptions( + "captureTarget", + reconcile({ variant: "window", id: value.id }), + ); + }} + value={options.window() ?? null} + placeholder="Window" + optionsEmptyText="No windows found" + selected={rawOptions.captureTarget.variant === "window"} + getName={(value) => + platform() === "windows" + ? value.name + : `${value.owner_name} | ${value.name}` + } + disabled={_windows()?.length === 0} + /> +
+
+ { + if (!v) setCamera.mutate(null); + else if (v.model_id) setCamera.mutate({ ModelID: v.model_id }); + else setCamera.mutate({ DeviceID: v.device_id }); + }} + /> + setMicInput.mutate(v)} + /> + +
+ {rawOptions.mode === "instant" && !auth.data ? ( + <> + + Sign In for{" "} + + Instant Mode + + + ) : ( + + )} +
+
+ ); } function useRequestPermission() { - const queryClient = useQueryClient(); - - async function requestPermission(type: "camera" | "microphone") { - try { - if (type === "camera") { - await commands.resetCameraPermissions(); - } else if (type === "microphone") { - console.log("wowzers"); - await commands.resetMicrophonePermissions(); - } - await commands.requestPermission(type); - await queryClient.refetchQueries(getPermissions); - } catch (error) { - console.error(`Failed to get ${type} permission:`, error); - } - } - - return requestPermission; + const queryClient = useQueryClient(); + + async function requestPermission(type: "camera" | "microphone") { + try { + if (type === "camera") { + await commands.resetCameraPermissions(); + } else if (type === "microphone") { + await commands.resetMicrophonePermissions(); + } + await commands.requestPermission(type); + await queryClient.refetchQueries(getPermissions); + } catch (error) { + console.error(`Failed to get ${type} permission:`, error); + } + } + + return requestPermission; } +import { createEventListener } from "@solid-primitives/event-listener"; import { makePersisted } from "@solid-primitives/storage"; import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { - getCurrentWebviewWindow, - WebviewWindow, + getCurrentWebviewWindow, + WebviewWindow, } from "@tauri-apps/api/webviewWindow"; import * as dialog from "@tauri-apps/plugin-dialog"; import { type as ostype, platform } from "@tauri-apps/plugin-os"; import * as updater from "@tauri-apps/plugin-updater"; import { Transition } from "solid-transition-group"; - import { SignInButton } from "~/components/SignInButton"; import { authStore, generalSettingsStore } from "~/store"; +import { createTauriEventListener } from "~/utils/createEventListener"; import { apiClient } from "~/utils/web-api"; import { WindowChromeHeader } from "./Context"; import { - RecordingOptionsProvider, - useRecordingOptions, + RecordingOptionsProvider, + useRecordingOptions, } from "./OptionsContext"; -import { createTauriEventListener } from "~/utils/createEventListener"; let hasChecked = false; function createUpdateCheck() { - if (import.meta.env.DEV) return; + if (import.meta.env.DEV) return; - const navigate = useNavigate(); + const navigate = useNavigate(); - onMount(async () => { - if (hasChecked) return; - hasChecked = true; + onMount(async () => { + if (hasChecked) return; + hasChecked = true; - await new Promise((res) => setTimeout(res, 1000)); + await new Promise((res) => setTimeout(res, 1000)); - const update = await updater.check(); - if (!update) return; + const update = await updater.check(); + if (!update) return; - const shouldUpdate = await dialog.confirm( - `Version ${update.version} of Cap is available, would you like to install it?`, - { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" } - ); + const shouldUpdate = await dialog.confirm( + `Version ${update.version} of Cap is available, would you like to install it?`, + { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" }, + ); - if (!shouldUpdate) return; - navigate("/update"); - }); + if (!shouldUpdate) return; + navigate("/update"); + }); } function AreaSelectButton(props: { - targetVariant: "screen" | "area" | "other"; - screen: CaptureScreen | undefined; - onChange(area?: number): void; + targetVariant: "screen" | "area" | "other"; + screen: CaptureScreen | undefined; + onChange(area?: number): void; }) { - const [areaSelection, setAreaSelection] = createStore({ pending: false }); - - async function closeAreaSelection() { - setAreaSelection({ pending: false }); - (await WebviewWindow.getByLabel("capture-area"))?.close(); - } - - createEffect(() => { - if (props.targetVariant === "other") closeAreaSelection(); - }); - - async function handleAreaSelectButtonClick() { - closeAreaSelection(); - if (props.targetVariant === "area") { - trackEvent("crop_area_disabled"); - props.onChange(); - return; - } - - const { screen } = props; - if (!screen) return; - - trackEvent("crop_area_enabled", { - screen_id: screen.id, - screen_name: screen.name, - }); - setAreaSelection({ pending: false }); - commands.showWindow({ - CaptureArea: { screen_id: screen.id }, - }); - } - - onMount(async () => { - const unlistenCaptureAreaWindow = - await getCurrentWebviewWindow().listen( - "cap-window://capture-area/state/pending", - (event) => setAreaSelection("pending", event.payload) - ); - onCleanup(unlistenCaptureAreaWindow); - }); - - return ( - - { - el.animate( - [ - { - transform: "scale(0.5)", - opacity: 0, - width: "0.2rem", - height: "0.2rem", - }, - { - transform: "scale(1)", - opacity: 1, - width: "2rem", - height: "2rem", - }, - ], - { - duration: 450, - easing: "cubic-bezier(0.65, 0, 0.35, 1)", - } - ).finished.then(done); - }} - onExit={(el, done) => - el - .animate( - [ - { - transform: "scale(1)", - opacity: 1, - width: "2rem", - height: "2rem", - }, - { - transform: "scale(0)", - opacity: 0, - width: "0.2rem", - height: "0.2rem", - }, - ], - { - duration: 500, - easing: "ease-in-out", - } - ) - .finished.then(done) - } - > - - {(targetScreenOrArea) => ( - - )} - - - - ); + const [areaSelection, setAreaSelection] = createStore({ pending: false }); + + async function closeAreaSelection() { + setAreaSelection({ pending: false }); + (await WebviewWindow.getByLabel("capture-area"))?.close(); + } + + createEffect(() => { + if (props.targetVariant === "other") closeAreaSelection(); + }); + + async function handleAreaSelectButtonClick() { + closeAreaSelection(); + if (props.targetVariant === "area") { + trackEvent("crop_area_disabled"); + props.onChange(); + return; + } + + const { screen } = props; + if (!screen) return; + + trackEvent("crop_area_enabled", { + screen_id: screen.id, + screen_name: screen.name, + }); + setAreaSelection({ pending: false }); + commands.showWindow({ + CaptureArea: { screen_id: screen.id }, + }); + } + + onMount(async () => { + const unlistenCaptureAreaWindow = + await getCurrentWebviewWindow().listen( + "cap-window://capture-area/state/pending", + (event) => setAreaSelection("pending", event.payload), + ); + onCleanup(unlistenCaptureAreaWindow); + }); + + return ( + + { + el.animate( + [ + { + transform: "scale(0.5)", + opacity: 0, + width: "0.2rem", + height: "0.2rem", + }, + { + transform: "scale(1)", + opacity: 1, + width: "2rem", + height: "2rem", + }, + ], + { + duration: 450, + easing: "cubic-bezier(0.65, 0, 0.35, 1)", + }, + ).finished.then(done); + }} + onExit={(el, done) => + el + .animate( + [ + { + transform: "scale(1)", + opacity: 1, + width: "2rem", + height: "2rem", + }, + { + transform: "scale(0)", + opacity: 0, + width: "0.2rem", + height: "0.2rem", + }, + ], + { + duration: 500, + easing: "ease-in-out", + }, + ) + .finished.then(done) + } + > + + {(targetScreenOrArea) => ( + + )} + + + + ); } const NO_CAMERA = "No Camera"; function CameraSelect(props: { - disabled?: boolean; - options: CameraInfo[]; - value: CameraInfo | null; - onChange: (cameraInfo: CameraInfo | null) => void; + disabled?: boolean; + options: CameraInfo[]; + value: CameraInfo | null; + onChange: (cameraInfo: CameraInfo | null) => void; }) { - const currentRecording = createCurrentRecordingQuery(); - const permissions = createQuery(() => getPermissions); - const requestPermission = useRequestPermission(); - - const permissionGranted = () => - permissions?.data?.camera === "granted" || - permissions?.data?.camera === "notNeeded"; - - const onChange = (cameraInfo: CameraInfo | null) => { - if (!cameraInfo && !permissionGranted()) return requestPermission("camera"); - - props.onChange(cameraInfo); - - trackEvent("camera_selected", { - camera_name: cameraInfo, - enabled: !!cameraInfo, - }); - }; - - return ( -
- -
- ); + const currentRecording = createCurrentRecordingQuery(); + const permissions = createQuery(() => getPermissions); + const requestPermission = useRequestPermission(); + + const permissionGranted = () => + permissions?.data?.camera === "granted" || + permissions?.data?.camera === "notNeeded"; + + const onChange = (cameraInfo: CameraInfo | null) => { + if (!cameraInfo && !permissionGranted()) return requestPermission("camera"); + + props.onChange(cameraInfo); + + trackEvent("camera_selected", { + camera_name: cameraInfo, + enabled: !!cameraInfo, + }); + }; + + return ( +
+ +
+ ); } const NO_MICROPHONE = "No Microphone"; function MicrophoneSelect(props: { - disabled?: boolean; - options: string[]; - value: string | null; - onChange: (micName: string | null) => void; + disabled?: boolean; + options: string[]; + value: string | null; + onChange: (micName: string | null) => void; }) { - const DB_SCALE = 40; - - const permissions = createQuery(() => getPermissions); - const currentRecording = createCurrentRecordingQuery(); - - const [dbs, setDbs] = createSignal(); - const [isInitialized, setIsInitialized] = createSignal(false); - - const requestPermission = useRequestPermission(); - - const permissionGranted = () => - permissions?.data?.microphone === "granted" || - permissions?.data?.microphone === "notNeeded"; - - type Option = { name: string }; - - const handleMicrophoneChange = async (item: Option | null) => { - if (!props.options) return; - - props.onChange(item ? item.name : null); - if (!item) setDbs(); - - trackEvent("microphone_selected", { - microphone_name: item?.name ?? null, - enabled: !!item, - }); - }; - - createTauriEventListener(events.audioInputLevelChange, (dbs) => { - if (!props.value) setDbs(); - else setDbs(dbs); - }); - - // visual audio level from 0 -> 1 - const audioLevel = () => - Math.pow(1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE, 0.5); - - // Initialize audio input if needed - only once when component mounts - onMount(() => { - if (!props.value || !permissionGranted() || isInitialized()) return; - - setIsInitialized(true); - handleMicrophoneChange({ name: props.value }); - }); - - return ( -
- -
- ); + const DB_SCALE = 40; + + const permissions = createQuery(() => getPermissions); + const currentRecording = createCurrentRecordingQuery(); + + const [dbs, setDbs] = createSignal(); + const [isInitialized, setIsInitialized] = createSignal(false); + + const requestPermission = useRequestPermission(); + + const permissionGranted = () => + permissions?.data?.microphone === "granted" || + permissions?.data?.microphone === "notNeeded"; + + type Option = { name: string }; + + const handleMicrophoneChange = async (item: Option | null) => { + if (!props.options) return; + + props.onChange(item ? item.name : null); + if (!item) setDbs(); + + trackEvent("microphone_selected", { + microphone_name: item?.name ?? null, + enabled: !!item, + }); + }; + + createTauriEventListener(events.audioInputLevelChange, (dbs) => { + if (!props.value) setDbs(); + else setDbs(dbs); + }); + + // visual audio level from 0 -> 1 + const audioLevel = () => + (1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE) ** 0.5; + + // Initialize audio input if needed - only once when component mounts + onMount(() => { + if (!props.value || !permissionGranted() || isInitialized()) return; + + setIsInitialized(true); + handleMicrophoneChange({ name: props.value }); + }); + + return ( +
+ +
+ ); } function SystemAudio() { - const { rawOptions, setOptions } = useRecordingOptions(); - const currentRecording = createCurrentRecordingQuery(); - - return ( - - ); + const { rawOptions, setOptions } = useRecordingOptions(); + const currentRecording = createCurrentRecordingQuery(); + + return ( + + ); } function TargetSelect(props: { - options: Array; - onChange: (value: T) => void; - value: T | null; - selected: boolean; - optionsEmptyText: string; - placeholder: string; - getName?: (value: T) => string; - disabled?: boolean; + options: Array; + onChange: (value: T) => void; + value: T | null; + selected: boolean; + optionsEmptyText: string; + placeholder: string; + getName?: (value: T) => string; + disabled?: boolean; }) { - const value = () => { - const v = props.value; - if (!v) return null; - - const o = props.options.find((o) => o.id === v.id); - if (o) return props.value; - - props.onChange(props.options[0]); - return props.options[0]; - }; - - const getName = (value?: T) => - value ? props.getName?.(value) ?? value.name : props.placeholder; - - return ( - - ); + const value = () => { + const v = props.value; + if (!v) return null; + + const o = props.options.find((o) => o.id === v.id); + if (o) return props.value; + + props.onChange(props.options[0]); + return props.options[0]; + }; + + const getName = (value?: T) => + value ? (props.getName?.(value) ?? value.name) : props.placeholder; + + return ( + + ); } function TargetSelectInfoPill(props: { - value: T | null; - permissionGranted: boolean; - requestPermission: () => void; - onClick: (e: MouseEvent) => void; + value: T | null; + permissionGranted: boolean; + requestPermission: () => void; + onClick: (e: MouseEvent) => void; }) { - return ( - { - if (!props.permissionGranted || props.value === null) return; - - e.stopPropagation(); - }} - onClick={(e) => { - if (!props.permissionGranted) { - props.requestPermission(); - e.stopPropagation(); - return; - } - - props.onClick(e); - }} - > - {!props.permissionGranted - ? "Request Permission" - : props.value !== null - ? "On" - : "Off"} - - ); + return ( + { + if (!props.permissionGranted || props.value === null) return; + + e.stopPropagation(); + }} + onClick={(e) => { + if (!props.permissionGranted) { + props.requestPermission(); + e.stopPropagation(); + return; + } + + props.onClick(e); + }} + > + {!props.permissionGranted + ? "Request Permission" + : props.value !== null + ? "On" + : "Off"} + + ); } function InfoPill( - props: ComponentProps<"button"> & { variant: "blue" | "red" } + props: ComponentProps<"button"> & { variant: "blue" | "red" }, ) { - return ( - - - ); + const [changelogState, setChangelogState] = makePersisted( + createStore({ + hasUpdate: false, + lastOpenedVersion: "", + changelogClicked: false, + }), + { name: "changelogState" }, + ); + + const [currentVersion] = createResource(() => getVersion()); + + const [changelogStatus] = createResource( + () => currentVersion(), + async (version) => { + if (!version) { + return { hasUpdate: false }; + } + const response = await apiClient.desktop.getChangelogStatus({ + query: { version }, + }); + if (response.status === 200) return response.body; + return null; + }, + ); + + const handleChangelogClick = () => { + commands.showWindow({ Settings: { page: "changelog" } }); + getCurrentWindow().hide(); + const version = currentVersion(); + if (version) { + setChangelogState({ + hasUpdate: false, + lastOpenedVersion: version, + changelogClicked: true, + }); + } + }; + + createEffect(() => { + if (changelogStatus.state === "ready" && currentVersion()) { + const hasUpdate = changelogStatus()?.hasUpdate || false; + if ( + hasUpdate === true && + changelogState.lastOpenedVersion !== currentVersion() + ) { + setChangelogState({ + hasUpdate: true, + lastOpenedVersion: currentVersion(), + changelogClicked: false, + }); + } + } + }); + + return ( + + + + ); } diff --git a/apps/desktop/src/routes/(window-chrome)/Context.tsx b/apps/desktop/src/routes/(window-chrome)/Context.tsx index 27240e0cb..f07bbae0a 100644 --- a/apps/desktop/src/routes/(window-chrome)/Context.tsx +++ b/apps/desktop/src/routes/(window-chrome)/Context.tsx @@ -1,41 +1,41 @@ import { createContextProvider } from "@solid-primitives/context"; -import { createSignal, JSX, onCleanup } from "solid-js"; +import { createSignal, type JSX, onCleanup } from "solid-js"; interface WindowChromeState { - hideMaximize?: boolean; - items?: JSX.Element; + hideMaximize?: boolean; + items?: JSX.Element; } export const [WindowChromeContext, useWindowChromeContext] = - createContextProvider(() => { - const [state, setState] = createSignal(); + createContextProvider(() => { + const [state, setState] = createSignal(); - return { state, setState }; - }); + return { state, setState }; + }); export function useWindowChrome(state: WindowChromeState) { - const ctx = useWindowChromeContext(); - if (!ctx) - throw new Error( - "useWindowChrome must be used within a WindowChromeContext" - ); + const ctx = useWindowChromeContext(); + if (!ctx) + throw new Error( + "useWindowChrome must be used within a WindowChromeContext", + ); - ctx.setState?.(state); - onCleanup(() => { - ctx.setState?.(); - }); + ctx.setState?.(state); + onCleanup(() => { + ctx.setState?.(); + }); } export function WindowChromeHeader(props: { - hideMaximize?: boolean; - children?: JSX.Element; + hideMaximize?: boolean; + children?: JSX.Element; }) { - useWindowChrome({ - hideMaximize: props.hideMaximize, - get items() { - return props.children; - }, - }); + useWindowChrome({ + hideMaximize: props.hideMaximize, + get items() { + return props.children; + }, + }); - return null; + return null; } diff --git a/apps/desktop/src/routes/(window-chrome)/OptionsContext.tsx b/apps/desktop/src/routes/(window-chrome)/OptionsContext.tsx index 48619a797..526529f34 100644 --- a/apps/desktop/src/routes/(window-chrome)/OptionsContext.tsx +++ b/apps/desktop/src/routes/(window-chrome)/OptionsContext.tsx @@ -2,17 +2,17 @@ import { createContextProvider } from "@solid-primitives/context"; import { createOptionsQuery } from "~/utils/queries"; const [RecordingOptionsProvider, useRecordingOptionsContext] = - createContextProvider(() => { - return createOptionsQuery(); - }); + createContextProvider(() => { + return createOptionsQuery(); + }); export function useRecordingOptions() { - return ( - useRecordingOptionsContext() ?? - (() => { - throw new Error("useOptions must be used within an OptionsProvider"); - })() - ); + return ( + useRecordingOptionsContext() ?? + (() => { + throw new Error("useOptions must be used within an OptionsProvider"); + })() + ); } export { RecordingOptionsProvider }; diff --git a/apps/desktop/src/routes/(window-chrome)/icons.tsx b/apps/desktop/src/routes/(window-chrome)/icons.tsx index a706c5b3d..5d93e9cca 100644 --- a/apps/desktop/src/routes/(window-chrome)/icons.tsx +++ b/apps/desktop/src/routes/(window-chrome)/icons.tsx @@ -1,22 +1,22 @@ export const IconLucideFlaskConical = (props: { class: string }) => { - return ( - - - - - - - - - - ); -}; \ No newline at end of file + return ( + + + + + + + + + + ); +}; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main.tsx b/apps/desktop/src/routes/(window-chrome)/new-main.tsx new file mode 100644 index 000000000..af5817273 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/new-main.tsx @@ -0,0 +1,752 @@ +import { useNavigate } from "@solidjs/router"; +import { + createMutation, + createQuery, + useQueryClient, +} from "@tanstack/solid-query"; +import { getVersion } from "@tauri-apps/api/app"; +import { + getCurrentWindow, + LogicalSize, + primaryMonitor, + Window, +} from "@tauri-apps/api/window"; +import { cx } from "cva"; +import { + type ComponentProps, + createEffect, + createResource, + createSignal, + onCleanup, + onMount, + Show, +} from "solid-js"; +import { createStore, reconcile } from "solid-js/store"; + +import Tooltip from "~/components/Tooltip"; +import { trackEvent } from "~/utils/analytics"; +import { + createCameraMutation, + createCurrentRecordingQuery, + createLicenseQuery, + getPermissions, + listAudioDevices, + listScreens, + listVideoDevices, + listWindows, +} from "~/utils/queries"; +import { + type CameraInfo, + type CaptureScreen, + commands, + type DeviceOrModelID, + events, + type ScreenCaptureTarget, +} from "~/utils/tauri"; + +function getWindowSize() { + return { + width: 270, + height: 255, + }; +} + +const findCamera = (cameras: CameraInfo[], id: DeviceOrModelID) => { + return cameras.find((c) => { + if (!id) return false; + return "DeviceID" in id + ? id.DeviceID === c.device_id + : id.ModelID === c.model_id; + }); +}; + +export default function () { + const generalSettings = generalSettingsStore.createQuery(); + + // We do this on focus so the window doesn't get revealed when toggling the setting + const navigate = useNavigate(); + createEventListener(window, "focus", () => { + if (generalSettings.data?.enableNewRecordingFlow === false) navigate("/"); + }); + + return ( + + + + ); +} + +function Page() { + const { rawOptions, setOptions } = useRecordingOptions(); + + const license = createLicenseQuery(); + + createUpdateCheck(); + + onMount(async () => { + // Enforce window size with multiple safeguards + const currentWindow = getCurrentWindow(); + + // We resize the window on mount as the user could be switching to the new recording flow + // which has a differently sized window. + const size = getWindowSize(); + currentWindow.setSize(new LogicalSize(size.width, size.height)); + + // Check size when app regains focus + const unlistenFocus = currentWindow.onFocusChanged( + ({ payload: focused }) => { + if (focused) { + const size = getWindowSize(); + + currentWindow.setSize(new LogicalSize(size.width, size.height)); + } + }, + ); + + // Listen for resize events + const unlistenResize = currentWindow.onResized(() => { + const size = getWindowSize(); + + currentWindow.setSize(new LogicalSize(size.width, size.height)); + }); + + onCleanup(async () => { + (await unlistenFocus)?.(); + (await unlistenResize)?.(); + }); + + const monitor = await primaryMonitor(); + if (!monitor) return; + }); + + createEffect(() => { + if (rawOptions.targetMode) commands.openTargetSelectOverlays(); + else commands.closeTargetSelectOverlays(); + }); + + const screens = createQuery(() => listScreens); + const windows = createQuery(() => listWindows); + const cameras = createQuery(() => listVideoDevices); + const mics = createQuery(() => listAudioDevices); + + cameras.promise.then((cameras) => { + if (rawOptions.cameraID && findCamera(cameras, rawOptions.cameraID)) { + setOptions("cameraLabel", null); + } + }); + + mics.promise.then((mics) => { + if (rawOptions.micName && !mics.includes(rawOptions.micName)) { + setOptions("micName", null); + } + }); + + // these options take the raw config values and combine them with the available options, + // allowing us to define fallbacks if the selected options aren't actually available + const options = { + screen: () => { + let screen; + + if (rawOptions.captureTarget.variant === "screen") { + const screenId = rawOptions.captureTarget.id; + screen = + screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + } else if (rawOptions.captureTarget.variant === "area") { + const screenId = rawOptions.captureTarget.screen; + screen = + screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + } + + return screen; + }, + window: () => { + let win; + + if (rawOptions.captureTarget.variant === "window") { + const windowId = rawOptions.captureTarget.id; + win = windows.data?.find((s) => s.id === windowId) ?? windows.data?.[0]; + } + + return win; + }, + camera: () => { + if (!rawOptions.cameraID) return undefined; + return findCamera(cameras.data || [], rawOptions.cameraID); + }, + + micName: () => mics.data?.find((name) => name === rawOptions.micName), + target: (): ScreenCaptureTarget => { + switch (rawOptions.captureTarget.variant) { + case "screen": + return { variant: "screen", id: options.screen()?.id ?? -1 }; + case "window": + return { variant: "window", id: options.window()?.id ?? -1 }; + case "area": + return { + variant: "area", + bounds: rawOptions.captureTarget.bounds, + screen: options.screen()?.id ?? -1, + }; + } + }, + }; + + // if target is window and no windows are available, switch to screen capture + createEffect(() => { + if (options.target().variant === "window" && windows.data?.length === 0) { + setOptions( + "captureTarget", + reconcile({ + variant: "screen", + id: options.screen()?.id ?? -1, + }), + ); + } + }); + + const setMicInput = createMutation(() => ({ + mutationFn: async (name: string | null) => { + await commands.setMicInput(name); + setOptions("micName", name); + }, + })); + + const setCamera = createCameraMutation(); + + onMount(() => { + if (rawOptions.cameraID && "ModelID" in rawOptions.cameraID) + setCamera.mutate({ ModelID: rawOptions.cameraID.ModelID }); + else if (rawOptions.cameraID && "DeviceID" in rawOptions.cameraID) + setCamera.mutate({ DeviceID: rawOptions.cameraID.DeviceID }); + else setCamera.mutate(null); + }); + + return ( +
+ +
+ Settings}> + + + Previous Recordings}> + + + + + + + + + + {import.meta.env.DEV && ( + + )} +
+
+
+ + setOptions("targetMode", (v) => (v === "screen" ? null : "screen")) + } + name="Screen" + /> + + setOptions("targetMode", (v) => (v === "window" ? null : "window")) + } + name="Window" + /> + + setOptions("targetMode", (v) => (v === "area" ? null : "area")) + } + name="Area" + /> +
+ { + if (!c) setCamera.mutate(null); + else if (c.model_id) setCamera.mutate({ ModelID: c.model_id }); + else setCamera.mutate({ DeviceID: c.device_id }); + }} + /> + setMicInput.mutate(v)} + /> + +
+ ); +} + +function useRequestPermission() { + const queryClient = useQueryClient(); + + async function requestPermission(type: "camera" | "microphone") { + try { + if (type === "camera") { + await commands.resetCameraPermissions(); + } else if (type === "microphone") { + await commands.resetMicrophonePermissions(); + } + await commands.requestPermission(type); + await queryClient.refetchQueries(getPermissions); + } catch (error) { + console.error(`Failed to get ${type} permission:`, error); + } + } + + return requestPermission; +} + +import { createEventListener } from "@solid-primitives/event-listener"; +import { makePersisted } from "@solid-primitives/storage"; +import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; +import * as dialog from "@tauri-apps/plugin-dialog"; +import { type as ostype } from "@tauri-apps/plugin-os"; +import * as updater from "@tauri-apps/plugin-updater"; +import type { Component } from "solid-js"; +import { generalSettingsStore } from "~/store"; +import { apiClient } from "~/utils/web-api"; +import { WindowChromeHeader } from "./Context"; +import { + RecordingOptionsProvider, + useRecordingOptions, +} from "./OptionsContext"; + +let hasChecked = false; +function createUpdateCheck() { + if (import.meta.env.DEV) return; + + const navigate = useNavigate(); + + onMount(async () => { + if (hasChecked) return; + hasChecked = true; + + await new Promise((res) => setTimeout(res, 1000)); + + const update = await updater.check(); + if (!update) return; + + const shouldUpdate = await dialog.confirm( + `Version ${update.version} of Cap is available, would you like to install it?`, + { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" }, + ); + + if (!shouldUpdate) return; + navigate("/update"); + }); +} + +const NO_CAMERA = "No Camera"; + +function CameraSelect(props: { + disabled?: boolean; + options: CameraInfo[]; + value: CameraInfo | null; + onChange: (camera: CameraInfo | null) => void; +}) { + const currentRecording = createCurrentRecordingQuery(); + const permissions = createQuery(() => getPermissions); + const requestPermission = useRequestPermission(); + + const permissionGranted = () => + permissions?.data?.camera === "granted" || + permissions?.data?.camera === "notNeeded"; + + const onChange = (cameraLabel: CameraInfo | null) => { + if (!cameraLabel && permissions?.data?.camera !== "granted") + return requestPermission("camera"); + + props.onChange(cameraLabel); + + trackEvent("camera_selected", { + camera_name: cameraLabel?.display_name ?? null, + enabled: !!cameraLabel, + }); + }; + + return ( +
+ +
+ ); +} + +const NO_MICROPHONE = "No Microphone"; + +function MicrophoneSelect(props: { + disabled?: boolean; + options: string[]; + value: string | null; + onChange: (micName: string | null) => void; +}) { + const DB_SCALE = 40; + + const permissions = createQuery(() => getPermissions); + const currentRecording = createCurrentRecordingQuery(); + + const [dbs, setDbs] = createSignal(); + const [isInitialized, setIsInitialized] = createSignal(false); + + const requestPermission = useRequestPermission(); + + const permissionGranted = () => + permissions?.data?.microphone === "granted" || + permissions?.data?.microphone === "notNeeded"; + + type Option = { name: string }; + + const handleMicrophoneChange = async (item: Option | null) => { + if (!props.options) return; + props.onChange(item ? item.name : null); + if (!item) setDbs(); + + trackEvent("microphone_selected", { + microphone_name: item?.name ?? null, + enabled: !!item, + }); + }; + + const result = events.audioInputLevelChange.listen((dbs) => { + if (!props.value) setDbs(); + else setDbs(dbs.payload); + }); + + onCleanup(() => result.then((unsub) => unsub())); + + // visual audio level from 0 -> 1 + const audioLevel = () => + (1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE) ** 0.5; + + // Initialize audio input if needed - only once when component mounts + onMount(() => { + if (!props.value || !permissionGranted() || isInitialized()) return; + + setIsInitialized(true); + }); + + return ( +
+ +
+ ); +} + +function SystemAudio() { + const { rawOptions, setOptions } = useRecordingOptions(); + const currentRecording = createCurrentRecordingQuery(); + + return ( + + ); +} + +function TargetSelectInfoPill(props: { + value: T | null; + permissionGranted: boolean; + requestPermission: () => void; + onClick: (e: MouseEvent) => void; +}) { + return ( + { + if (!props.permissionGranted || props.value === null) return; + + e.stopPropagation(); + }} + onClick={(e) => { + if (!props.permissionGranted) { + props.requestPermission(); + return; + } + + props.onClick(e); + }} + > + {!props.permissionGranted + ? "Request Permission" + : props.value !== null + ? "On" + : "Off"} + + ); +} + +function InfoPill( + props: ComponentProps<"button"> & { variant: "blue" | "red" }, +) { + return ( + + + ); +} + +function TargetTypeButton( + props: { + selected: boolean; + Component: Component>; + name: string; + } & ComponentProps<"div">, +) { + return ( +
+ + {props.name} +
+ ); +} diff --git a/apps/desktop/src/routes/(window-chrome)/settings.tsx b/apps/desktop/src/routes/(window-chrome)/settings.tsx index 42df5e13b..23753fe08 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings.tsx @@ -10,96 +10,96 @@ import { authStore } from "~/store"; import { trackEvent } from "~/utils/analytics"; export default function Settings(props: RouteSectionProps) { - const auth = authStore.createQuery(); - const [version] = createResource(() => getVersion()); + const auth = authStore.createQuery(); + const [version] = createResource(() => getVersion()); - const handleAuth = async () => { - if (auth.data) { - trackEvent("user_signed_out", { platform: "desktop" }); - authStore.set(undefined); - } - }; + const handleAuth = async () => { + if (auth.data) { + trackEvent("user_signed_out", { platform: "desktop" }); + authStore.set(undefined); + } + }; - return ( -
-
- -
- - {(v) =>

v{v()}

} -
- {auth.data ? ( - - ) : ( - Sign In - )} -
-
-
- - {props.children} - -
-
- ); + return ( +
+
+ +
+ + {(v) =>

v{v()}

} +
+ {auth.data ? ( + + ) : ( + Sign In + )} +
+
+
+ + {props.children} + +
+
+ ); } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx b/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx index c5149ee74..30b8eb7c3 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx @@ -1,40 +1,40 @@ import { Toggle } from "~/components/Toggle"; export function Setting(props: { - pro?: boolean; - label: string; - description?: string; - children: any; + pro?: boolean; + label: string; + description?: string; + children: any; }) { - return ( -
-
-
-

{props.label}

-
- {props.description && ( -

{props.description}

- )} -
- {props.children} -
- ); + return ( +
+
+
+

{props.label}

+
+ {props.description && ( +

{props.description}

+ )} +
+ {props.children} +
+ ); } export function ToggleSetting(props: { - pro?: boolean; - label: string; - description?: string; - value: boolean; - onChange(v: boolean): void; + pro?: boolean; + label: string; + description?: string; + value: boolean; + onChange(v: boolean): void; }) { - return ( - - props.onChange(v)} - /> - - ); + return ( + + props.onChange(v)} + /> + + ); } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx b/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx index 527ca51e2..958ae70ef 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx @@ -1,104 +1,104 @@ -import { For, Show, ErrorBoundary, Suspense, onMount } from "solid-js"; -import { SolidMarkdown } from "solid-markdown"; import { createQuery } from "@tanstack/solid-query"; import { cx } from "cva"; +import { ErrorBoundary, For, onMount, Show, Suspense } from "solid-js"; +import { SolidMarkdown } from "solid-markdown"; import { AbsoluteInsetLoader } from "~/components/Loader"; import { apiClient } from "~/utils/web-api"; export default function Page() { - console.log("[Changelog] Component mounted"); + console.log("[Changelog] Component mounted"); - const changelog = createQuery(() => { - console.log("[Changelog] Creating query"); - return { - queryKey: ["changelog"], - queryFn: async () => { - console.log("[Changelog] Executing query function"); - try { - const response = await apiClient.desktop.getChangelogPosts({ - query: { origin: window.location.origin }, - }); + const changelog = createQuery(() => { + console.log("[Changelog] Creating query"); + return { + queryKey: ["changelog"], + queryFn: async () => { + console.log("[Changelog] Executing query function"); + try { + const response = await apiClient.desktop.getChangelogPosts({ + query: { origin: window.location.origin }, + }); - console.log("[Changelog] Response", response); + console.log("[Changelog] Response", response); - if (response.status !== 200) { - console.error("[Changelog] Error status:", response.status); - throw new Error("Failed to fetch changelog"); - } - return response.body; - } catch (error) { - console.error("[Changelog] Error in query:", error); - throw error; - } - }, - }; - }); + if (response.status !== 200) { + console.error("[Changelog] Error status:", response.status); + throw new Error("Failed to fetch changelog"); + } + return response.body; + } catch (error) { + console.error("[Changelog] Error in query:", error); + throw error; + } + }, + }; + }); - onMount(() => { - console.log("[Changelog] Query state:", { - isLoading: changelog.isLoading, - isError: changelog.isError, - error: changelog.error, - data: changelog.data, - }); - }); + onMount(() => { + console.log("[Changelog] Query state:", { + isLoading: changelog.isLoading, + isError: changelog.isError, + error: changelog.error, + data: changelog.data, + }); + }); - let fadeIn = changelog.isLoading; + const fadeIn = changelog.isLoading; - return ( -
-
- }> -
- ( -
- {e.toString()} -
- )} - > - -
-
-
-
-
- ); + return ( + + ); } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 5db9e0d30..aed79541b 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -2,79 +2,104 @@ import { createResource, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; -import { type GeneralSettingsStore } from "~/utils/tauri"; +import type { GeneralSettingsStore } from "~/utils/tauri"; import { ToggleSetting } from "./Setting"; export default function ExperimentalSettings() { - const [store] = createResource(() => generalSettingsStore.get()); + const [store] = createResource(() => generalSettingsStore.get()); - return ( - - {(store) => } - - ); + return ( + + {(store) => } + + ); } function Inner(props: { initialStore: GeneralSettingsStore | null }) { - const [settings, setSettings] = createStore( - props.initialStore ?? { - uploadIndividualFiles: false, - openEditorAfterRecording: false, - hideDockIcon: false, - autoCreateShareableLink: false, - enableNotifications: true, - enableNativeCameraPreview: false, - autoZoomOnClicks: false, - } - ); + const [settings, setSettings] = createStore( + props.initialStore ?? { + uploadIndividualFiles: false, + hideDockIcon: false, + autoCreateShareableLink: false, + enableNotifications: true, + enableNativeCameraPreview: false, + enableNewRecordingFlow: false, + autoZoomOnClicks: false, + }, + ); - const handleChange = async ( - key: K, - value: (typeof settings)[K] - ) => { - console.log(`Handling settings change for ${key}: ${value}`); + const handleChange = async ( + key: K, + value: (typeof settings)[K], + ) => { + console.log(`Handling settings change for ${key}: ${value}`); - setSettings(key as keyof GeneralSettingsStore, value); - generalSettingsStore.set({ [key]: value }); - }; + setSettings(key as keyof GeneralSettingsStore, value); + generalSettingsStore.set({ [key]: value }); + }; - return ( -
-
-
-
-

- Experimental Features -

-

- These features are still in development and may not work as - expected. -

-
-
- handleChange("customCursorCapture", value)} - /> - - handleChange("enableNativeCameraPreview", value) - } - /> - handleChange("autoZoomOnClicks", value)} - /> -
-
-
-
- ); + return ( +
+
+
+
+

+ Experimental Features +

+

+ These features are still in development and may not work as + expected. +

+
+
+

Recording Features

+
+ handleChange("customCursorCapture", value)} + /> + + handleChange("enableNativeCameraPreview", value) + } + /> + { + handleChange("autoZoomOnClicks", value); + // This is bad code, but I just want the UI to not jank and can't seem to find the issue. + setTimeout( + () => window.scrollTo({ top: 0, behavior: "instant" }), + 5, + ); + }} + /> + {import.meta.env.DEV && ( + { + handleChange("enableNewRecordingFlow", value); + // This is bad code, but I just want the UI to not jank and can't seem to find the issue. + setTimeout( + () => window.scrollTo({ top: 0, behavior: "instant" }), + 5, + ); + }} + /> + )} +
+
+
+
+
+ ); } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx index b52fc9ae5..95412ae0a 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx @@ -1,79 +1,79 @@ import { Button } from "@cap/ui-solid"; import { action, useAction, useSubmission } from "@solidjs/router"; -import { createSignal } from "solid-js"; -import { type as ostype } from "@tauri-apps/plugin-os"; import { getVersion } from "@tauri-apps/api/app"; +import { type as ostype } from "@tauri-apps/plugin-os"; +import { createSignal } from "solid-js"; import { apiClient, protectedHeaders } from "~/utils/web-api"; const sendFeedbackAction = action(async (feedback: string) => { - const response = await apiClient.desktop.submitFeedback({ - body: { feedback, os: ostype() as any, version: await getVersion() }, - headers: await protectedHeaders(), - }); + const response = await apiClient.desktop.submitFeedback({ + body: { feedback, os: ostype() as any, version: await getVersion() }, + headers: await protectedHeaders(), + }); - if (response.status !== 200) throw new Error("Failed to submit feedback"); - return response.body; + if (response.status !== 200) throw new Error("Failed to submit feedback"); + return response.body; }); export default function FeedbackTab() { - const [feedback, setFeedback] = createSignal(""); + const [feedback, setFeedback] = createSignal(""); - const submission = useSubmission(sendFeedbackAction); - const sendFeedback = useAction(sendFeedbackAction); + const submission = useSubmission(sendFeedbackAction); + const sendFeedback = useAction(sendFeedbackAction); - return ( -
-
-
-
-

Send Feedback

-

- Help us improve Cap by submitting feedback or reporting bugs. - We'll get right on it. -

-
-
{ - e.preventDefault(); - sendFeedback(feedback()); - }} - > -
-
-