Compare commits
10 Commits
a588caf743
...
358aae92dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
358aae92dc | ||
|
|
fd90ac1df3 | ||
|
|
3e5d5aa848 | ||
|
|
de0ed2bdc2 | ||
|
|
f34021eae1 | ||
|
|
a0822153c1 | ||
|
|
54408a5933 | ||
|
|
a5f1b165fd | ||
|
|
0c307c319a | ||
|
|
0c5824d85c |
@@ -13,6 +13,7 @@
|
|||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
"lucide-vue-next": "^0.562.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.23
|
specifier: ^10.4.23
|
||||||
version: 10.4.23(postcss@8.5.6)
|
version: 10.4.23(postcss@8.5.6)
|
||||||
|
lucide-vue-next:
|
||||||
|
specifier: ^0.562.0
|
||||||
|
version: 0.562.0(vue@3.5.26(typescript@5.6.3))
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
|
version: 3.0.4(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
|
||||||
@@ -782,6 +785,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0:
|
||||||
|
resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.0.1'
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@@ -1524,6 +1532,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.30.2
|
lightningcss-win32-arm64-msvc: 1.30.2
|
||||||
lightningcss-win32-x64-msvc: 1.30.2
|
lightningcss-win32-x64-msvc: 1.30.2
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0(vue@3.5.26(typescript@5.6.3)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.26(typescript@5.6.3)
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|||||||
187
src-tauri/Cargo.lock
generated
187
src-tauri/Cargo.lock
generated
@@ -2,6 +2,22 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph_rasterizer",
|
||||||
|
"owned_ttf_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph_rasterizer"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -65,6 +81,15 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "approx"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@@ -1175,8 +1200,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1668,6 +1695,24 @@ dependencies = [
|
|||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imageproc"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
|
"approx",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"image",
|
||||||
|
"itertools 0.12.1",
|
||||||
|
"nalgebra",
|
||||||
|
"num",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rand_distr",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imgref"
|
name = "imgref"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -1733,6 +1778,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1920,6 +1974,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -1997,6 +2057,16 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matrixmultiply"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-rayon"
|
name = "maybe-rayon"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2080,6 +2150,21 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nalgebra"
|
||||||
|
version = "0.32.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
"simba",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2137,6 +2222,20 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2147,6 +2246,15 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-complex"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2173,6 +2281,17 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-rational"
|
name = "num-rational"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -2191,6 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2439,6 +2559,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owned_ttf_parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
||||||
|
dependencies = [
|
||||||
|
"ttf-parser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -2953,6 +3082,16 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_distr"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2987,7 +3126,7 @@ dependencies = [
|
|||||||
"built",
|
"built",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"interpolate_name",
|
"interpolate_name",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"log",
|
"log",
|
||||||
@@ -3027,6 +3166,12 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rawpointer"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -3202,6 +3347,15 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "safe_arch"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3483,6 +3637,19 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simba"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"num-complex",
|
||||||
|
"num-traits",
|
||||||
|
"paste",
|
||||||
|
"wide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@@ -4334,6 +4501,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -4607,7 +4780,9 @@ dependencies = [
|
|||||||
name = "watermark-wizard"
|
name = "watermark-wizard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
"image",
|
"image",
|
||||||
|
"imageproc",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4712,6 +4887,16 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wide"
|
||||||
|
version = "0.7.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"safe_arch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = ["protocol-asset"] }
|
tauri = { version = "2", features = ["protocol-asset"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
image = "0.25"
|
image = { version = "0.25", features = ["png", "jpeg", "webp"] }
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
imageproc = "0.25"
|
||||||
|
ab_glyph = "0.2.23"
|
||||||
|
|
||||||
|
|||||||
BIN
src-tauri/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
src-tauri/assets/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
@@ -1,32 +1,86 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
struct ImageItem {
|
struct ImageItem {
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
thumbnail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cache_dir() -> std::path::PathBuf {
|
||||||
|
let mut path = env::temp_dir();
|
||||||
|
path.push("watermark-wizard-thumbs");
|
||||||
|
if !path.exists() {
|
||||||
|
let _ = fs::create_dir_all(&path);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_thumbnail(original_path: &Path) -> Option<String> {
|
||||||
|
let cache_dir = get_cache_dir();
|
||||||
|
|
||||||
|
// Generate simple hash for filename
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
original_path.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
let file_name = format!("{}.jpg", hash);
|
||||||
|
let thumb_path = cache_dir.join(file_name);
|
||||||
|
|
||||||
|
// Return if exists
|
||||||
|
if thumb_path.exists() {
|
||||||
|
return Some(thumb_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate
|
||||||
|
if let Ok(img) = image::open(original_path) {
|
||||||
|
let thumb = img.thumbnail(u32::MAX, 200);
|
||||||
|
let _file = fs::File::create(&thumb_path).ok()?;
|
||||||
|
thumb.save_with_format(&thumb_path, image::ImageFormat::Jpeg).ok()?;
|
||||||
|
|
||||||
|
return Some(thumb_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
async fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
||||||
let mut images = Vec::new();
|
let entries = fs::read_dir(&path).map_err(|e| e.to_string())?;
|
||||||
let dir = fs::read_dir(&path).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
for entry in dir {
|
// Collect valid paths first to avoid holding fs locks or iterators during parallel proc
|
||||||
|
let mut valid_paths = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
let path = entry.path();
|
let p = entry.path();
|
||||||
if path.is_file() {
|
if p.is_file() {
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = p.extension() {
|
||||||
let ext_str = ext.to_string_lossy().to_lowercase();
|
let ext_str = ext.to_string_lossy().to_lowercase();
|
||||||
if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) {
|
if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) {
|
||||||
images.push(ImageItem {
|
valid_paths.push(p);
|
||||||
path: path.to_string_lossy().to_string(),
|
|
||||||
name: path.file_name().unwrap_or_default().to_string_lossy().to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process in parallel
|
||||||
|
let mut images: Vec<ImageItem> = valid_paths.par_iter().filter_map(|path| {
|
||||||
|
let name = path.file_name()?.to_string_lossy().to_string();
|
||||||
|
let path_str = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Generate thumbnail
|
||||||
|
let thumb = generate_thumbnail(path).unwrap_or_else(|| path_str.clone());
|
||||||
|
|
||||||
|
Some(ImageItem {
|
||||||
|
path: path_str,
|
||||||
|
name,
|
||||||
|
thumbnail: thumb,
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
// Sort by name
|
// Sort by name
|
||||||
images.sort_by(|a, b| a.name.cmp(&b.name));
|
images.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
Ok(images)
|
Ok(images)
|
||||||
@@ -34,6 +88,13 @@ fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
|||||||
|
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use image::Pixel;
|
use image::Pixel;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use imageproc::drawing::draw_text_mut;
|
||||||
|
use ab_glyph::{FontRef, PxScale};
|
||||||
|
|
||||||
|
// Embed the font to ensure it's always available without path issues
|
||||||
|
const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/Roboto-Regular.ttf");
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct ZcaResult {
|
struct ZcaResult {
|
||||||
@@ -42,13 +103,184 @@ struct ZcaResult {
|
|||||||
zone: String,
|
zone: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ExportImageTask {
|
||||||
|
path: String,
|
||||||
|
manual_position: Option<ManualPosition>,
|
||||||
|
scale: Option<f64>,
|
||||||
|
opacity: Option<f64>,
|
||||||
|
color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct WatermarkSettings {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
_w_type: String, // 'text' (image is deprecated for now per user request, but keeping struct flexible)
|
||||||
|
text: String, // Was 'source'
|
||||||
|
color: String, // Hex code e.g. "#FFFFFF"
|
||||||
|
opacity: f64,
|
||||||
|
scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height)
|
||||||
|
// Global manual override is deprecated in favor of per-task control, but kept for struct compatibility if needed
|
||||||
|
_manual_override: bool,
|
||||||
|
_manual_position: ManualPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ManualPosition {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hex_color(hex: &str) -> image::Rgba<u8> {
|
||||||
|
let hex = hex.trim_start_matches('#');
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
|
||||||
|
image::Rgba([r, g, b, 255])
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings, output_dir: String) -> Result<String, String> {
|
||||||
let img = image::open(&path).map_err(|e| e.to_string())?;
|
let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
|
||||||
|
|
||||||
|
// Note: Settings are now resolved per-task
|
||||||
|
|
||||||
|
let results: Vec<Result<(), String>> = images.par_iter().map(|task| {
|
||||||
|
let input_path = Path::new(&task.path);
|
||||||
|
let img_result = image::open(input_path);
|
||||||
|
|
||||||
|
if let Ok(dynamic_img) = img_result {
|
||||||
|
let mut base_img = dynamic_img.to_rgba8();
|
||||||
|
let (width, height) = base_img.dimensions();
|
||||||
|
|
||||||
|
// Determine effective settings (Task > Global)
|
||||||
|
let eff_scale = task.scale.unwrap_or(watermark.scale);
|
||||||
|
let eff_opacity = task.opacity.unwrap_or(watermark.opacity);
|
||||||
|
let eff_color_hex = task.color.as_ref().unwrap_or(&watermark.color);
|
||||||
|
|
||||||
|
// Calculate final color
|
||||||
|
let base_color = parse_hex_color(eff_color_hex);
|
||||||
|
let alpha = (eff_opacity * 255.0) as u8;
|
||||||
|
let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]);
|
||||||
|
|
||||||
|
// 1. Calculate Font Scale based on Image Height
|
||||||
|
let mut scale_px = height as f32 * eff_scale as f32;
|
||||||
|
|
||||||
|
// 2. Measure Text
|
||||||
|
let scaled_font = PxScale::from(scale_px);
|
||||||
|
let (t_width, _t_height) = imageproc::drawing::text_size(scaled_font, &font, &watermark.text);
|
||||||
|
|
||||||
|
// 3. Ensure it fits width (Padding 5%)
|
||||||
|
let max_width = (width as f32 * 0.95) as u32;
|
||||||
|
if t_width > max_width {
|
||||||
|
let ratio = max_width as f32 / t_width as f32;
|
||||||
|
scale_px *= ratio;
|
||||||
|
}
|
||||||
|
let final_scale = PxScale::from(scale_px);
|
||||||
|
let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text);
|
||||||
|
|
||||||
|
// 4. Determine Position (Task Specific > ZCA)
|
||||||
|
// If task has manual_position, use it. Otherwise calculate ZCA.
|
||||||
|
let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position {
|
||||||
|
(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
match calculate_zca_internal(&dynamic_img) {
|
||||||
|
Ok(res) => (res.x, res.y),
|
||||||
|
Err(_) => (0.5, 0.97),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate initial top-left based on center
|
||||||
|
let center_x = width as f64 * pos_x_pct;
|
||||||
|
let center_y = height as f64 * pos_y_pct;
|
||||||
|
|
||||||
|
let mut x = (center_x - (final_t_width as f64 / 2.0)) as i32;
|
||||||
|
let mut y = (center_y - (final_t_height as f64 / 2.0)) as i32;
|
||||||
|
|
||||||
|
// 5. Strict Boundary Clamping
|
||||||
|
// We ensure the text box (final_t_width, final_t_height) is always inside (0, 0, width, height)
|
||||||
|
let min_padding = 2; // Absolute minimum pixels from edge
|
||||||
|
|
||||||
|
if x < min_padding { x = min_padding; }
|
||||||
|
if y < min_padding { y = min_padding; }
|
||||||
|
if x + final_t_width as i32 > width as i32 - min_padding {
|
||||||
|
x = width as i32 - final_t_width as i32 - min_padding;
|
||||||
|
}
|
||||||
|
if y + final_t_height as i32 > height as i32 - min_padding {
|
||||||
|
y = height as i32 - final_t_height as i32 - min_padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-clamp just in case of very small images where text is larger than image
|
||||||
|
x = x.max(0);
|
||||||
|
y = y.max(0);
|
||||||
|
|
||||||
|
// 6. Draw Stroke (Simple 4-direction offset for black outline)
|
||||||
|
// Stroke alpha should match text alpha
|
||||||
|
let stroke_color = image::Rgba([0, 0, 0, text_color[3]]);
|
||||||
|
for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] {
|
||||||
|
draw_text_mut(
|
||||||
|
&mut base_img,
|
||||||
|
stroke_color,
|
||||||
|
x + offset.0,
|
||||||
|
y + offset.1,
|
||||||
|
final_scale,
|
||||||
|
&font,
|
||||||
|
&watermark.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Draw Main Text
|
||||||
|
draw_text_mut(
|
||||||
|
&mut base_img,
|
||||||
|
text_color,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
final_scale,
|
||||||
|
&font,
|
||||||
|
&watermark.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
let file_name = input_path.file_name().unwrap_or_default();
|
||||||
|
let output_path = Path::new(&output_dir).join(file_name);
|
||||||
|
|
||||||
|
// Handle format specific saving
|
||||||
|
// JPEG does not support Alpha channel. If we save Rgba8 to Jpeg, it might fail or look wrong.
|
||||||
|
let ext = output_path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
|
||||||
|
if ext == "jpg" || ext == "jpeg" {
|
||||||
|
// Convert to RGB8 (dropping alpha)
|
||||||
|
// Note: This simply drops alpha. If background was transparent, it becomes black.
|
||||||
|
// For photos (JPEGs) this is usually fine as they don't have alpha.
|
||||||
|
let rgb_img = image::DynamicImage::ImageRgba8(base_img).to_rgb8();
|
||||||
|
rgb_img.save(&output_path).map_err(|e| e.to_string())?;
|
||||||
|
} else {
|
||||||
|
// For PNG/WebP etc, keep RGBA
|
||||||
|
base_img.save(&output_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Failed to open {}", task.path))
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let failures: Vec<String> = results.into_iter().filter_map(|r| r.err()).collect();
|
||||||
|
|
||||||
|
if failures.is_empty() {
|
||||||
|
Ok("All images processed successfully".to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("Completed with errors: {:?}", failures))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to reuse logic (adapted from command)
|
||||||
|
fn calculate_zca_internal(img: &image::DynamicImage) -> Result<ZcaResult, String> {
|
||||||
let (width, height) = img.dimensions();
|
let (width, height) = img.dimensions();
|
||||||
|
|
||||||
let bottom_start_y = (height as f64 * 0.8) as u32;
|
let bottom_start_y = (height as f64 * 0.8) as u32;
|
||||||
let zone_height = height - bottom_start_y;
|
let zone_height = height - bottom_start_y;
|
||||||
let zone_width = width / 3;
|
let zone_width = width / 3;
|
||||||
|
|
||||||
let zones = [
|
let zones = [
|
||||||
("Left", 0, bottom_start_y),
|
("Left", 0, bottom_start_y),
|
||||||
("Center", zone_width, bottom_start_y),
|
("Center", zone_width, bottom_start_y),
|
||||||
@@ -57,10 +289,11 @@ fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
|||||||
|
|
||||||
let mut min_std_dev = f64::MAX;
|
let mut min_std_dev = f64::MAX;
|
||||||
let mut best_zone = "Center";
|
let mut best_zone = "Center";
|
||||||
let mut best_pos = (0.5, 0.9); // Default center
|
let mut best_pos = (0.5, 0.97);
|
||||||
|
|
||||||
for (name, start_x, start_y) in zones.iter() {
|
for (name, start_x, start_y) in zones.iter() {
|
||||||
let mut luma_values = Vec::with_capacity((zone_width * zone_height) as usize);
|
let mut luma_values = Vec::with_capacity((zone_width * zone_height) as usize);
|
||||||
|
|
||||||
for y in *start_y..height {
|
for y in *start_y..height {
|
||||||
for x in *start_x..(*start_x + zone_width) {
|
for x in *start_x..(*start_x + zone_width) {
|
||||||
if x >= width { continue; }
|
if x >= width { continue; }
|
||||||
@@ -82,9 +315,9 @@ fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
|||||||
min_std_dev = std_dev;
|
min_std_dev = std_dev;
|
||||||
best_zone = name;
|
best_zone = name;
|
||||||
let center_x_px = *start_x as f64 + (zone_width as f64 / 2.0);
|
let center_x_px = *start_x as f64 + (zone_width as f64 / 2.0);
|
||||||
// Position closer to bottom (75% of the zone height instead of 50%)
|
// Position closer to bottom
|
||||||
// Zone starts at 80%. Height is 20%. 0.8 + 0.2 * 0.75 = 0.95
|
// 0.8 + 0.2 * 0.85 = 0.97
|
||||||
let center_y_px = *start_y as f64 + (zone_height as f64 * 0.75);
|
let center_y_px = *start_y as f64 + (zone_height as f64 * 0.85);
|
||||||
best_pos = (center_x_px / width as f64, center_y_px / height as f64);
|
best_pos = (center_x_px / width as f64, center_y_px / height as f64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,12 +329,17 @@ fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
||||||
|
let img = image::open(&path).map_err(|e| e.to_string())?;
|
||||||
|
calculate_zca_internal(&img)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion])
|
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "watermark-wizard",
|
"title": "watermark-wizard",
|
||||||
"width": 800,
|
"width": 1650,
|
||||||
"height": 600
|
"height": 1000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
90
src/App.vue
90
src/App.vue
@@ -1,11 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HeroView from "./components/HeroView.vue";
|
import HeroView from "./components/HeroView.vue";
|
||||||
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
|
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
|
||||||
|
import SettingsPanel from "./components/SettingsPanel.vue";
|
||||||
import { useGalleryStore } from "./stores/gallery";
|
import { useGalleryStore } from "./stores/gallery";
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { FolderOpen, Download } from 'lucide-vue-next';
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
const store = useGalleryStore();
|
const store = useGalleryStore();
|
||||||
|
const isExporting = ref(false);
|
||||||
|
|
||||||
async function openFolder() {
|
async function openFolder() {
|
||||||
try {
|
try {
|
||||||
@@ -25,26 +29,98 @@ async function openFolder() {
|
|||||||
console.error("Failed to open folder:", e);
|
console.error("Failed to open folder:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportBatch() {
|
||||||
|
if (store.images.length === 0) return;
|
||||||
|
if (!store.watermarkSettings.text) {
|
||||||
|
alert("Please enter watermark text.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outputDir = await open({
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
title: "Select Output Directory"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outputDir && typeof outputDir === 'string') {
|
||||||
|
isExporting.value = true;
|
||||||
|
|
||||||
|
// Map images to include manual settings
|
||||||
|
const exportTasks = store.images.map(img => ({
|
||||||
|
path: img.path,
|
||||||
|
manual_position: img.manualPosition || null,
|
||||||
|
scale: img.scale || null,
|
||||||
|
opacity: img.opacity || null,
|
||||||
|
color: img.color || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Pass dummy globals for rust struct compatibility
|
||||||
|
// The backend struct fields are named _manual_override and _manual_position
|
||||||
|
const rustWatermarkSettings = {
|
||||||
|
...store.watermarkSettings,
|
||||||
|
_manual_override: false,
|
||||||
|
_manual_position: { x: 0.5, y: 0.5 }
|
||||||
|
};
|
||||||
|
|
||||||
|
await invoke('export_batch', {
|
||||||
|
images: exportTasks,
|
||||||
|
watermark: rustWatermarkSettings,
|
||||||
|
outputDir: outputDir
|
||||||
|
});
|
||||||
|
alert("Batch export completed!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Export failed:", e);
|
||||||
|
alert("Export failed: " + e);
|
||||||
|
} finally {
|
||||||
|
isExporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
|
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
|
||||||
<header class="h-12 bg-gray-800 flex items-center justify-between px-4 border-b border-gray-700 shrink-0">
|
<header class="h-14 bg-gray-800 flex items-center justify-between px-6 border-b border-gray-700 shrink-0 shadow-md z-10">
|
||||||
<h1 class="text-sm font-bold tracking-wider">WATERMARK WIZARD</h1>
|
<div class="flex items-center gap-4">
|
||||||
|
<h1 class="text-lg font-bold tracking-wider bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">WATERMARK WIZARD</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
@click="openFolder"
|
@click="openFolder"
|
||||||
class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm transition-colors"
|
class="flex items-center gap-2 bg-gray-700 hover:bg-gray-600 text-gray-200 px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg"
|
||||||
>
|
>
|
||||||
|
<FolderOpen class="w-4 h-4" />
|
||||||
Open Folder
|
Open Folder
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="exportBatch"
|
||||||
|
:disabled="isExporting"
|
||||||
|
class="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:bg-blue-800 disabled:cursor-not-allowed text-white px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg shadow-blue-900/20"
|
||||||
|
>
|
||||||
|
<Download class="w-4 h-4" v-if="!isExporting" />
|
||||||
|
<div v-else class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
{{ isExporting ? 'Exporting...' : 'Export Batch' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 relative bg-black overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
|
<main class="flex-1 relative bg-black flex flex-col min-w-0">
|
||||||
|
<div class="flex-1 relative overflow-hidden">
|
||||||
<HeroView />
|
<HeroView />
|
||||||
</main>
|
</div>
|
||||||
|
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0 z-10">
|
||||||
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0">
|
|
||||||
<ThumbnailStrip />
|
<ThumbnailStrip />
|
||||||
</footer>
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="w-80 shrink-0 h-full border-l border-gray-700">
|
||||||
|
<SettingsPanel />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,40 +1,199 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGalleryStore } from "../stores/gallery";
|
import { useGalleryStore } from "../stores/gallery";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||||
|
|
||||||
const store = useGalleryStore();
|
const store = useGalleryStore();
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStart = ref({ x: 0, y: 0 });
|
||||||
|
const imgRef = ref<HTMLImageElement | null>(null);
|
||||||
|
const parentRef = ref<HTMLElement | null>(null); // The black background container
|
||||||
|
|
||||||
|
// These dimensions will exactly match the rendered image size
|
||||||
|
const imageRect = ref({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const calculateLayout = () => {
|
||||||
|
if (!imgRef.value || !parentRef.value || !store.selectedImage) return;
|
||||||
|
|
||||||
|
// Wait for image natural dimensions to be available
|
||||||
|
const natW = imgRef.value.naturalWidth;
|
||||||
|
const natH = imgRef.value.naturalHeight;
|
||||||
|
|
||||||
|
if (!natW || !natH) return; // Not loaded yet
|
||||||
|
|
||||||
|
const parentW = parentRef.value.clientWidth;
|
||||||
|
const parentH = parentRef.value.clientHeight;
|
||||||
|
|
||||||
|
// Calculate 'contain' fit manually
|
||||||
|
const scale = Math.min(
|
||||||
|
(parentW - 64) / natW, // 64px = 2rem padding * 2 sides (p-8)
|
||||||
|
(parentH - 64) / natH
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prevent scaling up if image is smaller than screen?
|
||||||
|
// Usually hero view scales up to fit. Let's stick to contain logic (can scale up).
|
||||||
|
|
||||||
|
const finalW = Math.floor(natW * scale);
|
||||||
|
const finalH = Math.floor(natH * scale);
|
||||||
|
|
||||||
|
imageRect.value = { width: finalW, height: finalH };
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Observe the parent container (window size changes)
|
||||||
|
if (parentRef.value) {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
calculateLayout();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(parentRef.value);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', calculateLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
|
window.removeEventListener('resize', calculateLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-calculate when image changes
|
||||||
|
watch(() => store.selectedImage, () => {
|
||||||
|
// Reset size until loaded to avoid jump
|
||||||
|
// imageRect.value = { width: 0, height: 0 };
|
||||||
|
nextTick(() => calculateLayout());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use either manual position (if override is true) or ZCA suggestion
|
||||||
|
const position = computed(() => {
|
||||||
|
if (store.selectedImage?.manualPosition) {
|
||||||
|
return store.selectedImage.manualPosition;
|
||||||
|
}
|
||||||
|
// Default to bottom center if no ZCA
|
||||||
|
return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.97 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const effectiveScale = computed(() => store.selectedImage?.scale ?? store.watermarkSettings.scale);
|
||||||
|
const effectiveOpacity = computed(() => store.selectedImage?.opacity ?? store.watermarkSettings.opacity);
|
||||||
|
const effectiveColor = computed(() => store.selectedImage?.color ?? store.watermarkSettings.color);
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging.value = true;
|
||||||
|
dragStart.value = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
// Need exact dimensions for drag calculation
|
||||||
|
// Use imageRect for calculation as it matches the render size
|
||||||
|
if (!isDragging.value || !store.selectedImage || imageRect.value.width === 0) return;
|
||||||
|
|
||||||
|
const rect = imageRect.value;
|
||||||
|
|
||||||
|
const deltaX = (e.clientX - dragStart.value.x) / rect.width;
|
||||||
|
const deltaY = (e.clientY - dragStart.value.y) / rect.height;
|
||||||
|
|
||||||
|
// Update manual position
|
||||||
|
let newX = position.value.x + deltaX;
|
||||||
|
let newY = position.value.y + deltaY;
|
||||||
|
|
||||||
|
// Clamp logic
|
||||||
|
const padding = 0.005;
|
||||||
|
newX = Math.max(padding, Math.min(1 - padding, newX));
|
||||||
|
newY = Math.max(padding, Math.min(1 - padding, newY));
|
||||||
|
|
||||||
|
// Set ONLY for this image
|
||||||
|
store.setImageManualPosition(store.selectedIndex, newX, newY);
|
||||||
|
|
||||||
|
dragStart.value = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex items-center justify-center bg-black relative p-4 overflow-hidden">
|
|
||||||
<div v-if="store.selectedImage" class="relative inline-flex justify-center items-center" style="max-width: 100%; max-height: 100%;">
|
|
||||||
<img
|
|
||||||
:src="convertFileSrc(store.selectedImage.path)"
|
|
||||||
class="max-w-full max-h-full w-auto h-auto block shadow-lg"
|
|
||||||
style="max-height: calc(100vh - 10rem);"
|
|
||||||
alt="Hero Image"
|
|
||||||
/>
|
|
||||||
<!-- Watermark Overlay Placeholder -->
|
|
||||||
<div
|
<div
|
||||||
v-if="store.selectedImage.zcaSuggestion"
|
ref="parentRef"
|
||||||
class="absolute border-2 border-dashed border-green-400 text-green-400 px-4 py-2 bg-black/50 pointer-events-none transition-all duration-500"
|
class="absolute inset-0 flex items-center justify-center bg-black overflow-hidden"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<!--
|
||||||
|
Dynamic Wrapper:
|
||||||
|
Dimensions strictly equal to the rendered image size.
|
||||||
|
This serves as the coordinate system for the watermark.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-if="store.selectedImage"
|
||||||
|
class="relative shadow-2xl"
|
||||||
:style="{
|
:style="{
|
||||||
left: (store.selectedImage.zcaSuggestion.x * 100) + '%',
|
width: imageRect.width + 'px',
|
||||||
top: (store.selectedImage.zcaSuggestion.y * 100) + '%',
|
height: imageRect.height + 'px'
|
||||||
transform: 'translate(-50%, -50%)'
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
Smart Watermark ({{ store.selectedImage.zcaSuggestion.zone }})
|
<img
|
||||||
</div>
|
ref="imgRef"
|
||||||
|
:src="convertFileSrc(store.selectedImage.path)"
|
||||||
|
class="block w-full h-full select-none pointer-events-none"
|
||||||
|
alt="Hero Image"
|
||||||
|
loading="eager"
|
||||||
|
decoding="sync"
|
||||||
|
fetchpriority="high"
|
||||||
|
@load="calculateLayout"
|
||||||
|
@error="(e) => console.error('Hero Image Load Error:', e)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Text Watermark Overlay -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-if="store.watermarkSettings.text"
|
||||||
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 border-2 border-dashed border-white text-white px-4 py-2 bg-black/50 pointer-events-none"
|
class="absolute cursor-move select-none whitespace-nowrap font-sans font-medium"
|
||||||
|
:style="{
|
||||||
|
left: (position.x * 100) + '%',
|
||||||
|
top: (position.y * 100) + '%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
opacity: effectiveOpacity,
|
||||||
|
color: effectiveColor,
|
||||||
|
/* Scale based on HEIGHT of the IMAGE */
|
||||||
|
fontSize: (imageRect.height * effectiveScale) + 'px',
|
||||||
|
height: '0px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
>
|
>
|
||||||
Calculating...
|
<span>
|
||||||
|
{{ store.watermarkSettings.text }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Selection Ring when dragging -->
|
||||||
|
<div v-if="isDragging" class="absolute -inset-2 border border-blue-500 rounded-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-500">
|
<div v-else class="text-gray-500 flex flex-col items-center">
|
||||||
No image selected
|
<p>No image selected</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-container {
|
||||||
|
/* Removed container-type: size to prevent layout collapse */
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 0 #000,
|
||||||
|
1px -1px 0 #000,
|
||||||
|
-1px 1px 0 #000,
|
||||||
|
1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
147
src/components/SettingsPanel.vue
Normal file
147
src/components/SettingsPanel.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useGalleryStore } from "../stores/gallery";
|
||||||
|
import { Settings, CheckSquare, Type, Palette, Copy } from 'lucide-vue-next';
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const store = useGalleryStore();
|
||||||
|
|
||||||
|
// Computed properties to handle "Get from Image OR Global" and "Set to Image" logic
|
||||||
|
const currentScale = computed({
|
||||||
|
get: () => store.selectedImage?.scale ?? store.watermarkSettings.scale,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'scale', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ scale: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentOpacity = computed({
|
||||||
|
get: () => store.selectedImage?.opacity ?? store.watermarkSettings.opacity,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'opacity', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ opacity: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentColor = computed({
|
||||||
|
get: () => store.selectedImage?.color ?? store.watermarkSettings.color,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'color', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ color: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyAll = () => {
|
||||||
|
if (confirm("Apply current settings (Size, Opacity, Color) to ALL images?")) {
|
||||||
|
store.applySettingsToAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full bg-gray-800 text-white p-4 flex flex-col gap-6 overflow-y-auto border-l border-gray-700 w-80">
|
||||||
|
<h2 class="text-lg font-bold flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Settings class="w-5 h-5" />
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="applyAll"
|
||||||
|
title="Apply Settings to All Images"
|
||||||
|
class="bg-gray-700 hover:bg-gray-600 p-1.5 rounded text-xs text-blue-300 transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Copy class="w-3 h-3" /> All
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Text Input -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">Content</label>
|
||||||
|
<div class="relative">
|
||||||
|
<Type class="absolute left-3 top-2.5 w-4 h-4 text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="store.watermarkSettings.text"
|
||||||
|
class="w-full bg-gray-700 text-white pl-10 pr-3 py-2 rounded border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Color Picker -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">Color</label>
|
||||||
|
<div class="flex items-center gap-2 bg-gray-700 p-2 rounded border border-gray-600">
|
||||||
|
<Palette class="w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="currentColor"
|
||||||
|
class="w-8 h-8 rounded cursor-pointer bg-transparent border-none p-0"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-gray-300 font-mono">{{ currentColor }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label class="text-xs text-gray-400">Size (Scale)</label>
|
||||||
|
<span class="text-xs text-gray-300">{{ (currentScale * 100).toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.01"
|
||||||
|
max="0.20"
|
||||||
|
step="0.001"
|
||||||
|
v-model.number="currentScale"
|
||||||
|
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
/>
|
||||||
|
<p class="text-[10px] text-gray-500 mt-1">Relative to image height</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label class="text-xs text-gray-400">Opacity</label>
|
||||||
|
<span class="text-xs text-gray-300">{{ (currentOpacity * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.1"
|
||||||
|
max="1.0"
|
||||||
|
step="0.01"
|
||||||
|
v-model.number="currentOpacity"
|
||||||
|
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Placement Mode -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">Placement Status</label>
|
||||||
|
<div class="flex items-center gap-2 p-2 rounded bg-gray-700/50 border border-gray-600">
|
||||||
|
<div class="p-1 rounded bg-green-500/20 text-green-400">
|
||||||
|
<CheckSquare class="w-4 h-4" v-if="!store.selectedImage?.manualPosition" />
|
||||||
|
<div class="w-4 h-4" v-else></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-gray-200" v-if="!store.selectedImage?.manualPosition">Auto (ZCA)</p>
|
||||||
|
<p class="text-sm font-medium text-blue-300" v-else>Manual Override</p>
|
||||||
|
<p class="text-xs text-gray-500" v-if="!store.selectedImage?.manualPosition">Using smart algorithm</p>
|
||||||
|
<p class="text-xs text-gray-500" v-else>Specific position set</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1 italic">
|
||||||
|
* Drag the watermark on the image to set a manual position for that specific image.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -28,8 +28,14 @@ const onSelect = (index: number) => {
|
|||||||
@click="onSelect(index)"
|
@click="onSelect(index)"
|
||||||
>
|
>
|
||||||
<div class="h-full w-full bg-gray-800 flex items-center justify-center overflow-hidden rounded border border-gray-600">
|
<div class="h-full w-full bg-gray-800 flex items-center justify-center overflow-hidden rounded border border-gray-600">
|
||||||
<!-- Use actual thumbnail path later -->
|
<!-- Use generated thumbnail -->
|
||||||
<img :src="convertFileSrc(item.path)" class="w-full h-full object-cover" loading="lazy" />
|
<img
|
||||||
|
:src="convertFileSrc(item.thumbnail)"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchpriority="low"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,16 +4,35 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
|
|
||||||
export interface ImageItem {
|
export interface ImageItem {
|
||||||
path: string;
|
path: string;
|
||||||
thumbnail?: string;
|
thumbnail: string;
|
||||||
name: string;
|
name: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
zcaSuggestion?: { x: number; y: number; zone: string };
|
zcaSuggestion?: { x: number; y: number; zone: string };
|
||||||
|
manualPosition?: { x: number; y: number };
|
||||||
|
scale?: number;
|
||||||
|
opacity?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatermarkSettings {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
opacity: number;
|
||||||
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGalleryStore = defineStore("gallery", () => {
|
export const useGalleryStore = defineStore("gallery", () => {
|
||||||
const images = ref<ImageItem[]>([]);
|
const images = ref<ImageItem[]>([]);
|
||||||
const selectedIndex = ref<number>(-1);
|
const selectedIndex = ref<number>(-1);
|
||||||
|
const watermarkSettings = ref<WatermarkSettings>({
|
||||||
|
type: 'text',
|
||||||
|
text: 'Watermark',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: 0.03,
|
||||||
|
});
|
||||||
|
|
||||||
const selectedImage = computed(() => {
|
const selectedImage = computed(() => {
|
||||||
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
|
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
|
||||||
@@ -27,6 +46,46 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
selectedIndex.value = -1;
|
selectedIndex.value = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateWatermarkSettings(settings: Partial<WatermarkSettings>) {
|
||||||
|
watermarkSettings.value = { ...watermarkSettings.value, ...settings };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageManualPosition(index: number, x: number, y: number) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
images.value[index].manualPosition = { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageSetting(index: number, setting: 'scale' | 'opacity' | 'color', value: number | string) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
// @ts-ignore
|
||||||
|
images.value[index][setting] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies the settings from the CURRENT image (or global if not overridden) to ALL images
|
||||||
|
// Strategy: Update Global Settings to match current view, and clear individual overrides so everyone follows global.
|
||||||
|
function applySettingsToAll() {
|
||||||
|
const current = selectedImage.value;
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const newScale = current.scale ?? watermarkSettings.value.scale;
|
||||||
|
const newOpacity = current.opacity ?? watermarkSettings.value.opacity;
|
||||||
|
const newColor = current.color ?? watermarkSettings.value.color;
|
||||||
|
|
||||||
|
// 1. Update Global
|
||||||
|
watermarkSettings.value.scale = newScale;
|
||||||
|
watermarkSettings.value.opacity = newOpacity;
|
||||||
|
watermarkSettings.value.color = newColor;
|
||||||
|
|
||||||
|
// 2. Clear overrides on ALL images
|
||||||
|
images.value.forEach(img => {
|
||||||
|
img.scale = undefined;
|
||||||
|
img.opacity = undefined;
|
||||||
|
img.color = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function selectImage(index: number) {
|
async function selectImage(index: number) {
|
||||||
if (index < 0 || index >= images.value.length) return;
|
if (index < 0 || index >= images.value.length) return;
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
@@ -35,8 +94,6 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
if (!img.zcaSuggestion) {
|
if (!img.zcaSuggestion) {
|
||||||
try {
|
try {
|
||||||
const suggestion = await invoke<{x: number, y: number, zone: string}>("get_zca_suggestion", { path: img.path });
|
const suggestion = await invoke<{x: number, y: number, zone: string}>("get_zca_suggestion", { path: img.path });
|
||||||
// Update the item in the array
|
|
||||||
// Note: Directly modifying the object inside ref array is reactive in Vue 3
|
|
||||||
img.zcaSuggestion = suggestion;
|
img.zcaSuggestion = suggestion;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("ZCA failed", e);
|
console.error("ZCA failed", e);
|
||||||
@@ -48,7 +105,12 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
images,
|
images,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
|
watermarkSettings,
|
||||||
setImages,
|
setImages,
|
||||||
selectImage,
|
selectImage,
|
||||||
|
updateWatermarkSettings,
|
||||||
|
setImageManualPosition,
|
||||||
|
setImageSetting,
|
||||||
|
applySettingsToAll
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user