migrate: from central KunkunExtensions repo to this standalone repo for JSR experiment

This commit is contained in:
Huakun Shen 2025-01-08 04:02:45 -05:00
parent a80882758f
commit aba925da49
No known key found for this signature in database
39 changed files with 6394 additions and 0 deletions

23
.github/workflows/jsr-publish.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Publish
on:
push:
branches:
- main
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Build
run: bun run build
- name: Publish package
run: npx jsr publish

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
extensions_support/
dist

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
engine-strict=true
@jsr:registry=https://npm.jsr.io

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

15
.prettierrc Normal file
View File

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

24
common/constants.ts Normal file
View File

@ -0,0 +1,24 @@
export const sharpImageFormats = [
'ASTC',
'AVIF',
'BMP',
'DDS',
'EXR',
'GIF',
'HEIC',
'HEICS',
'ICNS',
'ICO',
'JPEG',
// "JP2",
'KTX',
'PBM',
// "PDF",
'PNG',
'PSD',
'PVR',
'TGA',
'TIFF',
'WEBP',
'SVG'
];

14
components.json Normal file
View File

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

12
deno-src/compress.ts Normal file
View File

@ -0,0 +1,12 @@
import sharp from 'sharp';
import * as v from 'valibot';
export function compressToJpeg(inputPath: string, outputPath: string, quality: number) {
// verify with valibot that quality is between 0 and 100
const validatedQuality = v.pipe(v.number(), v.minValue(0), v.maxValue(100));
const parsedQuality = v.safeParse(validatedQuality, quality);
if (!parsedQuality.success) {
throw new Error('Invalid quality');
}
return sharp(inputPath).jpeg({ quality: parsedQuality.output }).toFile(outputPath);
}

15
deno-src/convert.ts Normal file
View File

@ -0,0 +1,15 @@
import sharp from 'sharp';
/**
* Convert image to another format
* This is a direct conversion, expecting sharp to figure out the conversion based on output file extension
* Both paths should be absolute paths
* @param inputPath - The path to the input file
* @param outputPath - The path to the output file
*/
export function convert(inputPath: string, outputPath: string) {
if (!Deno.statSync(inputPath).isFile) {
throw new Error('Input path is not a file');
}
return sharp(inputPath).toFile(outputPath);
}

14
deno-src/deno.json Normal file
View File

@ -0,0 +1,14 @@
{
"tasks": {
"dev": "deno run --watch main.ts"
},
"imports": {
"@hk/photographer-toolbox": "jsr:@hk/photographer-toolbox@^0.1.12",
"@kunkun/api": "jsr:@kunkun/api@^0.0.40",
"@std/assert": "jsr:@std/assert@1",
"@std/path": "jsr:@std/path@^1.0.7",
"valibot": "jsr:@valibot/valibot@^0.42.1",
"sharp": "npm:sharp@0.33.5",
"exiftool-vendored": "npm:exiftool-vendored@29.0.0"
}
}

313
deno-src/deno.lock generated Normal file
View File

@ -0,0 +1,313 @@
{
"version": "4",
"specifiers": {
"jsr:@hk/photographer-toolbox@~0.1.12": "0.1.12",
"jsr:@kunkun/api@^0.0.40": "0.0.40",
"jsr:@std/assert@1": "1.0.6",
"jsr:@std/internal@^1.0.4": "1.0.4",
"jsr:@std/path@^1.0.7": "1.0.7",
"jsr:@valibot/valibot@~0.42.1": "0.42.1",
"npm:@types/sharp@*": "0.32.0",
"npm:exiftool-vendored@28.5.0": "28.5.0",
"npm:exiftool-vendored@29.0.0": "29.0.0",
"npm:fluent-ffmpeg@2.1.3": "2.1.3",
"npm:kkrpc@^0.0.12": "0.0.12_typescript@5.6.3",
"npm:semver@^7.6.3": "7.6.3",
"npm:sharp@*": "0.33.5",
"npm:sharp@0.33.5": "0.33.5"
},
"jsr": {
"@hk/comlink-stdio@0.1.6": {
"integrity": "77e0ec03157e61baba895142107b871bb1fc2f9ffbd4244413e12dab62478bab"
},
"@hk/photographer-toolbox@0.1.12": {
"integrity": "bf4a4b1c7ef0377e0e91eec181dc67a8befe3d5faba1947c6806739bac33565b",
"dependencies": [
"npm:exiftool-vendored@28.5.0",
"npm:fluent-ffmpeg",
"npm:sharp@0.33.5"
]
},
"@kunkun/api@0.0.40": {
"integrity": "eab67c01e1cc87f3e5e7f7613a302cba7fccb18a1745f1a5508cf48df1e3649e",
"dependencies": [
"npm:kkrpc"
]
},
"@std/assert@1.0.6": {
"integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/internal@1.0.4": {
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
},
"@std/path@1.0.7": {
"integrity": "76a689e07f0e15dcc6002ec39d0866797e7156629212b28f27179b8a5c3b33a1"
},
"@valibot/valibot@0.42.1": {
"integrity": "ba0f6f7964aaeec0e4b1f793d575061f325ae6254cbb9d7ff01fb65068a0a23b"
}
},
"npm": {
"@emnapi/runtime@1.3.1": {
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
"dependencies": [
"tslib"
]
},
"@img/sharp-darwin-arm64@0.33.5": {
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"dependencies": [
"@img/sharp-libvips-darwin-arm64"
]
},
"@img/sharp-darwin-x64@0.33.5": {
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"dependencies": [
"@img/sharp-libvips-darwin-x64"
]
},
"@img/sharp-libvips-darwin-arm64@1.0.4": {
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="
},
"@img/sharp-libvips-darwin-x64@1.0.4": {
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="
},
"@img/sharp-libvips-linux-arm64@1.0.4": {
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="
},
"@img/sharp-libvips-linux-arm@1.0.5": {
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="
},
"@img/sharp-libvips-linux-s390x@1.0.4": {
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="
},
"@img/sharp-libvips-linux-x64@1.0.4": {
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="
},
"@img/sharp-libvips-linuxmusl-arm64@1.0.4": {
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="
},
"@img/sharp-libvips-linuxmusl-x64@1.0.4": {
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="
},
"@img/sharp-linux-arm64@0.33.5": {
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"dependencies": [
"@img/sharp-libvips-linux-arm64"
]
},
"@img/sharp-linux-arm@0.33.5": {
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"dependencies": [
"@img/sharp-libvips-linux-arm"
]
},
"@img/sharp-linux-s390x@0.33.5": {
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"dependencies": [
"@img/sharp-libvips-linux-s390x"
]
},
"@img/sharp-linux-x64@0.33.5": {
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"dependencies": [
"@img/sharp-libvips-linux-x64"
]
},
"@img/sharp-linuxmusl-arm64@0.33.5": {
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"dependencies": [
"@img/sharp-libvips-linuxmusl-arm64"
]
},
"@img/sharp-linuxmusl-x64@0.33.5": {
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"dependencies": [
"@img/sharp-libvips-linuxmusl-x64"
]
},
"@img/sharp-wasm32@0.33.5": {
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"dependencies": [
"@emnapi/runtime"
]
},
"@img/sharp-win32-ia32@0.33.5": {
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="
},
"@img/sharp-win32-x64@0.33.5": {
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="
},
"@photostructure/tz-lookup@11.0.0": {
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw=="
},
"@types/luxon@3.4.2": {
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
},
"@types/sharp@0.32.0": {
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
"dependencies": [
"sharp"
]
},
"async@0.2.10": {
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"batch-cluster@13.0.0": {
"integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og=="
},
"color-convert@2.0.1": {
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": [
"color-name"
]
},
"color-name@1.1.4": {
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string@1.9.1": {
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": [
"color-name",
"simple-swizzle"
]
},
"color@4.2.3": {
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": [
"color-convert",
"color-string"
]
},
"detect-libc@2.0.3": {
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
},
"exiftool-vendored.exe@12.96.0": {
"integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ=="
},
"exiftool-vendored.exe@13.0.0": {
"integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg=="
},
"exiftool-vendored.pl@12.96.0": {
"integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A=="
},
"exiftool-vendored.pl@13.0.1": {
"integrity": "sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A=="
},
"exiftool-vendored@28.5.0": {
"integrity": "sha512-/XbVpZGP5P/tifRbO2BIBuDxLkHrUoxhJGOKAeASHnIBNNgBzp3UWtp0wLPhEd24ETe/ohuEUPmpUaKcNSDYsg==",
"dependencies": [
"@photostructure/tz-lookup",
"@types/luxon",
"batch-cluster",
"exiftool-vendored.exe@12.96.0",
"exiftool-vendored.pl@12.96.0",
"he",
"luxon"
]
},
"exiftool-vendored@29.0.0": {
"integrity": "sha512-BW2Fr7okYP1tN7KIIREy8gOx9WggpPsbKc3BTAS4dLgSup50LjdQttxF9kyDP+27ZayllK+d0rfMYPAixPBtQw==",
"dependencies": [
"@photostructure/tz-lookup",
"@types/luxon",
"batch-cluster",
"exiftool-vendored.exe@13.0.0",
"exiftool-vendored.pl@13.0.1",
"he",
"luxon"
]
},
"fluent-ffmpeg@2.1.3": {
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
"dependencies": [
"async",
"which"
]
},
"he@1.2.0": {
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"is-arrayish@0.3.2": {
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"isexe@2.0.0": {
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"kkrpc@0.0.12_typescript@5.6.3": {
"integrity": "sha512-PBk4AhGfkesIdAwmIoj7dHHIp7qN97XT4yr5Rl7h2WL79gxWQVgZRJYLt7Gb17GoLDh991rnL85mhCoPG5VC/Q==",
"dependencies": [
"typescript",
"ws"
]
},
"luxon@3.5.0": {
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="
},
"semver@7.6.3": {
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
},
"sharp@0.33.5": {
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"dependencies": [
"@img/sharp-darwin-arm64",
"@img/sharp-darwin-x64",
"@img/sharp-libvips-darwin-arm64",
"@img/sharp-libvips-darwin-x64",
"@img/sharp-libvips-linux-arm",
"@img/sharp-libvips-linux-arm64",
"@img/sharp-libvips-linux-s390x",
"@img/sharp-libvips-linux-x64",
"@img/sharp-libvips-linuxmusl-arm64",
"@img/sharp-libvips-linuxmusl-x64",
"@img/sharp-linux-arm",
"@img/sharp-linux-arm64",
"@img/sharp-linux-s390x",
"@img/sharp-linux-x64",
"@img/sharp-linuxmusl-arm64",
"@img/sharp-linuxmusl-x64",
"@img/sharp-wasm32",
"@img/sharp-win32-ia32",
"@img/sharp-win32-x64",
"color",
"detect-libc",
"semver"
]
},
"simple-swizzle@0.2.2": {
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": [
"is-arrayish"
]
},
"tslib@2.8.0": {
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
},
"typescript@5.6.3": {
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="
},
"which@1.3.1": {
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dependencies": [
"isexe"
]
},
"ws@8.18.0": {
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
}
},
"workspace": {
"dependencies": [
"jsr:@hk/photographer-toolbox@~0.1.12",
"jsr:@kunkun/api@^0.0.40",
"jsr:@std/assert@1",
"jsr:@std/path@^1.0.7",
"jsr:@valibot/valibot@~0.42.1",
"npm:exiftool-vendored@29.0.0",
"npm:sharp@0.33.5"
]
}
}

221
deno-src/dev.ts Normal file
View File

@ -0,0 +1,221 @@
import { ExifTool, ExifDateTime } from 'exiftool-vendored';
// import { image } from '@hk/photographer-toolbox';
// import { convertDate } from './lib.ts';
// import { batchReadImageMetadata, readImageMetadata2 } from './lib.ts';
export function batchSmartSetImageOriginalDate(
imagePaths: string[],
baseImagePath: string,
targetDate: ExifDateTime
) {
// Read metadata for all images
const loader = new ExifTool();
return loader
.read(baseImagePath)
.then(async (baseTags) => {
const baseOriginalDate = baseTags.DateTimeOriginal;
if (!baseOriginalDate) {
throw new Error('Base image has no DateTimeOriginal');
}
// Calculate offset between target and base dates
const targetMillis = targetDate.toMillis();
const baseMillis = ExifDateTime.from(baseOriginalDate)!.toMillis();
const offsetMillis = targetMillis - baseMillis;
// Read all image tags
const allTags = await Promise.all(imagePaths.map((path) => loader.read(path)));
// Update each image with offset-adjusted date
return Promise.all(
allTags.map((tags, i) => {
const originalDate = tags.DateTimeOriginal;
if (!originalDate) {
throw new Error(`Image ${imagePaths[i]} has no DateTimeOriginal`);
}
const imageMillis = ExifDateTime.from(originalDate)!.toMillis();
const newMillis = imageMillis + offsetMillis;
const newDate = ExifDateTime.fromMillis(newMillis);
// return setImageOriginalDate(imagePaths[i], newDate);
return loader.write(
imagePaths[i],
{
DateTimeOriginal: newDate,
CreateDate: newDate,
ModifyDate: newDate
},
['-overwrite_original']
);
})
).then(() => Promise.resolve());
})
.finally(() => loader.end());
}
const files = [
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2512.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2510.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2507.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2506.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2504.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2505.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2503.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2502.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2501.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2499.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2500.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2497.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2498.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2496.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2495.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2494.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2491.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2492.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2493.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2490.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2489.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2488.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2487.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2485.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2486.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2484.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2483.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2482.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2481.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2480.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2479.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2473.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2469.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2465.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2463.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2464.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2461.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2462.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2459.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2460.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2458.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2457.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2456.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2454.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2453.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2451.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2447.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2446.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2439.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2440.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2437.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2435.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2436.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2430.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2428.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2427.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2423.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2424.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2425.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2422.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2420.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2418.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2417.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2415.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2411.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2412.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2410.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2409.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2407.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2404.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2402.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2403.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2401.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2400.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2399.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2396.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2397.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2394.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2395.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2392.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2393.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2390.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2387.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2386.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2382.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2383.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2384.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2385.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2380.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2376.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2375.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2374.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2372.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2370.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2371.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2368.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2369.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2364.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2365.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2366.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2367.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2362.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2363.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2360.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2361.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2358.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2359.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2355.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2356.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2357.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2354.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2352.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2353.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2351.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2348.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2346.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2347.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2340.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2339.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2338.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2336.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2337.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2334.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2335.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2331.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2329.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2327.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2326.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2324.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2323.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2322.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2320.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2317.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2316.ARW',
'/Users/hk/Pictures/2024-Vancouver/11-27/_DSC2314.ARW'
];
batchSmartSetImageOriginalDate(files, files[0], ExifDateTime.from('2024-11-27T15:00:00.000Z'))
.then(console.log)
.catch(console.error);
// image.batchReadImageMetadata(files).then((xs) => {
// for (const x of xs) {
// console.log(convertDate(x));
// // console.log(x);
// }
// // console.log(xs.map(convertDate));
// });
//
// readImageMetadata2(files[4]).then(console.log);
// for (const promise of files.map(readImageMetadata2)) {
// promise.then(console.log);
// }
// await new Promise((resolve) => setTimeout(resolve, 1000));
// batchReadImageMetadata(files).then(console.log);
// files.forEach(async (file) => {
// console.log('file', file);
// const metadata = await image.readImageMetadata(file);
// console.log('metadata', metadata);
// });
// image
// .readImageMetadata(files[4])
// .then(console.log);

0
deno-src/image-time.ts Normal file
View File

77
deno-src/index.ts Normal file
View File

@ -0,0 +1,77 @@
import type { API } from '../src/types.ts';
import { expose } from '@kunkun/api/runtime/deno';
import { image } from '@hk/photographer-toolbox';
import { convertDate } from './lib.ts';
import { ExifTool, ExifDateTime } from 'exiftool-vendored';
export function batchSmartSetImageOriginalDate(
imagePaths: string[],
baseImagePath: string,
targetDate: ExifDateTime
) {
// Read metadata for all images
const loader = new ExifTool();
return loader
.read(baseImagePath)
.then(async (baseTags) => {
const baseOriginalDate = baseTags.DateTimeOriginal;
if (!baseOriginalDate) {
throw new Error('Base image has no DateTimeOriginal');
}
// Calculate offset between target and base dates
const targetMillis = targetDate.toMillis();
const baseMillis = ExifDateTime.from(baseOriginalDate)!.toMillis();
const offsetMillis = targetMillis - baseMillis;
// Read all image tags
const allTags = await Promise.all(imagePaths.map((path) => loader.read(path)));
// Update each image with offset-adjusted date
return Promise.all(
allTags.map((tags, i) => {
const originalDate = tags.DateTimeOriginal;
if (!originalDate) {
throw new Error(`Image ${imagePaths[i]} has no DateTimeOriginal`);
}
const imageMillis = ExifDateTime.from(originalDate)!.toMillis();
const newMillis = imageMillis + offsetMillis;
const newDate = ExifDateTime.fromMillis(newMillis);
// return setImageOriginalDate(imagePaths[i], newDate);
return loader.write(
imagePaths[i],
{
DateTimeOriginal: newDate
},
['-overwrite_original']
);
})
).then(() => Promise.resolve());
})
.finally(() => loader.end());
}
expose({
echo: (paths: string[]) => Promise.resolve(paths),
readImageMetadata: (imagePath: string) => image.readImageMetadata(imagePath).then(convertDate),
batchReadImageMetadata: async (paths: string[]) => {
const data = await image.batchReadImageMetadata(paths);
return data.map(convertDate);
},
batchSmartSetImageOriginalDate: (
imagePaths: string[],
baseImagePath: string,
targetDateIso: string
) => {
return image
.batchSmartSetImageOriginalDate(imagePaths, baseImagePath, ExifDateTime.from(targetDateIso))
.then(() => Promise.resolve())
.catch((err) => {
console.error(err);
throw new Error(err);
});
}
} satisfies API);

20
deno-src/lib.ts Normal file
View File

@ -0,0 +1,20 @@
import { ExifDateTime } from 'exiftool-vendored';
import type { ImageMetadata } from '@hk/photographer-toolbox/types';
import { ImageMetadataMod } from '../src/types.ts';
export function convertDate(data: ImageMetadata): ImageMetadataMod {
const dateKeys = [
'DateTimeOriginal',
'FileModifyDate',
'ModifyDate',
'CreateDate',
'dateCreated',
'dateModified'
];
for (const key of dateKeys) {
if (data[key] && typeof data[key] !== 'string') {
data[key] = (data[key] as ExifDateTime).toISOString();
}
}
return data;
}

40
eslint.config.js Normal file
View File

@ -0,0 +1,40 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.Config[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off' // Allow usage of 'any' type
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

6
jsr.json Normal file
View File

@ -0,0 +1,6 @@
{
"name": "@kunkun/ext-image-processing",
"version": "0.1.0",
"license": "MIT",
"exports": "./mod.ts"
}

116
package.json Normal file
View File

@ -0,0 +1,116 @@
{
"$schema": "https://schema.kunkun.sh",
"name": "image-processing",
"version": "0.0.3",
"kunkun": {
"name": "TODO: Change Display Name",
"shortDescription": "A Custom UI template for sveltekit",
"longDescription": "A Custom UI template for sveltekit",
"identifier": "image-processing",
"icon": {
"type": "iconify",
"value": "lucide:image"
},
"demoImages": [],
"permissions": [
"dialog:all",
"clipboard:read-files",
"notification:all",
"system:fs",
{
"permission": "shell:deno:spawn",
"allow": [
{
"path": "$EXTENSION/deno-src/index.ts",
"env": "*",
"ffi": "*",
"read": "*",
"sys": "*",
"run": "*"
}
]
},
"shell:stdin-write",
"shell:kill"
],
"customUiCmds": [
{
"name": "Smart Edit Image Capture Date",
"main": "/",
"devMain": "http://localhost:5173/",
"dist": "build",
"cmds": [],
"window": {
"title": "Smart Edit Image Capture Date"
}
}
],
"templateUiCmds": [
{
"name": "Image Info",
"cmds": [],
"main": "dist/image-info.js"
}
]
},
"scripts": {
"dev": "vite dev",
"dev:template": "bun scripts/build-template-ext.ts dev",
"build:template": "bun scripts/build-template-ext.ts",
"build:custom": "vite build",
"build": "bun scripts/build.ts",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"dependencies": {
"@hk/photographer-toolbox": "npm:@jsr/hk__photographer-toolbox@^0.1.12",
"@internationalized/date": "^3.6.0",
"@kksh/api": "^0.0.48",
"@kksh/svelte5": "^0.1.12",
"@tanstack/table-core": "^8.20.5",
"bits-ui": "1.0.0-next.77",
"clsx": "^2.1.1",
"embla-carousel-svelte": "^8.5.2",
"formsnap": "^2.0.0",
"lucide-svelte": "^0.469.0",
"mode-watcher": "^0.5.0",
"paneforge": "^0.0.6",
"svelte-radix": "^2.0.1",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0",
"valibot": "^1.0.0-beta.11",
"vaul-svelte": "^0.3.2"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.15.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.16.6",
"svelte-check": "^4.1.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^6.0.7"
},
"type": "module",
"files": [
"build",
".gitignore"
],
"packageManager": "pnpm@9.15.3"
}

4627
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@ -0,0 +1,36 @@
import { watch } from 'fs';
import { join } from 'path';
import { refreshTemplateWorkerExtension } from '@kksh/api/dev';
import { $ } from 'bun';
const entrypoints = ['./template-ext-src/image-info.ts'];
async function build() {
try {
// for (const entrypoint of entrypoints) {
// await $`bun build --minify --target=browser --outdir=./dist ${entrypoint}`;
// }
await Bun.build({
entrypoints,
target: 'browser',
outdir: './dist',
minify: false
});
if (Bun.argv.includes('dev')) {
await refreshTemplateWorkerExtension();
}
} catch (error) {
console.error(error);
}
}
const srcDir = join(import.meta.dir, '..', 'template-ext-src');
await build();
if (Bun.argv.includes('dev')) {
console.log(`Watching ${srcDir} for changes...`);
watch(srcDir, { recursive: true }, async (event, filename) => {
await build();
});
}

4
scripts/build.ts Normal file
View File

@ -0,0 +1,4 @@
import { $ } from 'bun';
await $`bun build:custom`;
await $`bun build:template`;

80
src/app.css Normal file
View File

@ -0,0 +1,80 @@
@import url("@kksh/svelte5/themes");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,136 @@
<script lang="ts">
import {
Calendar as CalendarPrimitive,
type CalendarRootProps,
type WithoutChildrenOrChild
} from 'bits-ui';
import { DateFormatter, getLocalTimeZone } from '@internationalized/date';
import { Calendar, Select } from '@kksh/svelte5';
import { cn } from '$lib/utils.js';
let {
value = $bindable(),
placeholder = $bindable(),
weekdayFormat,
class: className,
...restProps
}: WithoutChildrenOrChild<CalendarRootProps> = $props();
const monthOptions = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
].map((month, i) => ({ value: String(i + 1), label: month }));
const monthFmt = new DateFormatter('en-US', {
month: 'long'
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() - i),
value: String(new Date().getFullYear() - i)
}));
const defaultYear = $derived(
placeholder ? { value: String(placeholder.year), label: String(placeholder.year) } : undefined
);
const defaultMonth = $derived(
placeholder
? {
value: String(placeholder.month),
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined
);
const monthLabel = $derived(
monthOptions.find((m) => m.value === defaultMonth?.value)?.label ?? 'Select a month'
);
</script>
<CalendarPrimitive.Root
{weekdayFormat}
class={cn('rounded-md border p-3', className)}
bind:value={value as never}
bind:placeholder
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Header>
<Calendar.Heading class="flex w-full items-center justify-between gap-2">
<Select.Root
type="single"
value={defaultMonth?.value}
onValueChange={(v: string) => {
if (!placeholder) return;
if (v === `${placeholder.month}`) return;
placeholder = placeholder.set({ month: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
{monthLabel}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label }}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={defaultYear?.value}
onValueChange={(v: string) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.year}`) return;
placeholder = placeholder.set({ year: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
{defaultYear?.label ?? 'Select year'}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label }}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Calendar.Heading>
</Calendar.Header>
<Calendar.Months>
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

62
src/lib/utils.ts Normal file
View File

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

19
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,19 @@
<script>
import '../app.css';
import { ModeWatcher } from 'mode-watcher';
import { ThemeWrapper, updateTheme } from '@kksh/svelte5';
import { onMount } from 'svelte';
import { ui } from '@kksh/api/ui/iframe';
onMount(() => {
ui.registerDragRegion();
ui.getTheme().then((theme) => {
updateTheme(theme);
});
});
</script>
<ModeWatcher />
<ThemeWrapper>
<slot />
</ThemeWrapper>

2
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;

124
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,124 @@
<script lang="ts">
import { base } from '$app/paths';
import { NodeName } from '@kksh/api/models';
import { clipboard, notification, ui, toast, dialog, shell } from '@kksh/api/ui/iframe';
import {
// Calendar,
ModeToggle,
Command,
ModeWatcher,
Separator,
Button,
Input
} from '@kksh/svelte5';
import { ArrowLeftIcon } from 'lucide-svelte';
import { onMount } from 'svelte';
import type { API } from '../types';
import { getLocalTimeZone, today } from '@internationalized/date';
import Calendar from '$lib/components/calendar.svelte';
let targetDate = $state(today(getLocalTimeZone()));
let targetTime = $state(
new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
);
let targetDateIso = $derived(new Date(`${targetDate}T${targetTime}`).toISOString());
let images = $state<string[]>([]);
let baseImagePath = $state<string | null>(null);
onMount(() => {
toast.info('Loaded');
});
function pickImages() {
dialog
.open({
multiple: true,
directory: false
})
.then((files: string[]) => {
images = files;
});
}
async function submit() {
console.log('submit', images, baseImagePath, targetDateIso);
if (!baseImagePath) return;
const { rpcChannel, process, command } = await shell.createDenoRpcChannel<object, API>(
'$EXTENSION/deno-src/index.ts',
[],
{
allowAllEnv: true,
// allowEnv: ['NODE_V8_COVERAGE', 'npm_package_config_libvips', 'EXIFTOOL_HOME', 'OSTYPE'],
// allowFfi: ["*sharp-darwin-arm64.node"],
allowAllFfi: true,
allowAllRead: true,
allowAllSys: true,
// allowSys: ['uid', 'cpus'],
// allowRun: ["*exiftool"]
allowAllRun: true
},
{}
);
command.stderr.on('data', (data) => {
console.warn(data);
});
try {
const api = rpcChannel.getAPI();
await api.batchSmartSetImageOriginalDate(images, baseImagePath, targetDateIso);
toast.success('Images date set to ', { description: targetDateIso });
} catch (error) {
console.error(error);
toast.error('Failed to set date');
} finally {
process.kill();
}
}
$inspect(targetDateIso);
</script>
<svelte:window
on:keydown={(e) => {
if (e.key === 'Escape') {
if (document.activeElement?.nodeName === 'BODY') {
ui.goBack();
}
}
}}
/>
<div class="h-12"></div>
<Button variant="outline" size="icon" class="fixed left-2 top-2 z-50" onclick={ui.goBack}>
<ArrowLeftIcon class="h-4 w-4" />
</Button>
<div class="container">
<div class="grid grid-cols-2">
<div class="space-y-2">
<Button onclick={pickImages} variant="outline">Pick Images</Button>
<p>{images.length} Images Selected</p>
<Button
variant="outline"
onclick={() => {
dialog
.open({
multiple: false,
directory: false
})
.then((p: string) => {
baseImagePath = p;
});
}}>Pick Base Image</Button
>
{#if baseImagePath}
<span>Base Image Selected</span>
{/if}
<br />
<Button onclick={submit} variant="default">Submit</Button>
</div>
<div class="space-y-2">
<Input type="time" bind:value={targetTime} />
<div class="w-fit">
<Calendar type="single" bind:value={targetDate} class="rounded-md border" />
<pre>Target Date: {targetDate}</pre>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<script lang="ts">
import { clipboard, notification, ui, toast, dialog, shell } from '@kksh/api/ui/iframe';
import { ModeToggle, Command, ModeWatcher, Separator, Button, Input } from '@kksh/svelte5';
import { ArrowLeftIcon } from 'lucide-svelte';
</script>
<div class="h-12"></div>
<Button variant="outline" size="icon" class="fixed left-2 top-2 z-50" onclick={ui.goBack}>
<ArrowLeftIcon class="h-4 w-4" />
</Button>

22
src/types.ts Normal file
View File

@ -0,0 +1,22 @@
import { image } from '@hk/photographer-toolbox';
import type { ImageMetadata } from '@hk/photographer-toolbox/types';
export type ImageMetadataMod = ImageMetadata & {
DateTimeOriginal?: string;
FileModifyDate?: string;
ModifyDate?: string;
CreateDate?: string;
dateCreated?: string;
dateModified?: string;
};
export type API = {
echo: (paths: string[]) => Promise<string[]>;
readImageMetadata: (imagePath: string) => Promise<ImageMetadataMod>;
batchReadImageMetadata: (imagePaths: string[]) => Promise<ImageMetadataMod[]>;
batchSmartSetImageOriginalDate: (
imagePaths: string[],
baseImagePath: string,
targetDateIso: string
) => Promise<void>;
};

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

21
svelte.config.js Normal file
View File

@ -0,0 +1,21 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({}),
alias: {
'@/*': './src/lib/*'
}
}
};
export default config;

67
tailwind.config.ts Normal file
View File

@ -0,0 +1,67 @@
import { fontFamily } from 'tailwindcss/defaultTheme';
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: [
'./src/**/*.{html,js,svelte,ts}',
'node_modules/@kksh/svelte5/dist/**/*.{html,js,svelte,ts}'
],
safelist: ['dark'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
},
secondary: {
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
},
destructive: {
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
},
muted: {
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
},
accent: {
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
},
popover: {
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
},
card: {
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
}
};
export default config;

View File

@ -0,0 +1,187 @@
import { ImageMetadata } from '@hk/photographer-toolbox/types';
import type { API } from '../src/types.ts';
import {
Action,
app,
Child,
clipboard,
expose,
Form,
fs,
Icon,
IconEnum,
List,
path,
shell,
system,
toast,
ui,
WorkerExtension
} from '@kksh/api/ui/worker';
class ImageInfo extends WorkerExtension {
api: API | undefined;
apiProcess: Child | undefined;
imageMetadata: Record<string, ImageMetadata> = {};
async fillApi() {
if (this.api) return;
const { rpcChannel, process, command } = await shell.createDenoRpcChannel<object, API>(
'$EXTENSION/deno-src/index.ts',
[],
{
allowAllEnv: true,
// allowEnv: ['NODE_V8_COVERAGE', 'npm_package_config_libvips', 'EXIFTOOL_HOME', 'OSTYPE'],
// allowFfi: ["*sharp-darwin-arm64.node"],
allowAllFfi: true,
allowAllRead: true,
allowAllSys: true,
// allowSys: ['uid', 'cpus'],
// allowRun: ["*exiftool"]
allowAllRun: true
},
{}
);
command.stdout.on('data', (data) => {
console.log('stdout', data);
});
// command.stderr.on('data', (data) => {
// console.warn('stderr', data);
// });
this.api = rpcChannel.getAPI();
this.apiProcess = process;
}
async refreshList(paths: string[]) {
ui.render(new List.List({ items: [] }));
if (!this.api) await this.fillApi();
ui.showLoadingBar(true);
return this.api
?.batchReadImageMetadata(paths)
.then((metadata) => {
console.log('metadata 2', metadata);
this.imageMetadata = Object.fromEntries(
paths.map((file, index) => [file, metadata[index]])
);
})
.then(async () => {
return ui.render(
new List.List({
detail: new List.ItemDetail({
width: 60,
children: []
}),
items: await Promise.all(
paths.map(async (file) => {
const baseName = await path.basename(file);
return new List.Item({
title: baseName,
value: file
});
})
)
})
);
})
.catch((err) => {
console.error('error refreshList', err);
})
.finally(() => {
ui.showLoadingBar(false);
console.log('finally, kill api process', this.apiProcess?.pid);
this.apiProcess?.kill();
this.apiProcess = undefined;
this.api = undefined;
});
}
async load() {
ui.showLoadingBar(true);
let imagePaths = (
await Promise.all([
system.getSelectedFilesInFileExplorer().catch((err) => {
return [];
}),
clipboard.hasFiles().then((hasFiles) => {
if (hasFiles) return clipboard.readFiles();
return [];
})
])
).flat();
imagePaths = imagePaths.filter((path) => !!path);
this.refreshList(imagePaths);
}
async onFilesDropped(paths: string[]): Promise<void> {
return this.refreshList(paths);
}
async onHighlightedListItemChanged(filePath: string): Promise<void> {
const metadata = this.imageMetadata[filePath];
const metadataLabels = [
genMetadataLabel(metadata, 'Width', 'width'),
genMetadataLabel(metadata, 'Height', 'height'),
genMetadataLabel(metadata, 'Latitude', 'latitude'),
genMetadataLabel(metadata, 'LatitudeRef', 'latitudeRef'),
genMetadataLabel(metadata, 'Longitude', 'longitude'),
genMetadataLabel(metadata, 'LongitudeRef', 'longitudeRef'),
genMetadataLabel(metadata, 'Altitude', 'altitude'),
genMetadataLabel(metadata, 'AltitudeRef', 'altitudeRef'),
genMetadataLabel(metadata, 'MapDatum', 'mapDatum'),
genMetadataLabel(metadata, 'Bits Per Sample', 'bitsPerSample'),
genMetadataLabel(metadata, 'File Size', 'fileSize'),
genMetadataLabel(metadata, 'Make', 'make'),
genMetadataLabel(metadata, 'Model', 'model'),
genMetadataLabel(metadata, 'Date Modified', 'dateModified'),
genMetadataLabel(metadata, 'Focal Length', 'focalLength'),
genMetadataLabel(metadata, 'Focal Length in 35mm Format', 'focalLengthIn35mmFormat'),
genMetadataLabel(metadata, 'F Number', 'fNumber'),
genMetadataLabel(metadata, 'Exposure Time', 'exposureTime'),
genMetadataLabel(metadata, 'Exposure Mode', 'exposureMode'),
genMetadataLabel(metadata, 'Exposure Program', 'exposureProgram'),
genMetadataLabel(metadata, 'Date Time Original', 'DateTimeOriginal'),
genMetadataLabel(metadata, 'File Modify Date', 'FileModifyDate'),
genMetadataLabel(metadata, 'Modify Date', 'ModifyDate'),
genMetadataLabel(metadata, 'Create Date', 'CreateDate'),
genMetadataLabel(metadata, 'File Format', 'FileFormat'),
genMetadataLabel(metadata, 'Quality', 'Quality'),
genMetadataLabel(metadata, 'RAW File Type', 'RAWFileType'),
genMetadataLabel(metadata, 'Compression', 'Compression'),
genMetadataLabel(metadata, 'Camera Orientation', 'CameraOrientation'),
genMetadataLabel(metadata, 'Faces Detected', 'FacesDetected')
].filter((label) => label !== null);
return ui.render(
new List.List({
inherits: ['items'],
detail: new List.ItemDetail({
width: 55,
children: [new List.ItemDetailMetadata(metadataLabels)]
})
})
);
}
async onBeforeGoBack(): Promise<void> {
console.log('onBeforeGoBack, kill api process', this.apiProcess?.pid);
await this.apiProcess?.kill();
}
async onListItemSelected(value: string): Promise<void> {
return Promise.resolve();
}
}
function genMetadataLabel(metadata: ImageMetadata, title: string, key: string) {
if (!metadata[key]) return null;
return new List.ItemDetailMetadataLabel({
title,
text:
typeof metadata[key] === 'number'
? Number.isInteger(metadata[key])
? metadata[key].toString()
: metadata[key].toFixed(3).toString()
: metadata[key].toString()
});
}
expose(new ImageInfo());

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
vite.config.ts Normal file
View File

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});