diff --git a/package.json b/package.json index bda8cf6..a227bab 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@astrojs/tailwind": "^5.1.2", "@okikio/transferables": "npm:@jsr/okikio__transferables@^1.0.2", "@poolifier/poolifier-web-worker": "npm:@jsr/poolifier__poolifier-web-worker@^0.4.29", - "@std/path": "npm:@jsr/std__path@^1.0.6", + "@std/path": "npm:@jsr/std__path@^1.0.8", "@zip-js/zip-js": "npm:@jsr/zip-js__zip-js@^2.7.52", "astro": "^4.16.16", "comlink": "^4.4.1", @@ -24,6 +24,7 @@ "solid-js": "^1.9.2", "tailwindcss": "^3.4.14", "typescript": "^5.6.3", + "unstorage": "^1.13.1", "webm-muxer": "^5.0.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1485328..8eface8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: npm:@jsr/poolifier__poolifier-web-worker@^0.4.29 version: '@jsr/poolifier__poolifier-web-worker@0.4.29' '@std/path': - specifier: npm:@jsr/std__path@^1.0.6 - version: '@jsr/std__path@1.0.6' + specifier: npm:@jsr/std__path@^1.0.8 + version: '@jsr/std__path@1.0.8' '@zip-js/zip-js': specifier: npm:@jsr/zip-js__zip-js@^2.7.52 version: '@jsr/zip-js__zip-js@2.7.52' @@ -47,6 +47,9 @@ importers: typescript: specifier: ^5.6.3 version: 5.6.3 + unstorage: + specifier: ^1.13.1 + version: 1.13.1 webm-muxer: specifier: ^5.0.2 version: 5.0.2 @@ -586,8 +589,8 @@ packages: '@jsr/poolifier__poolifier-web-worker@0.4.29': resolution: {integrity: sha512-klEYEXNx/jrH1pBjotrs8uQGh/b6yyO1RNL41P5XPLprD0SVWYI07rUTVi6ReVrXP0tYcUNsr8ffmuJMlNd4IQ==, tarball: https://npm.jsr.io/~/11/@jsr/poolifier__poolifier-web-worker/0.4.29.tgz} - '@jsr/std__path@1.0.6': - resolution: {integrity: sha512-A8WmQshHm5W5jN1OO24qH0VHWVv6vvmUzJiODKvvIUW1jNGVf04b86b9XWMoZPwlMEHq1mGgxWN9AuXY0CY2yg==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.0.6.tgz} + '@jsr/std__path@1.0.8': + resolution: {integrity: sha512-eNBGlh/8ZVkMxtFH4bwIzlAeKoHYk5in4wrBZhi20zMdOiuX4QozP4+19mIXBT2lzHDjhuVLyECbhFeR304iDg==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.0.8.tgz} '@jsr/zip-js__zip-js@2.7.52': resolution: {integrity: sha512-fyqFInHruWXR+0+dePJnaQwEbPCpseCHkom5tQI6/ODeaODbLlrSOAmVmj0ldTy/vkWPQUXB3COd4wGlp+HqCw==, tarball: https://npm.jsr.io/~/11/@jsr/zip-js__zip-js/2.7.52.tgz} @@ -607,6 +610,94 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-wasm@2.5.0': + resolution: {integrity: sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -949,6 +1040,9 @@ packages: resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -961,6 +1055,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1002,9 +1100,19 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1013,6 +1121,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crossws@0.3.1: + resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1033,10 +1144,21 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -1128,6 +1250,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -1194,6 +1320,13 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-port-please@3.1.2: + resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -1220,6 +1353,9 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + h3@1.13.0: + resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1274,9 +1410,20 @@ packages: http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-shutdown@1.2.2: + resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -1326,6 +1473,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -1342,6 +1493,10 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1360,6 +1515,10 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jiti@2.4.0: + resolution: {integrity: sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1413,6 +1572,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listhen@1.9.0: + resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} + hasBin: true + load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -1489,6 +1652,9 @@ packages: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1581,6 +1747,15 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1593,6 +1768,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mp4-muxer@5.1.3: resolution: {integrity: sha512-artYS8Y7l9Cgqi1WyoNZtHW+pXOM0Tg75eupENoSEx59hMJlBqXxsTgJQ8T0ycrb6h9lw9iNKVB8cKEn7HjG2A==} @@ -1621,6 +1799,16 @@ packages: nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -1632,6 +1820,10 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1640,6 +1832,16 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -1695,6 +1897,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1702,6 +1908,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1729,6 +1938,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -1797,6 +2009,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -1960,6 +2175,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -1995,6 +2213,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -2012,6 +2234,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + tailwindcss@3.4.14: resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} engines: {node: '>=14.0.0'} @@ -2072,6 +2298,15 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unenv@1.10.0: + resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -2102,12 +2337,63 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unstorage@1.13.1: + resolution: {integrity: sha512-ELexQHUrG05QVIM/iUeQNdl9FXDZhqLJ4yP59fnmn2jGUh0TEulwOgov1ubOb3Gt2ZGK/VMchJwPDNVEGWQpRg==} + peerDependencies: + '@azure/app-configuration': ^1.7.0 + '@azure/cosmos': ^4.1.1 + '@azure/data-tables': ^13.2.2 + '@azure/identity': ^4.5.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.25.0 + '@capacitor/preferences': ^6.0.2 + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/kv': ^1.0.1 + idb-keyval: ^6.2.1 + ioredis: ^5.4.1 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/kv': + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + + untun@0.1.3: + resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} + hasBin: true + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2942,7 +3228,7 @@ snapshots: '@jsr/poolifier__poolifier-web-worker@0.4.29': {} - '@jsr/std__path@1.0.6': {} + '@jsr/std__path@1.0.8': {} '@jsr/zip-js__zip-js@2.7.52': {} @@ -2960,6 +3246,71 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@parcel/watcher-android-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-x64@2.5.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-wasm@2.5.0': + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.8 + + '@parcel/watcher-win32-arm64@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -3383,6 +3734,10 @@ snapshots: ci-info@4.1.0: {} + citty@0.1.6: + dependencies: + consola: 3.2.3 + cli-boxes@3.0.0: {} cli-cursor@5.0.0: @@ -3391,6 +3746,12 @@ snapshots: cli-spinners@2.9.2: {} + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3431,8 +3792,14 @@ snapshots: common-ancestor-path@1.0.1: {} + confbox@0.1.8: {} + + consola@3.2.3: {} + convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cookie@0.7.2: {} cross-spawn@7.0.3: @@ -3441,6 +3808,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.1: + dependencies: + uncrypto: 0.1.3 + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -3453,8 +3824,14 @@ snapshots: dependencies: character-entities: 2.0.2 + defu@6.1.4: {} + dequal@2.0.3: {} + destr@2.0.3: {} + + detect-libc@1.0.3: {} + detect-libc@2.0.3: optional: true @@ -3541,6 +3918,18 @@ snapshots: eventemitter3@5.0.1: {} + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -3599,6 +3988,10 @@ snapshots: get-east-asian-width@1.3.0: {} + get-port-please@3.1.2: {} + + get-stream@8.0.1: {} + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -3629,6 +4022,19 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + h3@1.13.0: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.1 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.4 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -3732,8 +4138,14 @@ snapshots: http-cache-semantics@4.1.1: {} + http-shutdown@1.2.2: {} + + human-signals@5.0.0: {} + import-meta-resolve@4.1.0: {} + iron-webcrypto@1.2.1: {} + is-arrayish@0.3.2: optional: true @@ -3767,6 +4179,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-stream@3.0.0: {} + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} @@ -3777,6 +4191,10 @@ snapshots: dependencies: is-inside-container: 1.0.0 + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + isexe@2.0.0: {} jackspeak@3.4.3: @@ -3796,6 +4214,8 @@ snapshots: jiti@1.21.6: {} + jiti@2.4.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -3829,6 +4249,27 @@ snapshots: lines-and-columns@1.2.4: {} + listhen@1.9.0: + dependencies: + '@parcel/watcher': 2.5.0 + '@parcel/watcher-wasm': 2.5.0 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.2.3 + crossws: 0.3.1 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.13.0 + http-shutdown: 1.2.2 + jiti: 2.4.0 + mlly: 1.7.3 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.8.0 + ufo: 1.5.4 + untun: 0.1.3 + uqr: 0.1.2 + load-yaml-file@0.2.0: dependencies: graceful-fs: 4.2.11 @@ -3990,6 +4431,8 @@ snapshots: dependencies: is-what: 4.1.16 + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.1: @@ -4188,6 +4631,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime@3.0.0: {} + + mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} minimatch@9.0.5: @@ -4196,6 +4643,13 @@ snapshots: minipass@7.1.2: {} + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + mp4-muxer@5.1.3: dependencies: '@types/dom-webcodecs': 0.1.13 @@ -4221,16 +4675,38 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 + node-addon-api@7.1.1: {} + + node-fetch-native@1.6.4: {} + + node-forge@1.3.1: {} + node-releases@2.0.18: {} normalize-path@3.0.0: {} normalize-range@0.1.2: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + object-assign@4.1.1: {} object-hash@3.0.0: {} + ofetch@1.4.1: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + ohash@1.1.4: {} + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -4295,6 +4771,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -4302,6 +4780,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pathe@1.1.2: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4318,6 +4798,12 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + postcss-import@15.1.0(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -4381,6 +4867,8 @@ snapshots: queue-microtask@1.2.3: {} + radix3@1.1.2: {} + react-is@18.3.1: {} read-cache@1.0.0: @@ -4628,6 +5116,8 @@ snapshots: sprintf-js@1.0.3: {} + std-env@3.8.0: {} + stdin-discarder@0.2.2: {} string-width@4.2.3: @@ -4665,6 +5155,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@3.0.0: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -4685,6 +5177,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + system-architecture@0.1.0: {} + tailwindcss@3.4.14: dependencies: '@alloc/quick-lru': 5.2.0 @@ -4751,6 +5245,18 @@ snapshots: typescript@5.6.3: {} + ufo@1.5.4: {} + + uncrypto@0.1.3: {} + + unenv@1.10.0: + dependencies: + consola: 3.2.3 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.4 + pathe: 1.1.2 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -4803,12 +5309,33 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + unstorage@1.13.1: + dependencies: + anymatch: 3.1.3 + chokidar: 3.6.0 + citty: 0.1.6 + destr: 2.0.3 + h3: 1.13.0 + listhen: 1.9.0 + lru-cache: 10.4.3 + node-fetch-native: 1.6.4 + ofetch: 1.4.1 + ufo: 1.5.4 + + untun@0.1.3: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + pathe: 1.1.2 + update-browserslist-db@1.1.1(browserslist@4.24.0): dependencies: browserslist: 4.24.0 escalade: 3.2.0 picocolors: 1.1.1 + uqr@0.1.2: {} + util-deprecate@1.0.2: {} validate-html-nesting@1.2.2: {} diff --git a/src/components/Converter.tsx b/src/components/Converter.tsx index 3235269..99c3c44 100644 --- a/src/components/Converter.tsx +++ b/src/components/Converter.tsx @@ -4,28 +4,61 @@ import type { } from "../workers/gif-to-mp4-worker.ts"; import WorkerURL from "../workers/gif-to-mp4-worker?worker&url"; - import { convertGifToMp4 } from "../converters/gif-to-mp4.ts"; -import { createResource, createSignal, For, Show } from "solid-js"; +import { createResource, createSignal, For, onMount, Show } from "solid-js"; import { effect } from "solid-js/web"; import "tailwindcss/tailwind.css"; import { BlobReader, BlobWriter, ZipWriter } from "@zip-js/zip-js"; import { chunkArray } from "../utils/utils.ts"; -import { basename } from "@std/path/posix"; +import { basename, fromFileUrl } from "@std/path/posix"; + +import { createStorage } from "unstorage"; +import opfsDriver from "../libs/opfs.ts"; + +const inputStorage = createStorage({ + driver: opfsDriver({ base: "tmp" }), +}); + +const outputStorage = createStorage({ + driver: opfsDriver({ base: "output" }), +}); + +const previewUrls = new Map(); +async function getPreviewUrl( + filePath: string, + storage: typeof outputStorage | typeof inputStorage = inputStorage +) { + if (previewUrls.has(filePath)) + return previewUrls.get(filePath); + + const file = await storage.getItem(filePath); + if (file) { + const previewUrl = URL.createObjectURL(file); + previewUrls.set(filePath, previewUrl); + return previewUrl; + } +} + +function revokePreviewUrls(filePath: string) { + if (previewUrls.has(filePath)) { + const previewUrl = previewUrls.get(filePath)!; + return URL.revokeObjectURL(previewUrl); + } +} export function Converter() { - const [gifUrls, setGifUrls] = createSignal< - { url: string; file?: File; filePath: string; selected: boolean; errored?: boolean }[] + const [gifList, setGifList] = createSignal< + { id: number; filePath: string; selected: boolean; errored?: boolean }[] >([]); const [mp4Videos, setMp4Videos] = createSignal< - { url: string; filePath: string; selected: boolean }[] + { filePath: string; selected: boolean }[] >([]); const [viewMode, setViewMode] = createSignal<"grid" | "list">("grid"); const [showAllInputs, setShowAllInputs] = createSignal(false); const [latestInputGifs, setLatestInputGifs] = createSignal< - { url: string; selected: boolean; errored?: boolean }[] + { filePath: string; selected: boolean; errored?: boolean }[] >([]); const [conversionProgress, setConversionProgress] = createSignal(0); @@ -40,17 +73,17 @@ export function Converter() { // Set up a fixed worker pool const _pool = new DynamicThreadPool( - Math.max(1, Math.floor(availableParallelism() / 2)), - availableParallelism(), + Math.max(1, Math.floor(availableParallelism() / 2) - 2), + Math.max(1, Math.floor(availableParallelism() - 2)), new URL(WorkerURL, location.origin), { errorEventHandler: (e) => { console.error("Worker pool error:", e); }, messageEventHandler: (e) => { - console.log({ e }) + console.log({ e }); }, - startWorkers: true + startWorkers: true, } ); @@ -65,49 +98,83 @@ export function Converter() { } }); - const handleSearch = (event: Event) => { + const handleSearch = async (event: Event) => { event.preventDefault(); const input = (event.target as HTMLFormElement)["search"].value.trim(); - if (input && !gifUrls().some((gif) => gif.url === input)) { - const filePath = basename(input); - fetch(input) - .then((response) => { + if (input) { + const filePath = fromFileUrl(input); + const currentUrls = gifList() + .filter(x => !x.errored) + .map((x) => x.filePath); + const currentUrlsLength = currentUrls.length; + + if (!currentUrls.includes(filePath)) { + try { + const response = await fetch(input); if (!response.ok) { - throw new Error("Failed to load GIF"); + throw new Error(`Failed to fetch gif: ${response.statusText}`); } - setGifUrls([...gifUrls(), { url: input, filePath, selected: false, errored: false }]); - }) - .catch(() => { - setGifUrls([ - ...gifUrls(), - { url: input, filePath, selected: false, errored: true }, + + const arrayBuffer = await response.arrayBuffer(); + const blob = new Blob([arrayBuffer], { + type: response.headers.get("Content-Type") || "application/octet-stream", + }); + + const file = new File([blob], filePath); + await inputStorage.setItem(file.name, file); + + setGifList([ + ...gifList(), + { + id: currentUrlsLength, + filePath, + selected: false, + errored: false, + }, + ]); + } catch (e) { + console.warn(e); + setGifList([ + ...gifList(), + { + id: currentUrlsLength, + filePath, + selected: false, + errored: true, + }, ]); - }); + + } + } } }; const handleUpload = async (event: Event) => { const files = (event.target as HTMLInputElement).files; if (files) { - const urls = Array.from(files).map((file) => { + const currentUrls = gifList() + .filter((x) => !x.errored) + .map((x) => x.filePath); + const currentUrlsLength = currentUrls.length; + + const newUrls = Array.from(files, (file) => { + if (currentUrls.includes(file.name)) return null; + return { - url: URL.createObjectURL(file), - file, - filePath: file.name, - selected: false, + key: file.name, + value: file, }; - }); - console.log({urls}) - const newUrls = urls.filter( - (urlObj) => !gifUrls().some((gif) => gif.url === urlObj.url) - ); - setGifUrls([...gifUrls(), ...newUrls]); + }).filter((x) => x) as { key: string; value: File }[]; + await inputStorage.setItems(newUrls); + + const newGifs = newUrls.map((x, index) => ({ id: currentUrlsLength + index, filePath: x.key, selected: false })) + setGifList([...gifList(), ...newGifs]); } }; const convertAllToMp4 = async () => { // Step 1: Filter out any GIFs that encountered errors - const validGifs = gifUrls().filter((gif) => !gif.errored); + const validGifs = gifList().filter((gif) => !gif.errored); // Set total GIFs for tracking progress setTotalGifs(validGifs.length); @@ -126,28 +193,31 @@ export function Converter() { UseThreadPools: true, pool, }) + // Distribute conversion tasks among worker threads await Promise.all( Array.from(validGifs.entries(), async ([index, gif]) => { - const result = await pool.pool.execute({ url: gif.url, file: gif.file, index }); + const result = await pool.pool.execute({ filePath: gif.filePath, index }); const output = result.message; - if (output.status === "success" && output.file) { - const videoUrl = URL.createObjectURL(output.file); - convertedVideos.push({ - url: videoUrl, - filePath: - gif.filePath?.replace(/\.gif$/, ".mp4") ?? - `video${index}.mp4`, - selected: false, - }); - - setConversionProgress((prev) => prev + 1); + if (output.status === "success" && output.filePath) { + const videoUrl = await getPreviewUrl(output.filePath, outputStorage); + if (videoUrl) { + convertedVideos.push({ + url: videoUrl, + filePath: + gif.filePath?.replace(/\.gif$/, ".mp4") ?? + `video${index}.mp4`, + selected: false, + }); + + setConversionProgress((prev) => prev + 1); + } } else { // Handle errors by marking them in the UI - const updatedGifs = [...gifUrls()]; + const updatedGifs = [...gifList()]; updatedGifs[index].errored = true; - setGifUrls(updatedGifs); + setGifList(updatedGifs); } }) ); @@ -159,7 +229,15 @@ export function Converter() { for (const chunk of gifChunks) { const chunkResults = await Promise.allSettled( chunk.map(async ([index, gif]) => { - const videoFile = await convertGifToMp4(gif.file ?? gif.url); + const file = await inputStorage.getItem(gif.filePath!); + if (!file) throw new Error(`Couldn't find file at file path ${gif.filePath}`); + + const videoFile = await convertGifToMp4(file); + if (!videoFile) + throw new Error(`Error converting file at file path ${gif.filePath}`); + + await outputStorage.setItem(videoFile.name, videoFile); + const videoUrl = URL.createObjectURL(videoFile!); return { url: videoUrl, @@ -177,9 +255,9 @@ export function Converter() { return result.value; } else { // Handle errors by marking them in the UI - const updatedGifs = [...gifUrls()]; + const updatedGifs = [...gifList()]; updatedGifs[chunk[index][0]].errored = true; - setGifUrls(updatedGifs); + setGifList(updatedGifs); } }).filter(video => video); @@ -216,7 +294,7 @@ export function Converter() { // Step 3: Loop through each video and add it to the zip file for (const video of videos) { try { - const response = await fetch(video.url); + const response = await fetch(video.preview); const blob = await response.blob(); // Add the MP4 file to the zip using its file name @@ -280,7 +358,7 @@ export function Converter() { const writable = await fileHandle.createWritable(); // Fetch the video data and write it to the file - const response = await fetch(video.url); + const response = await fetch(video.preview); const blob = await response.blob(); await writable.write(blob); await writable.close(); @@ -303,7 +381,7 @@ export function Converter() { for (const video of videos) { // Fallback for browsers without showSaveFilePicker const a = document.createElement("a"); - a.href = video.url; + a.href = video.preview; a.download = video.filePath.endsWith(".mp4") ? video.filePath : `${video.filePath}.mp4`; @@ -319,13 +397,13 @@ export function Converter() { }; const toggleGifSelection = (index: number) => { - const updatedGifs = [...gifUrls()]; + const updatedGifs = [...gifList()]; updatedGifs[index].selected = !updatedGifs[index].selected; console.log({ updatedGifs, index, }); - setGifUrls(updatedGifs); + setGifList(updatedGifs); }; const toggleVideoSelection = (index: number) => { @@ -334,20 +412,28 @@ export function Converter() { setMp4Videos(updatedVideos); }; - const deleteSelectedGifs = () => { - gifUrls().forEach((gif) => { + const deleteSelectedGifs = async () => { + await Promise.all( + gifList().map(async (gif) => { if (gif.selected) { - URL.revokeObjectURL(gif.url); + revokePreviewUrls(gif.filePath); + await inputStorage.removeItem(gif.filePath); } - }); - setGifUrls(gifUrls().filter((gif) => !gif.selected)); + })); + + setGifList(gifList().filter((gif) => !gif.selected)); }; effect(() => { - const urls = Array.from(gifUrls()); + const urls = Array.from(gifList()); setLatestInputGifs(urls.slice(-5)); }); + onMount(() => { + inputStorage.clear(); + outputStorage.clear(); + }) + return (

GIF-to-MP4 Converter

@@ -449,7 +535,7 @@ export function Converter() { onClick={() => toggleGifSelection(index)} > {`GIF @@ -462,7 +548,7 @@ export function Converter() {
))} - 5}> + 5}>
- + {(gif, index) => (
toggleGifSelection(index())} > {`GIF @@ -539,7 +625,7 @@ export function Converter() { onClick={() => toggleVideoSelection(index())} > diff --git a/src/libs/opfs-utils.ts b/src/libs/opfs-utils.ts new file mode 100644 index 0000000..a17e1a8 --- /dev/null +++ b/src/libs/opfs-utils.ts @@ -0,0 +1,383 @@ +import { createError } from "unstorage/drivers/utils/index"; + +export const DRIVER_NAME = "opfs"; + +function ignoreNotfoundError(error: any): null { + if (error.name === "NotFoundError") return null; + throw error; +} + +/** + * Normalize a path, removing empty segments and leading/trailing slashes + */ +export function normalizePath(path: string): string { + // Wrap path in slashes, remove . segments and collapse subsequent namespace separators + const normalizedWrappedPath = `/${path}/`.replace( + /[/:]+(\.[/:]+)*[/:]*/g, + "/" + ); + + // Disallow .. segments + if (normalizedWrappedPath.includes("/../")) { + throw createError( + DRIVER_NAME, + `Invalid key: ${JSON.stringify(path)}. It must not contain .. segments` + ); + } + + return normalizedWrappedPath.slice(1, -1); +} + +/** + * Join path segments and normalize the result + * Does not support resolving `.` or `..` segments + */ +export function joinPaths(...segments: string[]): string { + return normalizePath(segments.join("/")); +} + +/** + * Get the directory name of a path + */ +export function dirname(path: string): string { + const normalizedPath = normalizePath(path); + if (!normalizedPath.includes("/")) return ""; + return normalizedPath.slice(0, normalizedPath.lastIndexOf("/")); +} + +/** + * These correspond with the handle's `kind` property + */ +type SpecificHandleType = "file" | "directory"; +type UnspecificHandleType = SpecificHandleType | "any"; + +/** + * Options for the `getHandle` function + */ +type GetHandleOptions = { + /** + * Whether to create the handle if it doesn't exist + * Setting to `true` requires setting a specific `type` + * + * @default false + */ + create?: boolean; + + /** + * Which kind of handle is desired ('file', 'directory', or 'any') + * If a handle of the provided type cannot be obtained, an error will be thrown + * + * @default "any" + */ + type?: UnspecificHandleType; +}; + +/** + * This is a special subset of the `GetHandleOptions` type for improved + * type inference when overloading the `getHandle` function + * + * It represents options with the `create` option set to `true`, + * which requires setting a known `type` + */ +type GetHandleWithCreateOptions = { + create: true; + type: T; +}; + +/** + * This is a special subset of the `GetHandleOptions` type for improved + * type inference when overloading the `getHandle` function + * + * It represents options with the `create` option set to false or missing, + * which allows for a less specific `type` option + */ +type GetHandleWithoutCreateOptions = { + create?: false; + type?: T; +}; + +type HandleTypesByName = { + file: FileSystemFileHandle; + directory: FileSystemDirectoryHandle; + any: FileSystemHandle; +}; + +async function getHandle( + root: FileSystemDirectoryHandle, + path: string | string[], + opts: GetHandleWithCreateOptions +): Promise; +async function getHandle( + root: FileSystemDirectoryHandle, + path: string | string[], + opts?: GetHandleWithoutCreateOptions +): Promise; + +/** + * Get a FileSystemHandle for a given path + * + * @param root The directory handle to start at + * @param path The path to the desired handle + * @param opts Options for obtaining the handle + */ +async function getHandle( + root: FileSystemDirectoryHandle, + path: string | string[], + opts: GetHandleOptions = {} +): Promise { + // Split the path + const segments = Array.isArray(path) ? path : normalizePath(path).split("/"); + + // If there are no segments, return the root handle + if (segments.length === 0) return root; + + const create = Boolean(opts.create); + const handleType: UnspecificHandleType = opts.type ?? "any"; + + // If possibly creating the handle, a specific type must be provided + if (create && handleType !== "file" && handleType !== "directory") { + throw createError( + DRIVER_NAME, + "Invalid handle type, must be `file` or `directory` when creating" + ); + } + + // Resolve remaining segments recursively + if (segments.length > 1) { + const child = await root.getDirectoryHandle(segments[0], { create: true }); + return getHandle(child, segments.slice(1), { + create: create as any, + type: handleType, + }); + } + + // Get the handle for the last segment + + // If the segment is empty, return the root handle + if (segments[0].length === 0) { + if (handleType === "directory" || handleType === "any") { + return root; + } else { + throw createError( + DRIVER_NAME, + "Cannot get a file handle for the root directory" + ); + } + } + + try { + if (handleType === "directory") { + return await root.getDirectoryHandle(segments[0], { create }); + } else if (handleType === "file") { + return await root.getFileHandle(segments[0], { create }); + } else { + return await root.getFileHandle(segments[0]); + } + } catch (error: any) { + if (handleType === "any" && error?.name === "TypeMismatchError") { + return await root.getDirectoryHandle(segments[0]); + } else { + throw error; + } + } +} + +/** + * Check whether a handle exists at a given path + */ +export async function exists( + root: FileSystemDirectoryHandle, + path: string, + type: UnspecificHandleType = "any" +): Promise { + try { + await getHandle(root, path, { type }); + return true; + } catch { + return false; + } +} + +/** + * Write to a file at a given path + * If the file does not exist, it will be created + */ +export async function writeFile( + root: FileSystemDirectoryHandle, + path: string, + data: FileSystemWriteChunkType +): Promise { + await ensureDirectory(root, dirname(path)); + const handle = await getHandle(root, path, { create: true, type: "file" }); + + const writableStream = await handle.createWritable(); + await writableStream.write(data); + await writableStream.close(); +} + +/** + * Get a File object from a given path + */ +export async function getFileObject( + root: FileSystemDirectoryHandle, + path: string +): Promise { + const handle = await getHandle(root, path, { type: "file" }); + return await handle.getFile().catch(ignoreNotfoundError); +} + +export async function readFile( + root: FileSystemDirectoryHandle, + path: string +): Promise; +export async function readFile( + root: FileSystemDirectoryHandle, + path: string, + encoding: string +): Promise; + +/** + * Read a file at a given path + * + * @param root The root handle to read from + * @param path The path to the file to read + * @param encoding The encoding to use when reading the file. Can be omitted to return an Uint8Array. + */ +export async function readFile( + root: FileSystemDirectoryHandle, + path: string, + encoding?: string +): Promise { + const handle = await getHandle(root, path, { type: "file" }); + let file = await handle.getFile().catch(ignoreNotfoundError); + + if (!file) return null; + + const arrayBuffer = await file.arrayBuffer(); + if (!encoding) return new Uint8Array(arrayBuffer); + + const decoder = new TextDecoder(encoding); + return decoder.decode(arrayBuffer); +} + +/** + * Get file handles in a given directory + */ +export async function readdir( + root: FileSystemDirectoryHandle, + directoryPath: string +): Promise { + const handle = await getHandle(root, directoryPath, { + type: "directory", + }).catch(ignoreNotfoundError); + if (!handle) return []; + + const entries: FileSystemHandle[] = []; + + for await (const entry of (handle as any).values()) { + entries.push(entry); + } + + return entries; +} + +/** + * Ensure that a directory exists at a given path + * Throws if a file exists at the given path + */ +async function ensureDirectory( + root: FileSystemDirectoryHandle, + directoryPath: string +): Promise { + await getHandle(root, directoryPath, { create: true, type: "directory" }); +} + +/** + * Get all file paths in the given directory and its subdirectories + */ +export async function readdirRecursive( + root: FileSystemDirectoryHandle, + directoryPath: string, + ignore?: (path: string) => boolean +): Promise { + if (ignore && ignore(directoryPath)) { + return []; + } + const entries = await readdir(root, directoryPath); + const files: string[] = []; + await Promise.all( + entries.map(async (entry) => { + const entryPath = joinPaths(directoryPath, entry.name); + if (entry.kind === "directory") { + const dirFiles = await readdirRecursive(root, entryPath, ignore); + files.push(...dirFiles.map((f) => `${entry.name}/${f}`)); + } else { + if (!ignore?.(entry.name)) { + files.push(entry.name); + } + } + }) + ); + return files; +} + +/** + * Delete a file + * Ignores non-existent files, throws if a directory exists at the given path + */ +export async function unlink( + root: FileSystemDirectoryHandle, + file: string +): Promise { + const handle = await getHandle(root, file, { type: "file" }).catch( + ignoreNotfoundError + ); + if (!handle) return; + if (handle.name === "") { + throw createError(DRIVER_NAME, "Cannot delete root directory"); + } + const parentDirectoryHandle = await getHandle(root, dirname(file), { + type: "directory", + }); + await parentDirectoryHandle.removeEntry(handle.name); +} + +/** + * Remove contents from a directory without deleting the directory itself + */ +export async function removeChildren( + root: FileSystemDirectoryHandle, + directoryPath: string +): Promise { + const handle = await getHandle(root, directoryPath, { + type: "directory", + }).catch(ignoreNotfoundError); + if (!handle) return; + + const files = await readdir(handle, ""); + await Promise.all( + files.map((file) => handle.removeEntry(file.name, { recursive: true })) + ); +} + +/** + * Remove a file or directory with all of its contents + */ +export async function remove( + root: FileSystemDirectoryHandle, + path: string +): Promise { + if (!(await exists(root, path))) return; + + const handle = await getHandle(root, path); + if (handle.name === "") { + throw createError(DRIVER_NAME, "Cannot delete root directory"); + } + + const parentDirectoryHandle = await getHandle(root, dirname(path), { + type: "directory", + }); + await parentDirectoryHandle.removeEntry(handle.name, { + recursive: true, + }); +} \ No newline at end of file diff --git a/src/libs/opfs.ts b/src/libs/opfs.ts new file mode 100644 index 0000000..92a5ec3 --- /dev/null +++ b/src/libs/opfs.ts @@ -0,0 +1,104 @@ +import { defineDriver, createError } from "unstorage/drivers/utils/index"; +import { + DRIVER_NAME, + exists, + getFileObject, + joinPaths, + normalizePath, + readFile, + readdirRecursive, + remove, + removeChildren, + unlink, + writeFile, +} from "./opfs-utils.ts"; + +export interface OPFSStorageOptions { + /** + * The filesystem root to use + * Defaults to the OPFS root at `navigator.storage.getDirectory()` + */ + fs?: FileSystemDirectoryHandle | Promise; + + /** + * The base path to use for all operations + * Defaults to the root directory (empty string) + */ + base?: string; + + /** + * A callback to ignore certain files in getKeys() + */ + ignore?: (path: string) => boolean; +} + +let defaultFsHandle: Promise | undefined; +async function getDefaultFs() { + // Use memoized OPFS handle if available + if (typeof defaultFsHandle !== "undefined") return defaultFsHandle; + + // If no file system is provided, OPFS needs to be available + if (typeof globalThis?.navigator?.storage !== "object") { + throw createError( + DRIVER_NAME, + "No filesystem provided and navigator.storage is not available" + ); + } + + defaultFsHandle = navigator.storage.getDirectory(); + return defaultFsHandle; +} + +export default defineDriver( + (opts: OPFSStorageOptions = {}) => { + opts.base = normalizePath(opts.base ?? ""); + const getFs = () => opts.fs ?? getDefaultFs(); + const resolve = (path: string) => joinPaths(opts.base!, path); + + return { + name: DRIVER_NAME, + options: opts, + async hasItem(key) { + return exists(await getFs(), resolve(key), "file"); + }, + async getItem(key) { + if (!(await exists(await getFs(), resolve(key), "file"))) return null; + return readFile(await getFs(), resolve(key), "utf8"); + }, + async getItemRaw(key) { + if (!(await exists(await getFs(), resolve(key), "file"))) return null; + return readFile(await getFs(), resolve(key)); + }, + async getMeta(key) { + const file = await getFileObject(await getFs(), resolve(key)); + if (!file) return null; + + return { + mtime: new Date(file.lastModified), + size: file.size, + type: file.type, + }; + }, + async setItem(key, value) { + return writeFile(await getFs(), resolve(key), value); + }, + async setItemRaw(key, value) { + return writeFile(await getFs(), resolve(key), value); + }, + async removeItem(key) { + return unlink(await getFs(), resolve(key)); + }, + async getKeys() { + return readdirRecursive(await getFs(), resolve(""), opts.ignore); + }, + async clear() { + if (opts.base!.length === 0) { + // We cannot delete an OPFS root, so we just empty it + await removeChildren(await getFs(), resolve("")); + } else { + await remove(await getFs(), resolve("")); + } + }, + }; + } +); \ No newline at end of file diff --git a/src/workers/gif-to-mp4-worker.ts b/src/workers/gif-to-mp4-worker.ts index 8cfc650..a0e413d 100644 --- a/src/workers/gif-to-mp4-worker.ts +++ b/src/workers/gif-to-mp4-worker.ts @@ -3,15 +3,19 @@ import { ThreadWorker } from "../vendor/poolifier/worker/thread-worker.ts" import { convertGifToMp4 } from "../converters/gif-to-mp4.ts"; +import { basename, fromFileUrl } from "@std/path/posix"; + +import { createStorage } from "unstorage"; +import opfsDriver from "../libs/opfs.ts"; + export interface ConverterInput { - url: string; - file?: File | undefined; + filePath: string; index: number; } export type ConverterMessage = ({ status: 'success', - file?: File + filePath: string } | { status: 'error' }) & { index: number } @@ -21,6 +25,14 @@ export interface ConverterOutput { data?: ConverterInput; } +const inputStorage = createStorage({ + driver: opfsDriver({ base: "tmp" }), +}); + +const outputStorage = createStorage({ + driver: opfsDriver({ base: "output" }), +}); + class GifConverterThreadWorker extends ThreadWorker { constructor() { super(async (data?: ConverterInput) => await this.process(data), { @@ -34,16 +46,23 @@ class GifConverterThreadWorker extends ThreadWorker { - const { url, file, index } = data ?? { url: "about:blank", index: 0 } + const { filePath, index } = data ?? { url: "about:blank", index: 0 } try { - if (url === "about:blank") throw new Error("Invalid URL"); + if (filePath === "about:blank" || !filePath) + throw new Error(`Invalid File Path for Processing ${filePath}`); + + const file = await inputStorage.getItem(filePath!); + if (!file) throw new Error(`Couldn't find file at file path ${filePath}`); + + const videoFile = await convertGifToMp4(file); + if (!videoFile) throw new Error(`Error converting file at file path ${filePath}`); - const videoFile = await convertGifToMp4(file ?? url); + await outputStorage.setItem(videoFile.name, videoFile); return { message: { status: 'success', - file: videoFile, + filePath: videoFile.name, index }, data