diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3a2483..b7c860f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v7 with: submodules: recursive - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22.12 cache: npm @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v7 with: submodules: recursive - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22.12 cache: npm @@ -155,9 +155,9 @@ jobs: - name: Restore verified engine payload cache id: engine-cache if: runner.os != 'macOS' - # v4.2.0 pinned to an immutable commit; this cache is an optimization, + # v6.1.0 pinned to an immutable commit; this cache is an optimization, # and a miss always falls back to source provisioning below. - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/${{ matrix.target }} # The target, toolchain fingerprint, lockfiles, provisioning code, @@ -179,7 +179,7 @@ jobs: runner.os != 'macOS' && (steps.engine-cache.outputs.cache-hit != 'true' || steps.engine-cache-validation.outcome != 'success') - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/.aria2-cache/${{ matrix.target }} key: firelink-aria2-build-v1-${{ matrix.target }}-${{ steps.engine-toolchain.outputs.aria2-fingerprint }} @@ -216,7 +216,7 @@ jobs: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.aria2-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/.aria2-cache/${{ matrix.target }} key: firelink-aria2-build-v1-${{ matrix.target }}-${{ steps.engine-toolchain.outputs.aria2-fingerprint }} @@ -226,7 +226,7 @@ jobs: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.engine-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/${{ matrix.target }} key: firelink-engine-payload-v1-${{ matrix.target }}-${{ steps.engine-toolchain.outputs.fingerprint }}-${{ hashFiles('engine-sources.lock.json', 'engines.lock.json', 'scripts/aria2/**', 'scripts/engine-*.js', 'scripts/provision-engines.js', 'scripts/stage-engines.js', 'scripts/verify-binaries.js', 'scripts/aria2-route-contract.js', '.github/workflows/ci.yml', '.github/workflows/release.yml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c696770..0ab6d6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: with: submodules: recursive fetch-depth: 0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22.12 cache: npm @@ -125,9 +125,9 @@ jobs: - name: Restore verified engine payload cache id: engine-cache if: runner.os != 'macOS' - # v4.2.0 pinned to an immutable commit; release jobs never write cache + # v6.1.0 pinned to an immutable commit; release jobs never write cache # entries, so only trusted CI pushes can populate the shared payload. - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/${{ matrix.target }} key: firelink-engine-payload-v1-${{ matrix.target }}-${{ steps.engine-toolchain.outputs.fingerprint }}-${{ hashFiles('engine-sources.lock.json', 'engines.lock.json', 'scripts/aria2/**', 'scripts/engine-*.js', 'scripts/provision-engines.js', 'scripts/stage-engines.js', 'scripts/verify-binaries.js', 'scripts/aria2-route-contract.js', '.github/workflows/ci.yml', '.github/workflows/release.yml') }} @@ -147,7 +147,7 @@ jobs: runner.os != 'macOS' && (steps.engine-cache.outputs.cache-hit != 'true' || steps.engine-cache-validation.outcome != 'success') - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: src-tauri/provisioned-engines/.aria2-cache/${{ matrix.target }} key: firelink-aria2-build-v1-${{ matrix.target }}-${{ steps.engine-toolchain.outputs.aria2-fingerprint }} diff --git a/engine-sources.lock.json b/engine-sources.lock.json index 74a1856..e91a3ea 100644 --- a/engine-sources.lock.json +++ b/engine-sources.lock.json @@ -13,9 +13,9 @@ "sha256": "a0c3101b4158d1dfb7d6a78a7bf0f3de80c96bb423c152beec8beb22786f2238" }, "ffmpeg": { - "version": "9.0.1-29-gad500d59cb", - "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-14-13-17/ffmpeg-n9.0.1-29-gad500d59cb-win64-gpl-9.0.zip", - "sha256": "755186bad114362356359d8e4e6e6f1e2bac3ed4df12b94588f004b411859428" + "version": "9.0.2-3-ga5923073bf", + "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-23-14-55/ffmpeg-n9.0.2-3-ga5923073bf-win64-gpl-9.0.zip", + "sha256": "fdea132b8059ba9dfd6c1ce05bd831a85e665705f03e50162ae2fe599223b5ae" }, "aria2c": { "version": "1.37.0-firelink-native-dns-v1", @@ -45,9 +45,9 @@ "sha256": "c6527f24f4b16031d3ae4fa9f658d5f11534c8d84ce7dc8502420280919c3490" }, "ffmpeg": { - "version": "9.0.1-29-gad500d59cb", - "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-14-13-17/ffmpeg-n9.0.1-29-gad500d59cb-linux64-gpl-9.0.tar.xz", - "sha256": "f78852b0c4286d245a6bc989704ea03f8f7dcd8b66cd1ad55d190131d4f21bb9" + "version": "9.0.2-3-ga5923073bf", + "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-23-14-55/ffmpeg-n9.0.2-3-ga5923073bf-linux64-gpl-9.0.tar.xz", + "sha256": "f984634f1ba66f8f0a771c5ea681d72dd19233fc869e0e6c5aaac7c1e73ded00" }, "aria2c": { "version": "1.37.0-firelink-native-dns-v1", diff --git a/engines.lock.json b/engines.lock.json index f374351..f3f8c71 100644 --- a/engines.lock.json +++ b/engines.lock.json @@ -24,12 +24,12 @@ "patchSha256": "1210eeeb0c82a2fee1ef5d28521569259c18a122d3b439135bba57ee39c5f61d" }, "ffmpeg": { - "version": "9.0.1", + "version": "9.0.2", "source": "https://ffmpeg.org/", "build": "Stable GPLv3 build identified by binary as https://www.martin-riedl.de", - "url": "https://ffmpeg.martin-riedl.de/download/macos/arm64/1787073674_9.0.1/ffmpeg.zip", - "sourceSha256": "8287a1b2229e05eb41859f073e18e6c52c60a778f2f5e6881070fe51b79407fe", - "sha256": "393e4c395020a1cb7cbd77fbe00599ce69d1c6466fee0dbd59d13f86a81a1611" + "url": "https://ffmpeg.martin-riedl.de/download/macos/arm64/1789931890_9.0.2/ffmpeg.zip", + "sourceSha256": "c8ed4c4e6978a03c485edbfe4e0a5dc2380f8a30bba5150531b31b094492d924", + "sha256": "2e11c6f90993cdb79fff84d3f90044d28316b310e75b3e030cfc9a54f2c9d384" }, "deno": { "version": "2.9.7", diff --git a/package-lock.json b/package-lock.json index a0998e8..78c6b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,18 +20,18 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.3", "@tauri-apps/plugin-dialog": "^2.7.3", "@tauri-apps/plugin-fs": "^2.5.2", - "@tauri-apps/plugin-log": "^2.9.1", + "@tauri-apps/plugin-log": "^2.9.2", "@tauri-apps/plugin-notification": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.5", "i18next": "^26.4.2", "lucide-react": "^1.47.0", "react": "^19.3.0", "react-dom": "^19.3.0", - "react-i18next": "^17.0.14", + "react-i18next": "^17.0.15", "zustand": "^5.0.15" }, "devDependencies": { - "@tauri-apps/cli": "^2.11.4", + "@tauri-apps/cli": "^2.11.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", @@ -153,18 +153,18 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.149.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", - "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", - "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", + "integrity": "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==", "cpu": [ "arm" ], @@ -178,9 +178,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", - "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.10.tgz", + "integrity": "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==", "cpu": [ "arm64" ], @@ -194,9 +194,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", - "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.10.tgz", + "integrity": "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==", "cpu": [ "arm64" ], @@ -210,9 +210,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", - "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.10.tgz", + "integrity": "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==", "cpu": [ "x64" ], @@ -226,9 +226,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", - "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.10.tgz", + "integrity": "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==", "cpu": [ "x64" ], @@ -242,9 +242,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", - "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.10.tgz", + "integrity": "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==", "cpu": [ "arm" ], @@ -258,9 +258,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", - "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.10.tgz", + "integrity": "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==", "cpu": [ "arm64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", - "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.10.tgz", + "integrity": "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==", "cpu": [ "arm64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", - "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.10.tgz", + "integrity": "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==", "cpu": [ "ppc64" ], @@ -315,9 +315,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", - "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.10.tgz", + "integrity": "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==", "cpu": [ "s390x" ], @@ -334,9 +334,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", - "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.10.tgz", + "integrity": "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==", "cpu": [ "x64" ], @@ -353,9 +353,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", - "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.10.tgz", + "integrity": "sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==", "cpu": [ "x64" ], @@ -372,9 +372,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", - "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.10.tgz", + "integrity": "sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==", "cpu": [ "arm64" ], @@ -388,9 +388,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", - "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.10.tgz", + "integrity": "sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==", "cpu": [ "arm64" ], @@ -404,9 +404,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", - "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.10.tgz", + "integrity": "sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==", "cpu": [ "x64" ], @@ -648,66 +648,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -765,9 +705,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.5.tgz", + "integrity": "sha512-YbeJ6tctNoo40purO4u3Eeo3RVqmsjJr0NoKQRu+4pvO4gMLzkjta7x0k96SxSLmx+rQ+8um1ThOlw+HLpgFug==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -781,23 +721,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + "@tauri-apps/cli-darwin-arm64": "2.11.5", + "@tauri-apps/cli-darwin-x64": "2.11.5", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.5", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.5", + "@tauri-apps/cli-linux-arm64-musl": "2.11.5", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-musl": "2.11.5", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.5", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.5", + "@tauri-apps/cli-win32-x64-msvc": "2.11.5" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.5.tgz", + "integrity": "sha512-tggOiVOohjIHdiElbjeBfB41s6cmfsq+ZQ0PX0fEtCdsDdZ9cCi54e5gNK7tdPbR6JbdDe7RA4kbQdyyBUUk/Q==", "cpu": [ "arm64" ], @@ -812,9 +752,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.5.tgz", + "integrity": "sha512-SrO+KCbqvG1IvVPdlzQX4GEGZRQDISrmDY9bGcW68DLVFBCC06H+Loz6oyUZETUxsSMR6B4oDfvbQVgD4hxsqQ==", "cpu": [ "x64" ], @@ -829,9 +769,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.5.tgz", + "integrity": "sha512-qLrOaCa8hLD5QVPQRjAgOXvhlTFKXmTliVF773GgvADYcWZSdmncnF5zBFAkdu1uQK8KfQbDlBUwqQYZqBfBRQ==", "cpu": [ "arm" ], @@ -846,9 +786,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.5.tgz", + "integrity": "sha512-Tq32xpQjiQEdIrvwDq7lwHhXemn070CXWdbCvr9rdi9rsv9NFTbQ6izVSIfNE+59QaerxuwtUtZ5IxDNWaoYBw==", "cpu": [ "arm64" ], @@ -866,9 +806,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.5.tgz", + "integrity": "sha512-+OdWKj5Aq+C8HXypQ4t926V8cpVnc0ZcJIdhTXWjVAJHkaNuo8EuQR6KRZKHWIbJJgKdMPCDosvwzIFaBBqHKQ==", "cpu": [ "arm64" ], @@ -886,9 +826,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.5.tgz", + "integrity": "sha512-Q0V2hjpllyDW8dWQZSsITuQDfcY9BpBZuhXkMa2T5AFLZAn+FJPXQSyvnxWNVxRowdQ59Xn0QVtGm6HEHNUEvw==", "cpu": [ "riscv64" ], @@ -906,9 +846,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.5.tgz", + "integrity": "sha512-Xz1s87gFjZJZMQKAhLa77YeiCJM6yVog3f9OxDgVd/KxDIoB1dA5V2xOnMM+SRkOGk9juwIBqesVAjV0kUB0DA==", "cpu": [ "x64" ], @@ -926,9 +866,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.5.tgz", + "integrity": "sha512-6A88wqAZXZHgMaBrGiy/Bpa4GgUnftXM7uquKOeOXCuK78/XdBxEECicK/1oHgrT179XBFOaA6lQEHysjRmGig==", "cpu": [ "x64" ], @@ -946,9 +886,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.5.tgz", + "integrity": "sha512-sRGgF/ObRfLcS3PtUdyTbJtkGq3E+PNZTknvAURwMNUtHw9DU/BF6S5si43y9lS5yEO9bWJCA+GGYvnn6cphQw==", "cpu": [ "arm64" ], @@ -963,9 +903,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.5.tgz", + "integrity": "sha512-bHOv/yxqfKpI7ioIFQi5wOfZ7jkbPzJD/UhXY52I/hrmE3qNh71cohIPyqZB5GQn3LZeJWdCUOqmIg/w6XX+Wg==", "cpu": [ "ia32" ], @@ -980,9 +920,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.5.tgz", + "integrity": "sha512-vaaOmavqdCeS7oMib2banBZemgrC5v54tSkwoBV+bxrXQZhQZrOwE2EO2i4x+CHNtvQUM/6bSs3iymZMAsycWg==", "cpu": [ "x64" ], @@ -1024,9 +964,9 @@ } }, "node_modules/@tauri-apps/plugin-log": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.9.1.tgz", - "integrity": "sha512-8dYNEQOgZcIEqeFtHAsOIGLoptm+j94270Jf2MrGS/zbsYNd4C8DKyOdeYggAyE3ugwr12mTefIfC8lVVEhdlg==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.9.2.tgz", + "integrity": "sha512-QC2uTQyC+4269xawGXzVLDzIvMB2JwzZHd1lg3ltXtXqrauwikqlbNt6Tm3H++1RN0gPtcvp4cBc5bF6u/DUnw==", "license": "MIT OR Apache-2.0", "dependencies": { "@tauri-apps/api": "^2.11.0" @@ -1474,9 +1414,9 @@ } }, "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", - "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, "license": "MIT", "dependencies": { @@ -1530,9 +1470,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -1932,9 +1872,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "funding": [ { "type": "github", @@ -1950,9 +1890,9 @@ } }, "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -2031,9 +1971,9 @@ } }, "node_modules/react-i18next": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.14.tgz", - "integrity": "sha512-ZpMBfJL3BiXPuYHj5QMY1GwvKJNhE1vCxOJQ8BoMBdHP5XONTfi/Qss1nuRlnFdIl72PKo2jPmw+fxuLjRFgaQ==", + "version": "17.0.15", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.15.tgz", + "integrity": "sha512-7uYpx/oBkesLto7cOzOmfITlUBaXHCAcFahmgETMfWr8nbbWRRnVGvz8wsH/xpbNcyoH+NpCcnOQEqWPMO6/hw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2058,12 +1998,12 @@ } }, "node_modules/rolldown": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", - "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.10.tgz", + "integrity": "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.149.0", + "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2073,21 +2013,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.8", - "@rolldown/binding-android-arm64": "1.2.8", - "@rolldown/binding-darwin-arm64": "1.2.8", - "@rolldown/binding-darwin-x64": "1.2.8", - "@rolldown/binding-freebsd-x64": "1.2.8", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", - "@rolldown/binding-linux-arm64-gnu": "1.2.8", - "@rolldown/binding-linux-arm64-musl": "1.2.8", - "@rolldown/binding-linux-ppc64-gnu": "1.2.8", - "@rolldown/binding-linux-s390x-gnu": "1.2.8", - "@rolldown/binding-linux-x64-gnu": "1.2.8", - "@rolldown/binding-linux-x64-musl": "1.2.8", - "@rolldown/binding-openharmony-arm64": "1.2.8", - "@rolldown/binding-win32-arm64-msvc": "1.2.8", - "@rolldown/binding-win32-x64-msvc": "1.2.8" + "@rolldown/binding-android-arm-eabi": "1.2.10", + "@rolldown/binding-android-arm64": "1.2.10", + "@rolldown/binding-darwin-arm64": "1.2.10", + "@rolldown/binding-darwin-x64": "1.2.10", + "@rolldown/binding-freebsd-x64": "1.2.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", + "@rolldown/binding-linux-arm64-gnu": "1.2.10", + "@rolldown/binding-linux-arm64-musl": "1.2.10", + "@rolldown/binding-linux-ppc64-gnu": "1.2.10", + "@rolldown/binding-linux-s390x-gnu": "1.2.10", + "@rolldown/binding-linux-x64-gnu": "1.2.10", + "@rolldown/binding-linux-x64-musl": "1.2.10", + "@rolldown/binding-openharmony-arm64": "1.2.10", + "@rolldown/binding-win32-arm64-msvc": "1.2.10", + "@rolldown/binding-win32-x64-msvc": "1.2.10" } }, "node_modules/scheduler": { @@ -2217,9 +2157,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2647,13 +2587,13 @@ } }, "node_modules/vitest/node_modules/magic-string": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", - "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/why-is-node-running": { diff --git a/package.json b/package.json index ea61263..a7e8ee5 100644 --- a/package.json +++ b/package.json @@ -56,18 +56,18 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.3", "@tauri-apps/plugin-dialog": "^2.7.3", "@tauri-apps/plugin-fs": "^2.5.2", - "@tauri-apps/plugin-log": "^2.9.1", + "@tauri-apps/plugin-log": "^2.9.2", "@tauri-apps/plugin-notification": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.5", "i18next": "^26.4.2", "lucide-react": "^1.47.0", "react": "^19.3.0", "react-dom": "^19.3.0", - "react-i18next": "^17.0.14", + "react-i18next": "^17.0.15", "zustand": "^5.0.15" }, "devDependencies": { - "@tauri-apps/cli": "^2.11.4", + "@tauri-apps/cli": "^2.11.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", diff --git a/scripts/engine-cache-workflow.node-test.js b/scripts/engine-cache-workflow.node-test.js index 533f138..64656eb 100644 --- a/scripts/engine-cache-workflow.node-test.js +++ b/scripts/engine-cache-workflow.node-test.js @@ -12,7 +12,15 @@ const releaseWorkflow = fs.readFileSync( path.join(repositoryRoot, '.github', 'workflows', 'release.yml'), 'utf8', ); -const cacheActionSha = '1bd1e32a3bdc45362d1e726936510720a7c30a57'; +const cacheActionSha = '55cc8345863c7cc4c66a329aec7e433d2d1c52a9'; + +test('CI and release use setup-node v7 while installing the supported app Node version', () => { + for (const workflow of [ciWorkflow, releaseWorkflow]) { + assert.match(workflow, /uses: actions\/setup-node@v7/); + assert.match(workflow, /node-version: 22\.12/); + assert.doesNotMatch(workflow, /actions\/setup-node@v6/); + } +}); function assertSafeEngineCacheWorkflow(workflow) { assert.match(workflow, new RegExp(`actions/cache/restore@${cacheActionSha}`)); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7268c42..0fcf182 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1442,7 +1442,10 @@ dependencies = [ "keepawake", "keyring-core", "log", - "objc", + "objc2", + "objc2-app-kit", + "objc2-core-services", + "objc2-foundation", "regex", "reqwest", "rusqlite", @@ -2125,9 +2128,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -2662,9 +2665,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] @@ -2721,15 +2724,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "markup5ever" version = "0.38.0" @@ -3040,15 +3034,6 @@ dependencies = [ "libc", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "objc2" version = "0.6.4" @@ -3140,6 +3125,21 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "bitflags 2.13.2", + "block2", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -3178,6 +3178,7 @@ dependencies = [ "libc", "objc2", "objc2-core-foundation", + "objc2-core-services", ] [[package]] @@ -3741,9 +3742,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -4807,9 +4808,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.5" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" dependencies = [ "anyhow", "bytes", @@ -5015,9 +5016,9 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4e8861142c21636b03ff6eb9682a073814112e35220435670738a8be7b49896" +checksum = "86f2e3c40b77fd1eb7f8e1b6900fb036aee2da1ea2d3783c91255a82c636649d" dependencies = [ "android_logger", "fern", @@ -5098,9 +5099,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.4" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0cb5c412a5071b69bab6a6df1583cbb89460d4a83b6a24769b08d15b6b1e1" +checksum = "db817fe9295e19b7d8357e900af31edb93703dd9fb6de524b007b47b6afc63b0" dependencies = [ "serde", "serde_json", @@ -5734,7 +5735,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.10.2", + "rand 0.10.3", "sha1", "thiserror 2.0.20", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 63e4ce0..5f58f0a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,7 +43,7 @@ sha2 = "0.11" sha1 = "0.11" base64 = { version = "0.23.1", default-features = false, features = ["std"] } tauri-plugin-deep-link = "2.4.10" -tauri-plugin-single-instance = { version = "2.4.4", features = ["deep-link"] } +tauri-plugin-single-instance = { version = "2.4.5", features = ["deep-link"] } tempfile = "3" thiserror = "2.0.20" axum = "0.8.9" @@ -57,14 +57,17 @@ chrono = "0.4.38" url = "2" rusqlite = { version = "0.40.2", features = ["bundled"] } log = "0.4.34" -tauri-plugin-log = "2.9.1" +tauri-plugin-log = "2.9.2" trash = "5" async-trait = "0.1" keyring-core = "1.0.0" auto-launch = "0.6.0" [target.'cfg(target_os = "macos")'.dependencies] apple-native-keyring-store = { version = "1.0.2", features = ["keychain"] } -objc = "0.2.7" +objc2 = "0.6" +objc2-app-kit = { version = "0.3", default-features = false, features = ["NSApplication", "NSDockTile"] } +objc2-core-services = "0.3" +objc2-foundation = { version = "0.3", default-features = false, features = ["NSAppleEventDescriptor", "NSAppleEventManager", "NSAppleScript", "NSDictionary", "NSNotification", "NSObject", "NSString", "objc2-core-services", "std"] } smappservice-rs = "0.1.3" unicode-normalization = "0.1.25" diff --git a/src-tauri/binaries/ffmpeg-aarch64-apple-darwin b/src-tauri/binaries/ffmpeg-aarch64-apple-darwin index 30be566..cf137d2 100755 Binary files a/src-tauri/binaries/ffmpeg-aarch64-apple-darwin and b/src-tauri/binaries/ffmpeg-aarch64-apple-darwin differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f4ecc31..aa7b4e9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3821,6 +3821,48 @@ struct MainWindowRestoreState { requested: AtomicBool, startup_complete: AtomicBool, started_at_login: AtomicBool, + pending_opened_torrent_paths: std::sync::Mutex, +} + +#[derive(Default)] +struct PendingOpenedTorrentPaths { + app_state_ready: bool, + paths: Vec, +} + +impl MainWindowRestoreState { + fn route_opened_torrent_paths(&self, paths: Vec) -> Option> { + let mut pending = match self.pending_opened_torrent_paths.lock() { + Ok(pending) => pending, + Err(_) => { + log::error!("could not access pending opened Torrent paths during startup"); + return None; + } + }; + + if pending.app_state_ready { + return Some(paths); + } + + let remaining = MAX_DEEP_LINK_URLS.saturating_sub(pending.paths.len()); + let dropped = paths.len().saturating_sub(remaining); + pending.paths.extend(paths.into_iter().take(remaining)); + if dropped > 0 { + log::warn!( + "discarded {dropped} opened Torrent paths because the startup queue reached its capacity" + ); + } + None + } + + fn mark_app_state_ready_and_take_pending_paths(&self) -> Vec { + let Ok(mut pending) = self.pending_opened_torrent_paths.lock() else { + log::error!("could not drain pending opened Torrent paths during startup"); + return Vec::new(); + }; + pending.app_state_ready = true; + std::mem::take(&mut pending.paths) + } } #[cfg(target_os = "macos")] @@ -4220,7 +4262,24 @@ fn dispatch_opened_torrent_paths(app_handle: tauri::AppHandle, paths: Vec().download_coordinator.clone(); + let Some(startup_state) = app_handle.try_state::() else { + log::error!("could not route opened Torrent paths because startup state is unavailable"); + return; + }; + let Some(paths) = startup_state.route_opened_torrent_paths(paths) else { + return; + }; + if paths.is_empty() { + return; + } + let Some(app_state) = app_handle.try_state::() else { + // AppState is published before startup_state is marked ready. Reaching + // this branch indicates a broken startup ordering, so fail without + // panicking inside the platform's single-instance message callback. + log::error!("opened Torrent paths became dispatchable before app state was available"); + return; + }; + let coordinator = app_state.download_coordinator.clone(); tauri::async_runtime::spawn(async move { if let Err(error) = coordinator .send(download::DownloadCmd::CaptureUrls(paths)) @@ -8735,14 +8794,18 @@ fn update_dock_badge( properties_window::ensure_main_window(&caller)?; #[cfg(target_os = "macos")] { - use objc::runtime::Object; - use objc::{class, msg_send, sel, sel_impl}; - use std::ffi::CString; + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + use objc2_foundation::NSString; use std::sync::{Mutex, OnceLock}; static LAST_DOCK_BADGE_STATE: OnceLock> = OnceLock::new(); - let _ = app_handle.run_on_main_thread(move || { + if let Err(error) = app_handle.run_on_main_thread(move || { + let Some(main_thread) = MainThreadMarker::new() else { + log::error!("Tauri dispatched Dock badge update off the macOS main thread"); + return; + }; let state = LAST_DOCK_BADGE_STATE.get_or_init(|| Mutex::new((0, 0))); let Ok(mut state) = state.lock() else { return; @@ -8752,23 +8815,16 @@ fn update_dock_badge( } *state = (session, generation); drop(state); - unsafe { - let app_class = class!(NSApplication); - let app: *mut Object = msg_send![app_class, sharedApplication]; - let dock_tile: *mut Object = msg_send![app, dockTile]; - let label = if count > 0 { - count.to_string() + let dock_tile = NSApplication::sharedApplication(main_thread).dockTile(); + if count > 0 { + let label = NSString::from_str(&count.to_string()); + dock_tile.setBadgeLabel(Some(&label)); } else { - "".to_string() - }; - let c_label = CString::new(label).unwrap(); - let ns_string_class = class!(NSString); - let ns_label: *mut Object = msg_send![ns_string_class, alloc]; - let ns_label: *mut Object = msg_send![ns_label, initWithUTF8String: c_label.as_ptr()]; - let _: () = msg_send![dock_tile, setBadgeLabel: ns_label]; - let _: () = msg_send![ns_label, release]; + dock_tile.setBadgeLabel(None); } - }); + }) { + log::warn!("could not schedule macOS Dock badge update: {error}"); + } } Ok(()) } @@ -13271,43 +13327,66 @@ async fn set_global_speed_limit( }) } +#[cfg(target_os = "macos")] +fn check_automation_permission_on_main_thread( + _main_thread: objc2::MainThreadMarker, +) -> Result<(), String> { + use objc2::msg_send; + use objc2::rc::{autoreleasepool, Retained}; + use objc2::runtime::AnyObject; + use objc2::AnyThread; + use objc2_foundation::{NSAppleEventDescriptor, NSAppleScript, NSDictionary, NSString}; + + autoreleasepool(|_| { + let script_source = NSString::from_str("tell application \"System Events\" to get name"); + let Some(script) = NSAppleScript::initWithSource(NSAppleScript::alloc(), &script_source) + else { + return Err("Automation permission was not granted".to_string()); + }; + + let mut error_info: Option>> = None; + // Apple's SDK documents a nil result on failure, but its header + // omits nullable from the return type. Preserve the real failure + // contract by decoding the Objective-C object result as optional. + // objc2 retains +0 object returns and autoreleased out-parameters when + // converting them to Retained, so they remain valid through this pool. + let result: Option> = + unsafe { msg_send![&*script, executeAndReturnError: &mut error_info] }; + if result.is_none() || error_info.is_some() { + return Err("Automation permission was not granted".to_string()); + } + Ok(()) + }) +} + #[tauri::command] -fn check_automation_permission(caller: tauri::WebviewWindow) -> Result<(), String> { +async fn check_automation_permission( + caller: tauri::WebviewWindow, + app_handle: tauri::AppHandle, +) -> Result<(), String> { properties_window::ensure_main_window(&caller)?; #[cfg(target_os = "macos")] { - use objc::runtime::Object; - use objc::{class, msg_send, sel, sel_impl}; - use std::ffi::CString; - use std::ptr::null_mut; - - unsafe { - objc::rc::autoreleasepool(|| { - let script = "tell application \"System Events\" to get name"; - let c_script = CString::new(script).unwrap(); - let ns_string_class = class!(NSString); - let script_str: *mut Object = msg_send![ns_string_class, alloc]; - let script_str: *mut Object = msg_send![script_str, initWithUTF8String: c_script.as_ptr()]; - - let ns_apple_script: *mut Object = msg_send![class!(NSAppleScript), alloc]; - let ns_apple_script: *mut Object = msg_send![ns_apple_script, initWithSource: script_str]; - - let mut error_dict: *mut Object = null_mut(); - let result: *mut Object = msg_send![ns_apple_script, executeAndReturnError: &mut error_dict]; - - let _: () = msg_send![script_str, release]; - let _: () = msg_send![ns_apple_script, release]; - - if result.is_null() { - return Err("Automation permission was not granted".to_string()); - } - Ok(()) + let (result_sender, result_receiver) = tokio::sync::oneshot::channel(); + app_handle + .run_on_main_thread(move || { + let result = match objc2::MainThreadMarker::new() { + Some(main_thread) => check_automation_permission_on_main_thread(main_thread), + None => Err("macOS Automation check did not run on the main thread".to_string()), + }; + let _ = result_sender.send(result); }) - } + .map_err(|error| format!("could not schedule macOS Automation check: {error}"))?; + result_receiver + .await + .map_err(|_| "macOS Automation check did not complete".to_string())? } #[cfg(not(target_os = "macos"))] - Ok(()) + { + let _ = app_handle; + Ok(()) + } } #[tauri::command] @@ -18858,6 +18937,77 @@ mod tests { ]); } + #[test] + fn buffers_opened_torrent_paths_until_app_state_is_ready() { + let state = super::MainWindowRestoreState::default(); + + assert_eq!( + state.route_opened_torrent_paths(vec!["first.torrent".to_string()]), + None + ); + assert_eq!( + state.route_opened_torrent_paths(vec!["second.torrent".to_string()]), + None + ); + assert_eq!( + state.mark_app_state_ready_and_take_pending_paths(), + vec!["first.torrent".to_string(), "second.torrent".to_string()] + ); + assert_eq!( + state.route_opened_torrent_paths(vec!["third.torrent".to_string()]), + Some(vec!["third.torrent".to_string()]) + ); + } + + #[test] + fn bounds_opened_torrent_paths_buffer_during_startup() { + let state = super::MainWindowRestoreState::default(); + let paths = (0..super::MAX_DEEP_LINK_URLS) + .map(|index| format!("{index}.torrent")) + .collect::>(); + + assert_eq!(state.route_opened_torrent_paths(paths.clone()), None); + assert_eq!( + state.route_opened_torrent_paths(vec!["overflow.torrent".to_string()]), + None + ); + assert_eq!(state.mark_app_state_ready_and_take_pending_paths(), paths); + } + + #[test] + fn opened_torrent_paths_racing_startup_are_never_lost() { + use std::sync::{Arc, Barrier}; + + const PATH_COUNT: usize = 16; + let state = Arc::new(super::MainWindowRestoreState::default()); + let barrier = Arc::new(Barrier::new(PATH_COUNT + 1)); + let workers = (0..PATH_COUNT) + .map(|index| { + let state = Arc::clone(&state); + let barrier = Arc::clone(&barrier); + std::thread::spawn(move || { + barrier.wait(); + state.route_opened_torrent_paths(vec![format!("{index}.torrent")]) + }) + }) + .collect::>(); + + barrier.wait(); + let mut delivered = state.mark_app_state_ready_and_take_pending_paths(); + for worker in workers { + if let Some(paths) = worker.join().expect("path dispatch worker should complete") { + delivered.extend(paths); + } + } + + delivered.sort(); + let mut expected = (0..PATH_COUNT) + .map(|index| format!("{index}.torrent")) + .collect::>(); + expected.sort(); + assert_eq!(delivered, expected); + } + #[test] fn rejects_launch_variants_and_nested_schemes() { let links = [ @@ -20363,6 +20513,16 @@ pub fn run() { queue_manager, }); + let pending_opened_torrent_paths = app + .state::() + .mark_app_state_ready_and_take_pending_paths(); + if !pending_opened_torrent_paths.is_empty() { + dispatch_opened_torrent_paths( + app.handle().clone(), + pending_opened_torrent_paths, + ); + } + if let Err(error) = power_manager.activate() { log::error!("power: failed to activate backend power management: {error}"); } diff --git a/src-tauri/src/login_start.rs b/src-tauri/src/login_start.rs index 65a4912..1149818 100644 --- a/src-tauri/src/login_start.rs +++ b/src-tauri/src/login_start.rs @@ -61,112 +61,116 @@ pub fn install_macos_login_launch_detector() {} #[cfg(target_os = "macos")] mod macos_login_event { use super::{AtomicBool, Once, Ordering}; - use objc::declare::ClassDecl; - use objc::runtime::{Object, Sel}; - use objc::{class, msg_send, sel, sel_impl}; + use objc2::rc::{Allocated, Retained}; + use objc2::{define_class, msg_send, sel, ClassType, MainThreadMarker}; + use objc2_app_kit::{NSApplication, NSApplicationWillFinishLaunchingNotification}; + use objc2_core_services::{AEEventClass, AEEventID, AEKeyword}; + use objc2_foundation::{ + NSAppleEventDescriptor, NSAppleEventManager, NSNotification, NSNotificationCenter, + NSObject, NSObjectProtocol, + }; + use std::sync::OnceLock; // Core Event / Open Application / property-data / login-item marker. - const K_CORE_EVENT_CLASS: u32 = u32::from_be_bytes(*b"aevt"); - const K_AE_OPEN_APPLICATION: u32 = u32::from_be_bytes(*b"oapp"); - const KEY_AE_PROP_DATA: u32 = u32::from_be_bytes(*b"prdt"); + const K_CORE_EVENT_CLASS: AEEventClass = u32::from_be_bytes(*b"aevt"); + const K_AE_OPEN_APPLICATION: AEEventID = u32::from_be_bytes(*b"oapp"); + const KEY_AE_PROP_DATA: AEKeyword = u32::from_be_bytes(*b"prdt"); const KEY_AE_LAUNCHED_AS_LOGIN_ITEM: u32 = u32::from_be_bytes(*b"lgit"); const TYPE_TYPE: u32 = u32::from_be_bytes(*b"type"); static INSTALL: Once = Once::new(); + static OBSERVER: OnceLock> = OnceLock::new(); static LOGIN_LAUNCH: AtomicBool = AtomicBool::new(false); static DECIDED: AtomicBool = AtomicBool::new(false); - extern "C" fn application_will_finish_launching( - this: &Object, - _cmd: Sel, - _notification: *mut Object, - ) { - unsafe { - let manager: *mut Object = - msg_send![class!(NSAppleEventManager), sharedAppleEventManager]; - if manager.is_null() { - return; + define_class!( + // SAFETY: + // - NSObject has no subclassing requirements. + // - The observer has no Rust destructor or thread-confined state. + #[unsafe(super(NSObject))] + #[name = "FirelinkLoginLaunchObserver"] + #[ivars = ()] + struct LoginLaunchObserver; + + impl LoginLaunchObserver { + #[unsafe(method_id(init))] + fn init(this: Allocated) -> Retained { + let this = this.set_ivars(()); + unsafe { msg_send![super(this), init] } } - let _: () = msg_send![ - manager, - setEventHandler: this as *const Object as *mut Object - andSelector: sel!(handleAppleEvent:withReplyEvent:) - forEventClass: K_CORE_EVENT_CLASS - andEventID: K_AE_OPEN_APPLICATION - ]; - } - } - - extern "C" fn handle_apple_event( - _this: &Object, - _cmd: Sel, - event: *mut Object, - _reply_event: *mut Object, - ) { - let is_login_launch = unsafe { - if event.is_null() { - false - } else { - let property_data: *mut Object = - msg_send![event, paramDescriptorForKeyword: KEY_AE_PROP_DATA]; - if property_data.is_null() { - false - } else { - let descriptor_type: u32 = msg_send![property_data, descriptorType]; - if descriptor_type != TYPE_TYPE { - false - } else { - let value: u32 = msg_send![property_data, typeCodeValue]; - value == KEY_AE_LAUNCHED_AS_LOGIN_ITEM - } + #[unsafe(method(applicationWillFinishLaunching:))] + fn application_will_finish_launching(&self, _notification: &NSNotification) { + let manager = NSAppleEventManager::sharedAppleEventManager(); + // SAFETY: The selector is implemented below with the matching + // Apple Event handler signature, and `self` is this observer. + unsafe { + manager.setEventHandler_andSelector_forEventClass_andEventID( + self, + sel!(handleAppleEvent:withReplyEvent:), + K_CORE_EVENT_CLASS, + K_AE_OPEN_APPLICATION, + ); } } - }; - if DECIDED - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - LOGIN_LAUNCH.store(is_login_launch, Ordering::Release); + #[unsafe(method(handleAppleEvent:withReplyEvent:))] + fn handle_apple_event( + &self, + event: Option<&NSAppleEventDescriptor>, + _reply_event: Option<&NSAppleEventDescriptor>, + ) { + let is_login_launch = event + .and_then(|event| event.paramDescriptorForKeyword(KEY_AE_PROP_DATA)) + .is_some_and(|property_data| { + property_data.descriptorType() == TYPE_TYPE + && property_data.typeCodeValue() == KEY_AE_LAUNCHED_AS_LOGIN_ITEM + }); + + if DECIDED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + LOGIN_LAUNCH.store(is_login_launch, Ordering::Release); + } + } } - } + + unsafe impl NSObjectProtocol for LoginLaunchObserver {} + ); pub fn install() { - INSTALL.call_once(|| unsafe { - let mut declaration = ClassDecl::new("FirelinkLoginLaunchObserver", class!(NSObject)) - .expect("could not allocate macOS login-launch observer class"); - declaration.add_method( - sel!(applicationWillFinishLaunching:), - application_will_finish_launching as extern "C" fn(&Object, Sel, *mut Object), - ); - declaration.add_method( - sel!(handleAppleEvent:withReplyEvent:), - handle_apple_event as extern "C" fn(&Object, Sel, *mut Object, *mut Object), - ); - let observer_class = declaration.register(); - let observer: *mut Object = msg_send![observer_class, new]; - if observer.is_null() { - log::error!("could not create macOS login-launch observer"); + INSTALL.call_once(|| { + let Some(main_thread) = MainThreadMarker::new() else { + log::error!("could not install macOS login-launch observer off the main thread"); + return; + }; + + let application = NSApplication::sharedApplication(main_thread); + let observer: Retained = + unsafe { msg_send![LoginLaunchObserver::class(), new] }; + if OBSERVER.set(observer).is_err() { + log::error!("could not retain macOS login-launch observer"); return; } - // NotificationCenter does not own selector-based observers. The - // object is intentionally retained for the process lifetime. - let notification_center: *mut Object = - msg_send![class!(NSNotificationCenter), defaultCenter]; - let application: *mut Object = msg_send![class!(NSApplication), sharedApplication]; - let notification_name: *mut Object = msg_send![ - class!(NSString), - stringWithUTF8String: b"NSApplicationWillFinishLaunchingNotification\0".as_ptr() - ]; - let _: () = msg_send![ - notification_center, - addObserver: observer - selector: sel!(applicationWillFinishLaunching:) - name: notification_name - object: application - ]; + let Some(observer) = OBSERVER.get() else { + log::error!("macOS login-launch observer was not retained"); + return; + }; + let notification_center = NSNotificationCenter::defaultCenter(); + // NotificationCenter does not own selector-based observers. Keep + // the observer retained in OBSERVER for the process lifetime. + // SAFETY: The selector is implemented by LoginLaunchObserver and + // the notification name and object are the matching AppKit values. + unsafe { + notification_center.addObserver_selector_name_object( + observer, + sel!(applicationWillFinishLaunching:), + Some(NSApplicationWillFinishLaunchingNotification), + Some(&application), + ); + } }); }