Merge branch 'feat/merge-big-stuff'

This commit is contained in:
Maya 2025-09-03 00:00:50 +08:00
commit 237aa402cf
48 changed files with 2632 additions and 702 deletions

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"inlang.vs-code-extension"
]
}

373
bun.lock
View File

@ -2,59 +2,66 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "VERT",
"name": "vert",
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.4.1",
"@bjorn3/browser_wasi_shim": "^0.4.2",
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@fontsource/azeret-mono": "^5.1.1",
"@fontsource/lexend": "^5.1.2",
"@fontsource/radio-canada-big": "^5.1.1",
"@imagemagick/magick-wasm": "^0.0.34",
"@stripe/stripe-js": "^7.4.0",
"@fontsource/azeret-mono": "^5.2.9",
"@fontsource/lexend": "^5.2.9",
"@fontsource/radio-canada-big": "^5.2.6",
"@imagemagick/magick-wasm": "^0.0.35",
"@stripe/stripe-js": "^7.9.0",
"byte-data": "^19.0.1",
"client-zip": "^2.4.6",
"client-zip": "^2.5.0",
"clsx": "^2.1.1",
"lucide-svelte": "^0.475.0",
"music-metadata": "^11.0.0",
"lucide-svelte": "^0.542.0",
"music-metadata": "^11.8.3",
"overlayscrollbars": "^2.12.0",
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^8.1.0",
"riff-file": "^1.0.3",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2",
"vite-plugin-wasm": "^3.4.1",
"vite-plugin-wasm": "^3.5.0",
},
"devDependencies": {
"@poppanator/sveltekit-svg": "^5.0.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@inlang/paraglide-js": "2.2.0",
"@poppanator/sveltekit-svg": "^5.0.1",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.37.0",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"css-select": "5.1.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"sass": "^1.83.4",
"svelte": "^5.19.0",
"svelte-check": "^4.1.4",
"globals": "^15.15.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"sass": "^1.91.0",
"svelte": "^5.38.6",
"svelte-check": "^4.3.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
"vite": "^5.4.11",
"vite-plugin-top-level-await": "^1.5.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vite": "^5.4.19",
"vite-plugin-top-level-await": "^1.6.0",
},
},
},
"trustedDependencies": [
"@swc/core",
"@parcel/watcher",
],
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@bjorn3/browser_wasi_shim": ["@bjorn3/browser_wasi_shim@0.4.2", "", {}, "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA=="],
"@bjorn3/browser_wasi_shim": ["@bjorn3/browser_wasi_shim@0.4.1", "", {}, "sha512-54kpBQX69TZ8I1zyDC8sziv/zPT1zoIadv3CmdIZNZ5WDF1houMjAzRZ3dwWvhXObiEBjOxXyS8Ja7vA0EfGEQ=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.0", "", {}, "sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
@ -102,21 +109,23 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
"@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
"@ffmpeg/ffmpeg": ["@ffmpeg/ffmpeg@0.12.15", "", { "dependencies": { "@ffmpeg/types": "^0.12.4" } }, "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw=="],
@ -124,11 +133,11 @@
"@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="],
"@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="],
"@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.9", "", {}, "sha512-1qnbVspQPI38qhSTSidWU4bjG5ynWCfkMwfPxahqxejJO/u4yT1FbPqG73s4fDmQSuDQYoA8jfTpoQiod7+fuA=="],
"@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="],
"@fontsource/lexend": ["@fontsource/lexend@5.2.9", "", {}, "sha512-0a5xzwksBilec8Q+QwPvTFKFcXYw31oyf5CthPKd+C5NJZDl1aHw4FcMz9bcRMPhq0LXO69BXI8aVxmj15pzNA=="],
"@fontsource/radio-canada-big": ["@fontsource/radio-canada-big@5.2.5", "", {}, "sha512-cNwWV/zPZuxtmMfoD0/v1AIL0X09hZ6yNMqnTgUw5Jy0rV4bPn32+RhKziU/itO4zgxDKjrKnR7Tb1h0+Khh+Q=="],
"@fontsource/radio-canada-big": ["@fontsource/radio-canada-big@5.2.6", "", {}, "sha512-7ziSRyjIA1uTT5avHTaYHyoQfDiKihCy1wGz+hlKsxkHfpM5G5uVHAhiLcSzIjcMqykMtkyw5shRmgLZgebqTA=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@ -136,21 +145,31 @@
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@imagemagick/magick-wasm": ["@imagemagick/magick-wasm@0.0.34", "", {}, "sha512-vSTe4cfR8U8r2Ityo6s3iRFVK903W4fhur3t/ixc6vLSAJXmqXC9cKXz2NkkDBUVU7ssJTYMiTKAb1S9RY+ZdQ=="],
"@imagemagick/magick-wasm": ["@imagemagick/magick-wasm@0.0.35", "", {}, "sha512-Wh5C15QyOkJb1i37j9Vpi5OhqCoXwZ/v9LBxo+LUAhiBBtBnRK7Nlg98KGHdtiJocBE3C4FJ+rv+BMrFevzwnw=="],
"@inlang/paraglide-js": ["@inlang/paraglide-js@2.2.0", "", { "dependencies": { "@inlang/recommend-sherlock": "0.2.1", "@inlang/sdk": "2.4.9", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "bin": { "paraglide-js": "bin/run.js" } }, "sha512-pkpXu1LanvpcAbvpVPf7PgF11Uq7DliSEBngrcUN36l4ZOOpzn3QBTvVr/tJxvks0O67WseQgiMHet8KH7Oz5A=="],
"@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="],
"@inlang/sdk": ["@inlang/sdk@2.4.9", "", { "dependencies": { "@lix-js/sdk": "0.4.7", "@sinclair/typebox": "^0.31.17", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lix-js/sdk": ["@lix-js/sdk@0.4.7", "", { "dependencies": { "@lix-js/server-protocol-schema": "0.1.1", "dedent": "1.5.1", "human-id": "^4.1.1", "js-sha256": "^0.11.0", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ=="],
"@lix-js/server-protocol-schema": ["@lix-js/server-protocol-schema@0.1.1", "", {}, "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -188,91 +207,103 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@5.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "svelte": ">=5.x", "svgo": ">=3.x", "vite": ">=5.x" } }, "sha512-b7hk55SF0HjTS+xFgMG20hy6W0F/m+yRA/ZWcjnsa391rB3Ys3desCiUyIKQYcfvcyuRiQCPedUJMYgu00VdCA=="],
"@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@5.0.1", "", { "dependencies": { "@rollup/pluginutils": "5.1.4", "svgo": "^3.2.0" }, "peerDependencies": { "svelte": ">=5.x", "vite": ">=5.x || >= 6.x" } }, "sha512-eCOm9Wb1eO9N5N49JNokxgbUZN79QV/fe02cRrrWbDbcAOvGoIFU0YKDZEluIaFTP2zOtOxMeCSSobM4foMShA=="],
"@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.9", "", { "os": "android", "cpu": "arm" }, "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.9", "", { "os": "android", "cpu": "arm64" }, "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.9", "", { "os": "win32", "cpu": "x64" }, "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="],
"@stripe/stripe-js": ["@stripe/stripe-js@7.4.0", "", {}, "sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="],
"@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@stripe/stripe-js": ["@stripe/stripe-js@7.9.0", "", {}, "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.9", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw=="],
"@sveltejs/kit": ["@sveltejs/kit@2.18.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-4DGCGiwNzgnPJySlMe/Qi6rKMK3ntphJaV95BTW+aggaTIAVZ5x3Bp+LURVLMxAEAtWAI5U449NafVxTS+kXbQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.37.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-xgKtpjQ6Ry4mdShd01ht5AODUsW7+K1iValPDq7QX8zI1hWOKREH9GjG8SRCN5tC4K7UXmMhuQam7gbLByVcnw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@4.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.12", "vitefu": "^1.0.3" }, "peerDependencies": { "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="],
"@swc/core": ["@swc/core@1.11.21", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.21", "@swc/core-darwin-x64": "1.11.21", "@swc/core-linux-arm-gnueabihf": "1.11.21", "@swc/core-linux-arm64-gnu": "1.11.21", "@swc/core-linux-arm64-musl": "1.11.21", "@swc/core-linux-x64-gnu": "1.11.21", "@swc/core-linux-x64-musl": "1.11.21", "@swc/core-win32-arm64-msvc": "1.11.21", "@swc/core-win32-ia32-msvc": "1.11.21", "@swc/core-win32-x64-msvc": "1.11.21" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw=="],
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.21", "", { "os": "linux", "cpu": "arm" }, "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.21", "", { "os": "win32", "cpu": "ia32" }, "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.21", "", { "os": "win32", "cpu": "x64" }, "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="],
"@swc/types": ["@swc/types@0.1.24", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng=="],
"@swc/wasm": ["@swc/wasm@1.13.5", "", {}, "sha512-ZBZcxieydxNwgEU9eFAXGMaDb1Xoh+ZkZcUQ27LNJzc2lPSByoL6CSVqnYiaVo+n9JgqbYyHlMq+i7z0wRNTfA=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@ -282,33 +313,37 @@
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.0", "@typescript-eslint/type-utils": "8.26.0", "@typescript-eslint/utils": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.26.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.26.0", "@typescript-eslint/types": "8.26.0", "@typescript-eslint/typescript-estree": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.42.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.26.0", "", { "dependencies": { "@typescript-eslint/types": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0" } }, "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.42.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.42.0", "@typescript-eslint/types": "^8.42.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.26.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.26.0", "@typescript-eslint/utils": "8.26.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0" } }, "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.26.0", "", {}, "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.42.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.26.0", "", { "dependencies": { "@typescript-eslint/types": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.26.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.26.0", "@typescript-eslint/types": "8.26.0", "@typescript-eslint/typescript-estree": "8.26.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.42.0", "", {}, "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.0", "", { "dependencies": { "@typescript-eslint/types": "8.26.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.42.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.42.0", "@typescript-eslint/tsconfig-utils": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.42.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@ -322,7 +357,9 @@
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="],
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -332,11 +369,11 @@
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
"browserslist": ["browserslist@4.25.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg=="],
"byte-data": ["byte-data@19.0.1", "", {}, "sha512-xRvkTvO28wr0+0rErSETHD8Cw+P444Az3/jkTezaMw5R+TTW8ZNXuvPZf9/ZhnSRRvlMnJsVhc+ecYvOMy/MQQ=="],
@ -344,13 +381,13 @@
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001702", "", {}, "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001739", "", {}, "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"client-zip": ["client-zip@2.4.6", "", {}, "sha512-e7t1u14h/yT0A12qBwFsaus8UZZ8+MCaNAEn/z53mrukLq/LFcKX7TkbntAppGu8he2p8pz9vc5NEGE/h4ohlw=="],
"client-zip": ["client-zip@2.5.0", "", {}, "sha512-ydG4nDZesbFurnNq0VVCp/yyomIBh+X/1fZPI/P24zbnG4dtC4tQAfI5uQsomigsUMeiRO2wiTPizLWQh+IAyQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@ -358,27 +395,35 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"comment-json": ["comment-json@4.2.5", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1", "has-own-prop": "^2.0.0", "repeat-string": "^1.6.1" } }, "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@ -386,7 +431,7 @@
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
"devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
@ -402,7 +447,7 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.112", "", {}, "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.212", "", {}, "sha512-gE7ErIzSW+d8jALWMcOIgf+IB6lpfsg6NwOhPVwKzDtN2qcBix47vlin4yzSregYDxTCXOUqAZjVY/Z3naS7ww=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
@ -416,25 +461,27 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="],
"eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="],
"eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
"eslint-config-prettier": ["eslint-config-prettier@10.0.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-plugin-svelte": ["eslint-plugin-svelte@2.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@jridgewell/sourcemap-codec": "^1.4.15", "eslint-compat-utils": "^0.5.1", "esutils": "^2.0.3", "known-css-properties": "^0.35.0", "postcss": "^8.4.38", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", "svelte-eslint-parser": "^0.43.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw=="],
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
"esrap": ["esrap@2.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@ -456,11 +503,13 @@
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-type": ["file-type@19.6.0", "", { "dependencies": { "get-stream": "^9.0.1", "strtok3": "^9.0.1", "token-types": "^6.0.0", "uint8array-extras": "^1.3.0" } }, "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ=="],
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@ -478,8 +527,6 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@ -490,20 +537,22 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ieee754-buffer": ["ieee754-buffer@2.0.0", "", {}, "sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"immutable": ["immutable@5.0.3", "", {}, "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="],
"immutable": ["immutable@5.1.3", "", {}, "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
@ -520,14 +569,14 @@
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"js-sha256": ["js-sha256@0.11.1", "", {}, "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@ -536,20 +585,22 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"known-css-properties": ["known-css-properties@0.35.0", "", {}, "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A=="],
"kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"link": ["link@2.1.1", "", { "bin": { "link": "dist/cli.js" } }, "sha512-NV3AUVYBovJ6eVQcTeRoPnZSxzt2LOijNd+ugEZKRy/XeQlpTRhVRkuDv5kOlXwMAUx30vfUc7asRFb9RT65yg=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@ -558,9 +609,9 @@
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lucide-svelte": ["lucide-svelte@0.475.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-N5+hFTPHaZe9HhqJDxxxODfYuOmI6v+JIowzERcea/uxytN/JZlehVTcINBNp8wMo7l6ov1Jf5srrDbkI/WsJg=="],
"lucide-svelte": ["lucide-svelte@0.542.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-KxqJycY4EWaGy1zk/7sqEcf48j4YaP9zEujYczzrsw5j9b9b5pjAJ1qGFKZvD7T6xz9reSYAfUAd6Bz62TdqGw=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
@ -580,11 +631,11 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"music-metadata": ["music-metadata@11.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "content-type": "^1.0.5", "debug": "^4.4.0", "file-type": "^19.6.0", "link": "^2.1.1", "media-typer": "^1.1.0", "strtok3": "^10.2.1", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ZxppS3UuaV1xdWPDmXf4VX3M5+LdVng3xu1rXbZrZWlvtppdOfYrMCeMGDuOt31URTbkmbwuYaRYddkhY5DZMA=="],
"music-metadata": ["music-metadata@11.8.3", "", { "dependencies": { "@borewit/text-codec": "^0.2.0", "@tokenizer/token": "^0.3.0", "content-type": "^1.0.5", "debug": "^4.4.1", "file-type": "^21.0.0", "media-typer": "^1.1.0", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.1" } }, "sha512-Tgiv4MlCgDb6XzelziB1mmL2xeoHls0KTpCm3Z3qr+LfF4mBEpkuc5vNrc927IT5+S5fv+vzStfI+HYC0igDpA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@ -604,6 +655,10 @@
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"overlayscrollbars": ["overlayscrollbars@2.12.0", "", {}, "sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg=="],
"overlayscrollbars-svelte": ["overlayscrollbars-svelte@0.5.5", "", { "peerDependencies": { "overlayscrollbars": "^2.0.0", "svelte": "^5.0.0" } }, "sha512-+dRW3YZSvFbKi5vDCpnUOHuoPLLSdu0BUVVMYZdmfVghu7XkafDRebG2y91/ImPqj6YDAUsz1rcWVYhCJSS/pQ=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
@ -624,17 +679,15 @@
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"peek-readable": ["peek-readable@6.1.1", "", {}, "sha512-7QmvgRKhxM0E2PGV4ocfROItVode+ELI27n4q+lpufZ+tRKBu/pBP8WOmw9HXn2ui/AUizqtvaVQhcJrOkRqYg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
@ -654,11 +707,11 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.3.3", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw=="],
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@ -668,6 +721,8 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@ -676,15 +731,15 @@
"riff-file": ["riff-file@1.0.3", "", { "dependencies": { "byte-data": "^18.0.3" } }, "sha512-Vv8wwGr0BCks7VMI3Lv0houZee4DaHFjjTT0LMhMJKio2YmLncLeIVpK63ydSverngNk8XQPU3fbeP3bWgSIig=="],
"rollup": ["rollup@4.34.9", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.9", "@rollup/rollup-android-arm64": "4.34.9", "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", "@rollup/rollup-freebsd-arm64": "4.34.9", "@rollup/rollup-freebsd-x64": "4.34.9", "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", "@rollup/rollup-linux-arm-musleabihf": "4.34.9", "@rollup/rollup-linux-arm64-gnu": "4.34.9", "@rollup/rollup-linux-arm64-musl": "4.34.9", "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", "@rollup/rollup-linux-riscv64-gnu": "4.34.9", "@rollup/rollup-linux-s390x-gnu": "4.34.9", "@rollup/rollup-linux-x64-gnu": "4.34.9", "@rollup/rollup-linux-x64-musl": "4.34.9", "@rollup/rollup-win32-arm64-msvc": "4.34.9", "@rollup/rollup-win32-ia32-msvc": "4.34.9", "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ=="],
"rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"sass": ["sass@1.85.1", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag=="],
"sass": ["sass@1.91.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
@ -698,6 +753,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -708,7 +765,7 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strtok3": ["strtok3@10.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^6.1.1" } }, "sha512-Q2dTnW3UXokAvXmXvrvMoUj/me3LyJI76HNHeuGMh2o0As/vzd7eHV3ncLOyvu928vQIDbE7Vf9ldEnC7cwy1w=="],
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
@ -716,9 +773,9 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.22.5", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+2BDWVa/rqr34oM+5HFUSmCCPdBBeNqFv2Xc/SSB8kV4iQhWWBNvU9/nd5Dz3PkAGl7Bm/AndS1vY4fe1MgTKA=="],
"svelte": ["svelte@5.38.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg=="],
"svelte-check": ["svelte-check@4.1.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg=="],
"svelte-check": ["svelte-check@4.3.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg=="],
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
@ -734,26 +791,30 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript-eslint": ["typescript-eslint@8.26.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.26.0", "@typescript-eslint/parser": "8.26.0", "@typescript-eslint/utils": "8.26.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA=="],
"typescript-eslint": ["typescript-eslint@8.42.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/parser": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"utf8-buffer": ["utf8-buffer@1.0.0", "", {}, "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@ -762,13 +823,15 @@
"vert-wasm": ["vert-wasm@0.0.2", "", {}, "sha512-QCkhkATZkWp3OPpIrlcLVd5S4DQXeDEWIPI1eV+Ku+sUVoCM4s9DQbNKtnJ8s4leMW3AD4E7cY/gXB/9TsI3WA=="],
"vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="],
"vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="],
"vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.5.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.10.16", "uuid": "^10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ=="],
"vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.6.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.12.14", "@swc/wasm": "^1.12.14", "uuid": "10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww=="],
"vite-plugin-wasm": ["vite-plugin-wasm@3.4.1", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6" } }, "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA=="],
"vite-plugin-wasm": ["vite-plugin-wasm@3.5.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ=="],
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@ -790,6 +853,8 @@
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -798,8 +863,6 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"file-type/strtok3": ["strtok3@9.1.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.3.1" } }, "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -822,23 +885,25 @@
"svelte-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
"token-types/@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"file-type/strtok3/peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@ -846,7 +911,7 @@
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"tailwindcss/postcss-load-config/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
"tailwindcss/postcss-load-config/yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],

205
messages/en.json Normal file
View File

@ -0,0 +1,205 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"navbar": {
"upload": "Upload",
"convert": "Convert",
"settings": "Settings",
"about": "About",
"toggle_theme": "Toggle theme"
},
"footer": {
"copyright": "© {year} VERT.",
"source_code": "Source code",
"discord_server": "Discord server"
},
"upload": {
"title": "The file converter you'll love.",
"subtitle": "All image, audio, and document processing is done on your device. Videos are converted on our lightning-fast servers. No file size limit, no ads, and completely open source.",
"uploader": {
"text": "Drop or click to {action}",
"convert": "convert",
"jpegify": "jpegify"
},
"cards": {
"title": "VERT supports...",
"images": "Images",
"audio": "Audio",
"documents": "Documents",
"video": "Video",
"video_server_processing": "Server supported",
"local_supported": "Local supported",
"status": {
"text": "<b>Status:</b> {status}",
"ready": "ready",
"not_ready": "not ready"
},
"supported_formats": "Supported formats:"
},
"tooltip": {
"partial_support": "This format can only be converted as {direction}.",
"direction_input": "input (from)",
"direction_output": "output (to)",
"video_server_processing": "Video uploads to a server for processing by default, learn how to set it up locally here."
}
},
"convert": {
"panel": {
"convert_all": "Convert all",
"download_all": "Download all as .zip",
"remove_all": "Remove all files",
"set_all_to": "Set all to",
"na": "N/A"
},
"dropdown": {
"audio": "Audio",
"video": "Video",
"doc": "Document",
"image": "Image",
"placeholder": "Search format"
},
"tooltips": {
"unknown_file": "Unknown file type",
"audio_file": "Audio file",
"video_file": "Video file",
"document_file": "Document file",
"image_file": "Image file",
"convert_file": "Convert this file",
"download_file": "Download this file"
},
"errors": {
"cant_convert": "We can't convert this file.",
"vertd_server": "what are you doing..? you're supposed to run the vertd server!",
"unsupported_format": "Only image, video, audio, and document files are supported",
"vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?"
}
},
"settings": {
"title": "Settings",
"errors": {
"save_failed": "Failed to save settings!"
},
"appearance": {
"title": "Appearance",
"brightness_theme": "Brightness theme",
"brightness_description": "Want a sunny flash-bang, or a quiet lonely night?",
"light": "Light",
"dark": "Dark",
"effect_settings": "Effect settings",
"effect_description": "Would you like fancy effects, or a more static experience?",
"enable": "Enable",
"disable": "Disable"
},
"conversion": {
"title": "Conversion",
"filename_format": "File name format",
"filename_description": "This will determine the name of the file on download, <b>not including the file extension.</b> You can put these following templates in the format, which will be replaced with the relevant information: <b>%name%</b> for the original file name, <b>%extension%</b> for the original file extension, and <b>%date%</b> for a date string of when the file was converted.",
"placeholder": "VERT_%name%",
"quality": "Conversion quality",
"quality_description": "This changes the default output quality of the converted files (in its category). Higher values may result in longer conversion times and file size.",
"quality_video": "This changes the default output quality of the converted video files. Higher values may result in longer conversion times and file size.",
"quality_audio": "Audio (kbps)",
"quality_images": "Image (%)",
"rate": "Sample rate (Hz)"
},
"vertd": {
"title": "Video conversion",
"status": "status:",
"loading": "loading...",
"available": "available, commit id {commitId}",
"unavailable": "unavailable (is the url right?)",
"description": "The <code>vertd</code> project is a server wrapper for FFmpeg. This allows you to convert videos through the convenience of VERT's web interface, while still being able to harness the power of your GPU to do it as quickly as possible.",
"hosting_info": "We host a public instance for your convenience, but it is quite easy to host your own on your PC or server if you know what you are doing. You can download the server binaries [vertd_link]here[/vertd_link] - the process of setting this up will become easier in the future, so stay tuned!",
"instance_url": "Instance URL",
"url_placeholder": "Example: http://localhost:24153",
"conversion_speed": "Conversion speed",
"speed_description": "This describes the tradeoff between speed and quality. Faster speeds will result in lower quality, but will get the job done quicker.",
"speeds": {
"very_slow": "Very Slow",
"slower": "Slower",
"slow": "Slow",
"medium": "Medium",
"fast": "Fast",
"ultra_fast": "Ultra Fast"
}
},
"privacy": {
"title": "Privacy",
"plausible_title": "Plausible analytics",
"plausible_description": "We use [plausible_link]Plausible[/plausible_link], a privacy-focused analytics tool, to gather completely anonymous statistics. All data is anonymized and aggregated, and no identifiable information is ever sent or stored. You can view the analytics [analytics_link]here[/analytics_link] and choose to opt out below.",
"opt_in": "Opt-in",
"opt_out": "Opt-out"
},
"language": {
"title": "Language",
"description": "Select your preferred language for the VERT interface."
}
},
"about": {
"title": "About",
"why": {
"title": "Why VERT?",
"description": "<b>File converters have always disappointed us.</b> They're ugly, riddled with ads, and most importantly; slow. We decided to solve this problem once and for all by making an alternative that solves all those problems, and more.<br/><br/>All non-video files are converted completely on-device; this means that there's no delay between sending and receiving the files from a server, and we never get to snoop on the files you convert.<br/><br/>Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your videos stay on there for an hour if you do not convert them. If you do convert the file, the video will stay on the server for an hour, or until it is downloaded. The file will then be deleted from our server."
},
"sponsors": {
"title": "Sponsors",
"description": "Want to support us? Contact a developer in the [discord_link]Discord[/discord_link] server, or send an email to",
"email_copied": "Email copied to clipboard!"
},
"resources": {
"title": "Resources",
"discord": "Discord",
"source": "Source",
"email": "Email"
},
"donate": {
"title": "Donate to VERT",
"description": "With your support, we can keep maintaining and improving VERT.",
"one_time": "One-time",
"monthly": "Monthly",
"custom": "Custom",
"pay_now": "Pay now",
"donate_amount": "Donate ${amount} USD",
"thank_you": "Thank you for your donation!",
"payment_failed": "Payment failed: {message}{period} You have not been charged.",
"donation_error": "An error occurred while processing your donation. Please try again later.",
"payment_error": "Error fetching payment details. Please try again later."
},
"credits": {
"title": "Credits",
"contact_team": "If you would like to contact the development team, please use the email found on the \"Resources\" card.",
"notable_contributors": "Notable contributors",
"notable_description": "We'd like to thank these people for their major contributions to VERT.",
"github_contributors": "GitHub contributors",
"github_description": "Big [jpegify_link]thanks[/jpegify_link] to all these people for helping out! [github_link]Want to help too?[/github_link]",
"no_contributors": "Seems like no one has contributed yet... [contribute_link]be the first to contribute![/contribute_link]",
"libraries": "Libraries",
"libraries_description": "A big thanks to FFmpeg (audio, video), ImageMagick (images) and Pandoc (documents) for maintaining such excellent libraries for so many years. VERT relies on them to provide you with your conversions.",
"roles": {
"lead_developer": "Lead developer; conversion backend, UI implementation",
"developer": "Developer; UI implementation",
"designer": "Designer; UX, branding, marketing",
"docker_ci": "Maintaining Docker & CI support",
"former_cofounder": "Former co-founder & designer"
}
},
"errors": {
"github_contributors": "Error fetching GitHub contributors"
}
},
"workers": {
"errors": {
"general": "Error converting {file}: {message}",
"magick": "Error in Magick worker, image conversion may not work as expected.",
"ffmpeg": "Error loading ffmpeg, some features may not work.",
"no_audio": "No audio stream found.",
"invalid_rate": "Invalid sample rate specified: {rate}Hz"
}
},
"jpegify": {
"title": "SECRET JPEGIFY!!!",
"subtitle": "(shh... don't tell anyone!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Download",
"delete": "Delete"
}
}

205
messages/es.json Normal file
View File

@ -0,0 +1,205 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"navbar": {
"upload": "Subir",
"convert": "Convertir",
"settings": "Ajustes",
"about": "Acerca de",
"toggle_theme": "Cambiar tema"
},
"footer": {
"copyright": "© {year} VERT.",
"source_code": "Código fuente",
"discord_server": "Servidor de Discord"
},
"upload": {
"title": "El convertidor de archivos que te encantará.",
"subtitle": "Todo el procesamiento de imágenes, audio y documentos es hecho en tu dispositivo. Los vídeos son convertidos en nuestros servidores ultra rápidos. Sin límite de tamaño de archivo, sin anuncios y de código abierto.",
"uploader": {
"text": "Arrastra o haz clic para {action}",
"convert": "convertir",
"jpegify": "jpegificar"
},
"cards": {
"title": "VERT soporta...",
"images": "Imágenes",
"audio": "Audio",
"documents": "Documentos",
"video": "Vídeo",
"video_server_processing": "Soportado por el servidor",
"local_supported": "Soportado localmente",
"status": {
"text": "<b>Estado:</b> {status}",
"ready": "listo",
"not_ready": "no listo"
},
"supported_formats": "Formatos soportados:"
},
"tooltip": {
"partial_support": "Este formato solo se puede convertir a {direction}.",
"direction_input": "entrada (desde)",
"direction_output": "salida (hacia)",
"video_server_processing": "Por defecto, los vídeos se suben a un servidor para ser procesados. Aprende cómo instalarlo localmente aquí."
}
},
"convert": {
"panel": {
"convert_all": "Convertir todo",
"download_all": "Comprimir todo",
"remove_all": "Quitar todos los archivos",
"set_all_to": "Cambiar todos a",
"na": "N/A"
},
"dropdown": {
"audio": "Audio",
"video": "Vídeo",
"doc": "Documento",
"image": "Imagen",
"placeholder": "Buscar formato"
},
"tooltips": {
"unknown_file": "Formato de archivo desconocido",
"audio_file": "Audio",
"video_file": "Vídeo",
"document_file": "Documento",
"image_file": "Imagen",
"convert_file": "Convertir este archivo",
"download_file": "Descargar este archivo"
},
"errors": {
"cant_convert": "No podemos convertir este archivo.",
"vertd_server": "¿Qué estás haciendo..? ¡Debes ejecutar el servidor de vertd!",
"unsupported_format": "Solo aceptamos imágenes, vídeos, audios y documentos.",
"vertd_not_found": "No se encontró la instancia de vertd para iniciar la conversión de vídeos. ¿Estás seguro de que la URL es correcta?"
}
},
"settings": {
"title": "Ajustes",
"errors": {
"save_failed": "¡No se han podido guardar los ajustes!"
},
"appearance": {
"title": "Apariencia",
"brightness_theme": "Tema",
"brightness_description": "¿Prefieres una flash-bang soleada o una silenciosa y solitaria noche?",
"light": "Claro",
"dark": "Oscuro",
"effect_settings": "Efectos",
"effect_description": "¿Prefieres efectos en la interfaz o una experiencia más estática?",
"enable": "Habilitar",
"disable": "Deshabilitar"
},
"conversion": {
"title": "Conversión",
"filename_format": "Formato del nombre de archivo",
"filename_description": "Esto va a determinar el nombre del archivo al ser descargado <b>sin incluir la extensión</b>. Puedes poner las siguientes plantillas en el formato, las cuales serán reemplazadas con la información que les corresponde: <b>%name%</b> para el nombre original, <b>%extension%</b> para la extensión original del archivo y <b>%date%</b> para la fecha de cuando el archivo fue convertido.",
"placeholder": "VERT_%name%",
"quality": "Calidad de la conversión",
"quality_description": "Esto cambia la calidad por defecto de los archivos convertidos (en su categoría). Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.",
"quality_video": "Esto cambia la calidad por defecto de los vídeos convertidos. Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.",
"quality_audio": "Audio (kbps)",
"quality_images": "Imagen (%)",
"rate": "Tasa de muestreo (Hz)"
},
"vertd": {
"title": "Conversión de vídeo",
"status": "estado:",
"loading": "cargando...",
"available": "disponible, id del commit {commitId}",
"unavailable": "no disponible (¿has comprobado la url?)",
"description": "<code>vertd</code> es un proyecto que actúa como un servidor intermediario (\"wrapper\") para FFmpeg. Permite convertir vídeos sin dejar de lado la conveniente interfaz web de VERT y, a la vez, aprovecha la potencia de tu GPU para hacerlo lo más rápido posible.",
"hosting_info": "Alojamos una instancia pública para tu conveniencia, pero es bastante fácil alojar una propia en tu PC o servidor si sabes lo que estás haciendo. Puedes descargar los binarios del servidor [vertd_link]aquí[/vertd_link]. ¡El proceso de instalación será más fácil en el futuro, así que mantente atento!",
"instance_url": "URL de la instancia",
"url_placeholder": "Ejemplo: http://localhost:24153",
"conversion_speed": "Velocidad de conversión",
"speed_description": "Esto describe el equilibrio entre velocidad y calidad. Velocidades más rápidas resultarán en una calidad más baja, pero harán el trabajo más rápido.",
"speeds": {
"very_slow": "Extremadamente lento",
"slower": "Muy lento",
"slow": "Lento",
"medium": "Medio",
"fast": "Rápido",
"ultra_fast": "Súper rápido"
}
},
"privacy": {
"title": "Privacidad",
"plausible_title": "Analíticas de Plausible",
"plausible_description": "Usamos [plausible_link]Plausible[/plausible_link], una herramienta de analíticas orientada a la privacidad para recopilar estadísticas completamente anónimas. Toda la información que recopilamos es anonimizada y agregada, y en ningún momento se envía ni se almacena información que permita identificarte. Puedes ver las estadísticas [analytics_link]aquí[/analytics_link] y excluirte de ellas a continuación:",
"opt_in": "Participar",
"opt_out": "No participar"
},
"language": {
"title": "Lenguaje",
"description": "Selecciona el lenguaje que prefieres usar para la interfaz de VERT."
}
},
"about": {
"title": "Acerca de",
"why": {
"title": "¿Por qué VERT?",
"description": "<b>Los conversores de archivos siempre nos han decepcionado.</b> Son feos, están llenos de anuncios y, lo más importante, son lentos. Decidimos solucionar este problema de una vez por todas creando una alternativa que resuelve todo eso, y más.<br/><br/>Todos los archivos (exceptuando vídeos) se convierten directamente en tu dispositivo; esto significa que no hay demoras por subir o bajar archivos de un servidor, y nunca tenemos acceso a los archivos que conviertes.<br/><br/>Los vídeos se suben a nuestro servidor ultra rápido equipado con una RTX 4000 Ada. Tus vídeos permanecen allí durante una hora si no los conviertes. Si los conviertes, el archivo se guarda durante una hora, o hasta que lo descargues. Luego, el archivo se elimina del servidor."
},
"sponsors": {
"title": "Patrocinadores",
"description": "¿Quieres apoyarnos? Contacta a un desarrollador en el servidor de [discord_link]Discord[/discord_link] o envía un correo a",
"email_copied": "¡Email copiado al portapapeles!"
},
"resources": {
"title": "Recursos",
"discord": "Discord",
"source": "Fuente",
"email": "Email"
},
"donate": {
"title": "Donar a VERT",
"description": "Con tu apoyo, podemos seguir manteniendo y mejorando VERT.",
"one_time": "Una sola vez",
"monthly": "Mensual",
"custom": "Personalizado",
"pay_now": "Pagar ahora",
"donate_amount": "Donar ${amount} USD",
"thank_you": "¡Gracias por tu donación!",
"payment_failed": "Pago fallido: {message}{period} No se ha efectuado ningún cargo.",
"donation_error": "Ha ocurrido un error al procesar tu donación. Por favor, inténtalo de nuevo más tarde.",
"payment_error": "Ha ocurrido un error al obtener los detalles del pago. Por favor, inténtalo de nuevo más tarde."
},
"credits": {
"title": "Créditos",
"contact_team": "Si te gustaría contactar al equipo de desarrollo, por favor usa el email que se encuentra en la tarjeta de \"Recursos\".",
"notable_contributors": "Colaboradores destacados",
"notable_description": "Queremos dar las gracias a las siguientes personas por sus importantes contribuciones a VERT.",
"github_contributors": "Contribuidores de GitHub",
"github_description": "¡Muchas [jpegify_link]gracias[/jpegify_link] a todos los que han contribuido! [github_link]¿Quieres contribuir también?[/github_link]",
"no_contributors": "Parece que nadie ha contribuido todavía... [contribute_link]¡Sé el primero en hacerlo![/contribute_link]",
"libraries": "Librerías",
"libraries_description": "Muchas gracias a FFmpeg (audio, vídeo), ImageMagick (imágenes) y Pandoc (documentos) por mantener librerías excelentes por tantos años. VERT depende de ellas para proporcionar tus conversiones.",
"roles": {
"lead_developer": "Líder de desarrollo; implementación del backend de conversión e interfaz",
"developer": "Desarrollador; implementación de la interfaz",
"designer": "Diseñador; UX, branding y marketing",
"docker_ci": "Mantenimiento del soporte para Docker y CI",
"former_cofounder": "Excofundador; diseñador"
}
},
"errors": {
"github_contributors": "Ocurrió un error mientras se obtenían los contribuidores de GitHub."
}
},
"workers": {
"errors": {
"general": "Ocurrió un error mientras se convertía {file}: {message}",
"magick": "Ocurrió un error en el módulo de Magick, la conversión de imágenes puede que no funcione correctamente.",
"ffmpeg": "No se pudo cargar FFmpeg, algunas funciones podrían no funcionar.",
"no_audio": "No se encontró una pista de audio.",
"invalid_rate": "La tasa de muestreo especificada no es válida: {rate}Hz"
}
},
"jpegify": {
"title": "¡¡¡JPEGIFICADOR SECRETO!!!",
"subtitle": "(shh... ¡no se lo digas a nadie!)",
"button": "¡¡¡JPEGIFICAR {compression}%!!!",
"download": "Descargar",
"delete": "Eliminar"
}
}

View File

@ -12,47 +12,54 @@
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@poppanator/sveltekit-svg": "^5.0.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@poppanator/sveltekit-svg": "^5.0.1",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.37.0",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"css-select": "5.1.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"sass": "^1.83.4",
"svelte": "^5.19.0",
"svelte-check": "^4.1.4",
"globals": "^15.15.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"sass": "^1.91.0",
"svelte": "^5.38.6",
"svelte-check": "^4.3.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
"vite": "^5.4.11",
"vite-plugin-top-level-await": "^1.5.0"
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vite": "^5.4.19",
"vite-plugin-top-level-await": "^1.6.0",
"@inlang/paraglide-js": "2.2.0"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.4.1",
"@bjorn3/browser_wasi_shim": "^0.4.2",
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@fontsource/azeret-mono": "^5.1.1",
"@fontsource/lexend": "^5.1.2",
"@fontsource/radio-canada-big": "^5.1.1",
"@imagemagick/magick-wasm": "^0.0.34",
"@stripe/stripe-js": "^7.4.0",
"@fontsource/azeret-mono": "^5.2.9",
"@fontsource/lexend": "^5.2.9",
"@fontsource/radio-canada-big": "^5.2.6",
"@imagemagick/magick-wasm": "^0.0.35",
"@stripe/stripe-js": "^7.9.0",
"byte-data": "^19.0.1",
"client-zip": "^2.4.6",
"client-zip": "^2.5.0",
"clsx": "^2.1.1",
"lucide-svelte": "^0.475.0",
"music-metadata": "^11.0.0",
"lucide-svelte": "^0.542.0",
"music-metadata": "^11.8.3",
"overlayscrollbars": "^2.12.0",
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^8.1.0",
"riff-file": "^1.0.3",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2",
"vite-plugin-wasm": "^3.4.1"
}
"vite-plugin-wasm": "^3.5.0"
},
"trustedDependencies": [
"@parcel/watcher",
"@swc/core"
]
}

1
project.inlang/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
ff77Td2rnvEqQyzBYT

View File

@ -0,0 +1,15 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": [
"en",
"es"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
}
}

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="%lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

15
src/hooks.server.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
// creating a handle to use the paraglide middleware
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace('%lang%', locale);
}
});
});
export const handle: Handle = paraglideHandle;

6
src/hooks.ts Normal file
View File

@ -0,0 +1,6 @@
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => {
return deLocalizeUrl(request.url).pathname;
};

View File

@ -7,6 +7,7 @@
import ProgressBar from "../visual/ProgressBar.svelte";
import FormatDropdown from "./FormatDropdown.svelte";
import { categories } from "$lib/converters";
import { m } from "$lib/paraglide/messages";
const length = $derived(files.files.length);
const progress = $derived(files.files.filter((f) => f.result).length);
@ -27,7 +28,7 @@
disabled={!files.ready}
>
<RefreshCw size="24" />
<p>Convert all</p>
<p>{m["convert.panel.convert_all"]()}</p>
</button>
<button
class="btn {$effects
@ -37,7 +38,7 @@
onclick={() => files.downloadAll()}
>
<FolderArchiveIcon size="24" />
<p>Download all as .zip</p>
<p>{m["convert.panel.download_all"]()}</p>
</button>
{#if $isMobile}
<button
@ -48,10 +49,13 @@
onclick={() => (files.files = [])}
>
<Trash2Icon size="24" />
<p>Remove all files</p>
<p>{m["convert.panel.remove_all"]()}</p>
</button>
{:else}
<Tooltip text="Remove all files" position="right">
<Tooltip
text={m["convert.panel.remove_all"]()}
position="right"
>
<button
class="btn p-4 {$effects
? ''
@ -66,8 +70,11 @@
</div>
<div class="w-full bg-separator h-0.5 flex md:hidden"></div>
<div class="flex items-center gap-2">
<p class="whitespace-nowrap text-xl">Set all to</p>
{#if files.requiredConverters.length === 1}
<p class="whitespace-nowrap text-xl">
{m["convert.panel.set_all_to"]()}
</p>
<!-- video and audio together still have this dropdown disabled because audio has just ffmpeg (video has vertd & ffmpeg), even tho it can convert between video and audio -->
{#if files.files.length > 0 && files.files.every((f) => JSON.stringify(f.converters) === JSON.stringify(files.files[0].converters))}
<FormatDropdown
onselect={(r) =>
files.files.forEach((f) => {
@ -77,9 +84,10 @@
}
})}
{categories}
dropdownSize={"large"}
/>
{:else}
<Dropdown options={["N/A"]} disabled />
<Dropdown options={[m["convert.panel.na"]()]} disabled />
{/if}
</div>
</div>

View File

@ -101,7 +101,7 @@
{/each}
</div>
<ChevronDown
class="w-4 h-4 ml-4 mt-0.5 flex-shrink-0"
class="w-4 h-4 ml-3 mt-0.5 flex-shrink-0"
style="transform: rotate({open
? 180
: 0}deg); transition: transform {duration}ms {transition};"

View File

@ -7,6 +7,8 @@
extension?: string;
prefix?: string;
type?: string;
min?: number;
max?: number;
};
let {
@ -17,29 +19,34 @@
extension,
prefix,
type = "text",
min = 0,
max = 100,
}: Props = $props();
</script>
<div class="relative flex w-full {className}">
<input
{type}
{min}
{max}
bind:value
{placeholder}
{disabled}
class="w-full p-3 rounded-lg bg-panel border-2 border-button
{prefix ? 'pl-[2rem]' : 'pl-3'}
{extension ? 'pr-[4rem]' : 'pr-3'}"
{extension ? 'pr-[4rem]' : 'pr-3'}
{disabled && 'opacity-50 cursor-not-allowed'}"
/>
{#if prefix}
<div class="absolute left-0 top-0 bottom-0 flex items-center px-2">
<span class="text-sm text-gray-400 px-2 py-1 rounded"
>{prefix}</span
<span class="text-sm text-gray-400 px-2 py-1 rounded">{prefix}</span
>
</div>
{/if}
{#if extension}
<div class="absolute right-0 top-0 bottom-0 flex items-center px-4">
<span class="text-sm bg-button text-black dynadark:text-white px-2 py-1 rounded"
<span
class="text-sm bg-button text-black dynadark:text-white px-2 py-1 rounded"
>{extension}</span
>
</div>

View File

@ -1,7 +1,9 @@
<script lang="ts">
import { duration, fade, transition } from "$lib/animation";
import { m } from "$lib/paraglide/messages";
import { isMobile, files } from "$lib/store/index.svelte";
import type { Categories } from "$lib/types";
import clsx from "clsx";
import { ChevronDown, SearchIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { quintOut } from "svelte/easing";
@ -12,6 +14,7 @@
selected?: string;
onselect?: (option: string) => void;
disabled?: boolean;
dropdownSize?: "default" | "large" | "small";
};
let {
@ -20,76 +23,127 @@
selected = $bindable(""),
onselect,
disabled,
dropdownSize = "default",
}: Props = $props();
let open = $state(false);
let hover = $state(false);
let dropdown = $state<HTMLDivElement>();
let currentCategory = $state<string | null>();
let searchQuery = $state("");
let dropdownMenu: HTMLElement | undefined = $state();
let rootCategory: string | null = null;
// initialize current category
$effect(() => {
if (!currentCategory) {
if (selected) {
const foundCat = Object.keys(categories).find((cat) =>
categories[cat].formats.includes(selected),
);
currentCategory =
foundCat || Object.keys(categories)[0] || null;
} else {
// find category based on file types
const fileFormats = files.files.map((f) => f.from);
const foundCat = Object.keys(categories).find((cat) =>
fileFormats.some((format) =>
categories[cat].formats.includes(format),
),
);
currentCategory =
foundCat || Object.keys(categories)[0] || null;
}
if (currentCategory) return;
let foundCat: string | undefined;
if (selected) {
foundCat = Object.keys(categories).find((cat) =>
categories[cat].formats.includes(selected),
);
} else {
// find category based on file types
const fileFormats = files.files.map((f) => f.from);
foundCat = Object.keys(categories).find((cat) =>
fileFormats.some((format) =>
categories[cat].formats.includes(format),
),
);
}
currentCategory = foundCat || Object.keys(categories)[0] || null;
rootCategory = currentCategory;
});
// other available categories based on current category (e.g. converting between video and audio)
const availableCategories = $derived.by(() => {
if (!currentCategory) return Object.keys(categories);
if (!rootCategory) return Object.keys(categories);
return Object.keys(categories).filter(
let finalCategories = Object.keys(categories).filter(
(cat) =>
cat === currentCategory ||
categories[cat].canConvertTo?.includes(currentCategory || ""),
cat === rootCategory ||
categories[rootCategory!]?.canConvertTo?.includes(cat),
);
if (from === ".gif") finalCategories.push("video");
return finalCategories;
});
const shouldInclude = (format: string, category: string): boolean => {
// if converting from audio to video, dont show gifs
if (
categories["audio"]?.formats.includes(from ?? "") &&
format === ".gif"
) {
return false;
}
return true;
};
const filteredData = $derived.by(() => {
const normalize = (str: string) => str.replace(/^\./, "").toLowerCase();
// if no query, return formats for current category
if (!searchQuery) {
return {
categories: availableCategories,
formats: currentCategory
? categories[currentCategory].formats
? categories[currentCategory].formats.filter((format) =>
shouldInclude(format, currentCategory!),
)
: [],
};
}
const searchLower = normalize(searchQuery);
// filter categories that have matching formats
// find all categories that have formats matching the search query
const matchingCategories = availableCategories.filter((cat) =>
categories[cat].formats.some((format) =>
format.toLowerCase().includes(searchQuery.toLowerCase()),
categories[cat].formats.some(
(format) =>
normalize(format).includes(searchLower) &&
shouldInclude(format, cat),
),
);
if (matchingCategories.length === 0) {
return {
categories: availableCategories,
formats: [],
};
}
// only show formats from the current category that match the search
const filteredFormats =
currentCategory && categories[currentCategory]
? categories[currentCategory].formats.filter((format) =>
format
.toLowerCase()
.includes(searchQuery.toLowerCase()),
)
: [];
// if current category has no matches, switch to first category that does
const currentCategoryHasMatches =
currentCategory &&
matchingCategories.some((cat) => cat === currentCategory);
if (!currentCategoryHasMatches && matchingCategories.length > 0) {
const newCategory = matchingCategories[0];
currentCategory = newCategory;
}
// return formats only from the current category that match the search
let filteredFormats = currentCategory
? categories[currentCategory].formats.filter(
(format) =>
normalize(format).includes(searchLower) &&
shouldInclude(format, currentCategory!),
)
: [];
// sorting exact match first, then others
filteredFormats = filteredFormats.sort((a, b) => {
const aExact = normalize(a) === searchLower;
const bExact = normalize(b) === searchLower;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
return 0;
});
return {
categories: matchingCategories,
categories:
matchingCategories.length > 0
? matchingCategories
: availableCategories,
formats: filteredFormats,
};
});
@ -97,6 +151,21 @@
const selectOption = (option: string) => {
selected = option;
open = false;
// find the category of this option if it's not in the current category
if (
currentCategory &&
!categories[currentCategory].formats.includes(option)
) {
const formatCategory = Object.keys(categories).find((cat) =>
categories[cat].formats.includes(option),
);
if (formatCategory) {
currentCategory = formatCategory;
}
}
onselect?.(option);
};
@ -106,7 +175,39 @@
};
const handleSearch = (event: Event) => {
searchQuery = (event.target as HTMLInputElement).value;
const query = (event.target as HTMLInputElement).value;
searchQuery = query;
// find which categories have matching formats & switch
if (query) {
const queryLower = query.toLowerCase();
const categoriesWithMatches = availableCategories.filter((cat) =>
categories[cat].formats.some((format) =>
format.toLowerCase().includes(queryLower),
),
);
if (categoriesWithMatches.length > 0) {
const currentHasMatches =
currentCategory &&
categories[currentCategory].formats.some((format) =>
format.toLowerCase().includes(queryLower),
);
if (!currentHasMatches) {
currentCategory = categoriesWithMatches[0];
}
}
}
};
const onEnter = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
if (filteredData.formats.length > 0) {
selectOption(filteredData.formats[0]);
}
}
};
const clickDropdown = () => {
@ -159,12 +260,12 @@
class="relative flex items-center justify-center w-full font-display px-3 py-3.5 bg-button rounded-full overflow-hidden cursor-pointer focus:!outline-none
{disabled ? 'opacity-50 cursor-auto' : 'cursor-pointer'}"
onclick={() => clickDropdown()}
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
{disabled}
>
<!-- <p>{selected}</p> -->
<div class="grid grid-cols-1 grid-rows-1 w-fit flex-grow-0">
<div
class="grid grid-cols-1 grid-rows-1 w-fit flex-grow-0 max-h-[2.5rem] overflow-hidden"
>
{#key selected}
<p
in:fade={{
@ -175,7 +276,7 @@
duration,
easing: quintOut,
}}
class="col-start-1 row-start-1 text-center font-body font-medium"
class="col-start-1 row-start-1 text-center font-body font-medium truncate max-w-[4rem]"
>
{selected}
</p>
@ -183,7 +284,7 @@
{#if currentCategory}
{#each categories[currentCategory].formats as option}
<p
class="col-start-1 row-start-1 invisible pointer-events-none"
class="col-start-1 row-start-1 invisible pointer-events-none truncate max-w-[2.5rem]"
>
{option}
</p>
@ -191,7 +292,7 @@
{/if}
</div>
<ChevronDown
class="w-4 h-4 ml-4 mt-0.5 flex-shrink-0"
class="w-4 h-4 ml-3 mt-0.5 flex-shrink-0"
style="transform: rotate({open
? 180
: 0}deg); transition: transform {duration}ms {transition};"
@ -200,24 +301,31 @@
{#if open}
<div
bind:this={dropdownMenu}
style={hover ? "will-change: opacity, fade, transform" : ""}
transition:fade={{
duration,
easing: quintOut,
}}
class={$isMobile
? "fixed inset-x-0 bottom-0 w-full z-[200] shadow-xl bg-panel-alt shadow-black/25 rounded-t-2xl overflow-hidden"
: "w-[250%] min-w-full shadow-xl bg-panel-alt shadow-black/25 absolute -translate-x-1/2 top-full mt-2 z-50 rounded-2xl overflow-hidden"}
class={clsx(
$isMobile
? "fixed inset-x-0 bottom-0 w-full z-[200] shadow-xl bg-panel-alt shadow-black/25 rounded-t-2xl overflow-hidden"
: "min-w-full shadow-xl bg-panel-alt shadow-black/25 absolute -translate-x-1/2 top-full mt-2 z-50 rounded-2xl overflow-hidden",
!$isMobile && {
"w-[320%]": dropdownSize === "large",
"w-[250%]": dropdownSize === "default",
"w-[150%]": dropdownSize === "small",
},
)}
>
<!-- search box -->
<div class="p-3 w-full">
<div class="relative">
<input
type="text"
placeholder="Search format"
placeholder={m["convert.dropdown.placeholder"]()}
class="flex-grow w-full !pl-11 !pr-3 rounded-lg bg-panel text-foreground"
bind:value={searchQuery}
oninput={handleSearch}
onkeydown={onEnter}
onfocus={() => {}}
id="format-search"
autocomplete="off"
@ -227,38 +335,56 @@
>
<SearchIcon class="w-4 h-4" />
</span>
{#if searchQuery}
<span
class="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-muted"
style="font-size: 0.7rem;"
>
{filteredData.formats.length}
{filteredData.formats.length === 1
? "result"
: "results"}
</span>
{/if}
</div>
</div>
<!-- available categories -->
<div class="flex items-center justify-between">
{#each filteredData.categories as category}
<button
class="flex-grow text-lg hover:text-muted/20 border-b-[1px] pb-2 capitalize {currentCategory ===
category
class="flex-grow text-lg hover:text-muted/20 border-b-[1px] pb-2 capitalize
{currentCategory === category
? 'text-accent border-b-accent'
: 'border-b-separator text-muted'}"
onclick={() => selectCategory(category)}
>
{category}
{(m as any)[`convert.dropdown.${category}`]?.()}
</button>
{/each}
</div>
<!-- available formats -->
<div class="max-h-80 overflow-y-auto grid grid-cols-3 gap-2 p-2">
{#each filteredData.formats as format}
<button
class="w-full p-2 text-center rounded-xl
{format === selected
? 'bg-accent text-black'
: 'hover:bg-panel'}
{format === from ? 'bg-separator' : ''}"
onclick={() => selectOption(format)}
>
{format}
</button>
{/each}
{#if filteredData.formats.length > 0}
{#each filteredData.formats as format}
<button
class="w-full p-2 text-center rounded-xl
{format === selected
? 'bg-accent text-black'
: format === from
? 'bg-separator'
: 'hover:bg-panel'}"
onclick={() => selectOption(format)}
>
{format}
</button>
{/each}
{:else}
<div class="col-span-3 text-center p-4 text-muted">
{searchQuery
? "No formats match your search"
: "No formats available"}
</div>
{/if}
</div>
</div>
{/if}

View File

@ -7,6 +7,7 @@
import { converters } from "$lib/converters";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
type Props = {
class?: string;
@ -98,7 +99,11 @@
<UploadIcon class="w-full h-full text-on-accent" />
</div>
<h2 class="text-center text-2xl font-semibold mt-4">
Drop or click to {jpegify ? "JPEGIFY" : "convert"}
{m["upload.uploader.text"]({
action: jpegify
? m["upload.uploader.jpegify"]()
: m["upload.uploader.convert"]()
})}
</h2>
</Panel>
</button>

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { GITHUB_URL_VERT, DISCORD_URL } from "$lib/consts";
import { m } from "$lib/paraglide/messages";
const items = Object.entries({
//"Privacy policy": "#",
"Source code": GITHUB_URL_VERT,
"Discord server": DISCORD_URL,
});
const items = $derived([
[m["footer.source_code"](), GITHUB_URL_VERT],
[m["footer.discord_server"](), DISCORD_URL],
]);
const year = new Date().getFullYear();
</script>
@ -16,7 +16,7 @@
<div
class="w-full h-full flex items-center justify-center text-muted gap-3 relative"
>
<p>© {year} VERT.</p>
<p>{m["footer.copyright"]({ year })}</p>
{#each items as [name, url] (name)}
<!-- bullet point -->
<p></p>

View File

@ -22,6 +22,7 @@
import Logo from "../../visual/svg/Logo.svelte";
import { beforeNavigate } from "$app/navigation";
import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { m } from "$lib/paraglide/messages";
const items = $derived<
{
@ -33,13 +34,13 @@
}[]
>([
{
name: "Upload",
name: m["navbar.upload"](),
url: "/",
activeMatch: (pathname) => pathname === "/",
icon: UploadIcon,
},
{
name: "Convert",
name: m["navbar.convert"](),
url: "/convert/",
activeMatch: (pathname) =>
pathname === "/convert/" || pathname === "/convert",
@ -47,13 +48,13 @@
badge: files.files.length,
},
{
name: "Settings",
name: m["navbar.settings"](),
url: "/settings/",
activeMatch: (pathname) => pathname.startsWith("/settings"),
icon: SettingsIcon,
},
{
name: "About",
name: m["navbar.about"](),
url: "/about/",
activeMatch: (pathname) => pathname.startsWith("/about"),
icon: InfoIcon,
@ -63,6 +64,7 @@
let links = $state<HTMLAnchorElement[]>([]);
let container = $state<HTMLDivElement>();
let containerRect = $derived(container?.getBoundingClientRect());
let isInitialized = $state(false);
const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));
@ -72,6 +74,16 @@
const isSecretPage = $derived(selectedIndex === -1);
$effect(() => {
if (containerRect && linkRects.length > 0 && links.length > 0) {
setTimeout(() => {
isInitialized = true;
}, 10);
} else {
isInitialized = false;
}
});
beforeNavigate((e) => {
const oldIndex = items.findIndex((i) =>
i.activeMatch(e.from?.url.pathname || ""),
@ -144,7 +156,7 @@
</div>
{/if}
</div>
<p class="font-medium hidden md:flex">
<p class="font-medium hidden md:flex min-w-0">
{item.name}
</p>
</div>
@ -156,7 +168,7 @@
<div bind:this={container}>
<Panel class="max-w-[778px] w-screen h-20 flex items-center gap-3 relative">
{@const linkRect = linkRects.at(selectedIndex) || linkRects[0]}
{#if linkRect}
{#if linkRect && isInitialized}
<div
class="absolute bg-panel-highlight rounded-xl"
style="width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -
@ -180,7 +192,7 @@
{@render link(item, i)}
{/each}
<div class="w-0.5 bg-separator h-full hidden md:flex"></div>
<Tooltip text="Toggle theme" position="right">
<Tooltip text={m["navbar.toggle_theme"]()} position="right">
<button
onclick={() => {
const isDark =

View File

@ -51,7 +51,7 @@
</script>
<div
class="flex items-center justify-between max-w-sm p-4 gap-4 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md"
class="flex items-center justify-between max-w-[100%] md:max-w-md p-4 gap-4 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md"
in:fly={{
duration: durations.enter,
easing: quintOut,
@ -70,7 +70,9 @@
stroke="2"
fill="none"
/>
<p class="text-black font-normal whitespace-pre-wrap">{message}</p>
<p class="text-black font-normal whitespace-pre-wrap break-all">
{message}
</p>
</div>
<button
class="text-gray-600 hover:text-black flex-shrink-0"

View File

@ -1,18 +1,50 @@
<script lang="ts">
import { fade } from "$lib/animation";
interface Props {
children: () => any;
text: string;
className?: string;
position?: "top" | "bottom" | "left" | "right";
}
let { children, text, position = "top" }: Props = $props();
let { children, text, className, position = "top" }: Props = $props();
let showTooltip = $state(false);
let timeout: number = 0;
let triggerElement: HTMLElement;
let tooltipElement = $state<HTMLElement>();
let tooltipPosition = $state({ x: 0, y: 0 });
function show() {
timeout = setTimeout(() => {
if (!triggerElement) return;
const rect = triggerElement.getBoundingClientRect();
switch (position) {
case "top":
tooltipPosition = {
x: rect.left + rect.width / 2,
y: rect.top - 10,
};
break;
case "bottom":
tooltipPosition = {
x: rect.left + rect.width / 2,
y: rect.bottom + 10,
};
break;
case "left":
tooltipPosition = {
x: rect.left - 10,
y: rect.top + rect.height / 2,
};
break;
case "right":
tooltipPosition = {
x: rect.right + 10,
y: rect.top + rect.height / 2,
};
break;
}
showTooltip = true;
}, 500);
}
@ -21,10 +53,23 @@
showTooltip = false;
clearTimeout(timeout);
}
$effect(() => {
if (showTooltip && tooltipElement) {
document.body.appendChild(tooltipElement);
}
return () => {
if (tooltipElement && tooltipElement.parentNode === document.body) {
document.body.removeChild(tooltipElement);
}
};
});
</script>
<div
class="relative inline-block"
<span
bind:this={triggerElement}
class="relative inline-block {className}"
onmouseenter={show}
onmouseleave={hide}
onfocusin={show}
@ -34,26 +79,30 @@
role="tooltip"
>
{@render children()}
{#if showTooltip}
<div
class="tooltip tooltip-{position}"
transition:fade={{
duration: 100,
}}
>
{text}
</div>
{/if}
</div>
</span>
{#if showTooltip}
<span
bind:this={tooltipElement}
class="tooltip tooltip-{position}"
style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
transition:fade={{
duration: 100,
}}
>
{text}
</span>
{/if}
<style>
.tooltip {
--border-size: 1px;
@apply absolute z-10 bg-panel-alt text-foreground border border-stone-400 dynadark:border-white drop-shadow-lg text-xs px-4 py-2 rounded-full whitespace-nowrap pointer-events-none;
@apply fixed bg-panel-alt text-foreground border border-stone-400 dynadark:border-white drop-shadow-lg text-xs rounded-full pointer-events-none z-[999] max-w-xs break-words whitespace-normal;
@apply px-5 py-2.5;
}
.tooltip-top {
@apply bottom-full left-1/2 -translate-x-1/2 mb-3;
transform: translate(-50%, -100%);
}
.tooltip-top::after {
@ -67,7 +116,7 @@
}
.tooltip-bottom {
@apply top-full left-1/2 -translate-x-1/2 mt-3;
transform: translate(-50%, 20%);
}
.tooltip-bottom::after {
@ -81,7 +130,7 @@
}
.tooltip-left {
@apply right-full top-1/2 -translate-y-1/2 mr-3;
transform: translate(-100%, -50%);
}
.tooltip-left::after {
@ -89,7 +138,7 @@
}
.tooltip-right {
@apply left-full top-1/2 -translate-y-1/2 ml-3;
transform: translate(0%, -50%);
}
.tooltip-right::after {

View File

@ -5,8 +5,9 @@ export class FormatInfo {
constructor(
name: string,
public fromSupported: boolean,
public toSupported: boolean,
public fromSupported = true,
public toSupported = true,
public isNative = true,
) {
this.name = name;
if (!this.name.startsWith(".")) {

View File

@ -4,6 +4,34 @@ import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment";
import { error, log } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider";
import { m } from "$lib/paraglide/messages";
import { Settings } from "$lib/sections/settings/index.svelte";
// TODO: differentiate in UI? (not native formats)
const videoFormats = [
"mkv",
"mp4",
"avi",
"mov",
"webm",
"ts",
"mts",
"m2ts",
"wmv",
"mpg",
"mpeg",
"flv",
"f4v",
"vob",
"m4v",
"3gp",
"3g2",
"mxf",
"ogv",
"rm",
"rmvb",
"divx",
];
export class FFmpegConverter extends Converter {
private ffmpeg: FFmpeg = null!;
@ -15,6 +43,7 @@ export class FFmpegConverter extends Converter {
new FormatInfo("wav", true, true),
new FormatInfo("flac", true, true),
new FormatInfo("ogg", true, true),
new FormatInfo("mogg", true, false),
new FormatInfo("oga", true, true),
new FormatInfo("opus", true, true),
new FormatInfo("aac", true, true),
@ -22,9 +51,24 @@ export class FFmpegConverter extends Converter {
new FormatInfo("wma", true, true),
new FormatInfo("amr", true, true),
new FormatInfo("ac3", true, true),
new FormatInfo("alac", true, false),
new FormatInfo("alac", true, true),
new FormatInfo("aiff", true, true),
new FormatInfo("aifc", true, true),
new FormatInfo("aif", true, true),
new FormatInfo("mp1", true, false),
new FormatInfo("mp2", true, true),
new FormatInfo("mpc", true, false), // unknown if it works, can't find sample file but ffmpeg should support i think?
//new FormatInfo("raw", true, false), // usually pcm
new FormatInfo("dsd", true, false), // dsd
new FormatInfo("dsf", true, false), // dsd
new FormatInfo("dff", true, false), // dsd
new FormatInfo("mqa", true, false),
new FormatInfo("au", true, true),
new FormatInfo("caf", true, true),
new FormatInfo("m4b", true, true),
new FormatInfo("voc", true, true),
new FormatInfo("weba", true, true),
...videoFormats.map((f) => new FormatInfo(f, true, true, false)),
];
public readonly reportsProgress = true;
@ -47,41 +91,542 @@ export class FFmpegConverter extends Converter {
})();
} catch (err) {
error(["converters", this.name], `error loading ffmpeg: ${err}`);
addToast(
"error",
`Error loading ffmpeg, some features may not work.`,
);
addToast("error", m["workers.errors.ffmpeg"]());
}
}
public async convert(input: VertFile, to: string): Promise<VertFile> {
if (!to.startsWith(".")) to = `.${to}`;
const ffmpeg = new FFmpeg();
ffmpeg.on("progress", (progress) => {
input.progress = progress.progress * 100;
});
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
let conversionError: string | null = null;
const ffmpeg = await this.setupFFmpeg(input);
// listen for errors during conversion
const errorListener = (l: { message: string }) => {
const msg = l.message;
if (
msg.includes("Specified sample rate") &&
msg.includes("is not supported")
) {
const rate = Settings.instance.settings.ffmpegCustomSampleRate;
conversionError = m["workers.errors.invalid_rate"]({ rate });
} else if (msg.includes("Stream map '0:a:0' matches no streams.")) {
conversionError = m["workers.errors.no_audio"]();
} else if (
msg.includes("Error initializing output stream") ||
msg.includes("Error while opening encoder") ||
msg.includes("Error while opening decoder") ||
(msg.includes("Error") && msg.includes("stream")) ||
msg.includes("Conversion failed!")
) {
// other general errors
if (!conversionError) conversionError = msg;
}
};
ffmpeg.on("log", errorListener);
const buf = new Uint8Array(await input.file.arrayBuffer());
await ffmpeg.writeFile("input", buf);
log(
["converters", this.name],
`wrote ${input.name} to ffmpeg virtual fs`,
);
await ffmpeg.exec(["-i", "input", "output" + to]);
log(["converters", this.name], `executed ffmpeg command`);
const command = await this.buildConversionCommand(ffmpeg, input, to);
log(["converters", this.name], `FFmpeg command: ${command.join(" ")}`);
await ffmpeg.exec(command);
log(["converters", this.name], "executed ffmpeg command");
if (conversionError) {
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
throw new Error(conversionError);
}
const output = (await ffmpeg.readFile(
"output" + to,
)) as unknown as Uint8Array;
if (!output || output.length === 0) {
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
throw new Error("empty file returned");
}
const outputFileName =
input.name.split(".").slice(0, -1).join(".") + to;
log(
["converters", this.name],
`read ${input.name.split(".").slice(0, -1).join(".") + to} from ffmpeg virtual fs`,
`read ${outputFileName} from ffmpeg virtual fs`,
);
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
return new VertFile(new File([output], input.name), to);
const outBuf = new Uint8Array(output).buffer.slice(0);
return new VertFile(new File([outBuf], outputFileName), to);
}
private async setupFFmpeg(input: VertFile): Promise<FFmpeg> {
const ffmpeg = new FFmpeg();
ffmpeg.on("progress", (progress) => {
input.progress = progress.progress * 100;
});
ffmpeg.on("log", (l) => {
log(["converters", this.name], l.message);
});
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
return ffmpeg;
}
private async detectAudioBitrate(ffmpeg: FFmpeg): Promise<number | null> {
const args = [
"-v",
"quiet",
"-select_streams",
"a:0",
"-show_entries",
"stream=bit_rate",
"-of",
"default=noprint_wrappers=1:nokey=1",
"input",
];
try {
let bitrate: number | null = null;
const bitrateListener = (event: { message: string }) => {
if (bitrate !== null) return;
const n = parseInt(event.message.trim(), 10);
if (!n) return;
bitrate = Math.round(n / 1000);
log(
["converters", this.name],
`Detected stream audio bitrate: ${bitrate} kbps`,
);
};
ffmpeg.on("log", bitrateListener);
try {
await ffmpeg.ffprobe.call(ffmpeg, args);
return bitrate;
} finally {
ffmpeg.off("log", bitrateListener);
}
} catch {
return null;
}
}
private async detectAudioSampleRate(
ffmpeg: FFmpeg,
): Promise<number | null> {
const args = [
"-v",
"quiet",
"-select_streams",
"a:0",
"-show_entries",
"stream=sample_rate",
"-of",
"default=noprint_wrappers=1:nokey=1",
"input",
];
try {
let sampleRate: number | null = null;
const sampleRateListener = (event: { message: string }) => {
if (sampleRate !== null) return;
const n = parseInt(event.message.trim(), 10);
if (!n) return;
sampleRate = n;
log(
["converters", this.name],
`Detected stream audio sample rate: ${sampleRate} Hz`,
);
};
ffmpeg.on("log", sampleRateListener);
try {
await ffmpeg.ffprobe.call(ffmpeg, args);
return sampleRate;
} finally {
ffmpeg.off("log", sampleRateListener);
}
} catch {
return null;
}
}
private async buildConversionCommand(
ffmpeg: FFmpeg,
input: VertFile,
to: string,
): Promise<string[]> {
const inputFormat = input.from.slice(1);
const outputFormat = to.slice(1);
const lossless = ["flac", "alac", "wav", "dsd", "dsf", "dff"];
const userSetting = Settings.instance.settings.ffmpegQuality;
const userSampleRate = Settings.instance.settings.ffmpegSampleRate;
const customSampleRate =
Settings.instance.settings.ffmpegCustomSampleRate ?? 44100;
let audioBitrateArgs: string[] = [];
let sampleRateArgs: string[] = [];
const isLosslessToLossy =
lossless.includes(inputFormat) && !lossless.includes(outputFormat);
if (userSetting !== "auto") {
// user's setting
audioBitrateArgs = ["-b:a", `${userSetting}k`];
log(
["converters", this.name],
`using user setting for audio bitrate: ${userSetting}`,
);
} else {
// detect bitrate of original file and use
if (isLosslessToLossy) {
// use safe default
audioBitrateArgs = ["-b:a", "128k"];
log(
["converters", this.name],
`converting from lossless to lossy, using default audio bitrate: 128k`,
);
} else {
const inputBitrate = await this.detectAudioBitrate(ffmpeg);
audioBitrateArgs = inputBitrate
? ["-b:a", `${inputBitrate}k`]
: [];
log(
["converters", this.name],
`using detected audio bitrate: ${inputBitrate}k`,
);
}
}
// sample rate setting
if (userSampleRate !== "auto") {
const rate =
userSampleRate === "custom"
? customSampleRate.toString()
: userSampleRate;
sampleRateArgs = ["-ar", rate];
log(
["converters", this.name],
`using user setting for sample rate: ${rate}`,
);
} else {
// detect sample rate of original file and use
if (isLosslessToLossy) {
// use safe default
sampleRateArgs = ["-ar", "44100"];
log(
["converters", this.name],
`converting from lossless to lossy, using default sample rate: 44100Hz`,
);
} else {
const inputSampleRate =
await this.detectAudioSampleRate(ffmpeg);
sampleRateArgs = inputSampleRate
? ["-ar", inputSampleRate.toString()]
: [];
log(
["converters", this.name],
`using detected audio sample rate: ${inputSampleRate}Hz`,
);
}
}
// video to audio
if (videoFormats.includes(inputFormat)) {
log(
["converters", this.name],
`Converting video ${input.from} to audio ${to}`,
);
return [
"-i",
"input",
"-map",
"0:a:0",
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
];
}
// audio to video
if (videoFormats.includes(outputFormat)) {
log(
["converters", this.name],
`Converting audio ${input.from} to video ${to}`,
);
const hasAlbumArt = await this.extractAlbumArt(ffmpeg);
const codecArgs = toArgs(to);
if (hasAlbumArt) {
log(
["converters", this.name],
"Using album art as video background",
);
return [
"-loop",
"1",
"-i",
"cover.jpg",
"-i",
"input",
"-vf",
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
"-shortest",
"-pix_fmt",
"yuv420p",
"-r",
"1",
...codecArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
];
} else {
log(["converters", this.name], "Using solid color background");
return [
"-f",
"lavfi",
"-i",
"color=c=black:s=512x512:rate=1",
"-i",
"input",
"-shortest",
"-pix_fmt",
"yuv420p",
"-r",
"1",
...codecArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
];
}
}
// audio to audio
log(
["converters", this.name],
`Converting audio ${input.from} to audio ${to}`,
);
const { audio: audioCodec } = getCodecs(to);
return [
"-i",
"input",
"-c:a",
audioCodec,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
];
}
private async extractAlbumArt(ffmpeg: FFmpeg): Promise<boolean> {
// extract using stream mapping (should work for most)
if (
await this.tryExtractAlbumArt(ffmpeg, [
"-i",
"input",
"-map",
"0:1",
"-c:v",
"copy",
"-update",
"1",
"cover.jpg",
])
) {
log(
["converters", this.name],
"Successfully extracted album art from stream 0:1",
);
return true;
}
// fallback: extract without stream mapping (this probably won't happen)
if (
await this.tryExtractAlbumArt(ffmpeg, [
"-i",
"input",
"-an",
"-c:v",
"copy",
"-update",
"1",
"cover.jpg",
])
) {
log(
["converters", this.name],
"Successfully extracted album art (fallback method)",
);
return true;
}
log(
["converters", this.name],
"No album art found, will create solid color background",
);
return false;
}
private async tryExtractAlbumArt(
ffmpeg: FFmpeg,
command: string[],
): Promise<boolean> {
try {
await ffmpeg.exec(command);
const coverData = await ffmpeg.readFile("cover.jpg");
return !!(coverData && (coverData as Uint8Array).length > 0);
} catch {
return false;
}
}
}
// and here i was, thinking i'd be done with ffmpeg after finishing vertd
// but OH NO we just HAD to have someone suggest to allow album art video generation.
//
// i hate you SO much.
// - love, maddie
const toArgs = (ext: string): string[] => {
const codecs = getCodecs(ext);
const args = ["-c:v", codecs.video];
switch (codecs.video) {
case "libx264": {
args.push(
"-preset",
"ultrafast",
"-crf",
"18",
"-tune",
"stillimage",
);
break;
}
case "libvpx": {
args.push("-c:v", "libvpx-vp9");
break;
}
case "mpeg2video": {
// for mpeg, mpg, vob, mxf
if (ext === ".mxf") args.push("-ar", "48000"); // force 48kHz sample rate
break;
}
}
args.push("-c:a", codecs.audio);
if (codecs.audio === "aac") args.push("-strict", "experimental");
if (ext === ".divx") args.unshift("-f", "avi");
if (ext === ".mxf") args.push("-strict", "unofficial");
return args;
};
const getCodecs = (ext: string): { video: string; audio: string } => {
switch (ext) {
// video <-> audio
case ".mp4":
case ".mkv":
case ".mov":
case ".mts":
case ".ts":
case ".m2ts":
case ".flv":
case ".f4v":
case ".m4v":
case ".3gp":
case ".3g2":
return { video: "libx264", audio: "aac" };
case ".wmv":
return { video: "wmv2", audio: "wmav2" };
case ".webm":
case ".ogv":
return {
video: ext === ".webm" ? "libvpx" : "libtheora",
audio: "libvorbis",
};
case ".avi":
case ".divx":
return { video: "mpeg4", audio: "libmp3lame" };
case ".mpg":
case ".mpeg":
case ".vob":
return { video: "mpeg2video", audio: "mp2" };
case ".mxf":
return { video: "mpeg2video", audio: "pcm_s16le" };
// audio
case ".mp3":
return { video: "libx264", audio: "libmp3lame" };
case ".flac":
return { video: "libx264", audio: "flac" };
case ".wav":
return { video: "libx264", audio: "pcm_s16le" };
case ".ogg":
case ".oga":
return { video: "libx264", audio: "libvorbis" };
case ".opus":
return { video: "libx264", audio: "libopus" };
case ".aac":
return { video: "libx264", audio: "aac" };
case ".m4a":
return { video: "libx264", audio: "aac" };
case ".alac":
return { video: "libx264", audio: "alac" };
case ".wma":
return { video: "libx264", audio: "wmav2" };
default:
return { video: "libx264", audio: "aac" };
}
};
export const CONVERSION_BITRATES = [
"auto",
320,
256,
192,
128,
96,
64,
32,
] as const;
export type ConversionBitrate = (typeof CONVERSION_BITRATES)[number];
export const SAMPLE_RATES = [
"auto",
"custom",
"48000",
"44100",
"32000",
"22050",
"16000",
"11025",
"8000",
] as const;
export type SampleRate = (typeof SAMPLE_RATES)[number];

View File

@ -1,4 +1,5 @@
import type { Categories } from "$lib/types";
import type { Converter } from "./converter.svelte";
import { FFmpegConverter } from "./ffmpeg.svelte";
import { PandocConverter } from "./pandoc.svelte";
import { VertdConverter } from "./vertd.svelte";
@ -22,24 +23,40 @@ export function getConverterByFormat(format: string) {
export const categories: Categories = {
image: { formats: [""], canConvertTo: [] },
video: { formats: [""], canConvertTo: [] }, // add "audio" when "nullptr/experimental-audio-to-video" is implemented
audio: { formats: [""], canConvertTo: [] }, // add "video" when "nullptr/experimental-audio-to-video" is implemented
docs: { formats: [""], canConvertTo: [] },
video: { formats: [""], canConvertTo: ["audio"] },
audio: { formats: [""], canConvertTo: ["video"] },
doc: { formats: [""], canConvertTo: [] },
};
categories.audio.formats =
converters
.find((c) => c.name === "ffmpeg")
?.formatStrings((f) => f.toSupported) || [];
?.supportedFormats.filter((f) => f.toSupported && f.isNative)
.map((f) => f.name) || [];
categories.video.formats =
converters
.find((c) => c.name === "vertd")
?.formatStrings((f) => f.toSupported) || [];
?.supportedFormats.filter((f) => f.toSupported && f.isNative)
.map((f) => f.name) || [];
categories.image.formats =
converters
.find((c) => c.name === "imagemagick")
?.formatStrings((f) => f.toSupported) || [];
categories.docs.formats =
categories.doc.formats =
converters
.find((c) => c.name === "pandoc")
?.formatStrings((f) => f.toSupported) || [];
?.supportedFormats.filter((f) => f.toSupported && f.isNative)
.map((f) => f.name) || [];
export const byNative = (format: string) => {
return (a: Converter, b: Converter) => {
const aFormat = a.supportedFormats.find((f) => f.name === format);
const bFormat = b.supportedFormats.find((f) => f.name === format);
if (aFormat && bFormat) {
return aFormat.isNative ? -1 : 1;
}
return 0;
};
};

View File

@ -0,0 +1,141 @@
import { FormatInfo } from "./converter.svelte";
// formats added from maya's somewhat automated testing
// placed into this file to easily differentiate (and also clean up the main magick file)
// some formats also have a comment from what i saw during testing
export const imageFormats = [
// TODO: yaml, json, txt - not sure when these are used (just contains image info seemingly?), probably will remove cause its not very useful for 99.99% of people
new FormatInfo("a", false, true),
new FormatInfo("aai", true, true),
new FormatInfo("ai", false, true),
new FormatInfo("art", false, true),
new FormatInfo("avs", true, true),
new FormatInfo("b", false, true),
new FormatInfo("bgr", false, true),
new FormatInfo("bgra", false, true),
new FormatInfo("bgro", false, true),
new FormatInfo("bmp2", true, true),
new FormatInfo("bmp3", true, true),
new FormatInfo("brf", false, true),
new FormatInfo("cal", false, true),
new FormatInfo("cals", false, true),
new FormatInfo("cin", true, true), // not ideal (made the image more "shadowy"?)
new FormatInfo("cip", false, true),
new FormatInfo("cmyk", false, true),
new FormatInfo("cmyka", false, true),
new FormatInfo("dcx", true, true),
new FormatInfo("dds", true, true),
new FormatInfo("dpx", true, true),
new FormatInfo("dxt1", true, true),
new FormatInfo("dxt5", true, true),
new FormatInfo("epdf", false, true),
new FormatInfo("epi", false, true),
new FormatInfo("eps2", false, true),
new FormatInfo("eps3", false, true),
new FormatInfo("epsf", false, true),
new FormatInfo("epsi", false, true),
new FormatInfo("ept", false, true),
new FormatInfo("ept2", false, true),
new FormatInfo("ept3", false, true),
new FormatInfo("exr", true, true),
new FormatInfo("farbfeld", true, true),
new FormatInfo("fax", true, true), // not ideal (image became super long for some reason)
new FormatInfo("ff", true, true),
new FormatInfo("fit", true, true), // not ideal (grayscale)
new FormatInfo("fits", true, true), // not ideal (grayscale)
new FormatInfo("fl32", true, true),
new FormatInfo("fts", true, true), // not ideal (grayscale)
new FormatInfo("ftxt", false, true),
new FormatInfo("g", false, true),
new FormatInfo("g3", true, true), // not ideal (image became super long for some reason)
new FormatInfo("g4", false, true),
new FormatInfo("gif87", true, true),
new FormatInfo("gray", false, true),
new FormatInfo("graya", false, true),
new FormatInfo("group4", false, true),
new FormatInfo("hrz", true, true),
new FormatInfo("icb", true, true),
new FormatInfo("icon", true, true),
new FormatInfo("info", false, true),
new FormatInfo("ipl", true, true),
new FormatInfo("isobrl", false, true),
new FormatInfo("isobrl6", false, true),
new FormatInfo("j2c", true, true),
new FormatInfo("j2k", true, true),
new FormatInfo("jng", true, true),
new FormatInfo("jp2", true, true),
new FormatInfo("jpc", true, true),
new FormatInfo("jpm", true, true),
new FormatInfo("jps", true, true),
//new FormatInfo("json", false, true),
new FormatInfo("map", false, true),
new FormatInfo("miff", true, true),
new FormatInfo("mng", true, true),
new FormatInfo("mono", false, true),
new FormatInfo("mtv", true, true),
new FormatInfo("o", false, true),
new FormatInfo("otb", true, true), // not ideal (completely black and white - maybe format is like that)
new FormatInfo("pal", false, true),
new FormatInfo("palm", true, true), // not ideal (screwed up colours)
new FormatInfo("pam", true, true),
new FormatInfo("pcd", true, true), // not ideal (turned big, bg orange, and colour just shifted? - maybe format)
new FormatInfo("pcds", true, true), // not ideal (turned big, bg orange, and colour just shifted? - maybe format)
new FormatInfo("pcl", false, true),
new FormatInfo("pct", true, true),
new FormatInfo("pcx", true, true),
new FormatInfo("pdb", true, true), // not ideal (completely black and white - maybe format is like that)
// new FormatInfo("pdf", false, true),
// new FormatInfo("pdfa", false, true),
new FormatInfo("pgx", true, true), // not ideal (grayscale - maybe format is like that)
new FormatInfo("phm", true, true),
new FormatInfo("picon", true, true), // not ideal (smudged out colours - format probably)
new FormatInfo("pict", true, true),
new FormatInfo("pjpeg", true, true),
new FormatInfo("png00", true, true),
new FormatInfo("png24", true, true),
new FormatInfo("png32", true, true),
new FormatInfo("png48", true, true),
new FormatInfo("png64", true, true),
new FormatInfo("png8", true, true),
new FormatInfo("ps", false, true),
new FormatInfo("ps1", false, true),
new FormatInfo("ps2", false, true),
new FormatInfo("ps3", false, true),
new FormatInfo("psb", true, true),
new FormatInfo("ptif", true, true),
new FormatInfo("qoi", true, true),
new FormatInfo("r", false, true),
new FormatInfo("ras", true, true),
new FormatInfo("rgb", false, true),
new FormatInfo("rgba", false, true),
new FormatInfo("rgbo", false, true),
new FormatInfo("rgf", true, true), // not ideal (completely black and white - maybe format is like that)
new FormatInfo("sgi", true, true),
new FormatInfo("six", true, true),
new FormatInfo("sixel", true, true),
new FormatInfo("sparse-color", false, true),
new FormatInfo("strimg", false, true),
new FormatInfo("sun", true, true),
new FormatInfo("svgz", false, true),
new FormatInfo("tga", true, true),
new FormatInfo("tiff64", true, true),
//new FormatInfo("txt", true, true),
new FormatInfo("ubrl", false, true),
new FormatInfo("ubrl6", false, true),
new FormatInfo("uil", false, true),
new FormatInfo("uyvy", false, true),
new FormatInfo("vda", true, true),
new FormatInfo("vicar", true, true), // not ideal (grayscale - maybe format is like that)
new FormatInfo("viff", true, true),
new FormatInfo("vips", true, true),
new FormatInfo("vst", true, true),
new FormatInfo("wbmp", true, true), // not ideal (completely black and white - maybe format is like that)
new FormatInfo("wpg", true, true),
new FormatInfo("xbm", true, true), // not ideal (completely black and white - maybe format is like that)
new FormatInfo("xpm", true, true),
new FormatInfo("xv", true, true),
//new FormatInfo("yaml", false, true),
new FormatInfo("ycbcr", false, true),
new FormatInfo("ycbcra", false, true),
new FormatInfo("yuv", false, true),
];

View File

@ -1,10 +1,13 @@
import { browser } from "$app/environment";
import { error, log } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { addToast } from "$lib/store/ToastProvider";
import type { OmitBetterStrict, WorkerMessage } from "$lib/types";
import { VertFile } from "$lib/types";
import MagickWorker from "$lib/workers/magick?worker&url";
import { Converter, FormatInfo } from "./converter.svelte";
import { imageFormats } from "./magick-automated";
import { Settings } from "$lib/sections/settings/index.svelte";
export class MagickConverter extends Converter {
private worker: Worker = browser
@ -17,33 +20,64 @@ export class MagickConverter extends Converter {
public ready = $state(false);
public supportedFormats = [
// manually tested formats
new FormatInfo("png", true, true),
new FormatInfo("jpeg", true, true),
new FormatInfo("jpg", true, true),
new FormatInfo("webp", true, true),
new FormatInfo("gif", true, true),
new FormatInfo("heic", true, false),
new FormatInfo("svg", true, true),
new FormatInfo("jxl", true, true),
new FormatInfo("avif", true, true),
new FormatInfo("heic", true, false), // seems to be unreliable? HEIC/HEIF is very weird if it will actually work
new FormatInfo("heif", true, false),
// TODO: .ico files can encode multiple images at various
// sizes, bitdepths, etc. we should support that in future
new FormatInfo("ico", true, true),
new FormatInfo("bmp", true, false),
new FormatInfo("cur", true, false),
new FormatInfo("bmp", true, true),
new FormatInfo("cur", true, true),
new FormatInfo("ani", true, false),
new FormatInfo("icns", true, false),
new FormatInfo("nef", true, false),
new FormatInfo("cr2", true, false),
new FormatInfo("hdr", true, true),
new FormatInfo("jpe", true, true),
new FormatInfo("dng", true, false),
new FormatInfo("mat", true, true),
new FormatInfo("pbm", true, true),
new FormatInfo("pfm", true, true),
new FormatInfo("pgm", true, true),
new FormatInfo("pnm", true, true),
new FormatInfo("ppm", false, true),
new FormatInfo("tif", true, true),
new FormatInfo("ppm", true, true),
new FormatInfo("tiff", true, true),
new FormatInfo("jfif", true, true),
new FormatInfo("eps", false, true),
new FormatInfo("psd", true, true),
// raw camera formats
new FormatInfo("arw", true, false),
new FormatInfo("tif", true, true),
new FormatInfo("dng", true, false),
new FormatInfo("xcf", true, false),
new FormatInfo("rw2", true, false),
new FormatInfo("raf", true, false),
new FormatInfo("orf", true, false),
new FormatInfo("pef", true, false),
new FormatInfo("mos", true, false),
new FormatInfo("raw", true, false),
new FormatInfo("dcr", true, false),
new FormatInfo("crw", true, false),
new FormatInfo("cr3", true, false),
new FormatInfo("3fr", true, false),
new FormatInfo("erf", true, false),
new FormatInfo("mrw", true, false),
new FormatInfo("mef", true, false),
new FormatInfo("nrw", true, false),
new FormatInfo("srw", true, false),
new FormatInfo("sr2", true, false),
new FormatInfo("srf", true, false),
// formats added from maya's somewhat automated testing
...imageFormats,
];
public readonly reportsProgress = false;
@ -63,10 +97,7 @@ export class MagickConverter extends Converter {
["converters", this.name],
`error in worker: ${message.error}`,
);
addToast(
"error",
`Error in Magick worker, image conversion may not work as expected.`,
);
addToast("error", m["workers.errors.magick"]());
throw new Error(message.error);
}
};
@ -78,8 +109,33 @@ export class MagickConverter extends Converter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
): Promise<VertFile> {
const compression: number | undefined = args.at(0);
let compression: number | undefined = args.at(0);
if (!compression) {
compression = Settings.instance.settings.magickQuality ?? 100;
log(["converters", this.name], `using user setting for quality: ${compression}%`);
}
log(["converters", this.name], `converting ${input.name} to ${to}`);
// handle converting from SVG manually because magick-wasm doesn't support it
if (input.from === ".svg") {
try {
const blob = await this.svgToImage(input);
const pngFile = new VertFile(
new File([blob], input.name.replace(/\.svg$/i, ".png")),
input.to,
);
if (to === ".png") return pngFile; // if target is png, return it directly
return await this.convert(pngFile, to, ...args); // otherwise, recursively convert png to user's target format
} catch (err) {
error(
["converters", this.name],
`SVG conversion failed: ${err}`,
);
throw err;
}
}
// every other format handled by magick worker
const msg = {
type: "convert",
input: {
@ -138,4 +194,67 @@ export class MagickConverter extends Converter {
}
});
}
private async svgToImage(input: VertFile): Promise<Blob> {
log(["converters", this.name], `converting SVG to image (PNG)`);
const svgText = await input.file.text();
const svgBlob = new Blob([svgText], { type: "image/svg+xml" });
const svgUrl = URL.createObjectURL(svgBlob);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Failed to get canvas context");
const img = new Image();
// try to extract dimensions from SVG, and if not fallback to default
let width = 512;
let height = 512;
const widthMatch = svgText.match(/width=["'](\d+)["']/);
const heightMatch = svgText.match(/height=["'](\d+)["']/);
const viewBoxMatch = svgText.match(
/viewBox=["'][^"']*\s+(\d+)\s+(\d+)["']/,
);
if (widthMatch && heightMatch) {
width = parseInt(widthMatch[1]);
height = parseInt(heightMatch[1]);
} else if (viewBoxMatch) {
width = parseInt(viewBoxMatch[1]);
height = parseInt(viewBoxMatch[2]);
}
return new Promise((resolve, reject) => {
img.onload = () => {
try {
canvas.width = img.naturalWidth || width;
canvas.height = img.naturalHeight || height;
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
URL.revokeObjectURL(svgUrl);
if (blob) {
resolve(blob);
} else {
reject(
new Error("Failed to convert canvas to Blob"),
);
}
}, "image/png");
} catch (err) {
URL.revokeObjectURL(svgUrl);
reject(err);
}
};
img.onerror = () => {
URL.revokeObjectURL(svgUrl);
reject(new Error("Failed to load SVG image"));
};
img.src = svgUrl;
});
}
}

View File

@ -212,6 +212,21 @@ export class VertdConverter extends Converter {
new FormatInfo("mts", true, true),
new FormatInfo("ts", true, true),
new FormatInfo("m2ts", true, true),
new FormatInfo("mpg", true, true),
new FormatInfo("mpeg", true, true),
new FormatInfo("flv", true, true),
new FormatInfo("f4v", true, true),
new FormatInfo("vob", true, true),
new FormatInfo("m4v", true, true),
new FormatInfo("3gp", true, true),
new FormatInfo("3g2", true, true),
new FormatInfo("mxf", true, true),
new FormatInfo("ogv", true, true),
new FormatInfo("rm", true, false),
new FormatInfo("rmvb", true, false),
new FormatInfo("h264", true, true),
new FormatInfo("divx", true, true),
new FormatInfo("swf", true, true),
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -374,6 +374,17 @@ body {
@apply w-full p-3 rounded-lg bg-panel border-2 border-button pl-3 pr-[4rem];
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type="text"]::placeholder {
@apply text-muted font-normal;
}

View File

@ -2,6 +2,8 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { HeartHandshakeIcon } from "lucide-svelte";
import { GITHUB_URL_VERT } from "$lib/consts";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let { mainContribs, notableContribs, ghContribs } = $props();
</script>
@ -51,12 +53,11 @@
<div class="rounded-full bg-blue-300 p-2 inline-block mr-3 w-10 h-10">
<HeartHandshakeIcon color="black" />
</div>
Credits
{m["about.credits.title"]()}
</h2>
<p class="-mt-4 -mb-3 font-black text-lg">
If you would like to contact the development team, please use the email
found on the "Resources" card.
{m["about.credits.contact_team"]()}
</p>
<!-- Main contributors -->
@ -72,11 +73,12 @@
<!-- Notable contributors -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">Notable contributors</h2>
<h2 class="text-base font-bold">
{m["about.credits.notable_contributors"]()}
</h2>
<div class="flex flex-col gap-2">
<p class="text-base text-muted font-normal">
We'd like to thank these people for their major
contributions to VERT.
{m["about.credits.notable_description"]()}
</p>
<div class="flex flex-col gap-2">
{#each notableContribs as contrib}
@ -90,34 +92,26 @@
<!-- GitHub contributors -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">GitHub contributors</h2>
<h2 class="text-base font-bold">
{m["about.credits.github_contributors"]()}
</h2>
{#if ghContribs && ghContribs.length > 0}
<p class="text-base text-muted font-normal">
Big <a
class="text-black dynadark:text-white"
href="/jpegify">thanks</a
>
to all these people for helping out!
<a
class="text-blue-500 font-normal hover:underline"
href={GITHUB_URL_VERT}
target="_blank"
rel="noopener noreferrer"
>
Want to help too?
</a>
{@html link(
["jpegify_link", "github_link"],
m["about.credits.github_description"](),
["/jpegify", GITHUB_URL_VERT],
[false, true],
["text-black dynadark:text-white", "text-blue-500 font-normal hover:underline"]
)}
</p>
{:else}
<p class="text-base text-muted font-normal italic">
Seems like no one has contributed yet...
<a
class="text-blue-500 font-normal hover:underline"
href={GITHUB_URL_VERT}
target="_blank"
rel="noopener noreferrer"
>
be the first to contribute!
</a>
{@html link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
)}
</p>
{/if}
</div>
@ -131,12 +125,9 @@
</div>
{/if}
<h2 class="mt-2 -mb-2">Libraries</h2>
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
A big thanks to FFmpeg (audio, video), Imagemagick (images) and
Pandoc (documents) for maintaining such excellent libraries for
so many years. VERT relies on them to provide you with your
conversions.
{m["about.credits.libraries_description"]()}
</p>
</div>
</div></Panel

View File

@ -23,11 +23,8 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { effects } from "$lib/store/index.svelte";
import { addToast } from "$lib/store/ToastProvider";
import {
loadStripe,
type Stripe,
type StripeElements,
} from "@stripe/stripe-js";
import { loadStripe } from "@stripe/stripe-js/pure";
import { type Stripe, type StripeElements } from "@stripe/stripe-js";
import clsx from "clsx";
import {
CalendarHeartIcon,
@ -35,9 +32,10 @@
HeartIcon,
WalletIcon,
} from "lucide-svelte";
import { onMount, tick } from "svelte";
import { onMount } from "svelte";
import { Elements, PaymentElement } from "svelte-stripe";
import { quintOut } from "svelte/easing";
import { m } from "$lib/paraglide/messages";
let amount = $state(1);
let customAmount = $state("");
@ -66,10 +64,7 @@
if (!res.ok) {
paymentState = "prepay";
addToast(
"error",
"Error fetching payment details. Please try again later.",
);
addToast("error", m["about.donate.payment_error"]());
return;
}
@ -87,10 +82,6 @@
const payDuration = 400;
const transition = "cubic-bezier(0.23, 1, 0.320, 1)";
onMount(async () => {
stripe = await loadStripe(PUB_STRIPE_KEY);
});
const donate = async () => {
if (!stripe || !clientSecret || !elements) return;
@ -98,9 +89,13 @@
const submitResult = await elements.submit();
if (submitResult.error) {
const period = submitResult.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
`Payment failed: ${submitResult.error.message}${submitResult.error.message?.endsWith(".") ? "" : "."} You have not been charged.`,
m["about.donate.payment_failed"]({
message: submitResult.error.message || "",
period,
}),
);
enablePay = true;
return;
@ -116,12 +111,16 @@
});
if (res.error) {
const period = res.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
`Payment failed: ${res.error.message}${res.error.message?.endsWith(".") ? "" : "."} You have not been charged.`,
m["about.donate.payment_failed"]({
message: res.error.message || "",
period,
}),
);
} else {
addToast("success", "Thank you for your donation!");
addToast("success", m["about.donate.thank_you"]());
}
paymentState = "prepay";
@ -140,13 +139,10 @@
if (status) {
switch (status) {
case "succeeded":
addToast("success", "Thank you for your donation!");
addToast("success", m["about.donate.thank_you"]());
break;
default:
addToast(
"error",
"An error occurred while processing your donation. Please try again later.",
);
addToast("error", m["about.donate.donation_error"]());
}
goto("/about");
@ -162,10 +158,10 @@
>
<HeartIcon color="black" />
</div>
Donate to VERT
{m["about.donate.title"]()}
</h2>
<p class="text-base font-normal">
With your support, we can keep maintaining and improving VERT.
{m["about.donate.description"]()}
</p>
</div>
@ -192,7 +188,7 @@
)}
>
<HandCoinsIcon size="24" class="inline-block mr-2" />
One-time
{m["about.donate.one_time"]()}
</button>
<button
@ -207,7 +203,7 @@
)}
>
<CalendarHeartIcon size="24" class="inline-block mr-2" />
Monthly
{m["about.donate.monthly"]()}
</button>
</div>
<div class="grid grid-cols-4 gap-3 w-full">
@ -229,9 +225,10 @@
<div class="flex items-center justify-center">
<FancyInput
bind:value={customAmount}
placeholder="Custom"
placeholder={m["about.donate.custom"]()}
prefix="$"
type="number"
class="h-full"
/>
</div>
</div>
@ -288,7 +285,9 @@
class="btn w-full h-12 bg-accent-red text-black rounded-full mt-4"
onclick={donate}
>
Donate ${amount.toFixed(2)} USD
{m["about.donate.donate_amount"]({
amount: amount.toFixed(2),
})}
</button>
</div>
</div>
@ -304,7 +303,7 @@
class="row-start-1 col-start-1 flex justify-center items-center"
>
<WalletIcon size="24" class="inline-block mr-2" />
Pay now
{m["about.donate.pay_now"]()}
</div>
{/if}
</div>

View File

@ -8,6 +8,7 @@
MailIcon,
MessageCircleMoreIcon,
} from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
</script>
<Panel class="flex flex-col gap-4 p-6">
@ -17,7 +18,7 @@
>
<LinkIcon color="black" />
</div>
Resources
{m["about.resources.title"]()}
</h2>
<div class="flex gap-3">
<a
@ -29,7 +30,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<MessageCircleMoreIcon size="24" class="inline-block mr-2" />
Discord
{m["about.resources.discord"]()}
</a>
<a
href={GITHUB_URL_VERT}
@ -40,7 +41,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<GithubIcon size="24" class="inline-block mr-2" />
Source
{m["about.resources.source"]()}
</a>
<a
href="mailto:{CONTACT_EMAIL}"
@ -51,7 +52,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<MailIcon size="24" class="inline-block mr-2" />
Email
{m["about.resources.email"]()}
</a>
</div>
</Panel>

View File

@ -5,6 +5,8 @@
import { DISCORD_URL } from "$lib/consts";
import { error } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let copied = false;
let timeoutId: number | undefined;
@ -13,7 +15,7 @@
try {
navigator.clipboard.writeText("hello@vert.sh");
copied = true;
addToast("success", "Email copied to clipboard!");
addToast("success", m["about.sponsors.email_copied"]());
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => (copied = false), 2000);
@ -30,7 +32,7 @@
>
<PiggyBankIcon color="black" />
</div>
Sponsors
{m["about.sponsors.title"]()}
</h2>
<div class="mt-2 [&>*]:font-normal h-full flex justify-between flex-col">
<div class="flex gap-3 justify-center text-lg">
@ -43,11 +45,11 @@
</a>
</div>
<p class="text-muted">
Want to support us? Contact a developer in the <a
href={DISCORD_URL}
target="_blank">Discord</a
>
server, or send an email to
{@html link(
"discord_link",
m["about.sponsors.description"](),
DISCORD_URL,
)}
<span class="inline-block mx-[2px] relative top-[2px]">
<button
id="email"

View File

@ -1,6 +1,7 @@
<script lang="ts">
import Panel from "$lib/components/visual/Panel.svelte";
import { MessageCircleQuestionIcon } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
</script>
<Panel class="flex flex-col gap-3 p-6">
@ -10,22 +11,9 @@
>
<MessageCircleQuestionIcon color="black" />
</div>
Why VERT?
{m["about.why.title"]()}
</h2>
<p class="text-lg font-normal">
<b>File converters have always disappointed us.</b> They're ugly,
riddled with ads, and most importantly; slow. We decided to solve this
problem once and for all by making an alternative that solves all those
problems, and more.<br />
<br />
All non-video files are converted completely on-device; this means that there's
no delay between sending and receiving the files from a server, and we never
get to snoop on the files you convert.
<br />
<br />
Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your
videos stay on there for an hour if you do not convert them. If you do convert
the file, the video will stay on the server for an hour, or until it is downloaded.
The file will then be deleted from our server.
{@html m["about.why.description"]()}
</p>
</Panel>

View File

@ -5,6 +5,8 @@
effects,
setEffects,
setTheme,
updateLocale,
availableLocales,
} from "$lib/store/index.svelte";
import {
MoonIcon,
@ -14,6 +16,23 @@
SunIcon,
} from "lucide-svelte";
import { onMount, onDestroy } from "svelte";
import { m } from "$lib/paraglide/messages";
import { getLocale } from "$lib/paraglide/runtime";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
let currentLocale = $state("en");
const getLanguageDisplayName = (locale: string) => {
try {
return availableLocales[locale as keyof typeof availableLocales];
} catch {
return locale.toUpperCase();
}
};
const languageOptions = Object.keys(availableLocales).map((locale) =>
getLanguageDisplayName(locale),
);
let lightElement: HTMLButtonElement;
let darkElement: HTMLButtonElement;
@ -49,6 +68,8 @@
onMount(() => {
effectsUnsubscribe = effects.subscribe(updateEffectsClasses);
themeUnsubscribe = theme.subscribe(updateThemeClasses);
currentLocale = localStorage.getItem("locale") || getLocale();
});
onDestroy(() => {
@ -57,9 +78,20 @@
});
$effect(() => {
updateEffectsClasses($effects);
updateThemeClasses($theme);
});
updateEffectsClasses($effects);
updateThemeClasses($theme);
});
function handleLanguageChange(selectedLanguage: string) {
const selectedLocale = Object.keys(availableLocales).find(
(locale) => getLanguageDisplayName(locale) === selectedLanguage,
);
if (selectedLocale && selectedLocale !== currentLocale) {
currentLocale = selectedLocale;
updateLocale(selectedLocale);
}
}
</script>
<Panel class="flex flex-col gap-8 p-6">
@ -70,14 +102,16 @@
class="inline-block -mt-1 mr-2 bg-accent-purple p-2 rounded-full"
color="black"
/>
Appearance
{m["settings.appearance.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Brightness theme</p>
<p class="text-base font-bold">
{m["settings.appearance.brightness_theme"]()}
</p>
<p class="text-sm text-muted font-normal italic">
Want a sunny flash-bang, or a quiet lonely night?
{m["settings.appearance.brightness_description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -85,29 +119,34 @@
<button
bind:this={lightElement}
onclick={() => setTheme("light")}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<SunIcon size="24" class="inline-block mr-2" />
Light
{m["settings.appearance.light"]()}
</button>
<button
bind:this={darkElement}
onclick={() => setTheme("dark")}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black flex items-center justify-center"
>
<MoonIcon size="24" class="inline-block mr-2" />
Dark
{m["settings.appearance.dark"]()}
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Effect settings</p>
<p class="text-base font-bold">
{m["settings.appearance.effect_settings"]()}
</p>
<p class="text-sm text-muted font-normal italic">
Would you like fancy effects, or a more static
experience?
{m["settings.appearance.effect_description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -115,23 +154,45 @@
<button
bind:this={enableEffectsElement}
onclick={() => setEffects(true)}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
Enable
{m["settings.appearance.enable"]()}
</button>
<button
bind:this={disableEffectsElement}
onclick={() => setEffects(false)}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
Disable
{m["settings.appearance.disable"]()}
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.language.title"]()}
</p>
<p class="text-sm text-muted font-normal italic">
{m["settings.language.description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
<Dropdown
options={languageOptions}
settingsStyle
selected={getLanguageDisplayName(currentLocale)}
onselect={handleLanguageChange}
/>
</div>
</div>
</div>
</div>
</Panel>

View File

@ -3,6 +3,15 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { RefreshCwIcon } from "lucide-svelte";
import type { ISettings } from "./index.svelte";
import {
CONVERSION_BITRATES,
type ConversionBitrate,
SAMPLE_RATES,
type SampleRate,
} from "$lib/converters/ffmpeg.svelte";
import { m } from "$lib/paraglide/messages";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import FancyInput from "$lib/components/functional/FancyInput.svelte";
const { settings }: { settings: ISettings } = $props();
</script>
@ -15,34 +24,96 @@
class="inline-block -mt-1 mr-2 bg-accent p-2 rounded-full"
color="black"
/>
Conversion
{m["settings.conversion.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">File name format</p>
<p class="text-base font-bold">
{m["settings.conversion.filename_format"]()}
</p>
<p class="text-sm text-muted font-normal">
This will determine the name of the file on download, <span
class="font-bold italic"
>not including the file extension.</span
>
You can put these following templates in the format, which
will be replaced with the relevant information:
<span class="font-bold">%name%</span>
for the original file name,
<span class="font-bold">%extension%</span>
for the original file extension, and
<span class="font-bold">%date%</span>
for a date string of when the file was converted.
{@html m["settings.conversion.filename_description"]()}
</p>
</div>
<FancyTextInput
placeholder="VERT_%name%"
bind:value={settings.filenameFormat}
extension=".ext"
extension={".ext"}
type="text"
/>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.conversion.quality"]()}
</p>
<p class="text-sm text-muted font-normal">
{m["settings.conversion.quality_description"]()}
</p>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">
{m["settings.conversion.quality_images"]()}
</p>
<FancyInput
bind:value={
settings.magickQuality as unknown as string
}
type="number"
min={1}
max={100}
placeholder={"100"}
extension={"%"}
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">
{m["settings.conversion.quality_audio"]()}
</p>
<Dropdown
options={CONVERSION_BITRATES.map((b) =>
b.toString(),
)}
selected={settings.ffmpegQuality.toString()}
onselect={(option: string) =>
(settings.ffmpegQuality =
option as ConversionBitrate)}
settingsStyle
/>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">
{m["settings.conversion.rate"]()}
</p>
<Dropdown
options={SAMPLE_RATES.map((r) => r.toString())}
selected={settings.ffmpegSampleRate.toString()}
onselect={(option: string) => {
settings.ffmpegSampleRate =
option as SampleRate;
}}
settingsStyle
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold select-none">&nbsp;&nbsp;</p>
<FancyInput
bind:value={
settings.ffmpegCustomSampleRate as unknown as string
}
type="number"
min={1}
placeholder={"44100"}
extension={"Hz"}
disabled={settings.ffmpegSampleRate !== "custom"}
/>
</div>
</div>
</div>
</div>
</div>
</Panel>
</div></Panel
>

View File

@ -3,6 +3,8 @@
import { ChartColumnIcon, PauseIcon, PlayIcon } from "lucide-svelte";
import type { ISettings } from "./index.svelte";
import { effects } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
const { settings }: { settings: ISettings } = $props();
</script>
@ -15,26 +17,23 @@
class="inline-block -mt-1 mr-2 bg-accent-blue p-2 rounded-full"
color="black"
/>
Privacy
{m["settings.privacy.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Plausible analytics</p>
<p class="text-base font-bold">
{m["settings.privacy.plausible_title"]()}
</p>
<p class="text-sm text-muted font-normal">
We use <a
href="https://plausible.io/privacy-focused-web-analytics"
target="_blank"
rel="noopener noreferrer">Plausible</a
>, a privacy-focused analytics tool, to gather
completely anonymous statistics. All data is anonymized
and aggregated, and no identifiable information is ever
sent or stored. You can view the analytics
<a
href="https://ats.vert.sh/vert.sh"
target="_blank"
rel="noopener noreferrer">here</a
> and choose to opt out below.
{@html link(
["plausible_link", "analytics_link"],
m["settings.privacy.plausible_description"](),
[
"https://plausible.io/privacy-focused-web-analytics",
"https://ats.vert.sh/vert.sh",
],
)}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -48,7 +47,7 @@
: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
Opt-in
{m["settings.privacy.opt_in"]()}
</button>
<button
@ -60,7 +59,7 @@
: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
Opt-out
{m["settings.privacy.opt_out"]()}
</button>
</div>
</div>

View File

@ -6,6 +6,8 @@
import clsx from "clsx";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import { vertdLoaded } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let vertdCommit = $state<string | null>(null);
let abortController: AbortController | null = null;
@ -55,7 +57,7 @@
class="inline-block -mt-1 mr-2 bg-accent-red p-2 rounded-full overflow-visible"
color="black"
/>
Video conversion
{m["settings.vertd.title"]()}
</h2>
<p
class={clsx("text-sm font-normal", {
@ -64,90 +66,79 @@
"!text-muted": vertdCommit === "loading",
})}
>
status: {vertdCommit
{m["settings.vertd.status"]()} {vertdCommit
? vertdCommit === "loading"
? "loading..."
: `available, commit id ${vertdCommit}`
: "unavailable (is the url right?)"}
? m["settings.vertd.loading"]()
: m["settings.vertd.available"]({ commitId: vertdCommit })
: m["settings.vertd.unavailable"]()}
</p>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<p class="text-sm text-muted font-normal">
The <code>vertd</code> project is a server wrapper for FFmpeg.
This allows you to convert videos through the convenience of
VERT's web interface, while still being able to harness the power
of your GPU to do it as quickly as possible.
{@html m["settings.vertd.description"]()}
</p>
<p class="text-sm text-muted font-normal">
We host a public instance for your convenience, but it is
quite easy to host your own on your PC or server if you know
what you are doing. You can download the server binaries <a
href={GITHUB_URL_VERTD}
target="_blank">here</a
> - the process of setting this up will become easier in the
future, so stay tuned!
{@html link("vertd_link", m["settings.vertd.hosting_info"](), GITHUB_URL_VERTD)}
</p>
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Instance URL</p>
<p class="text-base font-bold">{m["settings.vertd.instance_url"]()}</p>
<input
type="text"
placeholder="Example: http://localhost:24153"
placeholder={m["settings.vertd.url_placeholder"]()}
bind:value={settings.vertdURL}
/>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Conversion speed</p>
<p class="text-base font-bold">{m["settings.vertd.conversion_speed"]()}</p>
<p class="text-sm text-muted font-normal">
This describes the tradeoff between speed and
quality. Faster speeds will result in lower quality,
but will get the job done quicker.
{m["settings.vertd.speed_description"]()}
</p>
</div>
<Dropdown
options={[
"Very Slow",
"Slower",
"Slow",
"Medium",
"Fast",
"Ultra Fast",
m["settings.vertd.speeds.very_slow"](),
m["settings.vertd.speeds.slower"](),
m["settings.vertd.speeds.slow"](),
m["settings.vertd.speeds.medium"](),
m["settings.vertd.speeds.fast"](),
m["settings.vertd.speeds.ultra_fast"](),
]}
settingsStyle
selected={(() => {
switch (settings.vertdSpeed) {
case "verySlow":
return "Very Slow";
return m["settings.vertd.speeds.very_slow"]();
case "slower":
return "Slower";
return m["settings.vertd.speeds.slower"]();
case "slow":
return "Slow";
return m["settings.vertd.speeds.slow"]();
case "medium":
return "Medium";
return m["settings.vertd.speeds.medium"]();
case "fast":
return "Fast";
return m["settings.vertd.speeds.fast"]();
case "ultraFast":
return "Ultra Fast";
return m["settings.vertd.speeds.ultra_fast"]();
}
})()}
onselect={(selected) => {
switch (selected) {
case "Very Slow":
case m["settings.vertd.speeds.very_slow"]():
settings.vertdSpeed = "verySlow";
break;
case "Slower":
case m["settings.vertd.speeds.slower"]():
settings.vertdSpeed = "slower";
break;
case "Slow":
case m["settings.vertd.speeds.slow"]():
settings.vertdSpeed = "slow";
break;
case "Medium":
case m["settings.vertd.speeds.medium"]():
settings.vertdSpeed = "medium";
break;
case "Fast":
case m["settings.vertd.speeds.fast"]():
settings.vertdSpeed = "fast";
break;
case "Ultra Fast":
case m["settings.vertd.speeds.ultra_fast"]():
settings.vertdSpeed = "ultraFast";
break;
}

View File

@ -1,4 +1,5 @@
import { PUB_VERTD_URL } from "$env/static/public";
import type { ConversionBitrate } from "$lib/converters/ffmpeg.svelte";
import type { ConversionSpeed } from "$lib/converters/vertd.svelte";
export { default as Appearance } from "./Appearance.svelte";
@ -10,7 +11,11 @@ export interface ISettings {
filenameFormat: string;
plausible: boolean;
vertdURL: string;
vertdSpeed: ConversionSpeed;
vertdSpeed: ConversionSpeed; // videos
magickQuality: number; // images
ffmpegQuality: ConversionBitrate; // audio (or audio <-> video)
ffmpegSampleRate: string; // audio (or audio <-> video)
ffmpegCustomSampleRate: number; // audio (or audio <-> video) - only used when ffmpegSampleRate is "custom"
}
export class Settings {
@ -21,6 +26,10 @@ export class Settings {
plausible: true,
vertdURL: PUB_VERTD_URL,
vertdSpeed: "slow",
magickQuality: 100,
ffmpegQuality: "auto",
ffmpegSampleRate: "auto",
ffmpegCustomSampleRate: 44100,
});
public save() {

View File

@ -1,11 +1,12 @@
import { browser } from "$app/environment";
import { converters } from "$lib/converters";
import { byNative, converters } from "$lib/converters";
import { error, log } from "$lib/logger";
import { VertFile } from "$lib/types";
import { parseBlob, selectCover } from "music-metadata";
import { writable } from "svelte/store";
import { addDialog } from "./DialogProvider";
import PQueue from "p-queue";
import { getLocale, setLocale } from "$lib/paraglide/runtime";
class Files {
public files = $state<VertFile[]>([]);
@ -32,11 +33,13 @@ class Files {
this.thumbnailQueue.add(async () => {
const isAudio = converters
.find((c) => c.name === "ffmpeg")
?.formatStrings()
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
?.includes(file.from.toLowerCase());
const isVideo = converters
.find((c) => c.name === "vertd")
?.formatStrings()
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
?.includes(file.from.toLowerCase());
try {
@ -120,11 +123,11 @@ class Files {
log(["files"], `no extension found for ${file.name}`);
return;
}
const converter = converters.find((c) =>
c
.formatStrings()
.includes(format || ".somenonexistentextension"),
);
const converter = converters
.sort(byNative(format))
.find((converter) =>
converter.formatStrings().includes(format),
);
if (!converter) {
log(["files"], `no converter found for ${file.name}`);
this.files.push(new VertFile(file, format));
@ -292,3 +295,47 @@ export const vertdLoaded = writable(false);
export const isMobile = writable(false);
export const effects = writable(true);
export const theme = writable<"light" | "dark">("light");
export const locale = writable(getLocale());
export const availableLocales = {
"en": "English",
"es": "Español",
}
export function updateLocale(newLocale: string) {
log(["locale"], `set to ${newLocale}`);
localStorage.setItem("locale", newLocale);
// @ts-expect-error shush
setLocale(newLocale, { reload: false });
// @ts-expect-error shush
locale.set(newLocale);
}
export function link(
tag: string | string[],
text: string,
links: string | string[],
newTab?: boolean | boolean[],
className?: string | string[]
) {
if (!text) return "";
const tags = Array.isArray(tag) ? tag : [tag];
const linksArr = Array.isArray(links) ? links : [links];
const newTabArr = Array.isArray(newTab) ? newTab : [newTab];
const classArr = Array.isArray(className) ? className : [className];
let result = text;
tags.forEach((t, i) => {
const link = linksArr[i] ?? "#";
const target = newTabArr[i] ? 'target="_blank" rel="noopener noreferrer"' : "";
const cls = classArr[i] ? `class="${classArr[i]}"` : "";
const regex = new RegExp(`\\[${t}\\](.*?)\\[\\/${t}\\]`, "g");
result = result.replace(regex, (_, inner) =>
`<a href="${link}" ${target} ${cls} >${inner}</a>`
);
});
return result;
}

View File

@ -1,6 +1,7 @@
import { converters } from "$lib/converters";
import { byNative, converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte";
import { error } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { addToast } from "$lib/store/ToastProvider";
export class VertFile {
@ -27,18 +28,35 @@ export class VertFile {
public converters: Converter[] = [];
public findConverters(supportedFormats: string[] = [this.from]) {
const converter = this.converters.filter((converter) =>
converter.formatStrings().map((f) => supportedFormats.includes(f)),
);
const converter = this.converters
.filter((converter) =>
converter
.formatStrings()
.map((f) => supportedFormats.includes(f)),
)
.sort(byNative(this.from));
return converter;
}
public findConverter() {
const converter = this.converters.find(
(converter) =>
converter.formatStrings().includes(this.from) &&
converter.formatStrings().includes(this.to),
);
const converter = this.converters.find((converter) => {
if (
!converter.formatStrings().includes(this.from) ||
!converter.formatStrings().includes(this.to)
) {
return false;
}
const theirFrom = converter.supportedFormats.find(
(f) => f.name === this.from,
);
const theirTo = converter.supportedFormats.find(
(f) => f.name === this.to,
);
if (!theirFrom || !theirTo) return false;
if (!theirFrom.isNative && !theirTo.isNative) return false;
return true;
});
return converter;
}
@ -75,7 +93,10 @@ export class VertFile {
error(["files"], castedErr.message);
addToast(
"error",
`Error converting file ${this.file.name}: ${castedErr.message || castedErr}`,
m["workers.errors.general"]({
file: this.file.name,
message: castedErr.message || castedErr,
}),
);
this.result = null;
}
@ -105,7 +126,8 @@ export class VertFile {
const blob = URL.createObjectURL(
new Blob([await this.result.file.arrayBuffer()], {
type: to.slice(1),
// type: to.slice(1),
type: "application/octet-stream", // use generic type to prevent browsers changing extension
}),
);
const a = document.createElement("a");

View File

@ -26,15 +26,12 @@ magickPromise
const handleMessage = async (message: any): Promise<any> => {
switch (message.type) {
case "convert": {
const compression: number | undefined = message.compression;
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.toLowerCase();
if (message.to === ".jfif") {
message.to = ".jpeg";
}
if (message.input.from === ".jfif") {
message.input.from = ".jpeg";
}
if (message.to === ".jfif") message.to = ".jpeg";
if (message.input.from === ".jfif") message.input.from = ".jpeg";
if (message.input.from === ".fit") message.input.from = ".fits";
const buffer = await message.input.file.arrayBuffer();
// only wait when we need to
@ -70,7 +67,7 @@ const handleMessage = async (message: any): Promise<any> => {
const convertedImgs: Uint8Array[] = [];
await Promise.all(
imgs.map(async (img, i) => {
const output = await magickConvert(img, message.to);
const output = await magickConvert(img, message.to, compression);
convertedImgs[i] = output;
}),
);
@ -78,7 +75,10 @@ const handleMessage = async (message: any): Promise<any> => {
const zip = makeZip(
convertedImgs.map(
(img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`),
new File(
[new Uint8Array(img)],
`image${i}.${message.to.slice(1)}`,
),
),
"images.zip",
);
@ -108,9 +108,13 @@ const handleMessage = async (message: any): Promise<any> => {
}),
),
message.to,
compression
);
files.push(
new File([blob], `image${i}${message.to}`),
new File(
[new Uint8Array(blob)],
`image${i}${message.to}`,
),
);
}),
);
@ -154,6 +158,7 @@ const handleMessage = async (message: any): Promise<any> => {
const converted = await magickConvert(
img,
message.to,
compression
);
outputs.push(converted);
break;
@ -167,7 +172,10 @@ const handleMessage = async (message: any): Promise<any> => {
const zip = makeZip(
outputs.map(
(img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`),
new File(
[new Uint8Array(img)],
`image${i}.${message.to.slice(1)}`,
),
),
"images.zip",
);
@ -179,6 +187,32 @@ const handleMessage = async (message: any): Promise<any> => {
};
}
// build frames of animated formats (webp/gif)
// APNG does not work on magick-wasm since it needs ffmpeg built-in (not in magick-wasm) - handle in ffmpeg
if (
(message.input.from === ".webp" ||
message.input.from === ".gif") &&
(message.to === ".gif" || message.to === ".webp")
) {
const collection = MagickImageCollection.create(
new Uint8Array(buffer),
);
const format =
message.to === ".gif"
? MagickFormat.Gif
: MagickFormat.WebP;
const result = await new Promise<Uint8Array>((resolve) => {
collection.write(format, (output) => {
resolve(structuredClone(output));
});
});
collection.dispose();
return {
type: "finished",
output: result,
};
}
const img = MagickImage.create(
new Uint8Array(buffer),
new MagickReadSettings({
@ -188,7 +222,7 @@ const handleMessage = async (message: any): Promise<any> => {
}),
);
const converted = await magickConvert(img, message.to);
const converted = await magickConvert(img, message.to, compression);
return {
type: "finished",
@ -206,7 +240,10 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
if (value) chunks.push(value);
done = d;
}
const blob = new Blob(chunks, { type: "application/zip" });
const blob = new Blob(
chunks.map((chunk) => new Uint8Array(chunk)),
{ type: "application/zip" },
);
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
};
@ -250,8 +287,12 @@ const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
return;
}
if (!data) {
reject(new Error("Pixel data is null"));
return;
}
const imageData = new ImageData(
new Uint8ClampedArray(data?.buffer || new ArrayBuffer(0)),
new Uint8ClampedArray(data),
img.width,
img.height,
);
@ -266,7 +307,11 @@ const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
);
};
const magickConvert = async (img: IMagickImage, to: string) => {
const magickConvert = async (
img: IMagickImage,
to: string,
compression?: number,
) => {
const intermediary = await magickToBlob(img);
const buf = new Uint8Array(await intermediary.arrayBuffer());
let fmt = to.slice(1).toUpperCase();
@ -274,6 +319,8 @@ const magickConvert = async (img: IMagickImage, to: string) => {
const result = await new Promise<Uint8Array>((resolve) => {
ImageMagick.read(buf, MagickFormat.Png, (image) => {
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) image.quality = compression;
image.write(fmt as unknown as MagickFormat, (o) => {
resolve(structuredClone(o));
});

View File

@ -113,8 +113,6 @@ const formatToReader = (format: Format): string => {
return "rtf";
case ".rst":
return "rst";
case ".xml":
return "xml";
}
throw new Error(`Unsupported format: ${format}`);

View File

@ -2,11 +2,7 @@
import { onMount } from "svelte";
import { goto, beforeNavigate, afterNavigate } from "$app/navigation";
import {
PUB_PLAUSIBLE_URL,
PUB_HOSTNAME,
PUB_DONATION_URL,
} from "$env/static/public";
import { PUB_PLAUSIBLE_URL, PUB_HOSTNAME } from "$env/static/public";
import { VERT_NAME } from "$lib/consts";
import * as Layout from "$lib/components/layout";
import * as Navbar from "$lib/components/layout/Navbar";
@ -19,11 +15,13 @@
theme,
dropping,
vertdLoaded,
locale,
} from "$lib/store/index.svelte";
import "$lib/css/app.scss";
import { browser } from "$app/environment";
import { page } from "$app/state";
import { initStores as initAnimStores } from "$lib/animation/index.js";
import { locales, localizeHref } from "$lib/paraglide/runtime";
let { children, data } = $props();
let enablePlausible = $state(false);
@ -72,7 +70,6 @@
theme.set(
(localStorage.getItem("theme") as "light" | "dark") || "light",
);
Settings.instance.load();
fetch(`${Settings.instance.settings.vertdURL}/api/version`).then(
@ -83,7 +80,6 @@
});
$effect(() => {
// Enable plausible if enabled
enablePlausible =
!!PUB_PLAUSIBLE_URL && Settings.instance.settings.plausible;
if (!enablePlausible && browser) {
@ -142,35 +138,44 @@
</svelte:head>
<!-- FIXME: if user resizes between desktop/mobile, highlight of page disappears (only shows on original size) -->
<div
class="flex flex-col min-h-screen h-full w-full overflow-x-hidden"
ondrop={dropFiles}
ondragenter={(e) => handleDrag(e, true)}
ondragover={(e) => handleDrag(e, true)}
ondragleave={(e) => handleDrag(e, false)}
role="region"
>
<Layout.UploadRegion />
{#key $locale}
<div
class="flex flex-col min-h-screen h-full w-full overflow-x-hidden"
ondrop={dropFiles}
ondragenter={(e) => handleDrag(e, true)}
ondragover={(e) => handleDrag(e, true)}
ondragleave={(e) => handleDrag(e, false)}
role="region"
>
<Layout.UploadRegion />
<div>
<Layout.MobileLogo />
<Navbar.Desktop />
</div>
<div>
<Layout.MobileLogo />
<Navbar.Desktop />
</div>
<!--
<!--
SvelteKit throws the following warning when developing - safe to ignore as we render the children in this component:
`<slot />` or `{@render ...}` tag missing — inner content will not be rendered
-->
<Layout.PageContent {children} />
<Layout.PageContent {children} />
<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}
>{locale}</a
>
{/each}
</div>
<Layout.Toasts />
<Layout.Dialogs />
<Layout.Toasts />
<Layout.Dialogs />
<div>
<Layout.Footer />
<Navbar.Mobile />
<div>
<Layout.Footer />
<Navbar.Mobile />
</div>
</div>
</div>
{/key}
<!-- Gradients placed here to prevent it overlapping in transitions -->
<Layout.Gradients />

View File

@ -5,6 +5,11 @@
import { vertdLoaded } from "$lib/store/index.svelte";
import clsx from "clsx";
import { AudioLines, BookText, Check, Film, Image } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
import { browser } from "$app/environment";
import "overlayscrollbars/overlayscrollbars.css";
import { onMount } from "svelte";
const getSupportedFormats = (name: string) =>
converters
@ -20,6 +25,7 @@
ready: boolean;
formats: string;
icon: typeof Image;
title: string;
};
} = $derived({
Images: {
@ -28,16 +34,19 @@
false,
formats: getSupportedFormats("imagemagick"),
icon: Image,
title: m["upload.cards.images"](),
},
Audio: {
ready: converters.find((c) => c.name === "ffmpeg")?.ready || false,
formats: getSupportedFormats("ffmpeg"),
icon: AudioLines,
title: m["upload.cards.audio"](),
},
Documents: {
ready: converters.find((c) => c.name === "pandoc")?.ready || false,
formats: getSupportedFormats("pandoc"),
icon: BookText,
title: m["upload.cards.documents"](),
},
Video: {
ready:
@ -45,6 +54,7 @@
(false && $vertdLoaded),
formats: getSupportedFormats("vertd"),
icon: Film,
title: m["upload.cards.video"](),
},
});
@ -58,12 +68,35 @@
);
if (formatInfo) {
return `This format can only be converted as ${
formatInfo.fromSupported ? "input (from)" : "output (to)"
}.`;
const direction = formatInfo.fromSupported
? m["upload.tooltip.direction_input"]()
: m["upload.tooltip.direction_output"]();
return m["upload.tooltip.partial_support"]({ direction });
}
return "";
};
let scrollContainers: HTMLElement[] = $state([]);
// svelte-ignore state_referenced_locally
let showBlur = $state(Array(Object.keys(status).length).fill(false));
onMount(() => {
const handleResize = () => {
for (let i = 0; i < scrollContainers.length; i++) {
// show bottom blur if scrollable
const container = scrollContainers[i];
if (!container) return;
showBlur[i] = container.scrollHeight > container.clientHeight;
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
</script>
<div class="max-w-6xl w-full mx-auto px-6 md:px-8">
@ -75,14 +108,12 @@
<h1
class="text-4xl px-12 md:p-0 md:text-6xl flex-wrap tracking-tight leading-tight md:leading-[72px] mb-4 md:mb-6"
>
The file converter you'll love.
{m["upload.title"]()}
</h1>
<p
class="font-normal px-5 md:p-0 text-lg md:text-xl text-black text-muted dynadark:text-muted"
>
All image, audio, and document processing is done on your
device. Videos are converted on our lightning-fast servers.
No file size limit, no ads, and completely open source.
{m["upload.subtitle"]()}
</p>
</div>
<div class="flex-grow w-full h-72">
@ -94,84 +125,150 @@
<hr />
<div class="mt-10 md:mt-16">
<h2 class="text-center text-4xl">VERT supports...</h2>
<h2 class="text-center text-4xl">{m["upload.cards.title"]()}</h2>
<div class="flex gap-4 mt-8 md:flex-row flex-col">
{#each Object.entries(status) as [key, s]}
{@const Icon = s.icon}
<div class="file-category-card w-full flex flex-col gap-4">
<div class="file-category-card-inner">
<div
class={clsx("icon-container", {
"bg-accent-blue": key === "Images",
"bg-accent-purple": key === "Audio",
"bg-accent-green": key === "Documents",
"bg-accent-red": key === "Video",
})}
>
<Icon size="20" />
{#if browser}
{#each Object.entries(status) as [key, s], i}
{@const Icon = s.icon}
<div class="file-category-card w-full flex flex-col gap-4">
<div class="file-category-card-inner">
<div
class={clsx("icon-container", {
"bg-accent-blue": key === "Images",
"bg-accent-purple": key === "Audio",
"bg-accent-green": key === "Documents",
"bg-accent-red": key === "Video",
})}
>
<Icon size="20" />
</div>
<span>{s.title}</span>
</div>
<span>{key}</span>
</div>
<div class="file-category-card-content flex-grow gap-4">
{#if key === "Video"}
<p>
Video uploads to a server for processing by
default, learn how to set it up locally <a
target="_blank"
href="https://github.com/VERT-sh/VERT/wiki/How-to-convert-video-with-VERT"
>here</a
>.
</p>
{:else}
<p class="flex tems-center justify-center gap-2">
<Check size="20" /> Local fully supported
</p>
{/if}
<p>
<b>Status: </b>
{s.ready ? "ready" : "not ready"}
</p>
<div>
<span class="flex flex-wrap justify-center">
<b>Supported formats:&nbsp;</b>
{#each s.formats.split(", ") as format, index}
{@const isPartial = format.endsWith("*")}
{@const formatName = isPartial
? format.slice(0, -1)
: format}
<span
class="text-sm font-normal flex items-center"
>
{#if isPartial}
<div
class="file-category-card-content flex-grow relative"
>
<OverlayScrollbarsComponent
options={{
scrollbars: {
autoHide: "move",
autoHideDelay: 1500,
},
}}
defer
>
<div
class="flex flex-col gap-4 h-[12.25rem] relative"
bind:this={scrollContainers[i]}
>
{#if key === "Video"}
<p
class="flex tems-center justify-center gap-2"
>
<Check size="20" />
<Tooltip
text={getTooltip(formatName)}
text={m[
"upload.tooltip.video_server_processing"
]()}
>
{formatName}<span
class="text-red-500">*</span
>
<span>
<a
href="https://github.com/VERT-sh/VERT/blob/main/docs/VIDEO_CONVERSION.md"
target="_blank"
rel="noopener noreferrer"
>
{m[
"upload.cards.video_server_processing"
]()}
</a>
<span
class="text-red-500 -ml-0.5"
>*</span
>
</span>
</Tooltip>
{:else}
{formatName}
{/if}
{#if index < s.formats.split(", ").length - 1}
<span>,&nbsp;</span>
{/if}
</span>
{/each}
</span>
</p>
{:else}
<p
class="flex tems-center justify-center gap-2"
>
<Check size="20" />
{m[
"upload.cards.local_supported"
]()}
</p>
{/if}
<p>
{@html m["upload.cards.status.text"]({
status: s.ready
? m[
"upload.cards.status.ready"
]()
: m[
"upload.cards.status.not_ready"
](),
})}
</p>
<div class="flex flex-col items-center relative">
<b
>{m[
"upload.cards.supported_formats"
]()}&nbsp;</b
>
<p
class="flex flex-wrap justify-center leading-tight px-2"
>
{#each s.formats.split(", ") as format, index}
{@const isPartial =
format.endsWith("*")}
{@const formatName = isPartial
? format.slice(0, -1)
: format}
<span
class="text-sm font-normal flex items-center relative"
>
{#if isPartial}
<Tooltip
text={getTooltip(
formatName,
)}
>
{formatName}<span
class="text-red-500"
>*</span
>
</Tooltip>
{:else}
{formatName}
{/if}
{#if index < s.formats.split(", ").length - 1}
<span>,&nbsp;</span>
{/if}
</span>
{/each}
</p>
</div>
</div>
</OverlayScrollbarsComponent>
<!-- blur at bottom if scrollable - positioned relative to the card container -->
{#if showBlur[i]}
<div
class="absolute left-0 bottom-0 w-full h-10 pointer-events-none"
style={`background: linear-gradient(to top, var(--bg-panel), transparent 100%);`}
></div>
{/if}
</div>
</div>
</div>
{/each}
{/each}
{/if}
</div>
</div>
</div>
<style>
.file-category-card {
@apply bg-panel rounded-2xl p-5 shadow-panel;
@apply bg-panel rounded-2xl p-5 shadow-panel relative;
}
.file-category-card p {

View File

@ -12,6 +12,7 @@
import { addToast } from "$lib/store/ToastProvider";
import { dev } from "$app/environment";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
// import { dev } from "$app/environment";
// import { page } from "$app/state";
@ -34,19 +35,19 @@
{
name: "nullptr",
github: "https://github.com/not-nullptr",
role: "Lead developer; conversion backend, UI implementation",
role: m["about.credits.roles.lead_developer"](),
avatar: avatarNullptr,
},
{
name: "JovannMC",
github: "https://github.com/JovannMC",
role: "Developer; UI implementation",
role: m["about.credits.roles.developer"](),
avatar: avatarJovannMC,
},
{
name: "Liam",
github: "https://x.com/z2rMC",
role: "Designer; UX, branding, marketing",
role: m["about.credits.roles.designer"](),
avatar: avatarLiam,
},
];
@ -55,13 +56,13 @@
{
name: "azurejelly",
github: "https://github.com/azurejelly",
role: "Maintaining Docker & CI support",
role: m["about.credits.roles.docker_ci"](),
avatar: avatarAzurejelly,
},
{
name: "Realmy",
github: "https://github.com/RealmyTheMan",
role: "Former co-founder & designer",
role: m["about.credits.roles.former_cofounder"](),
avatar: avatarRealmy,
},
];
@ -80,7 +81,7 @@
try {
const response = await fetch(`${GITHUB_API_URL}/contributors`);
if (!response.ok) {
addToast("error", "Error fetching GitHub contributors");
addToast("error", m["about.errors.github_contributors"]());
throw new Error(`HTTP error, status: ${response.status}`);
}
const allContribs = await response.json();
@ -89,6 +90,7 @@
const excludedNames = new Set([
...mainContribs.map((c) => c.github.split("/").pop()),
...notableContribs.map((c) => c.github.split("/").pop()),
"Z2r-YT"
]);
const filteredContribs = allContribs.filter(
@ -135,7 +137,7 @@
<div class="flex flex-col h-full items-center">
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
<InfoIcon size="40" class="inline-block -mt-2 mr-2" />
About
{m["about.title"]()}
</h1>
<div

View File

@ -5,7 +5,7 @@
import Panel from "$lib/components/visual/Panel.svelte";
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { categories, converters } from "$lib/converters";
import { categories, converters, byNative } from "$lib/converters";
import {
effects,
files,
@ -28,6 +28,7 @@
XIcon,
} from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
onMount(() => {
// depending on format, select right category and format
@ -51,21 +52,30 @@
$effect(() => {
// Set gradient color depending on the file types
// TODO: if more file types added, add a "fileType" property to the file object
const allAudio = files.files.every(
(file) => file.findConverter()?.name === "ffmpeg",
);
const allImages = files.files.every(
(file) =>
file.findConverter()?.name !== "ffmpeg" &&
file.findConverter()?.name !== "vertd",
);
const allVideos = files.files.every(
(file) => file.findConverter()?.name === "vertd",
);
const allDocuments = files.files.every(
(file) => file.findConverter()?.name === "pandoc",
);
const allAudio = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "ffmpeg";
});
const allImages = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "libvips";
});
const allVideos = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "vertd";
});
const allDocuments = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "pandoc";
});
if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) {
showGradient.set(false);
@ -75,7 +85,7 @@
if (
files.files.length === 0 ||
(!allAudio && !allImages && !allVideos)
(!allAudio && !allImages && !allVideos && !allDocuments)
) {
gradientColor.set("");
} else {
@ -95,7 +105,6 @@
</script>
{#snippet fileItem(file: VertFile, index: number)}
{@const availableConverters = file.findConverters()}
{@const currentConverter = converters.find(
(c) =>
c.formatStrings((f) => f.fromSupported).includes(file.from) &&
@ -103,11 +112,13 @@
)}
{@const isAudio = converters
.find((c) => c.name === "ffmpeg")
?.formatStrings((f) => f.fromSupported)
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
{@const isVideo = converters
.find((c) => c.name === "vertd")
?.formatStrings((f) => f.fromSupported)
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
{@const isImage = converters
.find((c) => c.name === "imagemagick")
@ -115,28 +126,29 @@
.includes(file.from)}
{@const isDocument = converters
.find((c) => c.name === "pandoc")
?.formatStrings((f) => f.fromSupported)
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
{#if !converters.length}
<Tooltip text="Unknown file type" position="bottom">
<Tooltip text={m["convert.tooltips.unknown_file"]()} position="bottom">
<FileQuestionIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isAudio}
<Tooltip text="Audio file" position="bottom">
<Tooltip text={m["convert.tooltips.audio_file"]()} position="bottom">
<AudioLines size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isVideo}
<Tooltip text="Video file" position="bottom">
<Tooltip text={m["convert.tooltips.video_file"]()} position="bottom">
<FilmIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isDocument}
<Tooltip text="Document file" position="bottom">
<Tooltip text={m["convert.tooltips.document_file"]()} position="bottom">
<BookText size="24" class="flex-shrink-0" />
</Tooltip>
{:else}
<Tooltip text="Image file" position="bottom">
<Tooltip text={m["convert.tooltips.image_file"]()} position="bottom">
<ImageIcon size="24" class="flex-shrink-0" />
</Tooltip>
{/if}
@ -172,11 +184,10 @@
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
{m["convert.errors.cant_convert"]()}
</p>
<p class="font-normal">
what are you doing..? you're supposed to run the vertd
server!
{m["convert.errors.vertd_server"]()}
</p>
</div>
{:else}
@ -184,11 +195,10 @@
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
{m["convert.errors.cant_convert"]()}
</p>
<p class="font-normal">
Only image, video, audio, and document files are
supported
{m["convert.errors.unsupported_format"]()}
</p>
</div>
{/if}
@ -196,10 +206,9 @@
<div
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">We can't convert this file.</p>
<p class="font-body font-bold">{m["convert.errors.cant_convert"]()}</p>
<p class="font-normal">
Could not find the vertd instance to start video conversion.
Are you sure the instance URL is set correctly?
{m["convert.errors.vertd_not_found"]()}
</p>
</div>
{:else}
@ -251,7 +260,7 @@
onselect={(option) => handleSelect(option, file)}
/>
<div class="w-full flex items-center justify-between">
<Tooltip text="Convert this file" position="bottom">
<Tooltip text={m["convert.tooltips.convert_file"]()} position="bottom">
<button
class="btn {$effects
? ''
@ -269,7 +278,7 @@
</button>
</Tooltip>
<Tooltip
text="Download this file"
text={m["convert.tooltips.download_file"]()}
position="bottom"
>
<button

View File

@ -5,6 +5,7 @@
import { files } from "$lib/store/index.svelte";
import { quintOut } from "svelte/easing";
import { blur } from "svelte/transition";
import { m } from "$lib/paraglide/messages";
const images = $derived(
files.files.filter((f) =>
@ -31,9 +32,9 @@
</script>
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
<h1 class="text-5xl text-center">SECRET JPEGIFY!!!</h1>
<h1 class="text-5xl text-center">{m["jpegify.title"]()}</h1>
<p class="text-muted text-center -mt-4 font-normal italic">
(shh... don't tell anyone!)
{m["jpegify.subtitle"]()}
</p>
<Uploader class="w-full h-64" jpegify={true} />
<input
@ -49,7 +50,7 @@
onclick={jpegify}
disabled={processing}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>JPEGIFY {compressionInverted}%!!!</button
>{m["jpegify.button"]({ compression: compressionInverted })}</button
>
<div class="flex flex-wrap flex-row justify-center gap-4">
{#each images as file, i (file.id)}
@ -89,7 +90,7 @@
disabled={!!!file.result}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>
Download
{m["jpegify.download"]()}
</button>
<button
onclick={() => {
@ -103,7 +104,7 @@
}}
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
>
Delete
{m["jpegify.delete"]()}
</button>
</div>
</Panel>

View File

@ -6,6 +6,7 @@
import { PUB_PLAUSIBLE_URL } from "$env/static/public";
import { SettingsIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
let settings = $state(Settings.Settings.instance.settings);
@ -31,7 +32,7 @@
log(["settings"], "saving settings");
} catch (error) {
log(["settings", "error"], `failed to save settings: ${error}`);
addToast("error", "Failed to save settings!");
addToast("error", m["settings.errors.save_failed"]());
}
});
@ -51,7 +52,7 @@
<div class="flex flex-col h-full items-center">
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
<SettingsIcon size="40" class="inline-block -mt-2 mr-2" />
Settings
{m["settings.title"]()}
</h1>
<div

View File

@ -1,3 +1,4 @@
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig, type PluginOption } from "vite";
import svg from "@poppanator/sveltekit-svg";
@ -6,6 +7,11 @@ import wasm from "vite-plugin-wasm";
export default defineConfig(({ command }) => {
const plugins: PluginOption[] = [
sveltekit(),
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/lib/paraglide",
strategy: ["localStorage", "preferredLanguage", "baseLocale"],
}),
svg({
includePaths: ["./src/lib/assets"],
svgoOptions: {