diff --git a/.vscode/settings.json b/.vscode/settings.json index 117effb..364f432 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "appimage", "compresso", "Deflaters", + "Exif", "FFPROBE", "ghostscript", "gifsicle", diff --git a/package.json b/package.json index 9eb81c3..c5da46a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "tauri:build:mac:x64": "tauri build --target x86_64-apple-darwin", "tauri:build:mac": "pnpm tauri:build:mac:arm64 && pnpm tauri:build:mac:x64", "generate:latest-json": "node scripts/generate-latest-json.mjs", - "homebrew:release": "./scripts/homebrew-release.sh", + "homebrew:release": "tsx scripts/homebrew-release.mts", "pnpm:devPreinstall": "fnm use", "postinstall": "husky install", "lint-staged": "lint-staged", @@ -73,6 +73,7 @@ "lint-staged": "^13.2.3", "postcss": "^8.4.31", "tailwindcss": "^3.4.0", + "tsx": "^4.21.0", "typescript": "^5.9.3", "vite": "^6.0.3", "vite-plugin-svgr": "^4.3.0" @@ -83,6 +84,8 @@ } }, "lint-staged": { - "src/**/*.{js,ts,jsx,tsx}": ["biome check --write"] + "src/**/*.{js,ts,jsx,tsx}": [ + "biome check --write" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3536da..689c922 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,7 +107,7 @@ importers: version: 0.5.19(tailwindcss@3.4.3) '@tanstack/router-plugin': specifier: ^1.97.22 - version: 1.97.22(@tanstack/react-router@1.97.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)) + version: 1.97.22(@tanstack/react-router@1.97.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0)) '@tauri-apps/cli': specifier: ^2.9.2 version: 2.9.2 @@ -125,7 +125,7 @@ importers: version: 18.2.15 '@vitejs/plugin-react': specifier: ^4.3.2 - version: 4.3.4(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)) + version: 4.3.4(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.31) @@ -141,15 +141,18 @@ importers: tailwindcss: specifier: ^3.4.0 version: 3.4.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^6.0.3 - version: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2) + version: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.32.0)(typescript@5.9.3)(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)) + version: 4.3.0(rollup@4.32.0)(typescript@5.9.3)(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0)) packages: @@ -402,23 +405,17 @@ packages: '@emotion/memoize@0.7.4': resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} - cpu: [arm64] - os: [android] + cpu: [ppc64] + os: [aix] '@esbuild/android-arm64@0.24.2': resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} @@ -426,10 +423,10 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [android] '@esbuild/android-arm@0.24.2': @@ -438,10 +435,10 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm] os: [android] '@esbuild/android-x64@0.24.2': @@ -450,11 +447,11 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] + cpu: [x64] + os: [android] '@esbuild/darwin-arm64@0.24.2': resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} @@ -462,10 +459,10 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.24.2': @@ -474,11 +471,11 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] + cpu: [x64] + os: [darwin] '@esbuild/freebsd-arm64@0.24.2': resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} @@ -486,10 +483,10 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.24.2': @@ -498,11 +495,11 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} - cpu: [arm64] - os: [linux] + cpu: [x64] + os: [freebsd] '@esbuild/linux-arm64@0.24.2': resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} @@ -510,10 +507,10 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.24.2': @@ -522,10 +519,10 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.24.2': @@ -534,10 +531,10 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} - cpu: [loong64] + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.24.2': @@ -546,10 +543,10 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} - cpu: [mips64el] + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.24.2': @@ -558,10 +555,10 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} - cpu: [ppc64] + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.24.2': @@ -570,10 +567,10 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} - cpu: [riscv64] + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.24.2': @@ -582,10 +579,10 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} - cpu: [s390x] + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.24.2': @@ -594,10 +591,10 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} - cpu: [x64] + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.24.2': @@ -606,16 +603,22 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.24.2': resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.24.2': @@ -624,11 +627,11 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] + cpu: [x64] + os: [netbsd] '@esbuild/openbsd-arm64@0.24.2': resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} @@ -636,10 +639,10 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.24.2': @@ -648,11 +651,17 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] - os: [sunos] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] '@esbuild/sunos-x64@0.24.2': resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} @@ -660,11 +669,11 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + cpu: [x64] + os: [sunos] '@esbuild/win32-arm64@0.24.2': resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} @@ -672,10 +681,10 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.24.2': @@ -684,10 +693,10 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.24.2': @@ -696,6 +705,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@formatjs/ecma402-abstract@1.18.0': resolution: {integrity: sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==} @@ -2713,13 +2728,13 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} hasBin: true - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -3727,8 +3742,8 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tsx@4.19.2: - resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true @@ -4236,153 +4251,159 @@ snapshots: '@emotion/memoize@0.7.4': optional: true - '@esbuild/aix-ppc64@0.23.1': - optional: true - '@esbuild/aix-ppc64@0.24.2': optional: true - '@esbuild/android-arm64@0.23.1': + '@esbuild/aix-ppc64@0.27.4': optional: true '@esbuild/android-arm64@0.24.2': optional: true - '@esbuild/android-arm@0.23.1': + '@esbuild/android-arm64@0.27.4': optional: true '@esbuild/android-arm@0.24.2': optional: true - '@esbuild/android-x64@0.23.1': + '@esbuild/android-arm@0.27.4': optional: true '@esbuild/android-x64@0.24.2': optional: true - '@esbuild/darwin-arm64@0.23.1': + '@esbuild/android-x64@0.27.4': optional: true '@esbuild/darwin-arm64@0.24.2': optional: true - '@esbuild/darwin-x64@0.23.1': + '@esbuild/darwin-arm64@0.27.4': optional: true '@esbuild/darwin-x64@0.24.2': optional: true - '@esbuild/freebsd-arm64@0.23.1': + '@esbuild/darwin-x64@0.27.4': optional: true '@esbuild/freebsd-arm64@0.24.2': optional: true - '@esbuild/freebsd-x64@0.23.1': + '@esbuild/freebsd-arm64@0.27.4': optional: true '@esbuild/freebsd-x64@0.24.2': optional: true - '@esbuild/linux-arm64@0.23.1': + '@esbuild/freebsd-x64@0.27.4': optional: true '@esbuild/linux-arm64@0.24.2': optional: true - '@esbuild/linux-arm@0.23.1': + '@esbuild/linux-arm64@0.27.4': optional: true '@esbuild/linux-arm@0.24.2': optional: true - '@esbuild/linux-ia32@0.23.1': + '@esbuild/linux-arm@0.27.4': optional: true '@esbuild/linux-ia32@0.24.2': optional: true - '@esbuild/linux-loong64@0.23.1': + '@esbuild/linux-ia32@0.27.4': optional: true '@esbuild/linux-loong64@0.24.2': optional: true - '@esbuild/linux-mips64el@0.23.1': + '@esbuild/linux-loong64@0.27.4': optional: true '@esbuild/linux-mips64el@0.24.2': optional: true - '@esbuild/linux-ppc64@0.23.1': + '@esbuild/linux-mips64el@0.27.4': optional: true '@esbuild/linux-ppc64@0.24.2': optional: true - '@esbuild/linux-riscv64@0.23.1': + '@esbuild/linux-ppc64@0.27.4': optional: true '@esbuild/linux-riscv64@0.24.2': optional: true - '@esbuild/linux-s390x@0.23.1': + '@esbuild/linux-riscv64@0.27.4': optional: true '@esbuild/linux-s390x@0.24.2': optional: true - '@esbuild/linux-x64@0.23.1': + '@esbuild/linux-s390x@0.27.4': optional: true '@esbuild/linux-x64@0.24.2': optional: true + '@esbuild/linux-x64@0.27.4': + optional: true + '@esbuild/netbsd-arm64@0.24.2': optional: true - '@esbuild/netbsd-x64@0.23.1': + '@esbuild/netbsd-arm64@0.27.4': optional: true '@esbuild/netbsd-x64@0.24.2': optional: true - '@esbuild/openbsd-arm64@0.23.1': + '@esbuild/netbsd-x64@0.27.4': optional: true '@esbuild/openbsd-arm64@0.24.2': optional: true - '@esbuild/openbsd-x64@0.23.1': + '@esbuild/openbsd-arm64@0.27.4': optional: true '@esbuild/openbsd-x64@0.24.2': optional: true - '@esbuild/sunos-x64@0.23.1': + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': optional: true '@esbuild/sunos-x64@0.24.2': optional: true - '@esbuild/win32-arm64@0.23.1': + '@esbuild/sunos-x64@0.27.4': optional: true '@esbuild/win32-arm64@0.24.2': optional: true - '@esbuild/win32-ia32@0.23.1': + '@esbuild/win32-arm64@0.27.4': optional: true '@esbuild/win32-ia32@0.24.2': optional: true - '@esbuild/win32-x64@0.23.1': + '@esbuild/win32-ia32@0.27.4': optional: true '@esbuild/win32-x64@0.24.2': optional: true + '@esbuild/win32-x64@0.27.4': + optional: true + '@formatjs/ecma402-abstract@1.18.0': dependencies: '@formatjs/intl-localematcher': 0.5.2 @@ -6738,12 +6759,12 @@ snapshots: dependencies: '@tanstack/virtual-file-routes': 1.97.8 prettier: 3.4.2 - tsx: 4.19.2 + tsx: 4.21.0 zod: 3.24.1 optionalDependencies: '@tanstack/react-router': 1.97.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.97.22(@tanstack/react-router@1.97.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))': + '@tanstack/router-plugin@1.97.22(@tanstack/react-router@1.97.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.26.7 '@babel/generator': 7.26.5 @@ -6767,7 +6788,7 @@ snapshots: unplugin: 2.1.2 zod: 3.24.1 optionalDependencies: - vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2) + vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0) transitivePeerDependencies: - '@tanstack/react-router' - supports-color @@ -6923,14 +6944,14 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.3.4(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2))': + '@vitejs/plugin-react@4.3.4(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.26.7 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2) + vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -7210,33 +7231,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - esbuild@0.23.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 - esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -7265,6 +7259,35 @@ snapshots: '@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-x64': 0.24.2 + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escalade@3.1.1: {} escalade@3.2.0: {} @@ -8480,9 +8503,9 @@ snapshots: tslib@2.6.2: {} - tsx@4.19.2: + tsx@4.21.0: dependencies: - esbuild: 0.23.1 + esbuild: 0.27.4 get-tsconfig: 4.10.0 optionalDependencies: fsevents: 2.3.3 @@ -8613,18 +8636,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-svgr@4.3.0(rollup@4.32.0)(typescript@5.9.3)(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2)): + vite-plugin-svgr@4.3.0(rollup@4.32.0)(typescript@5.9.3)(vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.32.0) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2) + vite: 6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.2): + vite@6.0.11(@types/node@20.9.0)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.21.0): dependencies: esbuild: 0.24.2 postcss: 8.5.1 @@ -8634,7 +8657,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.1 - tsx: 4.19.2 + tsx: 4.21.0 webpack-virtual-modules@0.6.2: {} diff --git a/scripts/homebrew-release.mts b/scripts/homebrew-release.mts new file mode 100644 index 0000000..24589a8 --- /dev/null +++ b/scripts/homebrew-release.mts @@ -0,0 +1,293 @@ +#!/usr/bin/env node +/** biome-ignore-all lint/suspicious/noConsole: <> */ + +/** + * Homebrew Cask Release Script for CompressO + * + * This script generates Homebrew cask files for both architectures + * + * Output: Generates Homebrew cask files in ./homebrew directory + */ + +import * as crypto from 'node:crypto' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Types +interface Config { + version: string + appName: string + author: string + repo: string + arm64DmgPath: string + x64DmgPath: string + homebrewOutputDir: string + homebrewCaskFile: string + homebrewTemplateFile: string +} + +interface Checksums { + arm64: string + x64: string +} + +// ANSI color codes +const colors = { + red: '\x1b[0;31m', + green: '\x1b[0;32m', + yellow: '\x1b[1;33m', + reset: '\x1b[0m', +} + +function log(message: string): void { + console.log(message) +} + +function logGreen(message: string): void { + log(`${colors.green}${message}${colors.reset}`) +} + +function logRed(message: string): void { + log(`${colors.red}${message}${colors.reset}`) +} + +function logYellow(message: string): void { + log(`${colors.yellow}${message}${colors.reset}`) +} + +/** + * Get the version from package.json + */ +function getVersion(): string { + const packageJsonPath = path.join(__dirname, '..', 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + return packageJson.version +} + +/** + * Initialize configuration + */ +function initConfig(version: string): Config { + const appName = 'CompressO' + const author = 'codeforreal1' + const repo = 'compressO' + const homebrewOutputDir = path.join(__dirname, '..', 'homebrew') + + return { + version, + appName, + author, + repo, + arm64DmgPath: path.join( + __dirname, + '..', + 'src-tauri', + 'target', + 'aarch64-apple-darwin', + 'release', + 'bundle', + 'dmg', + `${appName}_${version}_aarch64.dmg`, + ), + x64DmgPath: path.join( + __dirname, + '..', + 'src-tauri', + 'target', + 'x86_64-apple-darwin', + 'release', + 'bundle', + 'dmg', + `${appName}_${version}_x64.dmg`, + ), + homebrewOutputDir, + homebrewCaskFile: path.join(homebrewOutputDir, 'compresso.rb'), + homebrewTemplateFile: path.join(homebrewOutputDir, 'compresso.rb.template'), + } +} + +/** + * Check if a file exists + */ +function fileExists(filePath: string): boolean { + return fs.existsSync(filePath) +} + +/** + * Calculate SHA256 checksum of a file + */ +function calculateSha256(filePath: string): string { + const hash = crypto.createHash('sha256') + const fileBuffer = fs.readFileSync(filePath) + hash.update(fileBuffer as any) + return hash.digest('hex') +} + +/** + * Read and parse the template file + */ +function readTemplate(templatePath: string): string { + if (!fileExists(templatePath)) { + throw new Error(`Template file not found at ${templatePath}`) + } + return fs.readFileSync(templatePath, 'utf-8') +} + +/** + * Replace placeholders in template + */ +function replacePlaceholders( + template: string, + version: string, + arm64Sha256: string, + x64Sha256: string, +): string { + let content = template + content = content.replace(/\{\{VERSION\}\}/g, version) + content = content.replace(/\{\{ARM64_SHA256\}\}/g, arm64Sha256) + content = content.replace(/\{\{X64_SHA256\}\}/g, x64Sha256) + return content +} + +/** + * Write content to a file + */ +function writeFile(filePath: string, content: string): void { + fs.writeFileSync(filePath, content, 'utf-8') +} + +/** + * Ensure directory exists + */ +function ensureDir(dirPath: string): void { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }) + } +} + +/** + * Generate checksums file content + */ +function generateChecksumsContent( + version: string, + checksums: Checksums, +): string { + return `CompressO v${version} Checksums +================================ + +ARM64 (Apple Silicon): + File: CompressO_${version}_aarch64.dmg + SHA256: ${checksums.arm64} + +Intel (x86_64): + File: CompressO_${version}_x64.dmg + SHA256: ${checksums.x64} +` +} + +/** + * Main function + */ +function main(): void { + logGreen('=== Homebrew Cask Release Script ===') + + const version = getVersion() + logYellow(`Version: ${version}`) + log('') + + const config = initConfig(version) + + // Check if DMG files exist + logGreen('Checking for DMG files...') + + if (!fileExists(config.arm64DmgPath)) { + logRed(`Error: ARM64 DMG not found at ${config.arm64DmgPath}`) + log( + 'Please build the ARM64 version first using: npm run tauri:build -- --target aarch64-apple-darwin', + ) + process.exit(1) + } + + if (!fileExists(config.x64DmgPath)) { + logRed(`Error: x64 DMG not found at ${config.x64DmgPath}`) + log( + 'Please build the x64 version first using: npm run tauri:build -- --target x86_64-apple-darwin', + ) + process.exit(1) + } + + logGreen('✓ ARM64 DMG found') + logGreen('✓ x64 DMG found') + log('') + + // Calculate SHA256 checksums + logGreen('Calculating SHA256 checksums...') + const arm64Sha256 = calculateSha256(config.arm64DmgPath) + const x64Sha256 = calculateSha256(config.x64DmgPath) + + logYellow(`ARM64 SHA256: ${arm64Sha256}`) + logYellow(`x64 SHA256: ${x64Sha256}`) + log('') + + // Create Homebrew cask directories + ensureDir(config.homebrewOutputDir) + const casksDir = path.join(config.homebrewOutputDir, 'casks') + ensureDir(casksDir) + + // Read template + const template = readTemplate(config.homebrewTemplateFile) + + // Generate cask content + logGreen('Generating Homebrew cask file from template...') + const caskContent = replacePlaceholders( + template, + version, + arm64Sha256, + x64Sha256, + ) + + // Write main cask file + writeFile(config.homebrewCaskFile, caskContent) + logGreen(`✓ Main cask file generated: ${config.homebrewCaskFile}`) + + // Write versioned backup + const versionedCaskFile = path.join(casksDir, `compresso-${version}.rb`) + const versionedCaskContent = caskContent.replace( + 'cask "compresso"', + `cask "compresso@${version}"`, + ) + writeFile(versionedCaskFile, versionedCaskContent) + logGreen(`✓ Versioned backup created: ${versionedCaskFile}`) + log('') + + // Generate checksums file + const checksumsFile = path.join(config.homebrewOutputDir, 'checksums.txt') + const checksumsContent = generateChecksumsContent(version, { + arm64: arm64Sha256, + x64: x64Sha256, + }) + writeFile(checksumsFile, checksumsContent) + logGreen(`✓ Checksum file generated: ${checksumsFile}`) + log('') + + // Display the cask file content + logGreen('=== Generated Cask File ===') + log(caskContent) + log('') +} + +try { + main() +} catch (error) { + if (error instanceof Error) { + logRed(`Error: ${error.message}`) + process.exit(1) + } else { + logRed('Unknown error occurred') + process.exit(1) + } +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cbe51c5..166c298 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -443,6 +443,17 @@ dependencies = [ "syn_derive", ] +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 4.0.3", +] + [[package]] name = "brotli" version = "8.0.2" @@ -451,7 +462,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor", + "brotli-decompressor 5.0.0", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -686,7 +707,7 @@ dependencies = [ "bitflags 1.3.2", "strsim 0.8.0", "textwrap", - "unicode-width", + "unicode-width 0.1.14", "vec_map", ] @@ -753,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -780,6 +801,7 @@ dependencies = [ "cocoa", "crossbeam-channel", "dbus", + "exiftool-rs", "image 0.25.9", "img-parts", "infer 0.15.0", @@ -1436,6 +1458,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exiftool-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a972fd52a9c2ea042975eaa3ace0d8e2bdfbb85f435f2eb413d89fff73e63b23" +dependencies = [ + "brotli 7.0.0", + "byteorder", + "encoding_rs", + "flate2", + "thiserror 2.0.17", + "unicode-width 0.2.2", + "winres", + "xml-rs", +] + [[package]] name = "exr" version = "1.74.0" @@ -5650,7 +5688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" dependencies = [ "base64 0.22.1", - "brotli", + "brotli 8.0.2", "ico", "json-patch", "plist", @@ -5908,7 +5946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" dependencies = [ "anyhow", - "brotli", + "brotli 8.0.2", "cargo_metadata", "ctor", "dunce", @@ -5987,7 +6025,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -6181,6 +6219,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.2" @@ -6520,6 +6567,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -7703,6 +7756,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d4a8365..eace829 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -46,6 +46,7 @@ usvg = "0.45.1" svgcleaner = "0.9.5" vtracer = "0.6.5" resvg = "0.44.0" +exiftool-rs = "0.4" [target.'cfg(target_os = "linux")'.dependencies] dbus = "0.9" diff --git a/src-tauri/src/core/domain.rs b/src-tauri/src/core/domain.rs index ca036c4..f0fe7a9 100644 --- a/src-tauri/src/core/domain.rs +++ b/src-tauri/src/core/domain.rs @@ -449,3 +449,62 @@ pub enum MediaCompressionResult { pub struct MediaBatchCompressionResult { pub results: HashMap, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImageBasicInfo { + pub filename: String, + pub format: String, + pub format_long_name: String, + pub mime_type: String, + pub size: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImageDimensions { + pub width: u32, + pub height: u32, + pub aspect_ratio: String, + pub orientation: Option, + pub dpi: Option<(u32, u32)>, // (x, y) + pub megapixels: f64, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImageColorInfo { + pub color_type: String, + pub bit_depth: u8, + pub has_alpha: bool, + pub color_space: Option, + pub pixel_format: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExifTag { + pub key: String, + pub value: String, + pub category: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExifInfo { + pub tags: Vec, + pub make: Option, + pub model: Option, + pub software: Option, + pub date_time_original: Option, + pub date_time_digitized: Option, + pub copyright: Option, + pub artist: Option, + pub gps_coordinates: Option<(f64, f64)>, // (latitude, longitude) + pub lens_model: Option, + pub iso: Option, + pub exposure_time: Option, + pub f_number: Option, + pub focal_length: Option, + pub flash: Option, +} diff --git a/src-tauri/src/core/image.rs b/src-tauri/src/core/image.rs index b032b74..5b25ab1 100644 --- a/src-tauri/src/core/image.rs +++ b/src-tauri/src/core/image.rs @@ -959,7 +959,7 @@ impl ImageCompressor { Ok(()) } - pub fn copy_image_metadata( + fn copy_image_metadata( &self, container: ImageContainer, src: &str, diff --git a/src-tauri/src/core/image_info.rs b/src-tauri/src/core/image_info.rs new file mode 100644 index 0000000..96eada7 --- /dev/null +++ b/src-tauri/src/core/image_info.rs @@ -0,0 +1,296 @@ +use crate::core::domain::{ExifInfo, ExifTag, ImageBasicInfo, ImageColorInfo, ImageDimensions}; +use exiftool_rs::ExifTool; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +pub struct ImageInfo; + +impl ImageInfo { + pub fn get_basic_info(path: &str) -> Result { + let path_obj = Path::new(path); + let file_name = path_obj + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Unknown") + .to_string(); + + let metadata = std::fs::metadata(path).map_err(|e| e.to_string())?; + let size = metadata.len(); + + let format = Self::detect_format(path)?; + + let (format_long_name, mime_type) = match format.as_str() { + "JPEG" => ("Joint Photographic Experts Group", "image/jpeg".to_string()), + "PNG" => ("Portable Network Graphics", "image/png".to_string()), + "GIF" => ("Graphics Interchange Format", "image/gif".to_string()), + "WEBP" => ("WebP Image", "image/webp".to_string()), + "BMP" => ("Bitmap Image", "image/bmp".to_string()), + "TIFF" => ("Tagged Image File Format", "image/tiff".to_string()), + "AVIF" => ("AV1 Image File Format", "image/avif".to_string()), + "ICO" => ("Icon Image", "image/x-icon".to_string()), + "PNM" => ("Portable Any Map", "image/x-portable-anymap".to_string()), + "TGA" => ("Truevision Graphics Adapter", "image/x-tga".to_string()), + _ => ("Unknown Format", "application/octet-stream".to_string()), + }; + + Ok(ImageBasicInfo { + filename: file_name, + format, + format_long_name: format_long_name.to_string(), + mime_type, + size, + }) + } + + pub fn get_dimensions(path: &str) -> Result { + let file = File::open(path).map_err(|e| e.to_string())?; + let reader = BufReader::new(file); + let image = image::ImageReader::new(reader) + .with_guessed_format() + .map_err(|e| e.to_string())? + .decode() + .map_err(|e| e.to_string())?; + + let width = image.width(); + let height = image.height(); + + let gcd = Self::gcd(width, height); + let aspect_ratio = format!("{}/{}", width / gcd, height / gcd); + + let megapixels = (width as f64 * height as f64) / 1_000_000.0; + + let (orientation, dpi) = Self::get_orientation_and_dpi(path)?; + + Ok(ImageDimensions { + width, + height, + aspect_ratio, + orientation, + dpi, + megapixels, + }) + } + + pub fn get_color_info(path: &str) -> Result { + let file = File::open(path).map_err(|e| e.to_string())?; + let reader = BufReader::new(file); + let image = image::ImageReader::new(reader) + .with_guessed_format() + .map_err(|e| e.to_string())? + .decode() + .map_err(|e| e.to_string())?; + + let color = image.color(); + let (color_type, has_alpha) = match color { + image::ColorType::L8 => ("Grayscale (8-bit)".to_string(), false), + image::ColorType::La8 => ("Grayscale with Alpha (8-bit)".to_string(), true), + image::ColorType::La16 => ("Grayscale (16-bit)".to_string(), false), + image::ColorType::Rgb8 => ("RGB (8-bit)".to_string(), false), + image::ColorType::Rgba8 => ("RGBA (8-bit)".to_string(), true), + image::ColorType::Rgb16 => ("RGB (16-bit)".to_string(), false), + image::ColorType::Rgba16 => ("RGBA (16-bit)".to_string(), true), + _ => ("Unknown".to_string(), false), + }; + + let bit_depth = Self::get_bit_depth(color); + + Ok(ImageColorInfo { + color_type, + bit_depth, + has_alpha, + color_space: None, + pixel_format: format!("{:?}", color), + }) + } + + pub fn get_exif_info(path: &str) -> Result { + let et = ExifTool::new(); + let tags_result = et + .extract_info(path) + .map_err(|e| format!("Failed to read EXIF data: {}", e))?; + + let mut tags = Vec::new(); + let mut make = None; + let mut model = None; + let mut software = None; + let mut date_time_original = None; + let mut date_time_digitized = None; + let mut copyright = None; + let mut artist = None; + let mut gps_coordinates = None; + let mut lens_model = None; + let mut iso = None; + let mut exposure_time = None; + let mut f_number = None; + let mut focal_length = None; + let mut flash = None; + + for tag in &tags_result { + let tag_name = tag.name.to_string(); + let tag_value = tag.print_value.to_string(); + let category = "EXIF".to_string(); + + tags.push(ExifTag { + key: tag_name.clone(), + value: tag_value.clone(), + category, + }); + + match tag_name.as_str() { + "Make" => make = Some(tag_value), + "Model" => model = Some(tag_value), + "Software" => software = Some(tag_value), + "DateTimeOriginal" => date_time_original = Some(tag_value), + "DateTimeDigitized" | "CreateDate" => date_time_digitized = Some(tag_value), + "Copyright" => copyright = Some(tag_value), + "Artist" => artist = Some(tag_value), + "LensModel" => lens_model = Some(tag_value), + "ISO" => { + if let Ok(iso_val) = tag_value.parse::() { + iso = Some(iso_val); + } + } + "ExposureTime" => exposure_time = Some(tag_value), + "FNumber" => f_number = Some(tag_value), + "FocalLength" => focal_length = Some(tag_value), + "Flash" => flash = Some(tag_value), + "GPSLatitude" | "GPSLongitude" => { + // GPS coordinates handling will be done separately + } + _ => {} + } + } + + let gps_lat = tags_result + .iter() + .find(|t| t.name == "GPSLatitude") + .and_then(|t| Self::parse_gps_coordinate(&t.print_value)); + let gps_lon = tags_result + .iter() + .find(|t| t.name == "GPSLongitude") + .and_then(|t| Self::parse_gps_coordinate(&t.print_value)); + let gps_lat_ref = tags_result + .iter() + .find(|t| t.name == "GPSLatitudeRef") + .map(|t| t.print_value.to_string()); + let gps_lon_ref = tags_result + .iter() + .find(|t| t.name == "GPSLongitudeRef") + .map(|t| t.print_value.to_string()); + + if let (Some(lat), Some(lon), Some(lat_ref), Some(lon_ref)) = + (gps_lat, gps_lon, gps_lat_ref, gps_lon_ref) + { + let final_lat = if lat_ref == "S" { -lat } else { lat }; + let final_lon = if lon_ref == "W" { -lon } else { lon }; + gps_coordinates = Some((final_lat, final_lon)); + } + + Ok(ExifInfo { + tags, + make, + model, + software, + date_time_original, + date_time_digitized, + copyright, + artist, + gps_coordinates, + lens_model, + iso, + exposure_time, + f_number, + focal_length, + flash, + }) + } + + fn detect_format(path: &str) -> Result { + let file = File::open(path).map_err(|e| e.to_string())?; + let reader = BufReader::new(file); + let format = image::ImageReader::new(reader) + .with_guessed_format() + .map_err(|e| e.to_string())? + .format(); + + Ok(format!("{:?}", format).to_string()) + } + + fn gcd(a: u32, b: u32) -> u32 { + if b == 0 { + a + } else { + Self::gcd(b, a % b) + } + } + + fn get_orientation_and_dpi(path: &str) -> Result<(Option, Option<(u32, u32)>), String> { + let et = ExifTool::new(); + let tags_result = et + .extract_info(path) + .map_err(|e| format!("Failed to read EXIF data: {}", e))?; + + let mut orientation = None; + let mut x_density = None; + let mut y_density = None; + + for tag in &tags_result { + match tag.name.as_str() { + "Orientation" => { + if let Ok(val) = tag.print_value.parse::() { + orientation = Some(val); + } + } + "XResolution" => { + if let Ok(val) = tag.print_value.parse::() { + x_density = Some(val); + } + } + "YResolution" => { + if let Ok(val) = tag.print_value.parse::() { + y_density = Some(val); + } + } + _ => {} + } + } + + let dpi = match (x_density, y_density) { + (Some(x), Some(y)) => Some((x, y)), + _ => None, + }; + + Ok((orientation, dpi)) + } + + fn parse_gps_coordinate(coord_str: &str) -> Option { + let parts: Vec<&str> = coord_str.split_whitespace().collect(); + if parts.len() >= 4 { + let degrees = parts[0].parse::().ok()?; + let minutes = parts + .iter() + .find(|p| p.ends_with('\'')) + .and_then(|p| p.trim_end_matches('\'').parse::().ok())?; + let seconds = parts + .iter() + .find(|p| p.ends_with('"')) + .and_then(|p| p.trim_end_matches('"').parse::().ok())?; + + Some(degrees + minutes / 60.0 + seconds / 3600.0) + } else { + coord_str.parse::().ok() + } + } + + fn get_bit_depth(color: image::ColorType) -> u8 { + match color { + image::ColorType::L8 + | image::ColorType::Rgb8 + | image::ColorType::La8 + | image::ColorType::Rgba8 => 8, + image::ColorType::La16 | image::ColorType::Rgb16 | image::ColorType::Rgba16 => 16, + _ => 8, + } + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 5e058a5..43d6bd3 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -2,4 +2,5 @@ pub mod domain; pub mod ffmpeg; pub mod ffprobe; pub mod image; +pub mod image_info; pub mod media_process; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a95e24f..d9c3c37 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -24,7 +24,10 @@ use tauri_commands::{ copy_file_to_clipboard, delete_cache, delete_file, get_file_metadata, get_image_dimension, get_svg_dimension, move_file, read_files_from_clipboard, read_files_from_paths, }, - image::{compress_images_batch, convert_svg_to_png}, + image::{ + compress_images_batch, convert_svg_to_png, get_exif_info, get_image_basic_info, + get_image_color_info, get_image_dimensions, + }, media::compress_media_batch, updater::{check_update, download_and_install_update}, }; @@ -272,7 +275,11 @@ async fn main() { clear_dock_badge, check_update, download_and_install_update, - convert_svg_to_png + convert_svg_to_png, + get_image_basic_info, + get_image_dimensions, + get_image_color_info, + get_exif_info ]) .build(tauri::generate_context!()) .expect("error while running tauri application") diff --git a/src-tauri/src/tauri_commands/image.rs b/src-tauri/src/tauri_commands/image.rs index 0b07111..1793022 100644 --- a/src-tauri/src/tauri_commands/image.rs +++ b/src-tauri/src/tauri_commands/image.rs @@ -1,8 +1,12 @@ use std::path::PathBuf; use crate::core::{ - domain::{ImageBatchCompressionResult, ImageCompressionConfig}, + domain::{ + ExifInfo, ImageBasicInfo, ImageBatchCompressionResult, ImageColorInfo, + ImageCompressionConfig, ImageDimensions, + }, image, + image_info::ImageInfo, }; use crate::sys::fs::delete_stale_files; @@ -58,3 +62,23 @@ pub async fn convert_svg_to_png( )?; Ok(output_path_str.to_string()) } + +#[tauri::command] +pub async fn get_image_basic_info(image_path: &str) -> Result { + ImageInfo::get_basic_info(image_path) +} + +#[tauri::command] +pub async fn get_image_dimensions(image_path: &str) -> Result { + ImageInfo::get_dimensions(image_path) +} + +#[tauri::command] +pub async fn get_image_color_info(image_path: &str) -> Result { + ImageInfo::get_color_info(image_path) +} + +#[tauri::command] +pub async fn get_exif_info(image_path: &str) -> Result { + ImageInfo::get_exif_info(image_path) +} diff --git a/src/global.css b/src/global.css index 5a10255..041e05a 100644 --- a/src/global.css +++ b/src/global.css @@ -248,3 +248,19 @@ body > * { .advanced-cropper { @apply !bg-white1 dark:!bg-black1; } + +.select-text > p, +.select-text > span, +.select-text > code, +.select-text > pre, +.select-text > h1, +.select-text > h2, +.select-text > h3, +.select-text > h4, +.select-text > h5, +.select-text > h6 { + -webkit-user-select: text !important; + -moz-user-select: text !important; + -ms-user-select: text !important; + user-select: text !important; +} diff --git a/src/routes/(root)/index.tsx b/src/routes/(root)/index.tsx index 35f4268..5cb90bb 100644 --- a/src/routes/(root)/index.tsx +++ b/src/routes/(root)/index.tsx @@ -55,6 +55,7 @@ function Root() { const handleMediaSelection = React.useCallback( async (path: string | string[]) => { if (appProxy.state.isCompressing) return + const videoExtensions = Object.keys(extensions.video) const imageExtensions = Object.keys(extensions.image) diff --git a/src/routes/(root)/ui/ImageInfo.tsx b/src/routes/(root)/ui/ImageInfo.tsx new file mode 100644 index 0000000..106ea7d --- /dev/null +++ b/src/routes/(root)/ui/ImageInfo.tsx @@ -0,0 +1,562 @@ +import { Tab } from '@heroui/react' +import { motion } from 'framer-motion' +import { startCase } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' + +import Code from '@/components/Code' +import Divider from '@/components/Divider' +import ScrollShadow from '@/components/ScrollShadow' +import Spinner from '@/components/Spinner' +import Tabs from '@/components/Tabs' +import { + getExifInfo, + getImageBasicInfo, + getImageColorInfo, + getImageDimensions, +} from '@/tauri/commands/image' +import { + ExifInfo, + ImageBasicInfo, + ImageColorInfo, + ImageDimensions, +} from '@/types/compression' +import { formatBytes } from '@/utils/fs' +import { appProxy } from '../-state' + +type ImageInfoProps = { + mediaIndex: number + onClose?: () => void +} + +const TABS = { + container: { + id: 'container', + title: 'Container', + }, + color: { + id: 'color', + title: 'Color', + }, + exif: { + id: 'exif', + title: 'EXIF', + }, +} as const + +function ImageInfo({ mediaIndex, onClose }: ImageInfoProps) { + if (mediaIndex < 0) return null + + const { + state: { media }, + } = useSnapshot(appProxy) + + const image = + media.length && mediaIndex >= 0 && media[mediaIndex].type === 'image' + ? media[mediaIndex] + : null + const { pathRaw: imagePathRaw, imageInfoRaw } = image ?? {} + if (!image) return null + + const [tab, setTab] = useState('container') + const [loading, setLoading] = useState(false) + + const fetchTabData = useCallback( + async (tabKey: keyof typeof TABS) => { + const image = appProxy.state.media[mediaIndex] + + if (!imagePathRaw || !image || image.type !== 'image') { + return + } + + if (!image.imageInfoRaw) { + image.imageInfoRaw = {} + } + + setLoading(true) + try { + switch (tabKey) { + case 'container': { + if (!image?.imageInfoRaw?.basicInfo) { + const data = await getImageBasicInfo(imagePathRaw) + if (data) { + image.imageInfoRaw.basicInfo = data + } + } + if (!image?.imageInfoRaw?.dimensions) { + const data = await getImageDimensions(imagePathRaw) + if (data) { + image.imageInfoRaw.dimensions = data + } + } + break + } + case 'color': { + if (!image?.imageInfoRaw?.colorInfo) { + const data = await getImageColorInfo(imagePathRaw) + if (data) { + image.imageInfoRaw.colorInfo = data + } + } + break + } + case 'exif': { + if (!image?.imageInfoRaw?.exifInfo) { + const data = await getExifInfo(imagePathRaw) + if (data) { + image.imageInfoRaw.exifInfo = data + } + } + break + } + } + } catch { + // + } finally { + setLoading(false) + } + }, + [imagePathRaw, mediaIndex], + ) + + useEffect(() => { + fetchTabData(tab) + }, [tab, fetchTabData]) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose?.() + } + } + window.addEventListener('keydown', handleKeyDown) + return () => { + window.removeEventListener('keydown', handleKeyDown) + } + }, [onClose]) + + return ( +
+
+ setTab(t as keyof typeof TABS)} + classNames={{ + tabContent: 'text-[11px]', + tab: 'h-6', + }} + > + {Object.values(TABS).map((t) => ( + + ))} + +
+ + + {loading ? ( +
+ +
+ ) : null} + + {!loading && + tab === 'container' && + (imageInfoRaw?.basicInfo || imageInfoRaw?.dimensions) ? ( + + ) : null} + + {!loading && tab === 'color' && imageInfoRaw?.colorInfo ? ( + + ) : null} + + {!loading && tab === 'exif' && imageInfoRaw?.exifInfo ? ( + + ) : null} +
+
+ ) +} + +function BasicInfoDisplay({ + info, + dimensions, + imagePathRaw, +}: { + info: ImageBasicInfo + dimensions?: ImageDimensions + imagePathRaw?: string | null +}) { + return ( +
+ {imagePathRaw ? ( + <> + + {imagePathRaw} + + } + /> + + + ) : null} + + {info.filename ? ( + <> + + + + ) : null} + + {info.format ? ( + <> + + + + ) : null} + + {info.mimeType ? ( + <> + + + + ) : null} + + {info.size > 0 ? ( + <> + + + + ) : null} + + {dimensions && ( + <> +
+
+ + +
+
+ + +
+
+ + {dimensions.aspectRatio ? ( + <> + + + + ) : null} + + {dimensions.orientation ? ( + <> + + + + ) : null} + + {dimensions.dpi ? ( + <> + + + + ) : null} + + + + + )} +
+ ) +} + +function ColorInfoDisplay({ info }: { info: ImageColorInfo }) { + return ( +
+ {info.colorType ? ( + <> + + + + ) : null} + + {info.bitDepth ? ( + <> + + + + ) : null} + + + + + {info.colorSpace ? ( + <> + + + + ) : null} + + {info.pixelFormat ? ( + <> + + + + ) : null} +
+ ) +} + +function ExifDisplay({ exif }: { exif: ExifInfo }) { + const hasCameraInfo = + exif.make || exif.model || exif.lensModel || exif.software + const hasShootingInfo = + exif.iso || + exif.exposureTime || + exif.fNumber || + exif.focalLength || + exif.flash + const hasDateInfo = exif.dateTimeOriginal || exif.dateTimeDigitized + const hasCopyrightInfo = exif.copyright || exif.artist + const hasGpsInfo = exif.gpsCoordinates + + if ( + !hasCameraInfo && + !hasShootingInfo && + !hasDateInfo && + !hasCopyrightInfo && + !hasGpsInfo && + (!exif.tags || exif.tags.length === 0) + ) { + return ( +

+ No EXIF data found +

+ ) + } + + return ( +
+ {hasCameraInfo ? ( + +

+ Camera Information +

+ + {exif.make ? ( + <> + + + + ) : null} + + {exif.model ? ( + <> + + + + ) : null} + + {exif.lensModel ? ( + <> + + + + ) : null} + + {exif.software ? ( + <> + + + + ) : null} +
+ ) : null} + + {hasShootingInfo ? ( + +

+ Shooting Information +

+ + {exif.iso ? ( + <> + + + + ) : null} + + {exif.exposureTime ? ( + <> + + + + ) : null} + + {exif.fNumber ? ( + <> + + + + ) : null} + + {exif.focalLength ? ( + <> + + + + ) : null} + + {exif.flash ? ( + <> + + + + ) : null} +
+ ) : null} + + {hasDateInfo ? ( + +

+ Date Information +

+ + {exif.dateTimeOriginal ? ( + <> + + + + ) : null} + + {exif.dateTimeDigitized ? ( + <> + + + + ) : null} +
+ ) : null} + + {hasCopyrightInfo ? ( + +

+ Copyright Information +

+ + {exif.artist ? ( + <> + + + + ) : null} + + {exif.copyright ? ( + <> + + + + ) : null} +
+ ) : null} + + {hasGpsInfo ? ( + +

+ GPS Information +

+ + {exif.gpsCoordinates ? ( + <> + + + + ) : null} +
+ ) : null} + + {exif.tags && exif.tags.length > 0 ? ( + +

+ All EXIF Tags +

+
+ {exif.tags.map((tag, index) => ( +
+ + +
+ ))} +
+
+ ) : null} +
+ ) +} + +function InfoItem({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ + {label}: + + + {value || 'N/A'} + +
+ ) +} + +export default ImageInfo diff --git a/src/routes/(root)/ui/PreviewSingleMedia.tsx b/src/routes/(root)/ui/PreviewSingleMedia.tsx index a0d05f0..5568173 100644 --- a/src/routes/(root)/ui/PreviewSingleMedia.tsx +++ b/src/routes/(root)/ui/PreviewSingleMedia.tsx @@ -13,6 +13,7 @@ import { Ripple } from '@/ui/Patterns/Ripple' import { slideUpTransition, zoomInTransition } from '@/utils/animation' import { formatDuration } from '@/utils/string' import { cn } from '@/utils/tailwind' +import ImageInfo from './ImageInfo' import MediaOutputCompareSlider from './MediaOutputCompareSlider' import MediaThumbnail from './MediaThumbnail' import styles from './styles.module.css' @@ -223,12 +224,21 @@ function PreviewSingleMedia({ mediaIndex }: PreviewSingleMediaProps) { {...slideUpTransition} >
- { - appProxy.state.showMediaInfo = false - }} - /> + {mediaFile?.type === 'video' ? ( + { + appProxy.state.showMediaInfo = false + }} + /> + ) : mediaFile?.type === 'image' ? ( + { + appProxy.state.showMediaInfo = false + }} + /> + ) : null}