mirror of
https://github.com/codeforreal1/compressO
synced 2026-04-21 15:47:56 +00:00
feat: add view full info for images
This commit is contained in:
parent
abc0ad72f4
commit
b0315c73ba
23 changed files with 1700 additions and 220 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -8,6 +8,7 @@
|
|||
"appimage",
|
||||
"compresso",
|
||||
"Deflaters",
|
||||
"Exif",
|
||||
"FFPROBE",
|
||||
"ghostscript",
|
||||
"gifsicle",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
333
pnpm-lock.yaml
333
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: {}
|
||||
|
||||
|
|
|
|||
293
scripts/homebrew-release.mts
Normal file
293
scripts/homebrew-release.mts
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
74
src-tauri/Cargo.lock
generated
74
src-tauri/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -449,3 +449,62 @@ pub enum MediaCompressionResult {
|
|||
pub struct MediaBatchCompressionResult {
|
||||
pub results: HashMap<String, MediaCompressionResult>,
|
||||
}
|
||||
|
||||
#[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<u32>,
|
||||
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<String>,
|
||||
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<ExifTag>,
|
||||
pub make: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub software: Option<String>,
|
||||
pub date_time_original: Option<String>,
|
||||
pub date_time_digitized: Option<String>,
|
||||
pub copyright: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub gps_coordinates: Option<(f64, f64)>, // (latitude, longitude)
|
||||
pub lens_model: Option<String>,
|
||||
pub iso: Option<u32>,
|
||||
pub exposure_time: Option<String>,
|
||||
pub f_number: Option<String>,
|
||||
pub focal_length: Option<String>,
|
||||
pub flash: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -959,7 +959,7 @@ impl ImageCompressor {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_image_metadata(
|
||||
fn copy_image_metadata(
|
||||
&self,
|
||||
container: ImageContainer,
|
||||
src: &str,
|
||||
|
|
|
|||
296
src-tauri/src/core/image_info.rs
Normal file
296
src-tauri/src/core/image_info.rs
Normal file
|
|
@ -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<ImageBasicInfo, String> {
|
||||
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<ImageDimensions, String> {
|
||||
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<ImageColorInfo, String> {
|
||||
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<ExifInfo, String> {
|
||||
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::<u32>() {
|
||||
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<String, String> {
|
||||
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<u32>, 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::<u32>() {
|
||||
orientation = Some(val);
|
||||
}
|
||||
}
|
||||
"XResolution" => {
|
||||
if let Ok(val) = tag.print_value.parse::<u32>() {
|
||||
x_density = Some(val);
|
||||
}
|
||||
}
|
||||
"YResolution" => {
|
||||
if let Ok(val) = tag.print_value.parse::<u32>() {
|
||||
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<f64> {
|
||||
let parts: Vec<&str> = coord_str.split_whitespace().collect();
|
||||
if parts.len() >= 4 {
|
||||
let degrees = parts[0].parse::<f64>().ok()?;
|
||||
let minutes = parts
|
||||
.iter()
|
||||
.find(|p| p.ends_with('\''))
|
||||
.and_then(|p| p.trim_end_matches('\'').parse::<f64>().ok())?;
|
||||
let seconds = parts
|
||||
.iter()
|
||||
.find(|p| p.ends_with('"'))
|
||||
.and_then(|p| p.trim_end_matches('"').parse::<f64>().ok())?;
|
||||
|
||||
Some(degrees + minutes / 60.0 + seconds / 3600.0)
|
||||
} else {
|
||||
coord_str.parse::<f64>().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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ pub mod domain;
|
|||
pub mod ffmpeg;
|
||||
pub mod ffprobe;
|
||||
pub mod image;
|
||||
pub mod image_info;
|
||||
pub mod media_process;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<ImageBasicInfo, String> {
|
||||
ImageInfo::get_basic_info(image_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_image_dimensions(image_path: &str) -> Result<ImageDimensions, String> {
|
||||
ImageInfo::get_dimensions(image_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_image_color_info(image_path: &str) -> Result<ImageColorInfo, String> {
|
||||
ImageInfo::get_color_info(image_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_exif_info(image_path: &str) -> Result<ExifInfo, String> {
|
||||
ImageInfo::get_exif_info(image_path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
562
src/routes/(root)/ui/ImageInfo.tsx
Normal file
562
src/routes/(root)/ui/ImageInfo.tsx
Normal file
|
|
@ -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<keyof typeof TABS>('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 (
|
||||
<section className="w-full h-full bg-white1 dark:bg-black1 p-6">
|
||||
<div className="w-full flex justify-center">
|
||||
<Tabs
|
||||
aria-label="Image Information"
|
||||
size="sm"
|
||||
selectedKey={tab}
|
||||
onSelectionChange={(t) => setTab(t as keyof typeof TABS)}
|
||||
classNames={{
|
||||
tabContent: 'text-[11px]',
|
||||
tab: 'h-6',
|
||||
}}
|
||||
>
|
||||
{Object.values(TABS).map((t) => (
|
||||
<Tab key={t.id} value={t.id} title={t.title} />
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<ScrollShadow
|
||||
className="mt-6 overflow-y-auto max-h-[calc(100vh-200px)] pb-10"
|
||||
hideScrollBar
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading &&
|
||||
tab === 'container' &&
|
||||
(imageInfoRaw?.basicInfo || imageInfoRaw?.dimensions) ? (
|
||||
<BasicInfoDisplay
|
||||
info={imageInfoRaw.basicInfo as any}
|
||||
dimensions={imageInfoRaw.dimensions as any}
|
||||
imagePathRaw={imagePathRaw}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!loading && tab === 'color' && imageInfoRaw?.colorInfo ? (
|
||||
<ColorInfoDisplay info={imageInfoRaw.colorInfo as any} />
|
||||
) : null}
|
||||
|
||||
{!loading && tab === 'exif' && imageInfoRaw?.exifInfo ? (
|
||||
<ExifDisplay exif={imageInfoRaw.exifInfo as any} />
|
||||
) : null}
|
||||
</ScrollShadow>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function BasicInfoDisplay({
|
||||
info,
|
||||
dimensions,
|
||||
imagePathRaw,
|
||||
}: {
|
||||
info: ImageBasicInfo
|
||||
dimensions?: ImageDimensions
|
||||
imagePathRaw?: string | null
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{imagePathRaw ? (
|
||||
<>
|
||||
<InfoItem
|
||||
label="Full Path"
|
||||
value={
|
||||
<Code size="sm" className="text-xs max-w-[100%] truncate">
|
||||
{imagePathRaw}
|
||||
</Code>
|
||||
}
|
||||
/>
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.filename ? (
|
||||
<>
|
||||
<InfoItem label="File Name" value={info.filename} />
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.format ? (
|
||||
<>
|
||||
<InfoItem label="Format" value={info.format} />
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.mimeType ? (
|
||||
<>
|
||||
<InfoItem label="MIME Type" value={info.mimeType} />
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.size > 0 ? (
|
||||
<>
|
||||
<InfoItem label="Size" value={formatBytes(info.size)} />
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{dimensions && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<InfoItem label="Width" value={`${dimensions.width}px`} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
<div>
|
||||
<InfoItem label="Height" value={`${dimensions.height}px`} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dimensions.aspectRatio ? (
|
||||
<>
|
||||
<InfoItem label="Aspect Ratio" value={dimensions.aspectRatio} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{dimensions.orientation ? (
|
||||
<>
|
||||
<InfoItem
|
||||
label="Orientation"
|
||||
value={`${dimensions.orientation}°`}
|
||||
/>
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{dimensions.dpi ? (
|
||||
<>
|
||||
<InfoItem
|
||||
label="DPI"
|
||||
value={`${dimensions.dpi[0]} × ${dimensions.dpi[1]}`}
|
||||
/>
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<InfoItem
|
||||
label="Megapixels"
|
||||
value={`${dimensions.megapixels.toFixed(2)} MP`}
|
||||
/>
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ColorInfoDisplay({ info }: { info: ImageColorInfo }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{info.colorType ? (
|
||||
<>
|
||||
<InfoItem label="Color Type" value={info.colorType} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.bitDepth ? (
|
||||
<>
|
||||
<InfoItem label="Bit Depth" value={`${info.bitDepth}-bit`} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<InfoItem label="Alpha Channel" value={info.hasAlpha ? 'Yes' : 'No'} />
|
||||
<Divider className="my-3" />
|
||||
|
||||
{info.colorSpace ? (
|
||||
<>
|
||||
<InfoItem label="Color Space" value={info.colorSpace} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{info.pixelFormat ? (
|
||||
<>
|
||||
<InfoItem label="Pixel Format" value={info.pixelFormat} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<p className="text-center text-zinc-500 py-8 select-text">
|
||||
No EXIF data found
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{hasCameraInfo ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Camera Information
|
||||
</h3>
|
||||
|
||||
{exif.make ? (
|
||||
<>
|
||||
<InfoItem label="Make" value={exif.make} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.model ? (
|
||||
<>
|
||||
<InfoItem label="Model" value={exif.model} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.lensModel ? (
|
||||
<>
|
||||
<InfoItem label="Lens Model" value={exif.lensModel} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.software ? (
|
||||
<>
|
||||
<InfoItem label="Software" value={exif.software} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{hasShootingInfo ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Shooting Information
|
||||
</h3>
|
||||
|
||||
{exif.iso ? (
|
||||
<>
|
||||
<InfoItem label="ISO" value={`ISO ${exif.iso}`} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.exposureTime ? (
|
||||
<>
|
||||
<InfoItem label="Exposure Time" value={exif.exposureTime} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.fNumber ? (
|
||||
<>
|
||||
<InfoItem label="Aperture" value={`f/${exif.fNumber}`} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.focalLength ? (
|
||||
<>
|
||||
<InfoItem label="Focal Length" value={exif.focalLength} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.flash ? (
|
||||
<>
|
||||
<InfoItem label="Flash" value={exif.flash} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{hasDateInfo ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Date Information
|
||||
</h3>
|
||||
|
||||
{exif.dateTimeOriginal ? (
|
||||
<>
|
||||
<InfoItem label="Date Taken" value={exif.dateTimeOriginal} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.dateTimeDigitized ? (
|
||||
<>
|
||||
<InfoItem label="Date Digitized" value={exif.dateTimeDigitized} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{hasCopyrightInfo ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Copyright Information
|
||||
</h3>
|
||||
|
||||
{exif.artist ? (
|
||||
<>
|
||||
<InfoItem label="Artist" value={exif.artist} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{exif.copyright ? (
|
||||
<>
|
||||
<InfoItem label="Copyright" value={exif.copyright} />
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{hasGpsInfo ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
GPS Information
|
||||
</h3>
|
||||
|
||||
{exif.gpsCoordinates ? (
|
||||
<>
|
||||
<InfoItem
|
||||
label="Coordinates"
|
||||
value={`${exif.gpsCoordinates[0].toFixed(6)}°, ${exif.gpsCoordinates[1].toFixed(6)}°`}
|
||||
/>
|
||||
<Divider className="my-3" />
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{exif.tags && exif.tags.length > 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
All EXIF Tags
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{exif.tags.map((tag, index) => (
|
||||
<div key={index}>
|
||||
<InfoItem label={startCase(tag.key)} value={tag.value} />
|
||||
<Divider className="my-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InfoItem({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between select-text">
|
||||
<span className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{label}:
|
||||
</span>
|
||||
<span className="text-[13px] text-zinc-800 dark:text-zinc-200 ml-2 max-w-[75%] text-end">
|
||||
{value || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageInfo
|
||||
|
|
@ -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}
|
||||
>
|
||||
<div className="2xl:max-w-[50vw] mx-auto">
|
||||
<VideoInfo
|
||||
mediaIndex={mediaIndex}
|
||||
onClose={() => {
|
||||
appProxy.state.showMediaInfo = false
|
||||
}}
|
||||
/>
|
||||
{mediaFile?.type === 'video' ? (
|
||||
<VideoInfo
|
||||
mediaIndex={mediaIndex}
|
||||
onClose={() => {
|
||||
appProxy.state.showMediaInfo = false
|
||||
}}
|
||||
/>
|
||||
) : mediaFile?.type === 'image' ? (
|
||||
<ImageInfo
|
||||
mediaIndex={mediaIndex}
|
||||
onClose={() => {
|
||||
appProxy.state.showMediaInfo = false
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="absolute top-4 right-4">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
import { readFilesFromClipboard } from '@/tauri/commands/fs'
|
||||
import { appProxy } from '../-state'
|
||||
|
||||
type ReadFilesFromClipboardProps = {
|
||||
onFiles: (files: string[]) => void
|
||||
|
|
@ -8,16 +9,19 @@ type ReadFilesFromClipboardProps = {
|
|||
|
||||
function ReadFilesFromClipboard({ onFiles }: ReadFilesFromClipboardProps) {
|
||||
useEffect(() => {
|
||||
function handleReadFilesFromClipboard() {
|
||||
readFilesFromClipboard()
|
||||
.then((files: string[]) => {
|
||||
if (Array.isArray(files)) {
|
||||
onFiles?.(files)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
//ignore
|
||||
})
|
||||
async function handleReadFilesFromClipboard() {
|
||||
// Large blob files from clipboard can take some time, so we need a blocking UI
|
||||
appProxy.state.isLoadingMediaFiles = true
|
||||
try {
|
||||
const files = await readFilesFromClipboard()
|
||||
if (Array.isArray(files)) {
|
||||
onFiles?.(files)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
appProxy.state.isLoadingMediaFiles = false
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('paste', handleReadFilesFromClipboard)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ const TABS = {
|
|||
id: 'chapters',
|
||||
title: 'Chapters',
|
||||
},
|
||||
metadata: {
|
||||
id: 'metadata',
|
||||
title: 'Metadata',
|
||||
},
|
||||
} as const
|
||||
|
||||
function VideoInfo({ mediaIndex, onClose }: VideoInfoProps) {
|
||||
|
|
@ -144,9 +148,18 @@ function VideoInfo({ mediaIndex, onClose }: VideoInfoProps) {
|
|||
}
|
||||
break
|
||||
}
|
||||
case 'metadata': {
|
||||
if (!video?.videoInfoRaw?.containerInfo) {
|
||||
const data = await getContainerInfo(videoPathRaw)
|
||||
if (data) {
|
||||
video.videoInfoRaw.containerInfo = data
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to load video information')
|
||||
//
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -221,6 +234,10 @@ function VideoInfo({ mediaIndex, onClose }: VideoInfoProps) {
|
|||
videoPath={videoPathRaw}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!loading && tab === 'metadata' && videoInfoRaw?.containerInfo ? (
|
||||
<MetadataDisplay info={videoInfoRaw?.containerInfo as any} />
|
||||
) : null}
|
||||
</ScrollShadow>
|
||||
</section>
|
||||
)
|
||||
|
|
@ -228,7 +245,7 @@ function VideoInfo({ mediaIndex, onClose }: VideoInfoProps) {
|
|||
|
||||
function ContainerInfoDisplay({ info }: { info: ContainerInfo }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 select-text">
|
||||
{info.filename ? (
|
||||
<>
|
||||
<InfoItem
|
||||
|
|
@ -290,32 +307,41 @@ function ContainerInfoDisplay({ info }: { info: ContainerInfo }) {
|
|||
<Divider className="my-1" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{info.tags && info.tags.length > 0 ? (
|
||||
<div>
|
||||
<InfoItem label="Metadata Tags" value=" " />
|
||||
<div className="mt-2 space-y-2 mx-4">
|
||||
{info.tags.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<p className="font-bold text-zinc-600 dark:text-zinc-400 text-[13px]">
|
||||
{startCase(key)}:
|
||||
</p>{' '}
|
||||
<span className="text-zinc-800 dark:text-zinc-200 allow-user-selection text-[13px]">
|
||||
{value ?? 'N/A'}
|
||||
</span>
|
||||
<Divider className="mt-2" />
|
||||
</div>
|
||||
))}
|
||||
function MetadataDisplay({ info }: { info: ContainerInfo }) {
|
||||
if (!info.tags || info.tags.length === 0) {
|
||||
return (
|
||||
<p className="text-center text-zinc-500 py-8 select-text">
|
||||
No metadata found
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 select-text">
|
||||
<div className="mt-2 space-y-2">
|
||||
{info.tags.map(([key, value]) => (
|
||||
<div key={key} className="select-text">
|
||||
<p className="font-bold text-zinc-600 dark:text-zinc-400 text-[13px]">
|
||||
{startCase(key)}:
|
||||
</p>{' '}
|
||||
<span className="text-zinc-800 dark:text-zinc-200 text-[13px]">
|
||||
{value ?? 'N/A'}
|
||||
</span>
|
||||
<Divider className="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoStreamsDisplay({ streams }: { streams: VideoStream[] }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 select-text">
|
||||
{streams.map((stream, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
|
|
@ -324,7 +350,7 @@ function VideoStreamsDisplay({ streams }: { streams: VideoStream[] }) {
|
|||
transition={{ delay: index * 0.05 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary">
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Video Stream {streams.length > 1 ? `${index + 1}` : ''}
|
||||
</h3>
|
||||
|
||||
|
|
@ -502,12 +528,14 @@ function VideoStreamsDisplay({ streams }: { streams: VideoStream[] }) {
|
|||
function AudioStreamsDisplay({ streams }: { streams: AudioStream[] }) {
|
||||
if (streams.length === 0) {
|
||||
return (
|
||||
<p className="text-center text-zinc-500 py-8">No audio streams found</p>
|
||||
<p className="text-center text-zinc-500 py-8 select-text">
|
||||
No audio streams found
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 select-text">
|
||||
{streams.map((stream, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
|
|
@ -516,7 +544,7 @@ function AudioStreamsDisplay({ streams }: { streams: AudioStream[] }) {
|
|||
transition={{ delay: index * 0.05 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-primary">
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Audio Stream {streams.length > 1 ? `${index + 1}` : ''}
|
||||
</h3>
|
||||
|
||||
|
|
@ -595,7 +623,7 @@ function AudioStreamsDisplay({ streams }: { streams: AudioStream[] }) {
|
|||
<InfoItem label="Metadata Tags" value=" " />
|
||||
<div className="mt-2 space-y-2 mx-4">
|
||||
{stream.tags.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<div key={key} className="select-text">
|
||||
<span className="font-medium text-zinc-600 dark:text-zinc-400 text-[13px]">
|
||||
{startCase(key)}:
|
||||
</span>{' '}
|
||||
|
|
@ -648,7 +676,7 @@ function SubtitleStreamsDisplay({
|
|||
|
||||
if (streams.length === 0) {
|
||||
return (
|
||||
<p className="text-center text-zinc-500 py-8">
|
||||
<p className="text-center text-zinc-500 py-8 select-text">
|
||||
No subtitle streams found
|
||||
</p>
|
||||
)
|
||||
|
|
@ -690,14 +718,14 @@ function SubtitleStreamsDisplay({
|
|||
|
||||
toast.success(`Subtitle extracted and saved as ${format.toUpperCase()}.`)
|
||||
} catch {
|
||||
toast.error('Failed to extract subtitle.')
|
||||
//
|
||||
} finally {
|
||||
setDownloadingIndex(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 select-text">
|
||||
{streams.map((stream, index) => {
|
||||
const isExtractable = isSubtitleExtractable(stream.codec)
|
||||
const formatConfig = SUBTITLE_FORMATS[selectedFormat]
|
||||
|
|
@ -710,7 +738,7 @@ function SubtitleStreamsDisplay({
|
|||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-primary">
|
||||
<h3 className="text-lg font-semibold text-primary select-text">
|
||||
Subtitle Stream {index + 1}
|
||||
</h3>
|
||||
<ButtonGroup variant="flat" size="sm">
|
||||
|
|
@ -767,7 +795,7 @@ function SubtitleStreamsDisplay({
|
|||
value={`${stream.codec} (${stream.codecLongName})`}
|
||||
/>
|
||||
{!isExtractable && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 select-text">
|
||||
⚠️ This subtitle format ({stream.codec}) cannot be converted to
|
||||
SRT. It is likely an image-based format (e.g., Blu-ray PGS or
|
||||
DVD VobSub).
|
||||
|
|
@ -795,7 +823,7 @@ function SubtitleStreamsDisplay({
|
|||
stream.disposition.comment ||
|
||||
stream.disposition.karaoke ||
|
||||
stream.disposition.lyrics ? (
|
||||
<div>
|
||||
<div className="select-text">
|
||||
<InfoItem label="Disposition" value=" " />
|
||||
<div className="mt-2 space-y-1 ml-4">
|
||||
{stream.disposition.default ? (
|
||||
|
|
@ -840,11 +868,15 @@ function SubtitleStreamsDisplay({
|
|||
|
||||
function ChaptersDisplay({ chapters }: { chapters: Chapter[] }) {
|
||||
if (chapters.length === 0) {
|
||||
return <p className="text-center text-zinc-500 py-8">No chapters found</p>
|
||||
return (
|
||||
<p className="text-center text-zinc-500 py-8 select-text">
|
||||
No chapters found
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 select-text">
|
||||
{chapters.map((chapter, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
|
|
@ -852,7 +884,7 @@ function ChaptersDisplay({ chapters }: { chapters: Chapter[] }) {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start justify-between select-text">
|
||||
<h3 className="text-lg font-semibold text-primary">
|
||||
Chapter {index + 1} {chapter.id ? `(#${chapter.id})` : ''}
|
||||
</h3>
|
||||
|
|
@ -891,11 +923,11 @@ function ChaptersDisplay({ chapters }: { chapters: Chapter[] }) {
|
|||
|
||||
function InfoItem({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between !select-text !before:select-text">
|
||||
<div className="flex items-baseline justify-between select-text">
|
||||
<span className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{label}:
|
||||
</span>
|
||||
<span className="text-[13px] text-zinc-800 dark:text-zinc-200 ml-2 allow-user-selection max-w-[75%] text-end">
|
||||
<span className="text-[13px] text-zinc-800 dark:text-zinc-200 ml-2 max-w-[75%] text-end">
|
||||
{value || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export function copyFileToClipboard(filePath: string) {
|
|||
export function readFilesFromClipboard() {
|
||||
return core.invoke<string[]>('read_files_from_clipboard')
|
||||
}
|
||||
|
||||
export function readFilesFromPaths(paths: string[]) {
|
||||
return core.invoke<string[]>('read_files_from_paths', { paths })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,33 @@
|
|||
import { core } from '@tauri-apps/api'
|
||||
|
||||
import {
|
||||
ExifInfo,
|
||||
ImageBasicInfo,
|
||||
ImageColorInfo,
|
||||
ImageDimensions,
|
||||
} from '@/types/compression'
|
||||
|
||||
export async function convertSvgToPng(imagePath: string, imageId: string) {
|
||||
return await core.invoke<string>('convert_svg_to_png', {
|
||||
imagePath,
|
||||
imageId,
|
||||
})
|
||||
}
|
||||
|
||||
export function getImageBasicInfo(imagePath: string): Promise<ImageBasicInfo> {
|
||||
return core.invoke('get_image_basic_info', { imagePath })
|
||||
}
|
||||
|
||||
export function getImageDimensions(
|
||||
imagePath: string,
|
||||
): Promise<ImageDimensions> {
|
||||
return core.invoke('get_image_dimensions', { imagePath })
|
||||
}
|
||||
|
||||
export function getImageColorInfo(imagePath: string): Promise<ImageColorInfo> {
|
||||
return core.invoke('get_image_color_info', { imagePath })
|
||||
}
|
||||
|
||||
export function getExifInfo(imagePath: string): Promise<ExifInfo> {
|
||||
return core.invoke('get_exif_info', { imagePath })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import {
|
|||
Chapter,
|
||||
ContainerInfo,
|
||||
compressionPresets,
|
||||
ExifInfo,
|
||||
extensions,
|
||||
ImageBasicInfo,
|
||||
ImageColorInfo,
|
||||
ImageDimensions,
|
||||
MediaTransformHistory,
|
||||
MediaTransforms,
|
||||
SubtitleStream,
|
||||
|
|
@ -180,6 +184,12 @@ export type Image = {
|
|||
config: ImageConfig
|
||||
isConfigDirty?: boolean
|
||||
dimensions?: { width: number; height: number }
|
||||
imageInfoRaw?: {
|
||||
basicInfo?: ImageBasicInfo
|
||||
dimensions?: ImageDimensions
|
||||
colorInfo?: ImageColorInfo
|
||||
exifInfo?: ExifInfo
|
||||
}
|
||||
}
|
||||
|
||||
export type App = {
|
||||
|
|
|
|||
|
|
@ -289,3 +289,52 @@ export type BatchMediaIndividualCompressionResult = {
|
|||
export type MediaBatchCompressionResult = {
|
||||
results: Record<string, MediaCompressionResult>
|
||||
}
|
||||
|
||||
export type ImageBasicInfo = {
|
||||
filename: string
|
||||
format: string
|
||||
formatLongName: string
|
||||
mimeType: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type ImageDimensions = {
|
||||
width: number
|
||||
height: number
|
||||
aspectRatio: string
|
||||
orientation: number | null
|
||||
dpi: [number, number] | null
|
||||
megapixels: number
|
||||
}
|
||||
|
||||
export type ImageColorInfo = {
|
||||
colorType: string
|
||||
bitDepth: number
|
||||
hasAlpha: boolean
|
||||
colorSpace: string | null
|
||||
pixelFormat: string
|
||||
}
|
||||
|
||||
export type ExifTag = {
|
||||
key: string
|
||||
value: string
|
||||
category: string
|
||||
}
|
||||
|
||||
export type ExifInfo = {
|
||||
tags: ExifTag[]
|
||||
make: string | null
|
||||
model: string | null
|
||||
software: string | null
|
||||
dateTimeOriginal: string | null
|
||||
dateTimeDigitized: string | null
|
||||
copyright: string | null
|
||||
artist: string | null
|
||||
gpsCoordinates: [number, number] | null
|
||||
lensModel: string | null
|
||||
iso: number | null
|
||||
exposureTime: string | null
|
||||
fNumber: string | null
|
||||
focalLength: string | null
|
||||
flash: string | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@
|
|||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.mts"],
|
||||
"exclude": ["node_modules", "website", "dist"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue