Merge pull request #275 from PasteBar/special-copy-paste-history-items
Special copy paste history items
This commit is contained in:
commit
510b3ffd46
5
.changeset/sharp-shoes-care.md
Normal file
5
.changeset/sharp-shoes-care.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'pastebar-app-ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added Special copy/paste for history items context menu and settings
|
423
package-lock.json
generated
423
package-lock.json
generated
@ -141,6 +141,7 @@
|
|||||||
"@types/codemirror": "^5.60.15",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/linkify-it": "^3.0.5",
|
"@types/linkify-it": "^3.0.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@ -168,7 +169,6 @@
|
|||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.3.5",
|
||||||
"taze": "^0.12.2",
|
"taze": "^0.12.2",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.0.6",
|
"vite": "^6.0.6",
|
||||||
"vite-plugin-tauri": "^4.0.0"
|
"vite-plugin-tauri": "^4.0.0"
|
||||||
@ -2658,28 +2658,6 @@
|
|||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
|
||||||
"version": "0.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
|
||||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/trace-mapping": "0.3.9"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
|
||||||
"version": "0.3.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
|
||||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/resolve-uri": "^3.0.3",
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dnd-kit/accessibility": {
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
|
||||||
@ -6328,6 +6306,19 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@selderee/plugin-htmlparser2": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sigstore/bundle": {
|
"node_modules/@sigstore/bundle": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
||||||
@ -7304,30 +7295,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node12": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node14": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node16": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@tufjs/canonical-json": {
|
"node_modules/@tufjs/canonical-json": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||||
@ -7515,6 +7482,13 @@
|
|||||||
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/html-to-text": {
|
||||||
|
"version": "9.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz",
|
||||||
|
"integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
@ -7839,18 +7813,6 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn-walk": {
|
|
||||||
"version": "8.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
|
||||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"acorn": "^8.11.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@ -9246,12 +9208,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/create-require": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/crelt": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
@ -9744,15 +9700,6 @@
|
|||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dir-glob": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@ -11205,6 +11152,41 @@
|
|||||||
"void-elements": "3.1.0"
|
"void-elements": "3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-text": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
@ -12071,6 +12053,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leac": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
@ -12194,12 +12185,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||||
},
|
},
|
||||||
"node_modules/make-error": {
|
|
||||||
"version": "1.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
|
||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/make-fetch-happen": {
|
"node_modules/make-fetch-happen": {
|
||||||
"version": "11.1.1",
|
"version": "11.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
|
||||||
@ -13468,6 +13453,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/parseley": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"leac": "^0.6.0",
|
||||||
|
"peberminta": "^0.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pastebar-app-ui": {
|
"node_modules/pastebar-app-ui": {
|
||||||
"resolved": "packages/pastebar-app-ui",
|
"resolved": "packages/pastebar-app-ui",
|
||||||
"link": true
|
"link": true
|
||||||
@ -13538,6 +13536,15 @@
|
|||||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/peberminta": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/performance-now": {
|
"node_modules/performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
@ -15117,6 +15124,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
||||||
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="
|
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/selderee": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parseley": "^0.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@ -16179,55 +16198,6 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ts-node": {
|
|
||||||
"version": "10.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
|
||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
|
||||||
"@tsconfig/node10": "^1.0.7",
|
|
||||||
"@tsconfig/node12": "^1.0.7",
|
|
||||||
"@tsconfig/node14": "^1.0.0",
|
|
||||||
"@tsconfig/node16": "^1.0.2",
|
|
||||||
"acorn": "^8.4.1",
|
|
||||||
"acorn-walk": "^8.1.1",
|
|
||||||
"arg": "^4.1.0",
|
|
||||||
"create-require": "^1.1.0",
|
|
||||||
"diff": "^4.0.1",
|
|
||||||
"make-error": "^1.1.1",
|
|
||||||
"v8-compile-cache-lib": "^3.0.1",
|
|
||||||
"yn": "3.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ts-node": "dist/bin.js",
|
|
||||||
"ts-node-cwd": "dist/bin-cwd.js",
|
|
||||||
"ts-node-esm": "dist/bin-esm.js",
|
|
||||||
"ts-node-script": "dist/bin-script.js",
|
|
||||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
|
||||||
"ts-script": "dist/bin-script-deprecated.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@swc/core": ">=1.2.50",
|
|
||||||
"@swc/wasm": ">=1.2.50",
|
|
||||||
"@types/node": "*",
|
|
||||||
"typescript": ">=2.7"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@swc/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@swc/wasm": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ts-node/node_modules/arg": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||||
@ -16662,12 +16632,6 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/validate-npm-package-license": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
@ -17150,15 +17114,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yn": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/youtube-player": {
|
"node_modules/youtube-player": {
|
||||||
"version": "5.5.2",
|
"version": "5.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
||||||
@ -17307,6 +17262,7 @@
|
|||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.5",
|
||||||
"garbados-crypt": "^3.0.0-beta",
|
"garbados-crypt": "^3.0.0-beta",
|
||||||
"glob-all": "^3.3.1",
|
"glob-all": "^3.3.1",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
@ -19906,27 +19862,6 @@
|
|||||||
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@cspotcode/source-map-support": {
|
|
||||||
"version": "0.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
|
||||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/trace-mapping": "0.3.9"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/trace-mapping": {
|
|
||||||
"version": "0.3.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
|
||||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/resolve-uri": "^3.0.3",
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@dnd-kit/accessibility": {
|
"@dnd-kit/accessibility": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
|
||||||
@ -22193,6 +22128,15 @@
|
|||||||
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
|
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@selderee/plugin-htmlparser2": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||||
|
"requires": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@sigstore/bundle": {
|
"@sigstore/bundle": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
|
||||||
@ -22697,30 +22641,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
|
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
|
||||||
},
|
},
|
||||||
"@tsconfig/node10": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@tsconfig/node12": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@tsconfig/node14": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@tsconfig/node16": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@tufjs/canonical-json": {
|
"@tufjs/canonical-json": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||||
@ -22898,6 +22818,12 @@
|
|||||||
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/html-to-text": {
|
||||||
|
"version": "9.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz",
|
||||||
|
"integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/istanbul-lib-coverage": {
|
"@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
@ -23173,15 +23099,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="
|
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
|
||||||
"version": "8.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
|
||||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"acorn": "^8.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent-base": {
|
"agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@ -24199,12 +24116,6 @@
|
|||||||
"yaml": "^1.10.0"
|
"yaml": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create-require": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"crelt": {
|
"crelt": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
@ -24540,12 +24451,6 @@
|
|||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"diff": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"dir-glob": {
|
"dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@ -25638,6 +25543,29 @@
|
|||||||
"void-elements": "3.1.0"
|
"void-elements": "3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-to-text": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||||
|
"requires": {
|
||||||
|
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"http-cache-semantics": {
|
"http-cache-semantics": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
@ -26246,6 +26174,11 @@
|
|||||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"leac": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="
|
||||||
|
},
|
||||||
"lilconfig": {
|
"lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
@ -26353,12 +26286,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"make-error": {
|
|
||||||
"version": "1.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
|
||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"make-fetch-happen": {
|
"make-fetch-happen": {
|
||||||
"version": "11.1.1",
|
"version": "11.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
|
||||||
@ -27335,6 +27262,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parseley": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||||
|
"requires": {
|
||||||
|
"leac": "^0.6.0",
|
||||||
|
"peberminta": "^0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pastebar-app-ui": {
|
"pastebar-app-ui": {
|
||||||
"version": "file:packages/pastebar-app-ui",
|
"version": "file:packages/pastebar-app-ui",
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -27435,6 +27371,7 @@
|
|||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.5",
|
||||||
"garbados-crypt": "^3.0.0-beta",
|
"garbados-crypt": "^3.0.0-beta",
|
||||||
"glob-all": "^3.3.1",
|
"glob-all": "^3.3.1",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
@ -27883,6 +27820,11 @@
|
|||||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"peberminta": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="
|
||||||
|
},
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
@ -28896,6 +28838,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
||||||
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="
|
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="
|
||||||
},
|
},
|
||||||
|
"selderee": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||||
|
"requires": {
|
||||||
|
"parseley": "^0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@ -29686,35 +29636,6 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ts-node": {
|
|
||||||
"version": "10.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
|
||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
|
||||||
"@tsconfig/node10": "^1.0.7",
|
|
||||||
"@tsconfig/node12": "^1.0.7",
|
|
||||||
"@tsconfig/node14": "^1.0.0",
|
|
||||||
"@tsconfig/node16": "^1.0.2",
|
|
||||||
"acorn": "^8.4.1",
|
|
||||||
"acorn-walk": "^8.1.1",
|
|
||||||
"arg": "^4.1.0",
|
|
||||||
"create-require": "^1.1.0",
|
|
||||||
"diff": "^4.0.1",
|
|
||||||
"make-error": "^1.1.1",
|
|
||||||
"v8-compile-cache-lib": "^3.0.1",
|
|
||||||
"yn": "3.1.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"arg": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tsconfig-paths": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||||
@ -30009,12 +29930,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
||||||
},
|
},
|
||||||
"v8-compile-cache-lib": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"validate-npm-package-license": {
|
"validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
@ -30343,12 +30258,6 @@
|
|||||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"yn": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"youtube-player": {
|
"youtube-player": {
|
||||||
"version": "5.5.2",
|
"version": "5.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
||||||
|
@ -156,6 +156,7 @@
|
|||||||
"@types/codemirror": "^5.60.15",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/linkify-it": "^3.0.5",
|
"@types/linkify-it": "^3.0.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.5",
|
||||||
"garbados-crypt": "^3.0.0-beta",
|
"garbados-crypt": "^3.0.0-beta",
|
||||||
"glob-all": "^3.3.1",
|
"glob-all": "^3.3.1",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
|
@ -34,6 +34,8 @@ import {
|
|||||||
settingsStoreAtom,
|
settingsStoreAtom,
|
||||||
themeStoreAtom,
|
themeStoreAtom,
|
||||||
uiStoreAtom,
|
uiStoreAtom,
|
||||||
|
DEFAULT_SPECIAL_PASTE_OPERATIONS,
|
||||||
|
DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
} from './store'
|
} from './store'
|
||||||
|
|
||||||
const appIdleEvents = ['mousemove', 'keydown', 'scroll', 'keypress', 'mousedown']
|
const appIdleEvents = ['mousemove', 'keydown', 'scroll', 'keypress', 'mousedown']
|
||||||
@ -238,6 +240,14 @@ function App() {
|
|||||||
globalTemplates: settings.globalTemplates?.valueText
|
globalTemplates: settings.globalTemplates?.valueText
|
||||||
? settings.globalTemplates.valueText // Will be parsed by initSettings in store
|
? settings.globalTemplates.valueText // Will be parsed by initSettings in store
|
||||||
: [], // Default to empty array
|
: [], // Default to empty array
|
||||||
|
isSpecialCopyPasteHistoryEnabled:
|
||||||
|
settings.isSpecialCopyPasteHistoryEnabled?.valueBool ?? true,
|
||||||
|
enabledSpecialPasteOperations: settings.enabledSpecialPasteOperations?.valueText
|
||||||
|
? settings.enabledSpecialPasteOperations.valueText.split(',').filter(Boolean)
|
||||||
|
: [...DEFAULT_SPECIAL_PASTE_OPERATIONS],
|
||||||
|
specialPasteCategoriesOrder: settings.specialPasteCategoriesOrder?.valueText
|
||||||
|
? settings.specialPasteCategoriesOrder.valueText.split(',').filter(Boolean)
|
||||||
|
: [...DEFAULT_SPECIAL_PASTE_CATEGORIES],
|
||||||
isAppReady: true,
|
isAppReady: true,
|
||||||
})
|
})
|
||||||
settingsStore.initConstants({
|
settingsStore.initConstants({
|
||||||
|
@ -13,7 +13,7 @@ const useDeleteConfirmationTimer = ({
|
|||||||
onConfirmedDelete: () => Promise<void>
|
onConfirmedDelete: () => Promise<void>
|
||||||
onConfirmedReset?: () => void
|
onConfirmedReset?: () => void
|
||||||
selectedHistoryItems: UniqueIdentifier[]
|
selectedHistoryItems: UniqueIdentifier[]
|
||||||
hoveringHistoryRowId: Signal<UniqueIdentifier | null> | null
|
hoveringHistoryRowId: Signal<UniqueIdentifier | null>
|
||||||
timerDuration?: number
|
timerDuration?: number
|
||||||
}) => {
|
}) => {
|
||||||
const timerRef = useRef(null) as React.MutableRefObject<NodeJS.Timeout | null>
|
const timerRef = useRef(null) as React.MutableRefObject<NodeJS.Timeout | null>
|
||||||
@ -34,15 +34,17 @@ const useDeleteConfirmationTimer = ({
|
|||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current)
|
clearTimeout(timerRef.current)
|
||||||
}
|
}
|
||||||
if (hoveringHistoryRowId?.value && selectedHistoryItems.length === 0) {
|
|
||||||
|
if (selectedHistoryItems.length === 0) {
|
||||||
seHoveringHistoryIdDelete(hoveringHistoryRowId.value)
|
seHoveringHistoryIdDelete(hoveringHistoryRowId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowConfirmation(true)
|
setShowConfirmation(true)
|
||||||
|
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
resetTimer()
|
resetTimer()
|
||||||
}, timerDuration)
|
}, timerDuration)
|
||||||
}, [timerDuration, resetTimer, selectedHistoryItems])
|
}, [timerDuration, resetTimer, selectedHistoryItems, hoveringHistoryRowId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
import { Signal } from '@preact/signals-react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
const useKeyboardDeleteConfirmation = ({
|
||||||
|
onConfirmedDelete,
|
||||||
|
keyboardSelectedItemId,
|
||||||
|
onConfirmedReset,
|
||||||
|
selectedHistoryItems,
|
||||||
|
timerDuration = 3000,
|
||||||
|
}: {
|
||||||
|
onConfirmedDelete: () => Promise<void>
|
||||||
|
onConfirmedReset?: () => void
|
||||||
|
selectedHistoryItems: UniqueIdentifier[]
|
||||||
|
keyboardSelectedItemId: Signal<UniqueIdentifier | null>
|
||||||
|
timerDuration?: number
|
||||||
|
}) => {
|
||||||
|
const timerRef = useRef(null) as React.MutableRefObject<NodeJS.Timeout | null>
|
||||||
|
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||||
|
const [keyboardItemIdDelete, setKeyboardItemIdDelete] =
|
||||||
|
useState<UniqueIdentifier | null>(null)
|
||||||
|
|
||||||
|
const resetTimer = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
}
|
||||||
|
setKeyboardItemIdDelete(null)
|
||||||
|
setShowConfirmation(false)
|
||||||
|
onConfirmedReset?.()
|
||||||
|
}, [onConfirmedReset])
|
||||||
|
|
||||||
|
const startTimer = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if there's a keyboard selected item and no multi-selection
|
||||||
|
if (!keyboardSelectedItemId.value || selectedHistoryItems.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeyboardItemIdDelete(keyboardSelectedItemId.value)
|
||||||
|
setShowConfirmation(true)
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
resetTimer()
|
||||||
|
}, timerDuration)
|
||||||
|
}, [timerDuration, resetTimer, selectedHistoryItems, keyboardSelectedItemId])
|
||||||
|
|
||||||
|
// Reset confirmation when the keyboard selected item changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (showConfirmation && keyboardItemIdDelete !== keyboardSelectedItemId.value) {
|
||||||
|
resetTimer()
|
||||||
|
}
|
||||||
|
}, [keyboardSelectedItemId.value, showConfirmation, keyboardItemIdDelete, resetTimer])
|
||||||
|
|
||||||
|
// Reset confirmation when there are selected items (multi-selection mode)
|
||||||
|
useEffect(() => {
|
||||||
|
if (showConfirmation && selectedHistoryItems.length > 0) {
|
||||||
|
resetTimer()
|
||||||
|
}
|
||||||
|
}, [selectedHistoryItems.length, showConfirmation, resetTimer])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['delete', 'backspace'],
|
||||||
|
async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Only handle keyboard delete when there's a keyboard selected item and no multi-selection
|
||||||
|
if (!keyboardSelectedItemId.value || selectedHistoryItems.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showConfirmation) {
|
||||||
|
await onConfirmedDelete()
|
||||||
|
resetTimer()
|
||||||
|
} else {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableOnFormTags: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
showConfirmation,
|
||||||
|
keyboardItemIdDelete,
|
||||||
|
resetTimer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useKeyboardDeleteConfirmation
|
@ -0,0 +1,164 @@
|
|||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { UniqueIdentifier } from '@dnd-kit/core/dist/types'
|
||||||
|
import { signal } from '@preact/signals-react'
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { settingsStoreAtom } from '~/store'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { applyTransform, TEXT_TRANSFORMS } from '~/lib/text-transforms'
|
||||||
|
|
||||||
|
// Signals for tracking special copy/paste operations
|
||||||
|
export const specialCopiedItem = signal<UniqueIdentifier>('')
|
||||||
|
export const specialPastedItem = signal<UniqueIdentifier>('')
|
||||||
|
export const specialPastedItemCountDown = signal<number>(0)
|
||||||
|
|
||||||
|
interface UseSpecialCopyPasteOptions {
|
||||||
|
delay?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSpecialCopyPasteHistoryItem = ({
|
||||||
|
delay = 800,
|
||||||
|
}: UseSpecialCopyPasteOptions = {}) => {
|
||||||
|
const { copyPasteDelay } = useAtomValue(settingsStoreAtom)
|
||||||
|
const countdownRef = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
|
// Special copy function - applies transformation and copies to clipboard
|
||||||
|
const specialCopy = async (
|
||||||
|
historyId: UniqueIdentifier,
|
||||||
|
value: string,
|
||||||
|
transformId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!value || !historyId) {
|
||||||
|
console.warn('No value or historyId to copy')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the signal to show UI feedback
|
||||||
|
specialCopiedItem.value = historyId
|
||||||
|
|
||||||
|
// Apply the text transformation - this will throw if it fails
|
||||||
|
const transformedText = await applyTransform(value, transformId)
|
||||||
|
|
||||||
|
// Only copy if transformation was successful
|
||||||
|
setTimeout(() => {
|
||||||
|
invoke('copy_text', { text: transformedText })
|
||||||
|
.then(res => {
|
||||||
|
if (res === 'ok') {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
specialCopiedItem.value = ''
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
specialCopiedItem.value = ''
|
||||||
|
console.error('Failed to copy transformed text', res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
specialCopiedItem.value = ''
|
||||||
|
console.error('Failed to copy transformed text', err)
|
||||||
|
})
|
||||||
|
}, delay)
|
||||||
|
} catch (error) {
|
||||||
|
// Clear UI feedback immediately on transformation error
|
||||||
|
specialCopiedItem.value = ''
|
||||||
|
console.error('Failed to special copy - transformation error:', error)
|
||||||
|
// Don't copy anything to clipboard when transformation fails
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown helper for paste operations
|
||||||
|
const pasteCountdown = useCallback(
|
||||||
|
(initialCount: number, intervalMs = 1000): Promise<void> => {
|
||||||
|
clearInterval(countdownRef.current)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
specialPastedItemCountDown.value = initialCount
|
||||||
|
countdownRef.current = setInterval(() => {
|
||||||
|
if (specialPastedItemCountDown.value > 0) {
|
||||||
|
if (specialPastedItemCountDown.value === 1) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
specialPastedItemCountDown.value -= 1
|
||||||
|
} else {
|
||||||
|
clearInterval(countdownRef.current)
|
||||||
|
}
|
||||||
|
}, intervalMs)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute paste action with transformed text
|
||||||
|
const executePasteAction = (text: string, delay = 0): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
invoke('copy_paste', { text, delay })
|
||||||
|
.then(res => {
|
||||||
|
if (res === 'ok') {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
console.error('Failed to paste transformed text', res)
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to paste transformed text', err)
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special paste function - applies transformation and pastes directly
|
||||||
|
const specialPaste = async (
|
||||||
|
historyId: UniqueIdentifier,
|
||||||
|
value: string,
|
||||||
|
transformId: string,
|
||||||
|
delaySeconds?: number
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
delaySeconds = delaySeconds ?? copyPasteDelay
|
||||||
|
|
||||||
|
if (!value || !historyId) {
|
||||||
|
console.warn('No value or historyId to paste')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the signal to show UI feedback
|
||||||
|
specialPastedItem.value = historyId
|
||||||
|
|
||||||
|
// Apply the text transformation - this will throw if it fails
|
||||||
|
const transformedText = await applyTransform(value, transformId)
|
||||||
|
|
||||||
|
// Handle countdown if delay is specified (only if transformation succeeded)
|
||||||
|
if (delaySeconds > 0) {
|
||||||
|
await pasteCountdown(delaySeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute paste with transformed text (only if transformation succeeded)
|
||||||
|
await executePasteAction(transformedText, 0)
|
||||||
|
|
||||||
|
// Clear the signal after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
specialPastedItem.value = ''
|
||||||
|
specialPastedItemCountDown.value = 0
|
||||||
|
})
|
||||||
|
}, delay)
|
||||||
|
} catch (error) {
|
||||||
|
// Clear UI feedback immediately on transformation error
|
||||||
|
specialPastedItem.value = ''
|
||||||
|
specialPastedItemCountDown.value = 0
|
||||||
|
console.error('Failed to special paste - transformation error:', error)
|
||||||
|
// Don't paste anything when transformation fails
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
specialCopy,
|
||||||
|
specialPaste,
|
||||||
|
availableTransforms: TEXT_TRANSFORMS,
|
||||||
|
specialCopiedItem: specialCopiedItem.value,
|
||||||
|
specialPastedItem: specialPastedItem.value,
|
||||||
|
specialPastedItemCountDown: specialPastedItemCountDown.value,
|
||||||
|
}
|
||||||
|
}
|
505
packages/pastebar-app-ui/src/lib/text-transforms.ts
Normal file
505
packages/pastebar-app-ui/src/lib/text-transforms.ts
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
/**
|
||||||
|
* Text transformation utilities for special copy/paste operations
|
||||||
|
* Organized by categories with enable/disable controls
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
export interface TextTransform {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
transform: (text: string) => string | Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformCategory {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
transforms?: TextTransform[]
|
||||||
|
subcategories?: TransformSubcategory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformSubcategory {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
transforms: TextTransform[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform functions for Text Case
|
||||||
|
const toUpperCase = (text: string): string => text.toUpperCase()
|
||||||
|
const toLowerCase = (text: string): string => text.toLowerCase()
|
||||||
|
const toTitleCase = (text: string): string =>
|
||||||
|
text.replace(/\b\w/g, char => char.toUpperCase()) // Renamed from toCapitalize
|
||||||
|
const toSentenceCase = (text: string): string =>
|
||||||
|
text.charAt(0).toUpperCase() + text.slice(1).toLowerCase() // Keep as is, assumes single string capitalization
|
||||||
|
const toInvertCase = (text: string): string =>
|
||||||
|
text.replace(/[a-zA-Z]/g, char =>
|
||||||
|
char === char.toUpperCase() ? char.toLowerCase() : char.toUpperCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transform functions for Code Formatting
|
||||||
|
const toCamelCase = (text: string): string => {
|
||||||
|
const normalized = text.replace(/[^a-zA-Z0-9]+/g, ' ').trim() // Normalize spaces/delimiters
|
||||||
|
return normalized
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
|
||||||
|
index === 0 ? word.toLowerCase() : word.toUpperCase()
|
||||||
|
)
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSnakeCase = (text: string): string => {
|
||||||
|
return text
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2') // Add underscore before capital letters (camelCase to snake_case part)
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, '_') // Replace non-alphanumeric with underscore
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
||||||
|
}
|
||||||
|
|
||||||
|
const toKebabCase = (text: string): string => {
|
||||||
|
return text
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2') // Add hyphen before capital letters
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, '-') // Replace non-alphanumeric with hyphen
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||||
|
}
|
||||||
|
|
||||||
|
const toPascalCase = (text: string): string => {
|
||||||
|
const normalized = text.replace(/[^a-zA-Z0-9]+/g, ' ').trim() // Normalize spaces/delimiters
|
||||||
|
return normalized
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, word => word.toUpperCase())
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform functions for Whitespace & Lines
|
||||||
|
const trimWhiteSpace = (text: string): string => text.trim()
|
||||||
|
const removeLineFeeds = (
|
||||||
|
text: string
|
||||||
|
): string => // More aggressive removal of multiple line feeds to single space
|
||||||
|
text
|
||||||
|
.replace(/\r?\n|\r/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
const addOneLineFeed = (text: string): string => text + '\n'
|
||||||
|
const addTwoLineFeeds = (text: string): string => text + '\n\n'
|
||||||
|
const removeExtraSpaces = (text: string): string => text.replace(/\s+/g, ' ')
|
||||||
|
const sortLinesAlphabetically = (text: string): string => {
|
||||||
|
const lines = text.split(/\r?\n|\r/)
|
||||||
|
return lines.sort((a, b) => a.localeCompare(b)).join('\n')
|
||||||
|
}
|
||||||
|
const removeDuplicateLines = (text: string): string => {
|
||||||
|
const lines = text.split(/\r?\n|\r/)
|
||||||
|
const uniqueLines = [...new Set(lines)]
|
||||||
|
return uniqueLines.join('\n')
|
||||||
|
}
|
||||||
|
const addLineNumbers = (text: string): string => {
|
||||||
|
const lines = text.split(/\r?\n|\r/)
|
||||||
|
return lines.map((line, index) => `${index + 1}. ${line}`).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform functions for Encoding & Security
|
||||||
|
const toBase64Encode = (text: string): string => {
|
||||||
|
try {
|
||||||
|
// This is generally the most robust method for UTF-8 in browsers without polyfills
|
||||||
|
return btoa(String.fromCharCode(...new TextEncoder().encode(text)))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Base64 encode error:', e)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toBase64Decode = (text: string): string => {
|
||||||
|
try {
|
||||||
|
// This is generally the most robust method for UTF-8 in browsers without polyfills
|
||||||
|
return new TextDecoder().decode(
|
||||||
|
Uint8Array.from(atob(text), charCode => charCode.charCodeAt(0))
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Base64 decode error:', e)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toUrlEncode = (text: string): string => encodeURIComponent(text)
|
||||||
|
const toUrlDecode = (text: string): string => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(text)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('URL decode error:', e)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHtmlEncode(str: string): string {
|
||||||
|
// Ensure the input is a string
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
console.warn('Input to encodeHtmlSpecialChars was not a string:', str)
|
||||||
|
return '' // Or throw an error, depending on desired behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.replace(/[&<>"']/g, function (char) {
|
||||||
|
switch (char) {
|
||||||
|
case '&':
|
||||||
|
return '&' // Ampersand
|
||||||
|
case '<':
|
||||||
|
return '<' // Less than
|
||||||
|
case '>':
|
||||||
|
return '>' // Greater than
|
||||||
|
case '"':
|
||||||
|
return '"' // Double quote
|
||||||
|
case "'":
|
||||||
|
return ''' // Single quote (apostrophe)
|
||||||
|
default:
|
||||||
|
return char // Should not happen with the given regex, but good practice
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toHtmlDecode = (text: string): string => {
|
||||||
|
const sanitized = DOMPurify.sanitize(text, { RETURN_DOM: true })
|
||||||
|
return sanitized.textContent || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform functions for Text Tools
|
||||||
|
const reverseText = (text: string): string => text.split('').reverse().join('')
|
||||||
|
const addCurrentDateTime = (text: string): string => {
|
||||||
|
const now = new Date()
|
||||||
|
return text + '\n' + now.toLocaleString()
|
||||||
|
}
|
||||||
|
const countCharacters = (text: string): string => {
|
||||||
|
const count = text.length
|
||||||
|
return `Character count: ${count}`
|
||||||
|
}
|
||||||
|
const countWords = (text: string): string => {
|
||||||
|
const words = text
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(word => word.length > 0)
|
||||||
|
const count = words.length
|
||||||
|
return `Word count: ${count}`
|
||||||
|
}
|
||||||
|
const countLines = (text: string): string => {
|
||||||
|
const lines = text.split(/\r?\n|\r/)
|
||||||
|
const count = lines.length
|
||||||
|
return `Line count: ${count}`
|
||||||
|
}
|
||||||
|
const countSentences = (text: string): string => {
|
||||||
|
const sentences = text.split(/[.!?]+/).filter(sentence => sentence.trim().length > 0)
|
||||||
|
const count = sentences.length
|
||||||
|
return `Sentence count: ${count}`
|
||||||
|
}
|
||||||
|
const toJsonStringify = (text: string): string => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return JSON.stringify(parsed, null, 2)
|
||||||
|
} catch {
|
||||||
|
return JSON.stringify(text) // If not valid JSON, stringify the text itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format Converter subcategories - organized by source format
|
||||||
|
const formatConverterSubcategories = [
|
||||||
|
{
|
||||||
|
id: 'html',
|
||||||
|
label: 'HTML',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'htmlToMarkdown',
|
||||||
|
label: 'HTML to Markdown',
|
||||||
|
transform: (text: string) => convertFormat(text, 'html_to_markdown'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'htmlToReact',
|
||||||
|
label: 'HTML to React JSX',
|
||||||
|
transform: (text: string) => convertFormat(text, 'html_to_react'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'htmlToReactComponent',
|
||||||
|
label: 'HTML to React Component',
|
||||||
|
transform: (text: string) => convertFormat(text, 'html_to_react_components'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'htmlToText',
|
||||||
|
label: 'HTML to Text',
|
||||||
|
transform: (text: string) => convertFormat(text, 'html_to_text'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'htmlToPlainText',
|
||||||
|
label: 'HTML to Plain Text',
|
||||||
|
transform: async (text: string) => {
|
||||||
|
// dynamic import for html-to-text
|
||||||
|
const { convert } = await import('html-to-text')
|
||||||
|
return convert(text, {
|
||||||
|
wordwrap: 130,
|
||||||
|
preserveNewlines: false,
|
||||||
|
// selectors: [{ selector: 'a', format: }],
|
||||||
|
})
|
||||||
|
.replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with double newline
|
||||||
|
.trim() // Remove leading and trailing whitespace
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'markdown',
|
||||||
|
label: 'Markdown',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'markdownToHtml',
|
||||||
|
label: 'Markdown to HTML',
|
||||||
|
transform: (text: string) => convertFormat(text, 'markdown_to_html'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'markdownToText',
|
||||||
|
label: 'Markdown to Text',
|
||||||
|
transform: (text: string) => convertFormat(text, 'markdown_to_text'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'json',
|
||||||
|
label: 'JSON',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'jsonToCsv',
|
||||||
|
label: 'JSON to CSV',
|
||||||
|
transform: (text: string) => convertFormat(text, 'json_to_csv'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jsonToYaml',
|
||||||
|
label: 'JSON to YAML',
|
||||||
|
transform: (text: string) => convertFormat(text, 'json_to_yaml'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jsonToXml',
|
||||||
|
label: 'JSON to XML',
|
||||||
|
transform: (text: string) => convertFormat(text, 'json_to_xml'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jsonToToml',
|
||||||
|
label: 'JSON to TOML',
|
||||||
|
transform: (text: string) => convertFormat(text, 'json_to_toml'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jsonToTable',
|
||||||
|
label: 'JSON to Markdown Table',
|
||||||
|
transform: (text: string) => convertFormat(text, 'json_to_table'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'csv',
|
||||||
|
label: 'CSV',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'csvToJson',
|
||||||
|
label: 'CSV to JSON',
|
||||||
|
transform: (text: string) => convertFormat(text, 'csv_to_json'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'csvToTable',
|
||||||
|
label: 'CSV to Markdown Table',
|
||||||
|
transform: (text: string) => convertFormat(text, 'csv_to_table'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yaml',
|
||||||
|
label: 'YAML',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'yamlToJson',
|
||||||
|
label: 'YAML to JSON',
|
||||||
|
transform: (text: string) => convertFormat(text, 'yaml_to_json'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xml',
|
||||||
|
label: 'XML',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'xmlToJson',
|
||||||
|
label: 'XML to JSON',
|
||||||
|
transform: (text: string) => convertFormat(text, 'xml_to_json'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toml',
|
||||||
|
label: 'TOML',
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: 'tomlToJson',
|
||||||
|
label: 'TOML to JSON',
|
||||||
|
transform: (text: string) => convertFormat(text, 'toml_to_json'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Helper function to call Rust format conversion
|
||||||
|
function convertFormat(text: string, conversionType: string): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const { invoke } = await import('@tauri-apps/api/tauri')
|
||||||
|
const result = await invoke('format_convert', { text, conversionType })
|
||||||
|
resolve(result as string)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Format conversion failed for ${conversionType}:`, error)
|
||||||
|
|
||||||
|
// Show error dialog to user
|
||||||
|
try {
|
||||||
|
const { message } = await import('@tauri-apps/api/dialog')
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
// Clean up the error message for better user experience
|
||||||
|
const cleanErrorMessage = errorMessage
|
||||||
|
.replace(/^Error: /, '')
|
||||||
|
.replace(/^format_convert returned an error: /, '')
|
||||||
|
|
||||||
|
await message(`${cleanErrorMessage}`, {
|
||||||
|
title: 'Format Conversion Error',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} catch (dialogError) {
|
||||||
|
console.error('Failed to show error dialog:', dialogError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject the promise so the error propagates and prevents copy/paste
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Convert category will be implemented in Rust later for better performance
|
||||||
|
|
||||||
|
// Categorized transformations
|
||||||
|
export const TRANSFORM_CATEGORIES: TransformCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'textCase',
|
||||||
|
label: 'Text Case',
|
||||||
|
transforms: [
|
||||||
|
{ id: 'upperCase', label: 'UPPER CASE', transform: toUpperCase },
|
||||||
|
{ id: 'lowerCase', label: 'lower case', transform: toLowerCase },
|
||||||
|
{ id: 'titleCase', label: 'Title Case', transform: toTitleCase },
|
||||||
|
{ id: 'sentenceCase', label: 'Sentence case', transform: toSentenceCase },
|
||||||
|
{ id: 'invertCase', label: 'iNVERT cASE', transform: toInvertCase },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'codeFormatting',
|
||||||
|
label: 'Code Formatting',
|
||||||
|
transforms: [
|
||||||
|
{ id: 'camelCase', label: 'camelCase', transform: toCamelCase },
|
||||||
|
{ id: 'snakeCase', label: 'snake_case', transform: toSnakeCase },
|
||||||
|
{ id: 'kebabCase', label: 'kebab-case', transform: toKebabCase },
|
||||||
|
{ id: 'pascalCase', label: 'PascalCase', transform: toPascalCase },
|
||||||
|
{ id: 'jsonStringify', label: 'JSON Stringify', transform: toJsonStringify },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'whitespaceLines',
|
||||||
|
label: 'Whitespace & Lines',
|
||||||
|
transforms: [
|
||||||
|
{ id: 'trimWhiteSpace', label: 'Trim White Space', transform: trimWhiteSpace },
|
||||||
|
{ id: 'removeLineFeeds', label: 'Remove Line Feeds', transform: removeLineFeeds },
|
||||||
|
{ id: 'addOneLineFeed', label: 'Add One Line Feed', transform: addOneLineFeed },
|
||||||
|
{ id: 'addTwoLineFeeds', label: 'Add Two Line Feeds', transform: addTwoLineFeeds },
|
||||||
|
{
|
||||||
|
id: 'removeExtraSpaces',
|
||||||
|
label: 'Remove Extra Spaces',
|
||||||
|
transform: removeExtraSpaces,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sortLinesAlphabetically',
|
||||||
|
label: 'Sort Lines Alphabetically',
|
||||||
|
transform: sortLinesAlphabetically,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'removeDuplicateLines',
|
||||||
|
label: 'Remove Duplicate Lines',
|
||||||
|
transform: removeDuplicateLines,
|
||||||
|
},
|
||||||
|
{ id: 'addLineNumbers', label: 'Add Line Numbers', transform: addLineNumbers },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'encodingSecurity',
|
||||||
|
label: 'Encode/Decode',
|
||||||
|
transforms: [
|
||||||
|
{ id: 'base64Encode', label: 'Base64 Encode', transform: toBase64Encode },
|
||||||
|
{ id: 'base64Decode', label: 'Base64 Decode', transform: toBase64Decode },
|
||||||
|
{ id: 'urlEncode', label: 'URL Encode', transform: toUrlEncode },
|
||||||
|
{ id: 'urlDecode', label: 'URL Decode', transform: toUrlDecode },
|
||||||
|
{ id: 'htmlEncode', label: 'HTML Encode', transform: toHtmlEncode },
|
||||||
|
{ id: 'htmlDecode', label: 'HTML Decode', transform: toHtmlDecode },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'textTools',
|
||||||
|
label: 'Text Tools',
|
||||||
|
transforms: [
|
||||||
|
{ id: 'reverseText', label: 'Reverse Text', transform: reverseText },
|
||||||
|
{
|
||||||
|
id: 'addCurrentDateTime',
|
||||||
|
label: 'Add Current Date/Time',
|
||||||
|
transform: addCurrentDateTime,
|
||||||
|
},
|
||||||
|
{ id: 'countCharacters', label: 'Count Characters', transform: countCharacters },
|
||||||
|
{ id: 'countWords', label: 'Count Words', transform: countWords },
|
||||||
|
{ id: 'countLines', label: 'Count Lines', transform: countLines },
|
||||||
|
{ id: 'countSentences', label: 'Count Sentences', transform: countSentences },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'formatConverter',
|
||||||
|
label: 'Format Converter',
|
||||||
|
subcategories: formatConverterSubcategories,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Flat list of all transformations for backward compatibility
|
||||||
|
export const TEXT_TRANSFORMS: TextTransform[] = TRANSFORM_CATEGORIES.flatMap(category => {
|
||||||
|
if (category.subcategories) {
|
||||||
|
// For categories with subcategories, flatten all transforms from all subcategories
|
||||||
|
return category.subcategories.flatMap(subcategory => subcategory.transforms)
|
||||||
|
} else {
|
||||||
|
// For categories with direct transforms
|
||||||
|
return category.transforms || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper to get a transform by ID
|
||||||
|
export const getTransformById = (id: string): TextTransform | undefined =>
|
||||||
|
TEXT_TRANSFORMS.find(t => t.id === id)
|
||||||
|
|
||||||
|
// Helper to get a category by ID
|
||||||
|
export const getCategoryById = (id: string): TransformCategory | undefined =>
|
||||||
|
TRANSFORM_CATEGORIES.find(c => c.id === id)
|
||||||
|
|
||||||
|
// Helper to apply a transform by ID
|
||||||
|
export const applyTransform = async (
|
||||||
|
text: string,
|
||||||
|
transformId: string
|
||||||
|
): Promise<string> => {
|
||||||
|
const transform = getTransformById(transformId)
|
||||||
|
if (!transform) {
|
||||||
|
throw new Error(`Transform not found: ${transformId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = transform.transform(text)
|
||||||
|
// Handle both sync and async transforms
|
||||||
|
return await Promise.resolve(result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Transform failed for ${transformId}:`, error)
|
||||||
|
// Re-throw the error so calling functions can handle it
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get all category IDs
|
||||||
|
export const getAllCategoryIds = (): string[] => TRANSFORM_CATEGORIES.map(c => c.id)
|
||||||
|
|
||||||
|
// Helper to get all transform IDs in a category
|
||||||
|
export const getTransformIdsInCategory = (categoryId: string): string[] => {
|
||||||
|
const category = getCategoryById(categoryId)
|
||||||
|
return category ? category.transforms?.map(t => t.id) ?? [] : []
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Aktuelles Datum/Uhrzeit hinzufügen
|
||||||
|
Add Line Numbers: Zeilennummern hinzufügen
|
||||||
|
Add One Line Feed: Einen Zeilenvorschub hinzufügen
|
||||||
|
Add Two Line Feeds: Zwei Zeilenvorschübe hinzufügen
|
||||||
|
Base64 Decode: Base64 dekodieren
|
||||||
|
Base64 Encode: Base64 kodieren
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV zu JSON
|
||||||
|
CSV to Markdown Table: CSV zu Markdown-Tabelle
|
||||||
|
Capitalize Case: Erster Buchstabe groß
|
||||||
|
Code Formatting: Code-Formatierung
|
||||||
|
Count Characters: Zeichen zählen
|
||||||
|
Count Lines: Zeilen zählen
|
||||||
|
Count Sentences: Sätze zählen
|
||||||
|
Count Words: Wörter zählen
|
||||||
|
Data Conversion: Datenkonvertierung
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Ziehe eine Kategorie per Drag & Drop, um ihre Reihenfolge im speziellen Kopieren/Einfügen-Menü zu priorisieren.
|
||||||
|
Enable All: Alle aktivieren
|
||||||
|
Enable special text transformation options for clipboard history items: Spezielle Textumwandlungsoptionen für Zwischenablage-Verlaufselemente aktivieren
|
||||||
|
Enabled Operations: Aktivierte Operationen
|
||||||
|
Encode/Decode: Kodieren/Dekodieren
|
||||||
|
Format Converter: Formatkonverter
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: HTML dekodieren
|
||||||
|
HTML Encode: HTML kodieren
|
||||||
|
HTML to Markdown: HTML zu Markdown
|
||||||
|
HTML to Plain Text: HTML zu reinem Text
|
||||||
|
HTML to React Component: HTML zu React-Komponente
|
||||||
|
HTML to React JSX: HTML zu React JSX
|
||||||
|
HTML to Text: HTML zu Text
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON Stringify
|
||||||
|
JSON to CSV: JSON zu CSV
|
||||||
|
JSON to Markdown Table: JSON zu Markdown-Tabelle
|
||||||
|
JSON to TOML: JSON zu TOML
|
||||||
|
JSON to XML: JSON zu XML
|
||||||
|
JSON to YAML: JSON zu YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown zu HTML
|
||||||
|
Markdown to Text: Markdown zu Text
|
||||||
|
Operations: Operationen
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Kategorie-Reihenfolge priorisieren
|
||||||
|
Remove Duplicate Lines: Doppelte Zeilen entfernen
|
||||||
|
Remove Extra Spaces: Zusätzliche Leerzeichen entfernen
|
||||||
|
Remove Line Feeds: Zeilenvorschübe entfernen
|
||||||
|
Reverse Text: Text umkehren
|
||||||
|
Select Operations: Operationen auswählen
|
||||||
|
Sentence case: Satzbau
|
||||||
|
Sort Lines Alphabetically: Zeilen alphabetisch sortieren
|
||||||
|
Special Copy: Spezielles Kopieren
|
||||||
|
Special Copy/Paste Operations: Spezielle Kopier-/Einfüge-Operationen
|
||||||
|
Special Paste: Spezielles Einfügen
|
||||||
|
Special Settings: Spezielle Einstellungen
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML zu JSON
|
||||||
|
Text Case: Groß-/Kleinschreibung
|
||||||
|
Text Tools: Textwerkzeuge
|
||||||
|
Title Case: Titel-Fall
|
||||||
|
Trim White Space: Leerzeichen entfernen
|
||||||
|
UPPER CASE: GROSSBUCHSTABEN
|
||||||
|
URL Decode: URL dekodieren
|
||||||
|
URL Encode: URL kodieren
|
||||||
|
Whitespace & Lines: Leerraum & Zeilen
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML zu JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML zu JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: gROSS/kLEIN uMKEHREN
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: kleinbuchstaben
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1 @@
|
|||||||
|
Special Settings: Special Settings
|
@ -0,0 +1,74 @@
|
|||||||
|
Add Current Date/Time: Add Current Date/Time
|
||||||
|
Add Line Numbers: Add Line Numbers
|
||||||
|
Add One Line Feed: Add One Line Feed
|
||||||
|
Add Two Line Feeds: Add Two Line Feeds
|
||||||
|
Base64 Decode: Base64 Decode
|
||||||
|
Base64 Encode: Base64 Encode
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV to JSON
|
||||||
|
CSV to Markdown Table: CSV to Markdown Table
|
||||||
|
Capitalize Case: Capitalize Case
|
||||||
|
Code Formatting: Code Formatting
|
||||||
|
Count Characters: Count Characters
|
||||||
|
Count Lines: Count Lines
|
||||||
|
Count Sentences: Count Sentences
|
||||||
|
Count Words: Count Words
|
||||||
|
Data Conversion: Data Conversion
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Drag and drop category to prioritize its order in the special copy/paste menu.
|
||||||
|
Enable All: Enable All
|
||||||
|
Enable special text transformation options for clipboard history items: Enable special text transformation options for clipboard history items
|
||||||
|
Enabled Operations: Enabled Operations
|
||||||
|
Encode/Decode: Encode/Decode
|
||||||
|
Format Converter: Format Converter
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: HTML Decode
|
||||||
|
HTML Encode: HTML Encode
|
||||||
|
HTML to Markdown: HTML to Markdown
|
||||||
|
HTML to Plain Text: HTML to Plain Text
|
||||||
|
HTML to React Component: HTML to React Component
|
||||||
|
HTML to React JSX: HTML to React JSX
|
||||||
|
HTML to Text: HTML to Text
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON Stringify
|
||||||
|
JSON to CSV: JSON to CSV
|
||||||
|
JSON to Markdown Table: JSON to Markdown Table
|
||||||
|
JSON to TOML: JSON to TOML
|
||||||
|
JSON to XML: JSON to XML
|
||||||
|
JSON to YAML: JSON to YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown to HTML
|
||||||
|
Markdown to Text: Markdown to Text
|
||||||
|
Operations: Operations
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Prioritize Category Order
|
||||||
|
PrioritizeCategoryOrderNote: PrioritizeCategoryOrderNote
|
||||||
|
Remove Duplicate Lines: Remove Duplicate Lines
|
||||||
|
Remove Extra Spaces: Remove Extra Spaces
|
||||||
|
Remove Line Feeds: Remove Line Feeds
|
||||||
|
Reverse Text: Reverse Text
|
||||||
|
Select Operations: Select Operations
|
||||||
|
Sentence case: Sentence case
|
||||||
|
Sort Lines Alphabetically: Sort Lines Alphabetically
|
||||||
|
Special Copy: Special Copy
|
||||||
|
Special Copy/Paste Operations: Special Copy/Paste Operations
|
||||||
|
Special Paste: Special Paste
|
||||||
|
Special Settings: Special Settings
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML to JSON
|
||||||
|
Text Case: Text Case
|
||||||
|
Text Tools: Text Tools
|
||||||
|
Title Case: Title Case
|
||||||
|
Trim White Space: Trim White Space
|
||||||
|
UPPER CASE: UPPER CASE
|
||||||
|
URL Decode: URL Decode
|
||||||
|
URL Encode: URL Encode
|
||||||
|
Whitespace & Lines: Whitespace & Lines
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML to JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML to JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: iNVERT cASE
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: lower case
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Agregar Fecha/Hora Actual
|
||||||
|
Add Line Numbers: Agregar Números de Línea
|
||||||
|
Add One Line Feed: Agregar un Salto de Línea
|
||||||
|
Add Two Line Feeds: Agregar dos Saltos de Línea
|
||||||
|
Base64 Decode: Decodificar Base64
|
||||||
|
Base64 Encode: Codificar Base64
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV a JSON
|
||||||
|
CSV to Markdown Table: CSV a Tabla Markdown
|
||||||
|
Capitalize Case: Poner en Mayúscula
|
||||||
|
Code Formatting: Formateo de Código
|
||||||
|
Count Characters: Contar Caracteres
|
||||||
|
Count Lines: Contar Líneas
|
||||||
|
Count Sentences: Contar Oraciones
|
||||||
|
Count Words: Contar Palabras
|
||||||
|
Data Conversion: Conversión de Datos
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Arrastra y suelta la categoría para priorizar su orden en el menú especial de copiar/pegar.
|
||||||
|
Enable All: Habilitar Todo
|
||||||
|
Enable special text transformation options for clipboard history items: Habilitar opciones especiales de transformación de texto para elementos del historial del portapapeles
|
||||||
|
Enabled Operations: Operaciones Habilitadas
|
||||||
|
Encode/Decode: Codificar/Decodificar
|
||||||
|
Format Converter: Convertidor de Formato
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: Decodificar HTML
|
||||||
|
HTML Encode: Codificar HTML
|
||||||
|
HTML to Markdown: HTML a Markdown
|
||||||
|
HTML to Plain Text: HTML a Texto Plano
|
||||||
|
HTML to React Component: HTML a Componente React
|
||||||
|
HTML to React JSX: HTML a React JSX
|
||||||
|
HTML to Text: HTML a Texto
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON a Cadena
|
||||||
|
JSON to CSV: JSON a CSV
|
||||||
|
JSON to Markdown Table: JSON a Tabla Markdown
|
||||||
|
JSON to TOML: JSON a TOML
|
||||||
|
JSON to XML: JSON a XML
|
||||||
|
JSON to YAML: JSON a YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown a HTML
|
||||||
|
Markdown to Text: Markdown a Texto
|
||||||
|
Operations: Operaciones
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Priorizar Orden de Categorías
|
||||||
|
Remove Duplicate Lines: Eliminar Líneas Duplicadas
|
||||||
|
Remove Extra Spaces: Eliminar Espacios Extra
|
||||||
|
Remove Line Feeds: Eliminar Saltos de Línea
|
||||||
|
Reverse Text: Invertir Texto
|
||||||
|
Select Operations: Seleccionar Operaciones
|
||||||
|
Sentence case: Tipo oración
|
||||||
|
Sort Lines Alphabetically: Ordenar Líneas Alfabéticamente
|
||||||
|
Special Copy: Copia Especial
|
||||||
|
Special Copy/Paste Operations: Operaciones Especiales de Copiar/Pegar
|
||||||
|
Special Paste: Pegado Especial
|
||||||
|
Special Settings: Ajustes Especiales
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML a JSON
|
||||||
|
Text Case: Mayúsculas/Minúsculas
|
||||||
|
Text Tools: Herramientas de Texto
|
||||||
|
Title Case: Tipo Título
|
||||||
|
Trim White Space: Recortar Espacios en Blanco
|
||||||
|
UPPER CASE: MAYÚSCULAS
|
||||||
|
URL Decode: Decodificar URL
|
||||||
|
URL Encode: Codificar URL
|
||||||
|
Whitespace & Lines: Espacios y Líneas
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML a JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML a JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: iNVERTIR cASO
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: minúsculas
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Ajouter Date/Heure Actuelle
|
||||||
|
Add Line Numbers: Ajouter Numéros de Ligne
|
||||||
|
Add One Line Feed: Ajouter un Saut de Ligne
|
||||||
|
Add Two Line Feeds: Ajouter deux Sauts de Ligne
|
||||||
|
Base64 Decode: Décoder Base64
|
||||||
|
Base64 Encode: Encoder Base64
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV vers JSON
|
||||||
|
CSV to Markdown Table: CSV vers Tableau Markdown
|
||||||
|
Capitalize Case: Mettre en Capitale
|
||||||
|
Code Formatting: Formatage de Code
|
||||||
|
Count Characters: Compter les Caractères
|
||||||
|
Count Lines: Compter les Lignes
|
||||||
|
Count Sentences: Compter les Phrases
|
||||||
|
Count Words: Compter les Mots
|
||||||
|
Data Conversion: Conversion de Données
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Glissez-déposez la catégorie pour prioriser son ordre dans le menu spécial copier/coller.
|
||||||
|
Enable All: Activer Tout
|
||||||
|
Enable special text transformation options for clipboard history items: Activer les options de transformation de texte spéciales pour les éléments de l'historique du presse-papiers
|
||||||
|
Enabled Operations: Opérations Activées
|
||||||
|
Encode/Decode: Encoder/Décoder
|
||||||
|
Format Converter: Convertisseur de Format
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: Décoder HTML
|
||||||
|
HTML Encode: Encoder HTML
|
||||||
|
HTML to Markdown: HTML vers Markdown
|
||||||
|
HTML to Plain Text: HTML vers Texte Brut
|
||||||
|
HTML to React Component: HTML vers Composant React
|
||||||
|
HTML to React JSX: HTML vers React JSX
|
||||||
|
HTML to Text: HTML vers Texte
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON Stringify
|
||||||
|
JSON to CSV: JSON vers CSV
|
||||||
|
JSON to Markdown Table: JSON vers Tableau Markdown
|
||||||
|
JSON to TOML: JSON vers TOML
|
||||||
|
JSON to XML: JSON vers XML
|
||||||
|
JSON to YAML: JSON vers YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown vers HTML
|
||||||
|
Markdown to Text: Markdown vers Texte
|
||||||
|
Operations: Opérations
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Prioriser l'Ordre des Catégories
|
||||||
|
Remove Duplicate Lines: Supprimer Lignes Doubles
|
||||||
|
Remove Extra Spaces: Supprimer Espaces Superflus
|
||||||
|
Remove Line Feeds: Supprimer Sauts de Ligne
|
||||||
|
Reverse Text: Inverser le Texte
|
||||||
|
Select Operations: Sélectionner Opérations
|
||||||
|
Sentence case: Cas de la phrase
|
||||||
|
Sort Lines Alphabetically: Trier Lignes par Ordre Alphabétique
|
||||||
|
Special Copy: Copie Spéciale
|
||||||
|
Special Copy/Paste Operations: Opérations Spéciales Copier/Coller
|
||||||
|
Special Paste: Collage Spécial
|
||||||
|
Special Settings: Paramètres Spéciaux
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML vers JSON
|
||||||
|
Text Case: Casse du Texte
|
||||||
|
Text Tools: Outils Texte
|
||||||
|
Title Case: Cas du Titre
|
||||||
|
Trim White Space: Supprimer Espaces Blancs
|
||||||
|
UPPER CASE: MAJUSCULES
|
||||||
|
URL Decode: Décoder URL
|
||||||
|
URL Encode: Encoder URL
|
||||||
|
Whitespace & Lines: Espaces & Lignes
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML vers JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML vers JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: iNVERSER cASSE
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: minuscules
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Aggiungi Data/Ora Attuale
|
||||||
|
Add Line Numbers: Aggiungi Numeri di Riga
|
||||||
|
Add One Line Feed: Aggiungi un a Capo
|
||||||
|
Add Two Line Feeds: Aggiungi due a Capo
|
||||||
|
Base64 Decode: Decodifica Base64
|
||||||
|
Base64 Encode: Codifica Base64
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV in JSON
|
||||||
|
CSV to Markdown Table: CSV in Tabella Markdown
|
||||||
|
Capitalize Case: Maiuscolo Iniziale
|
||||||
|
Code Formatting: Formattazione Codice
|
||||||
|
Count Characters: Conta Caratteri
|
||||||
|
Count Lines: Conta Righe
|
||||||
|
Count Sentences: Conta Frasi
|
||||||
|
Count Words: Conta Parole
|
||||||
|
Data Conversion: Conversione Dati
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Trascina e rilascia la categoria per dare priorità al suo ordine nel menu speciale di copia/incolla.
|
||||||
|
Enable All: Abilita Tutto
|
||||||
|
Enable special text transformation options for clipboard history items: Abilita opzioni speciali di trasformazione del testo per gli elementi della cronologia degli appunti
|
||||||
|
Enabled Operations: Operazioni Abilitate
|
||||||
|
Encode/Decode: Codifica/Decodifica
|
||||||
|
Format Converter: Convertitore di Formato
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: Decodifica HTML
|
||||||
|
HTML Encode: Codifica HTML
|
||||||
|
HTML to Markdown: HTML in Markdown
|
||||||
|
HTML to Plain Text: HTML in Testo Semplice
|
||||||
|
HTML to React Component: HTML in Componente React
|
||||||
|
HTML to React JSX: HTML in React JSX
|
||||||
|
HTML to Text: HTML in Testo
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON Stringify
|
||||||
|
JSON to CSV: JSON in CSV
|
||||||
|
JSON to Markdown Table: JSON in Tabella Markdown
|
||||||
|
JSON to TOML: JSON in TOML
|
||||||
|
JSON to XML: JSON in XML
|
||||||
|
JSON to YAML: JSON in YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown in HTML
|
||||||
|
Markdown to Text: Markdown in Testo
|
||||||
|
Operations: Operazioni
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Priorità Ordine Categorie
|
||||||
|
Remove Duplicate Lines: Rimuovi Righe Duplicate
|
||||||
|
Remove Extra Spaces: Rimuovi Spazi Extra
|
||||||
|
Remove Line Feeds: Rimuovi a Capo
|
||||||
|
Reverse Text: Inverti Testo
|
||||||
|
Select Operations: Seleziona Operazioni
|
||||||
|
Sentence case: Maiuscolo a inizio frase
|
||||||
|
Sort Lines Alphabetically: Ordina Righe Alfabeticamente
|
||||||
|
Special Copy: Copia Speciale
|
||||||
|
Special Copy/Paste Operations: Operazioni Speciali di Copia/Incolla
|
||||||
|
Special Paste: Incolla Speciale
|
||||||
|
Special Settings: Impostazioni Speciali
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML in JSON
|
||||||
|
Text Case: Maiuscole/Minuscole
|
||||||
|
Text Tools: Strumenti Testo
|
||||||
|
Title Case: Maiuscolo a Inizio Parola
|
||||||
|
Trim White Space: Taglia Spazi Bianchi
|
||||||
|
UPPER CASE: MAIUSCOLO
|
||||||
|
URL Decode: Decodifica URL
|
||||||
|
URL Encode: Codifica URL
|
||||||
|
Whitespace & Lines: Spazi & Righe
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML in JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML in JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: iNVERTI cASO
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: minuscolo
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Добавить текущие дату/время
|
||||||
|
Add Line Numbers: Добавить номера строк
|
||||||
|
Add One Line Feed: Добавить один перевод строки
|
||||||
|
Add Two Line Feeds: Добавить два перевода строки
|
||||||
|
Base64 Decode: Декодировать Base64
|
||||||
|
Base64 Encode: Кодировать Base64
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV в JSON
|
||||||
|
CSV to Markdown Table: CSV в таблицу Markdown
|
||||||
|
Capitalize Case: С большой буквы
|
||||||
|
Code Formatting: Форматирование кода
|
||||||
|
Count Characters: Подсчет символов
|
||||||
|
Count Lines: Подсчет строк
|
||||||
|
Count Sentences: Подсчет предложений
|
||||||
|
Count Words: Подсчет слов
|
||||||
|
Data Conversion: Преобразование данных
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Перетащите категорию, чтобы задать ей приоритет в специальном меню копирования/вставки.
|
||||||
|
Enable All: Включить все
|
||||||
|
Enable special text transformation options for clipboard history items: Включить специальные параметры преобразования текста для элементов истории буфера обмена
|
||||||
|
Enabled Operations: Включенные операции
|
||||||
|
Encode/Decode: Кодировать/Декодировать
|
||||||
|
Format Converter: Конвертер форматов
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: Декодировать HTML
|
||||||
|
HTML Encode: Кодировать HTML
|
||||||
|
HTML to Markdown: HTML в Markdown
|
||||||
|
HTML to Plain Text: HTML в обычный текст
|
||||||
|
HTML to React Component: HTML в компонент React
|
||||||
|
HTML to React JSX: HTML в React JSX
|
||||||
|
HTML to Text: HTML в текст
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON в строку
|
||||||
|
JSON to CSV: JSON в CSV
|
||||||
|
JSON to Markdown Table: JSON в таблицу Markdown
|
||||||
|
JSON to TOML: JSON в TOML
|
||||||
|
JSON to XML: JSON в XML
|
||||||
|
JSON to YAML: JSON в YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown в HTML
|
||||||
|
Markdown to Text: Markdown в текст
|
||||||
|
Operations: Операции
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Приоритет порядка категорий
|
||||||
|
Remove Duplicate Lines: Удалить дублирующиеся строки
|
||||||
|
Remove Extra Spaces: Удалить лишние пробелы
|
||||||
|
Remove Line Feeds: Удалить переводы строк
|
||||||
|
Reverse Text: Обратный текст
|
||||||
|
Select Operations: Выбрать операции
|
||||||
|
Sentence case: Регистр предложений
|
||||||
|
Sort Lines Alphabetically: Сортировать строки по алфавиту
|
||||||
|
Special Copy: Специальное копирование
|
||||||
|
Special Copy/Paste Operations: Специальные операции копирования/вставки
|
||||||
|
Special Paste: Специальная вставка
|
||||||
|
Special Settings: Специальные настройки
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML в JSON
|
||||||
|
Text Case: Регистр текста
|
||||||
|
Text Tools: Инструменты для текста
|
||||||
|
Title Case: Регистр заголовков
|
||||||
|
Trim White Space: Обрезать пробелы
|
||||||
|
UPPER CASE: ВЕРХНИЙ РЕГИСТР
|
||||||
|
URL Decode: Декодировать URL
|
||||||
|
URL Encode: Кодировать URL
|
||||||
|
Whitespace & Lines: Пробелы и строки
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML в JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML в JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: иНВЕРТИРОВАТЬ рЕГИСТР
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: нижний регистр
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Geçerli Tarih/Saati Ekle
|
||||||
|
Add Line Numbers: Satır Numaraları Ekle
|
||||||
|
Add One Line Feed: Bir Satır Başı Ekle
|
||||||
|
Add Two Line Feeds: İki Satır Başı Ekle
|
||||||
|
Base64 Decode: Base64 Kodunu Çöz
|
||||||
|
Base64 Encode: Base64 Kodla
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV'yi JSON'a Dönüştür
|
||||||
|
CSV to Markdown Table: CSV'yi Markdown Tablosuna Dönüştür
|
||||||
|
Capitalize Case: Baş Harfleri Büyüt
|
||||||
|
Code Formatting: Kod Formatlama
|
||||||
|
Count Characters: Karakter Sayısı
|
||||||
|
Count Lines: Satır Sayısı
|
||||||
|
Count Sentences: Cümle Sayısı
|
||||||
|
Count Words: Kelime Sayısı
|
||||||
|
Data Conversion: Veri Dönüştürme
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Özel kopyala/yapıştır menüsünde kategorinin sırasını önceliklendirmek için sürükleyip bırakın.
|
||||||
|
Enable All: Tümünü Etkinleştir
|
||||||
|
Enable special text transformation options for clipboard history items: Pano geçmişi öğeleri için özel metin dönüştürme seçeneklerini etkinleştirin
|
||||||
|
Enabled Operations: Etkin İşlemler
|
||||||
|
Encode/Decode: Kodla/Kodu Çöz
|
||||||
|
Format Converter: Format Dönüştürücü
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: HTML Kodunu Çöz
|
||||||
|
HTML Encode: HTML Kodla
|
||||||
|
HTML to Markdown: HTML'yi Markdown'a Dönüştür
|
||||||
|
HTML to Plain Text: HTML'yi Düz Metne Dönüştür
|
||||||
|
HTML to React Component: HTML'yi React Bileşenine Dönüştür
|
||||||
|
HTML to React JSX: HTML'yi React JSX'e Dönüştür
|
||||||
|
HTML to Text: HTML'yi Metne Dönüştür
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON Stringify
|
||||||
|
JSON to CSV: JSON'u CSV'ye Dönüştür
|
||||||
|
JSON to Markdown Table: JSON'u Markdown Tablosuna Dönüştür
|
||||||
|
JSON to TOML: JSON'u TOML'a Dönüştür
|
||||||
|
JSON to XML: JSON'u XML'e Dönüştür
|
||||||
|
JSON to YAML: JSON'u YAML'a Dönüştür
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown'u HTML'ye Dönüştür
|
||||||
|
Markdown to Text: Markdown'u Metne Dönüştür
|
||||||
|
Operations: İşlemler
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Kategori Sırasını Önceliklendir
|
||||||
|
Remove Duplicate Lines: Yinelenen Satırları Kaldır
|
||||||
|
Remove Extra Spaces: Fazla Boşlukları Kaldır
|
||||||
|
Remove Line Feeds: Satır Başlarını Kaldır
|
||||||
|
Reverse Text: Metni Ters Çevir
|
||||||
|
Select Operations: İşlemleri Seç
|
||||||
|
Sentence case: Cümle düzeni
|
||||||
|
Sort Lines Alphabetically: Satırları Alfabetik Olarak Sırala
|
||||||
|
Special Copy: Özel Kopyala
|
||||||
|
Special Copy/Paste Operations: Özel Kopyala/Yapıştır İşlemleri
|
||||||
|
Special Paste: Özel Yapıştır
|
||||||
|
Special Settings: Özel Ayarlar
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML'u JSON'a Dönüştür
|
||||||
|
Text Case: Metin Kılıfı
|
||||||
|
Text Tools: Metin Araçları
|
||||||
|
Title Case: Başlık Düzeni
|
||||||
|
Trim White Space: Beyaz Boşlukları Kırp
|
||||||
|
UPPER CASE: BÜYÜK HARF
|
||||||
|
URL Decode: URL Kodunu Çöz
|
||||||
|
URL Encode: URL Kodla
|
||||||
|
Whitespace & Lines: Boşluk & Satırlar
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML'i JSON'a Dönüştür
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML'ı JSON'a Dönüştür
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: dURUMU tERSINE cEVIR
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: küçük harf
|
||||||
|
snake_case: snake_case
|
@ -15,7 +15,7 @@ confirmDeleteTemplateMessage: "'{{name}}' global şablonunu silmek istediğinizd
|
|||||||
confirmDeleteTemplateTitle: Silmeyi Onayla
|
confirmDeleteTemplateTitle: Silmeyi Onayla
|
||||||
deleteTemplateButtonTooltip: Şablonu Sil
|
deleteTemplateButtonTooltip: Şablonu Sil
|
||||||
enableGlobalTemplatesLabel: Global Şablonları Etkinleştir
|
enableGlobalTemplatesLabel: Global Şablonları Etkinleştir
|
||||||
globalTemplatesDescription: "{{template_name}} kullanarak herhangi bir klibe eklenebilen yeniden kullanılabilir metin parçacıklarını yönetin."
|
globalTemplatesDescription: '{{template_name}} kullanarak herhangi bir klibe eklenebilen yeniden kullanılabilir metin parçacıklarını yönetin.'
|
||||||
globalTemplatesTitle: Global Şablonlar
|
globalTemplatesTitle: Global Şablonlar
|
||||||
localTemplateConflictWarning: "'{{label}}' adlı bir global şablon da mevcut. Yerel şablon bu klibin formu içinde öncelik alacaktır."
|
localTemplateConflictWarning: "'{{label}}' adlı bir global şablon da mevcut. Yerel şablon bu klibin formu içinde öncelik alacaktır."
|
||||||
noGlobalTemplatesYet: Henüz global şablon tanımlanmamış. Bir tane oluşturmak için 'Şablon Ekle'ye tıklayın.
|
noGlobalTemplatesYet: Henüz global şablon tanımlanmamış. Bir tane oluşturmak için 'Şablon Ekle'ye tıklayın.
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: Додати поточну дату/час
|
||||||
|
Add Line Numbers: Додати номери рядків
|
||||||
|
Add One Line Feed: Додати один розрив рядка
|
||||||
|
Add Two Line Feeds: Додати два розриви рядка
|
||||||
|
Base64 Decode: Декодувати Base64
|
||||||
|
Base64 Encode: Кодувати Base64
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV у JSON
|
||||||
|
CSV to Markdown Table: CSV у таблицю Markdown
|
||||||
|
Capitalize Case: З великої літери
|
||||||
|
Code Formatting: Форматування коду
|
||||||
|
Count Characters: Підрахунок символів
|
||||||
|
Count Lines: Підрахунок рядків
|
||||||
|
Count Sentences: Підрахунок речень
|
||||||
|
Count Words: Підрахунок слів
|
||||||
|
Data Conversion: Перетворення даних
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: Перетягніть категорію, щоб задати їй пріоритет у спеціальному меню копіювання/вставки.
|
||||||
|
Enable All: Увімкнути все
|
||||||
|
Enable special text transformation options for clipboard history items: Увімкнути спеціальні параметри перетворення тексту для елементів історії буфера обміну
|
||||||
|
Enabled Operations: Увімкнені операції
|
||||||
|
Encode/Decode: Кодувати/Декодувати
|
||||||
|
Format Converter: Конвертер форматів
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: Декодувати HTML
|
||||||
|
HTML Encode: Кодувати HTML
|
||||||
|
HTML to Markdown: HTML у Markdown
|
||||||
|
HTML to Plain Text: HTML у звичайний текст
|
||||||
|
HTML to React Component: HTML у компонент React
|
||||||
|
HTML to React JSX: HTML у React JSX
|
||||||
|
HTML to Text: HTML у текст
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON у рядок
|
||||||
|
JSON to CSV: JSON у CSV
|
||||||
|
JSON to Markdown Table: JSON у таблицю Markdown
|
||||||
|
JSON to TOML: JSON у TOML
|
||||||
|
JSON to XML: JSON у XML
|
||||||
|
JSON to YAML: JSON у YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown у HTML
|
||||||
|
Markdown to Text: Markdown у текст
|
||||||
|
Operations: Операції
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: Пріоритет порядку категорій
|
||||||
|
Remove Duplicate Lines: Видалити дублікати рядків
|
||||||
|
Remove Extra Spaces: Видалити зайві пробіли
|
||||||
|
Remove Line Feeds: Видалити розриви рядків
|
||||||
|
Reverse Text: Зворотний текст
|
||||||
|
Select Operations: Вибрати операції
|
||||||
|
Sentence case: Регістр речень
|
||||||
|
Sort Lines Alphabetically: Сортувати рядки за алфавітом
|
||||||
|
Special Copy: Спеціальне копіювання
|
||||||
|
Special Copy/Paste Operations: Спеціальні операції копіювання/вставки
|
||||||
|
Special Paste: Спеціальна вставка
|
||||||
|
Special Settings: Спеціальні налаштування
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML у JSON
|
||||||
|
Text Case: Регістр тексту
|
||||||
|
Text Tools: Інструменти для тексту
|
||||||
|
Title Case: Регістр заголовків
|
||||||
|
Trim White Space: Обрізати пробіли
|
||||||
|
UPPER CASE: ВЕРХНІЙ РЕГІСТР
|
||||||
|
URL Decode: Декодувати URL
|
||||||
|
URL Encode: Кодувати URL
|
||||||
|
Whitespace & Lines: Пробіли та рядки
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML у JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML у JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: іНВЕРТУВАТИ рЕГІСТР
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: нижній регістр
|
||||||
|
snake_case: snake_case
|
@ -0,0 +1,73 @@
|
|||||||
|
Add Current Date/Time: 添加当前日期/时间
|
||||||
|
Add Line Numbers: 添加行号
|
||||||
|
Add One Line Feed: 添加一个换行符
|
||||||
|
Add Two Line Feeds: 添加两个换行符
|
||||||
|
Base64 Decode: Base64 解码
|
||||||
|
Base64 Encode: Base64 编码
|
||||||
|
CSV: CSV
|
||||||
|
CSV to JSON: CSV 转 JSON
|
||||||
|
CSV to Markdown Table: CSV 转 Markdown 表格
|
||||||
|
Capitalize Case: 首字母大写
|
||||||
|
Code Formatting: 代码格式化
|
||||||
|
Count Characters: 计算字符数
|
||||||
|
Count Lines: 计算行数
|
||||||
|
Count Sentences: 计算句子数
|
||||||
|
Count Words: 计算单词数
|
||||||
|
Data Conversion: 数据转换
|
||||||
|
Drag and drop category to prioritize its order in the special copy/paste menu.: 拖放类别以在特殊复制/粘贴菜单中优先排列其顺序。
|
||||||
|
Enable All: 全部启用
|
||||||
|
Enable special text transformation options for clipboard history items: 为剪贴板历史记录项启用特殊文本转换选项
|
||||||
|
Enabled Operations: 已启用的操作
|
||||||
|
Encode/Decode: 编码/解码
|
||||||
|
Format Converter: 格式转换器
|
||||||
|
HTML: HTML
|
||||||
|
HTML Decode: HTML 解码
|
||||||
|
HTML Encode: HTML 编码
|
||||||
|
HTML to Markdown: HTML 转 Markdown
|
||||||
|
HTML to Plain Text: HTML 转纯文本
|
||||||
|
HTML to React Component: HTML 转 React 组件
|
||||||
|
HTML to React JSX: HTML 转 React JSX
|
||||||
|
HTML to Text: HTML 转文本
|
||||||
|
JSON: JSON
|
||||||
|
JSON Stringify: JSON 字符串化
|
||||||
|
JSON to CSV: JSON 转 CSV
|
||||||
|
JSON to Markdown Table: JSON 转 Markdown 表格
|
||||||
|
JSON to TOML: JSON 转 TOML
|
||||||
|
JSON to XML: JSON 转 XML
|
||||||
|
JSON to YAML: JSON 转 YAML
|
||||||
|
Markdown: Markdown
|
||||||
|
Markdown to HTML: Markdown 转 HTML
|
||||||
|
Markdown to Text: Markdown 转文本
|
||||||
|
Operations: 操作
|
||||||
|
PascalCase: PascalCase
|
||||||
|
Prioritize Category Order: 确定类别顺序优先级
|
||||||
|
Remove Duplicate Lines: 删除重复行
|
||||||
|
Remove Extra Spaces: 删除多余空格
|
||||||
|
Remove Line Feeds: 删除换行符
|
||||||
|
Reverse Text: 反转文本
|
||||||
|
Select Operations: 选择操作
|
||||||
|
Sentence case: 句子大小写
|
||||||
|
Sort Lines Alphabetically: 按字母顺序对行排序
|
||||||
|
Special Copy: 特殊复制
|
||||||
|
Special Copy/Paste Operations: 特殊复制/粘贴操作
|
||||||
|
Special Paste: 特殊粘贴
|
||||||
|
Special Settings: 特殊设置
|
||||||
|
TOML: TOML
|
||||||
|
TOML to JSON: TOML 转 JSON
|
||||||
|
Text Case: 文本大小写
|
||||||
|
Text Tools: 文本工具
|
||||||
|
Title Case: 标题大小写
|
||||||
|
Trim White Space: 修剪空白
|
||||||
|
UPPER CASE: 大写
|
||||||
|
URL Decode: URL 解码
|
||||||
|
URL Encode: URL 编码
|
||||||
|
Whitespace & Lines: 空白和行
|
||||||
|
XML: XML
|
||||||
|
XML to JSON: XML 转 JSON
|
||||||
|
YAML: YAML
|
||||||
|
YAML to JSON: YAML 转 JSON
|
||||||
|
camelCase: camelCase
|
||||||
|
iNVERT cASE: 反转大小写
|
||||||
|
kebab-case: kebab-case
|
||||||
|
lower case: 小写
|
||||||
|
snake_case: snake_case
|
@ -63,7 +63,6 @@ export const ClipboardHistoryIconMenu = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
setIsDeleting,
|
setIsDeleting,
|
||||||
isDark,
|
isDark,
|
||||||
setSelectHistoryItem,
|
|
||||||
setSelectedHistoryItems,
|
setSelectedHistoryItems,
|
||||||
showSelectHistoryItems,
|
showSelectHistoryItems,
|
||||||
}: ClipboardHistoryIconMenuProps) => {
|
}: ClipboardHistoryIconMenuProps) => {
|
||||||
@ -85,12 +84,6 @@ export const ClipboardHistoryIconMenu = ({
|
|||||||
setShowSelectHistoryItems(!showSelectHistoryItems)
|
setShowSelectHistoryItems(!showSelectHistoryItems)
|
||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys(['control+s'], () => {
|
|
||||||
if (hoveringHistoryRowId.value) {
|
|
||||||
setSelectHistoryItem(hoveringHistoryRowId.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useHotkeys(['alt+h', 'meta+h'], () => {
|
useHotkeys(['alt+h', 'meta+h'], () => {
|
||||||
setIsHistoryEnabled(!isHistoryEnabled)
|
setIsHistoryEnabled(!isHistoryEnabled)
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
isKeyAltPressed,
|
isKeyAltPressed,
|
||||||
isKeyCtrlPressed,
|
isKeyCtrlPressed,
|
||||||
showHistoryDeleteConfirmationId,
|
showHistoryDeleteConfirmationId,
|
||||||
|
showKeyboardNavContextMenuHistoryId,
|
||||||
} from '~/store'
|
} from '~/store'
|
||||||
import {
|
import {
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
@ -42,7 +43,7 @@ import ImageWithFallback from '~/components/atoms/image/image-with-fallback-on-e
|
|||||||
import LinkCard from '~/components/atoms/link-card/link-card'
|
import LinkCard from '~/components/atoms/link-card/link-card'
|
||||||
import PlayButton from '~/components/atoms/play-button/PlayButton'
|
import PlayButton from '~/components/atoms/play-button/PlayButton'
|
||||||
import ToolTip from '~/components/atoms/tooltip'
|
import ToolTip from '~/components/atoms/tooltip'
|
||||||
import { Badge, Box, ContextMenu, ContextMenuTrigger, Flex, Text } from '~/components/ui'
|
import { Badge, Box, Flex, Text } from '~/components/ui'
|
||||||
import YoutubeEmbed from '~/components/video-player/YoutubeEmbed'
|
import YoutubeEmbed from '~/components/video-player/YoutubeEmbed'
|
||||||
|
|
||||||
import { useSignal } from '~/hooks/use-signal'
|
import { useSignal } from '~/hooks/use-signal'
|
||||||
@ -55,7 +56,7 @@ import {
|
|||||||
hyperlinkText,
|
hyperlinkText,
|
||||||
hyperlinkTextWithPreview,
|
hyperlinkTextWithPreview,
|
||||||
} from '../helpers'
|
} from '../helpers'
|
||||||
import ClipboardHistoryRowContextMenu from './context-menu/ClipboardHistoryRowContextMenu'
|
import ContextMenuTrigger from './context-menu/ContextMenuTrigger'
|
||||||
|
|
||||||
interface ClipboardHistoryRowProps {
|
interface ClipboardHistoryRowProps {
|
||||||
index?: number
|
index?: number
|
||||||
@ -123,7 +124,7 @@ interface ClipboardHistoryRowProps {
|
|||||||
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
isSingleClickToCopyPaste?: boolean
|
isSingleClickToCopyPaste?: boolean
|
||||||
historyPreviewLineLimit?: number
|
historyPreviewLineLimit?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@ -193,6 +194,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
|
|
||||||
const contentElementRendered = useSignal<boolean>(false)
|
const contentElementRendered = useSignal<boolean>(false)
|
||||||
const contextMenuOpen = useSignal<boolean>(false)
|
const contextMenuOpen = useSignal<boolean>(false)
|
||||||
|
const isDeleteConfirmationFromContext = useSignal<boolean>(false)
|
||||||
|
|
||||||
const isHovering = !isPinnedTop
|
const isHovering = !isPinnedTop
|
||||||
? (hoveringHistoryRowId.value === clipboard?.historyId &&
|
? (hoveringHistoryRowId.value === clipboard?.historyId &&
|
||||||
@ -254,6 +256,30 @@ export function ClipboardHistoryRowComponent({
|
|||||||
})
|
})
|
||||||
}, [isExpanded, isWrapText])
|
}, [isExpanded, isWrapText])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showKeyboardNavContextMenuHistoryId.value === clipboard?.historyId) {
|
||||||
|
if (contextMenuTriggerRef?.current) {
|
||||||
|
const targetElement = contextMenuTriggerRef.current
|
||||||
|
const rect = targetElement.getBoundingClientRect()
|
||||||
|
|
||||||
|
const contextMenuEvent = new MouseEvent('contextmenu', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
clientX: rect.x + 50,
|
||||||
|
clientY: rect.y + 20,
|
||||||
|
button: 2, // Right mouse button
|
||||||
|
})
|
||||||
|
|
||||||
|
targetElement.dispatchEvent(contextMenuEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
showKeyboardNavContextMenuHistoryId.value,
|
||||||
|
clipboard?.historyId,
|
||||||
|
contextMenuTriggerRef?.current,
|
||||||
|
])
|
||||||
|
|
||||||
if (!clipboard) {
|
if (!clipboard) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -361,12 +387,14 @@ export function ClipboardHistoryRowComponent({
|
|||||||
|
|
||||||
const pinnedTopOffsetFirst = !isPinnedTopFirst ? 'top-[-10px]' : 'top-[5px]'
|
const pinnedTopOffsetFirst = !isPinnedTopFirst ? 'top-[-10px]' : 'top-[5px]'
|
||||||
const bgToolsPanel = `${
|
const bgToolsPanel = `${
|
||||||
isKeyboardSelected
|
!isPinnedTop && isOverPinned && !isNowItem
|
||||||
? 'bg-blue-50 dark:bg-blue-950/80'
|
|
||||||
: !isPinnedTop && isOverPinned && !isNowItem
|
|
||||||
? 'bg-orange-50 dark:!bg-transparent'
|
? 'bg-orange-50 dark:!bg-transparent'
|
||||||
: isDeleting
|
: isDeleting || isDeleteConfirmationFromContext.value
|
||||||
? 'bg-red-50 dark:bg-red-950/80'
|
? 'bg-red-50 dark:bg-red-950/80'
|
||||||
|
: isSelected
|
||||||
|
? 'bg-yellow-50 dark:bg-amber-950/80'
|
||||||
|
: isKeyboardSelected
|
||||||
|
? 'bg-blue-50 dark:bg-blue-950/80'
|
||||||
: contextMenuOpen.value
|
: contextMenuOpen.value
|
||||||
? `bg-slate-100 dark:bg-slate-900 ${
|
? `bg-slate-100 dark:bg-slate-900 ${
|
||||||
isNowItem ? 'bg-teal-50/80 dark:bg-sky-900/80' : ''
|
isNowItem ? 'bg-teal-50/80 dark:bg-sky-900/80' : ''
|
||||||
@ -375,8 +403,6 @@ export function ClipboardHistoryRowComponent({
|
|||||||
? 'dark:bg-green-950/80'
|
? 'dark:bg-green-950/80'
|
||||||
: isSaved
|
: isSaved
|
||||||
? 'dark:bg-sky-950/80'
|
? 'dark:bg-sky-950/80'
|
||||||
: isSelected
|
|
||||||
? 'bg-yellow-50 dark:bg-amber-950/80'
|
|
||||||
: isNowItem
|
: isNowItem
|
||||||
? 'bg-teal-50/90 dark:bg-sky-950'
|
? 'bg-teal-50/90 dark:bg-sky-950'
|
||||||
: 'bg-white dark:bg-slate-950/80'
|
: 'bg-white dark:bg-slate-950/80'
|
||||||
@ -387,7 +413,8 @@ export function ClipboardHistoryRowComponent({
|
|||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
opacity:
|
opacity:
|
||||||
(isDeleting || isOverPinned) && !isDragPreview
|
(isDeleting || isDeleteConfirmationFromContext.value || isOverPinned) &&
|
||||||
|
!isDragPreview
|
||||||
? 1
|
? 1
|
||||||
: isDragging
|
: isDragging
|
||||||
? 0.7
|
? 0.7
|
||||||
@ -414,14 +441,56 @@ export function ClipboardHistoryRowComponent({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContextMenu
|
<ContextMenuTrigger
|
||||||
|
ref={contextMenuTriggerRef}
|
||||||
onOpenChange={isOpen => {
|
onOpenChange={isOpen => {
|
||||||
contextMenuOpen.value = isOpen
|
contextMenuOpen.value = isOpen
|
||||||
showHistoryDeleteConfirmationId.value = null
|
showHistoryDeleteConfirmationId.value = null
|
||||||
|
// Reset he keyboard nav signal when menu opens
|
||||||
|
if (
|
||||||
|
!isOpen &&
|
||||||
|
showKeyboardNavContextMenuHistoryId.value === clipboard.historyId
|
||||||
|
) {
|
||||||
|
showKeyboardNavContextMenuHistoryId.value = null
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
historyId={clipboard.historyId}
|
||||||
|
copiedFromApp={clipboard.copiedFromApp}
|
||||||
|
isMasked={clipboard.isMasked}
|
||||||
|
setSavingItem={setSavingItem}
|
||||||
|
value={clipboard.value}
|
||||||
|
isImage={clipboard.isImage}
|
||||||
|
isMp3={isMp3}
|
||||||
|
isText={clipboard.isText}
|
||||||
|
isPinned={clipboard.isPinned}
|
||||||
|
isFavorite={clipboard.isFavorite}
|
||||||
|
isImageData={clipboard.isImageData}
|
||||||
|
detectedLanguage={clipboard.detectedLanguage}
|
||||||
|
setLargeViewItemId={setLargeViewItemId}
|
||||||
|
isLargeView={isLargeView}
|
||||||
|
arrLinks={clipboard.arrLinks}
|
||||||
|
hasLinkCard={hasLinkCard}
|
||||||
|
isSelected={isSelected}
|
||||||
|
invalidateClipboardHistoryQuery={invalidateClipboardHistoryQuery}
|
||||||
|
generateLinkMetaData={generateLinkMetaData}
|
||||||
|
removeLinkMetaData={removeLinkMetaData}
|
||||||
|
setSelectHistoryItem={setSelectHistoryItem}
|
||||||
|
setSelectedHistoryItems={setSelectedHistoryItems}
|
||||||
|
selectedHistoryItems={selectedHistoryItems}
|
||||||
|
onCopyPaste={onCopyPaste}
|
||||||
|
setHistoryFilters={setHistoryFilters}
|
||||||
|
setAppFilters={setAppFilters}
|
||||||
|
onDeleteConfirmationChange={(historyId, isMultiSelect) => {
|
||||||
|
if (isMultiSelect) {
|
||||||
|
// For multi-select, highlight this item if it's in the selected items
|
||||||
|
isDeleteConfirmationFromContext.value = selectedHistoryItems.includes(
|
||||||
|
clipboard.historyId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// For single select, highlight only if this is the specific item
|
||||||
|
isDeleteConfirmationFromContext.value = historyId === clipboard.historyId
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<ContextMenuTrigger
|
|
||||||
ref={isHovering || isSelected ? contextMenuTriggerRef : null}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="relative select-none history-item focus:outline-none"
|
className="relative select-none history-item focus:outline-none"
|
||||||
@ -432,6 +501,10 @@ export function ClipboardHistoryRowComponent({
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className={`rounded-md justify-start duration-300 history-box relative px-3 py-1 hover:shadow-sm my-0.5 shadow-none border-2 flex flex-col ${
|
className={`rounded-md justify-start duration-300 history-box relative px-3 py-1 hover:shadow-sm my-0.5 shadow-none border-2 flex flex-col ${
|
||||||
|
isKeyboardSelected
|
||||||
|
? 'ring-2 scale-[.98] ring-blue-400 dark:!ring-blue-600 ring-offset-1 !shadow-sm ring-offset-white dark:ring-offset-gray-800'
|
||||||
|
: ''
|
||||||
|
} ${
|
||||||
index === 0 &&
|
index === 0 &&
|
||||||
clipboard.updatedAt > Date.now() - MINUTE_IN_MS &&
|
clipboard.updatedAt > Date.now() - MINUTE_IN_MS &&
|
||||||
!isCopiedOrPasted &&
|
!isCopiedOrPasted &&
|
||||||
@ -439,22 +512,23 @@ export function ClipboardHistoryRowComponent({
|
|||||||
!isKeyboardSelected &&
|
!isKeyboardSelected &&
|
||||||
!isSelected
|
!isSelected
|
||||||
? 'bg-teal-50 hover:border-slate-300 dark:bg-sky-900/40 dark:hover:border-slate-700 hover:bg-teal-50/90 hover:dark:bg-sky-950'
|
? 'bg-teal-50 hover:border-slate-300 dark:bg-sky-900/40 dark:hover:border-slate-700 hover:bg-teal-50/90 hover:dark:bg-sky-950'
|
||||||
|
: (isDeleting || isDeleteConfirmationFromContext.value) &&
|
||||||
|
!isDragPreview
|
||||||
|
? 'border-red-400 bg-red-50 dark:bg-red-950/80 dark:border-red-900/80 dark:hover:border-red-800'
|
||||||
|
: isSelected
|
||||||
|
? `bg-amber-50 border-amber-300 dark:bg-amber-950/80 dark:border-amber-900/80 hover:border-amber-300/80 dark:hover:border-amber-800 hover:bg-amber-50/80 ${
|
||||||
|
isPinnedTop ? '!border dark:!bg-amber-950' : ''
|
||||||
|
}`
|
||||||
: isKeyboardSelected
|
: isKeyboardSelected
|
||||||
? `bg-blue-50 ring-2 scale-[.98] ring-blue-400 dark:!ring-blue-600 ring-offset-1 !shadow-sm border-blue-300 dark:bg-blue-950/80 dark:hover:border-blue-800 hover:bg-blue-50/80 ring-offset-white dark:ring-offset-gray-800 ${
|
? `bg-blue-50 border-blue-300 dark:bg-blue-950/80 dark:hover:border-blue-800 hover:bg-blue-50/80 ${
|
||||||
isPinnedTop ? ' dark:!bg-amber-950' : ''
|
isPinnedTop ? ' dark:!bg-amber-950' : ''
|
||||||
}`
|
}`
|
||||||
: isDeleting && !isDragPreview
|
|
||||||
? 'border-red-400 bg-red-50 dark:bg-red-950/80 dark:border-red-900/80 dark:hover:border-red-800'
|
|
||||||
: contextMenuOpen.value
|
: contextMenuOpen.value
|
||||||
? 'bg-slate-100 dark:bg-slate-950/80 border-slate-300 dark:border-slate-600'
|
? 'bg-slate-100 dark:bg-slate-950/80 border-slate-300 dark:border-slate-600'
|
||||||
: isSaved && !isDragPreview
|
: isSaved && !isDragPreview
|
||||||
? 'bg-sky-50 border-sky-600 dark:bg-sky-950/80 dark:border-sky-900/80 dark:hover:border-sky-800'
|
? 'bg-sky-50 border-sky-600 dark:bg-sky-950/80 dark:border-sky-900/80 dark:hover:border-sky-800'
|
||||||
: isCopiedOrPasted && !isDragPreview
|
: isCopiedOrPasted && !isDragPreview
|
||||||
? `bg-green-50 border-green-600 dark:bg-green-950/80 dark:border-green-800`
|
? `bg-green-50 border-green-600 dark:bg-green-950/80 dark:border-green-800`
|
||||||
: isSelected
|
|
||||||
? `bg-amber-50 border-amber-300 dark:bg-amber-950/80 dark:border-amber-900/80 hover:border-amber-300/80 dark:hover:border-amber-800 hover:bg-amber-50/80 ${
|
|
||||||
isPinnedTop ? '!border dark:!bg-amber-950' : ''
|
|
||||||
}`
|
|
||||||
: `hover:bg-white dark:hover:bg-slate-950/80 ${
|
: `hover:bg-white dark:hover:bg-slate-950/80 ${
|
||||||
isLargeView
|
isLargeView
|
||||||
? 'border-slate-500 bg-white dark:bg-slate-950 hover:dark:border-slate-500'
|
? 'border-slate-500 bg-white dark:bg-slate-950 hover:dark:border-slate-500'
|
||||||
@ -504,11 +578,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
return // Don't copy/paste if clicking on context menu
|
return // Don't copy/paste if clicking on context menu
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (e.altKey || (e.metaKey && isWindows) || (e.ctrlKey && !isWindows)) {
|
||||||
e.altKey ||
|
|
||||||
(e.metaKey && isWindows) ||
|
|
||||||
(e.ctrlKey && !isWindows)
|
|
||||||
) {
|
|
||||||
onCopyPaste(clipboard.historyId)
|
onCopyPaste(clipboard.historyId)
|
||||||
} else {
|
} else {
|
||||||
onCopy(clipboard.historyId)
|
onCopy(clipboard.historyId)
|
||||||
@ -661,9 +731,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
key={i}
|
key={i}
|
||||||
{...getLineProps({ line })}
|
{...getLineProps({ line })}
|
||||||
className={`${
|
className={`${
|
||||||
isWrapText
|
isWrapText ? 'whitespace-pre-wrap' : 'whitespace-pre'
|
||||||
? 'whitespace-pre-wrap'
|
|
||||||
: 'whitespace-pre'
|
|
||||||
} overflow-hidden text-ellipsis`}
|
} overflow-hidden text-ellipsis`}
|
||||||
>
|
>
|
||||||
{line.map((token, key) => (
|
{line.map((token, key) => (
|
||||||
@ -674,10 +742,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
>
|
>
|
||||||
{!searchTerm
|
{!searchTerm
|
||||||
? token.content
|
? token.content
|
||||||
: highlightMatchedText(
|
: highlightMatchedText(token.content, searchTerm)}
|
||||||
token.content,
|
|
||||||
searchTerm
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -723,10 +788,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
) : (
|
) : (
|
||||||
<code className="justify-start cursor-pointer whitespace-pre">
|
<code className="justify-start cursor-pointer whitespace-pre">
|
||||||
{searchTerm
|
{searchTerm
|
||||||
? highlightWithPreviewMatchedText(
|
? highlightWithPreviewMatchedText(stringValue ?? '', searchTerm)
|
||||||
stringValue ?? '',
|
|
||||||
searchTerm
|
|
||||||
)
|
|
||||||
: hyperlinkTextWithPreview({
|
: hyperlinkTextWithPreview({
|
||||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||||
isPreviewError: hasClipboardHistoryURLErrors,
|
isPreviewError: hasClipboardHistoryURLErrors,
|
||||||
@ -1137,35 +1199,6 @@ export function ClipboardHistoryRowComponent({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ClipboardHistoryRowContextMenu
|
|
||||||
historyId={clipboard.historyId}
|
|
||||||
copiedFromApp={clipboard.copiedFromApp}
|
|
||||||
isMasked={clipboard.isMasked}
|
|
||||||
setSavingItem={setSavingItem}
|
|
||||||
value={clipboard.value}
|
|
||||||
isImage={clipboard.isImage}
|
|
||||||
isMp3={isMp3}
|
|
||||||
isText={clipboard.isText}
|
|
||||||
isPinned={clipboard.isPinned}
|
|
||||||
isFavorite={clipboard.isFavorite}
|
|
||||||
isImageData={clipboard.isImageData}
|
|
||||||
detectedLanguage={clipboard.detectedLanguage}
|
|
||||||
setLargeViewItemId={setLargeViewItemId}
|
|
||||||
isLargeView={isLargeView}
|
|
||||||
arrLinks={clipboard.arrLinks}
|
|
||||||
hasLinkCard={hasLinkCard}
|
|
||||||
isSelected={isSelected}
|
|
||||||
invalidateClipboardHistoryQuery={invalidateClipboardHistoryQuery}
|
|
||||||
generateLinkMetaData={generateLinkMetaData}
|
|
||||||
removeLinkMetaData={removeLinkMetaData}
|
|
||||||
setSelectHistoryItem={setSelectHistoryItem}
|
|
||||||
setSelectedHistoryItems={setSelectedHistoryItems}
|
|
||||||
selectedHistoryItems={selectedHistoryItems}
|
|
||||||
onCopyPaste={onCopyPaste}
|
|
||||||
setHistoryFilters={setHistoryFilters}
|
|
||||||
setAppFilters={setAppFilters}
|
|
||||||
/>
|
|
||||||
</ContextMenu>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
@ -70,12 +70,6 @@ export const ClipboardHistoryWindowIcons = ({
|
|||||||
setShowSelectHistoryItems(!showSelectHistoryItems)
|
setShowSelectHistoryItems(!showSelectHistoryItems)
|
||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys(['control+s'], () => {
|
|
||||||
if (hoveringHistoryRowId.value) {
|
|
||||||
setSelectHistoryItem(hoveringHistoryRowId.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useHotkeys(['alt+h', 'meta+h'], () => {
|
useHotkeys(['alt+h', 'meta+h'], () => {
|
||||||
setIsHistoryEnabled(!isHistoryEnabled)
|
setIsHistoryEnabled(!isHistoryEnabled)
|
||||||
})
|
})
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { Dispatch, SetStateAction } from 'react'
|
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||||
import { UniqueIdentifier } from '@dnd-kit/core'
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { invoke } from '@tauri-apps/api'
|
import { invoke } from '@tauri-apps/api'
|
||||||
import { message } from '@tauri-apps/api/dialog'
|
import { message } from '@tauri-apps/api/dialog'
|
||||||
import { emit } from '@tauri-apps/api/event'
|
import { emit } from '@tauri-apps/api/event'
|
||||||
import {
|
import {
|
||||||
clipboardHistoryStoreAtom,
|
|
||||||
createClipHistoryItemIds,
|
createClipHistoryItemIds,
|
||||||
createMenuItemFromHistoryId,
|
createMenuItemFromHistoryId,
|
||||||
|
DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
hasDashboardItemCreate,
|
hasDashboardItemCreate,
|
||||||
isCreatingMenuItem,
|
isCreatingMenuItem,
|
||||||
|
isKeyAltPressed,
|
||||||
settingsStoreAtom,
|
settingsStoreAtom,
|
||||||
} from '~/store'
|
} from '~/store'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
PanelTop,
|
PanelTop,
|
||||||
Pin,
|
Pin,
|
||||||
PinOff,
|
PinOff,
|
||||||
|
Settings,
|
||||||
Shrink,
|
Shrink,
|
||||||
SquareAsterisk,
|
SquareAsterisk,
|
||||||
Star,
|
Star,
|
||||||
@ -35,6 +37,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { TRANSFORM_CATEGORIES, type TransformCategory } from '~/lib/text-transforms'
|
||||||
import { ensureUrlPrefix } from '~/lib/utils'
|
import { ensureUrlPrefix } from '~/lib/utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -58,6 +61,7 @@ import {
|
|||||||
useUpdateClipboardHistoryById,
|
useUpdateClipboardHistoryById,
|
||||||
} from '~/hooks/queries/use-history-items'
|
} from '~/hooks/queries/use-history-items'
|
||||||
import { useSignal } from '~/hooks/use-signal'
|
import { useSignal } from '~/hooks/use-signal'
|
||||||
|
import { useSpecialCopyPasteHistoryItem } from '~/hooks/use-special-copypaste-history-item'
|
||||||
|
|
||||||
import { LinkMetadata } from '~/types/history'
|
import { LinkMetadata } from '~/types/history'
|
||||||
import { CreateDashboardItemType } from '~/types/menu'
|
import { CreateDashboardItemType } from '~/types/menu'
|
||||||
@ -92,6 +96,10 @@ interface ClipboardHistoryRowContextMenuProps {
|
|||||||
onCopyPaste: (id: UniqueIdentifier, delay?: number) => void
|
onCopyPaste: (id: UniqueIdentifier, delay?: number) => void
|
||||||
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
|
onDeleteConfirmationChange?: (
|
||||||
|
historyId: UniqueIdentifier | null,
|
||||||
|
isMultiSelect?: boolean
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ClipboardHistoryRowContextMenu({
|
export default function ClipboardHistoryRowContextMenu({
|
||||||
@ -121,8 +129,10 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
setSelectHistoryItem,
|
setSelectHistoryItem,
|
||||||
selectedHistoryItems,
|
selectedHistoryItems,
|
||||||
onCopyPaste,
|
onCopyPaste,
|
||||||
|
onDeleteConfirmationChange = () => {},
|
||||||
}: ClipboardHistoryRowContextMenuProps) {
|
}: ClipboardHistoryRowContextMenuProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {
|
const {
|
||||||
copyPasteDelay,
|
copyPasteDelay,
|
||||||
@ -130,14 +140,67 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
historyDetectLanguagesEnabledList,
|
historyDetectLanguagesEnabledList,
|
||||||
setIsExclusionAppListEnabled,
|
setIsExclusionAppListEnabled,
|
||||||
addToHistoryExclusionAppList,
|
addToHistoryExclusionAppList,
|
||||||
|
enabledSpecialPasteOperations,
|
||||||
|
specialPasteCategoriesOrder,
|
||||||
|
isSpecialCopyPasteHistoryEnabled,
|
||||||
} = useAtomValue(settingsStoreAtom)
|
} = useAtomValue(settingsStoreAtom)
|
||||||
|
|
||||||
const showDeleteMenuItemsConfirmation = useSignal(false)
|
const [specialActionInProgress, setSpecialActionInProgress] = useState<string | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
const deleteTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// Moved hook declarations before useHotkeys to resolve TS errors
|
||||||
const { updateClipboardHistoryById } = useUpdateClipboardHistoryById()
|
const { updateClipboardHistoryById } = useUpdateClipboardHistoryById()
|
||||||
const { deleteClipboardHistoryByIds } = useDeleteClipboardHistoryByIds()
|
const { deleteClipboardHistoryByIds } = useDeleteClipboardHistoryByIds()
|
||||||
const { pinnedClipboardHistoryByIds } = usePinnedClipboardHistoryByIds()
|
const { pinnedClipboardHistoryByIds } = usePinnedClipboardHistoryByIds()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
// Track pending delete ID for two-step deletion
|
||||||
|
const [pendingDeleteId, setPendingDeleteId] = useState<UniqueIdentifier | null>(null)
|
||||||
|
|
||||||
|
// Cleanup timer on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
deleteTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { specialCopy, specialPaste } = useSpecialCopyPasteHistoryItem()
|
||||||
|
|
||||||
|
// Ensure we always have an array of categories
|
||||||
|
const categoriesOrder = specialPasteCategoriesOrder || [
|
||||||
|
...DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
|
]
|
||||||
|
|
||||||
|
// Filter categories to only include those with enabled transforms
|
||||||
|
const categoriesWithTransforms = categoriesOrder
|
||||||
|
.map(categoryId => TRANSFORM_CATEGORIES.find(c => c.id === categoryId))
|
||||||
|
.filter((category): category is TransformCategory => {
|
||||||
|
if (!category || !categoriesOrder.includes(category.id)) return false
|
||||||
|
|
||||||
|
// Check if category has any enabled transforms
|
||||||
|
if (category.subcategories) {
|
||||||
|
// For categories with subcategories, check if any subcategory has enabled transforms
|
||||||
|
const hasEnabledSubcategories = category.subcategories.some(subcategory =>
|
||||||
|
subcategory.transforms.some(transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return hasEnabledSubcategories
|
||||||
|
} else {
|
||||||
|
// For categories with transforms, check if any transform is enabled
|
||||||
|
const enabledTransforms =
|
||||||
|
category.transforms?.filter(transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
) || []
|
||||||
|
return enabledTransforms.length > 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasEnabledCategories = categoriesWithTransforms.length > 0
|
||||||
|
|
||||||
const errorMessage = (err: string) => {
|
const errorMessage = (err: string) => {
|
||||||
message(
|
message(
|
||||||
@ -155,7 +218,67 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenuPortal>
|
<ContextMenuPortal>
|
||||||
<ContextMenuContent className="max-w-[210px]">
|
<ContextMenuContent
|
||||||
|
className="max-w-[210px]"
|
||||||
|
onInteractOutside={e => {
|
||||||
|
// Prevent closing on interaction outside during deletion confirmation
|
||||||
|
if (pendingDeleteId) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEscapeKeyDown={e => {
|
||||||
|
// Allow escape to close even during confirmation
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
// Handle Delete/Backspace keys
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (isSelected && selectedHistoryItems && selectedHistoryItems.length > 1) {
|
||||||
|
// Multi-select delete
|
||||||
|
if (pendingDeleteId === 'multi') {
|
||||||
|
// Confirm multi-delete
|
||||||
|
deleteClipboardHistoryByIds({ historyIds: selectedHistoryItems })
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedHistoryItems([])
|
||||||
|
}, 400)
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start multi-delete confirmation
|
||||||
|
setPendingDeleteId('multi')
|
||||||
|
onDeleteConfirmationChange?.(null, true)
|
||||||
|
deleteTimerRef.current = setTimeout(() => {
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
onDeleteConfirmationChange?.(null, false)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single delete
|
||||||
|
if (pendingDeleteId === historyId) {
|
||||||
|
// Confirm single delete
|
||||||
|
deleteClipboardHistoryByIds({ historyIds: [historyId] })
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start single delete confirmation
|
||||||
|
setPendingDeleteId(historyId)
|
||||||
|
onDeleteConfirmationChange?.(historyId, false)
|
||||||
|
deleteTimerRef.current = setTimeout(() => {
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
onDeleteConfirmationChange?.(null, false)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectHistoryItem(historyId)
|
setSelectHistoryItem(historyId)
|
||||||
@ -247,6 +370,167 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
</ContextMenuCheckboxItem>
|
</ContextMenuCheckboxItem>
|
||||||
</ContextMenuSubContent>
|
</ContextMenuSubContent>
|
||||||
</ContextMenuSub>
|
</ContextMenuSub>
|
||||||
|
|
||||||
|
{/* Special Copy/Paste submenu - only show for text items when enabled */}
|
||||||
|
{isSpecialCopyPasteHistoryEnabled && !isImage && value && (
|
||||||
|
<>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
{isKeyAltPressed.value
|
||||||
|
? t('Special Paste', { ns: 'specialCopyPaste' })
|
||||||
|
: t('Special Copy', { ns: 'specialCopyPaste' })}
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent className="w-48">
|
||||||
|
{categoriesWithTransforms.map(category => {
|
||||||
|
// Handle categories with subcategories (like Format Converter)
|
||||||
|
if (category.subcategories) {
|
||||||
|
const enabledSubcategories = category.subcategories.filter(
|
||||||
|
subcategory =>
|
||||||
|
subcategory.transforms.some(transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenuSub key={category.id}>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
{t(category.label, {
|
||||||
|
ns: 'specialCopyPaste',
|
||||||
|
})}
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent className="w-48">
|
||||||
|
{enabledSubcategories.map(subcategory => {
|
||||||
|
const enabledTransforms = subcategory.transforms.filter(
|
||||||
|
transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenuSub key={subcategory.id}>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
{t(subcategory.label, {
|
||||||
|
ns: 'specialCopyPaste',
|
||||||
|
})}
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent className="w-44">
|
||||||
|
{enabledTransforms.map(transform => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={transform.id}
|
||||||
|
disabled={specialActionInProgress === transform.id}
|
||||||
|
onClick={async () => {
|
||||||
|
setSpecialActionInProgress(transform.id)
|
||||||
|
try {
|
||||||
|
if (isKeyAltPressed.value) {
|
||||||
|
await specialPaste(
|
||||||
|
historyId,
|
||||||
|
value,
|
||||||
|
transform.id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await specialCopy(
|
||||||
|
historyId,
|
||||||
|
value,
|
||||||
|
transform.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setSpecialActionInProgress(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Special copy/paste failed:',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
setSpecialActionInProgress(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(transform.label, {
|
||||||
|
ns: 'specialCopyPaste',
|
||||||
|
})}
|
||||||
|
{specialActionInProgress === transform.id && (
|
||||||
|
<div className="ml-auto">
|
||||||
|
<Text className="text-xs text-muted-foreground">
|
||||||
|
...
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Handle categories with direct transforms
|
||||||
|
const enabledTransforms =
|
||||||
|
category.transforms?.filter(transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenuSub key={category.id}>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
{t(category.label, {
|
||||||
|
ns: 'specialCopyPaste',
|
||||||
|
})}
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent className="w-44">
|
||||||
|
{enabledTransforms.map(transform => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={transform.id}
|
||||||
|
disabled={specialActionInProgress === transform.id}
|
||||||
|
onClick={async () => {
|
||||||
|
setSpecialActionInProgress(transform.id)
|
||||||
|
try {
|
||||||
|
if (isKeyAltPressed.value) {
|
||||||
|
await specialPaste(historyId, value, transform.id)
|
||||||
|
} else {
|
||||||
|
await specialCopy(historyId, value, transform.id)
|
||||||
|
}
|
||||||
|
setSpecialActionInProgress(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Special copy/paste failed:', error)
|
||||||
|
setSpecialActionInProgress(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(transform.label, {
|
||||||
|
ns: 'specialCopyPaste',
|
||||||
|
})}
|
||||||
|
{specialActionInProgress === transform.id && (
|
||||||
|
<div className="ml-auto">
|
||||||
|
<Text className="text-xs text-muted-foreground">
|
||||||
|
...
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{hasEnabledCategories && <ContextMenuSeparator />}
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/app-settings/history#specialCopyPasteHistory', {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Special Settings', { ns: 'specailCopyPaste' })}
|
||||||
|
<div className="ml-auto">
|
||||||
|
<Settings size={15} />
|
||||||
|
</div>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -552,25 +836,34 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
{isSelected && selectedHistoryItems && selectedHistoryItems.length > 1 ? (
|
{isSelected && selectedHistoryItems && selectedHistoryItems.length > 1 ? (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={async e => {
|
className={
|
||||||
if (showDeleteMenuItemsConfirmation.value) {
|
pendingDeleteId === 'multi' ? 'bg-red-500/20 dark:bg-red-600/20' : ''
|
||||||
|
}
|
||||||
|
onSelect={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (pendingDeleteId === 'multi') {
|
||||||
await deleteClipboardHistoryByIds({ historyIds: selectedHistoryItems })
|
await deleteClipboardHistoryByIds({ historyIds: selectedHistoryItems })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSelectedHistoryItems([])
|
setSelectedHistoryItems([])
|
||||||
}, 400)
|
}, 400)
|
||||||
showDeleteMenuItemsConfirmation.value = false
|
setPendingDeleteId(null)
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
setPendingDeleteId('multi')
|
||||||
showDeleteMenuItemsConfirmation.value = true
|
onDeleteConfirmationChange?.(null, true)
|
||||||
setTimeout(() => {
|
deleteTimerRef.current = setTimeout(() => {
|
||||||
showDeleteMenuItemsConfirmation.value = false
|
setPendingDeleteId(null)
|
||||||
|
onDeleteConfirmationChange?.(null, false)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Text className="!text-red-500 dark:!text-red-600">
|
<Text className="!text-red-500 dark:!text-red-600">
|
||||||
{!showDeleteMenuItemsConfirmation.value
|
{pendingDeleteId !== 'multi'
|
||||||
? t('Delete', { ns: 'common' })
|
? t('Delete', { ns: 'common' })
|
||||||
: t('Click to Confirm', { ns: 'common' })}
|
: t('Click to Confirm', { ns: 'common' })}
|
||||||
<Badge
|
<Badge
|
||||||
@ -581,7 +874,7 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{!showDeleteMenuItemsConfirmation.value && (
|
{pendingDeleteId !== 'multi' && (
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<Badge variant="default" className="ml-1 py-0 font-semibold">
|
<Badge variant="default" className="ml-1 py-0 font-semibold">
|
||||||
DEL
|
DEL
|
||||||
@ -591,29 +884,36 @@ export default function ClipboardHistoryRowContextMenu({
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={async e => {
|
className={
|
||||||
if (showDeleteMenuItemsConfirmation.value) {
|
pendingDeleteId === historyId ? 'bg-red-500/20 dark:bg-red-600/20' : ''
|
||||||
await deleteClipboardHistoryByIds({ historyIds: [historyId] })
|
}
|
||||||
setTimeout(() => {
|
onSelect={async e => {
|
||||||
showDeleteMenuItemsConfirmation.value = false
|
|
||||||
}, 400)
|
|
||||||
} else {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
showDeleteMenuItemsConfirmation.value = true
|
|
||||||
setTimeout(() => {
|
if (pendingDeleteId === historyId) {
|
||||||
showDeleteMenuItemsConfirmation.value = false
|
await deleteClipboardHistoryByIds({ historyIds: [historyId] })
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPendingDeleteId(historyId)
|
||||||
|
onDeleteConfirmationChange?.(historyId, false)
|
||||||
|
deleteTimerRef.current = setTimeout(() => {
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
onDeleteConfirmationChange?.(null, false)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Text className="!text-red-500 dark:!text-red-600">
|
<Text className="!text-red-500 dark:!text-red-600">
|
||||||
{!showDeleteMenuItemsConfirmation.value
|
{pendingDeleteId !== historyId
|
||||||
? t('Delete', { ns: 'common' })
|
? t('Delete', { ns: 'common' })
|
||||||
: t('Click to Confirm', { ns: 'common' })}
|
: t('Click to Confirm', { ns: 'common' })}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{!showDeleteMenuItemsConfirmation.value && (
|
{pendingDeleteId !== historyId && (
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<Badge variant="default" className="ml-1 py-0 font-semibold">
|
<Badge variant="default" className="ml-1 py-0 font-semibold">
|
||||||
DEL
|
DEL
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
import { Dispatch, forwardRef, SetStateAction, useState } from 'react'
|
||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger as ContextMenuTriggerPrimitive,
|
||||||
|
} from '~/components/ui'
|
||||||
|
|
||||||
|
import { LinkMetadata } from '~/types/history'
|
||||||
|
|
||||||
|
import ClipboardHistoryRowContextMenu from './ClipboardHistoryRowContextMenu'
|
||||||
|
|
||||||
|
// // Lazy load the heavy context menu component
|
||||||
|
// const ClipboardHistoryRowContextMenu = lazy(
|
||||||
|
// () => import('./ClipboardHistoryRowContextMenu')
|
||||||
|
// )
|
||||||
|
|
||||||
|
interface ContextMenuTriggerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
onOpenChange?: (isOpen: boolean) => void
|
||||||
|
historyId: UniqueIdentifier
|
||||||
|
value: string | null
|
||||||
|
arrLinks: string[]
|
||||||
|
isImage: boolean
|
||||||
|
isText: boolean
|
||||||
|
copiedFromApp?: string | null
|
||||||
|
isMasked: boolean
|
||||||
|
isImageData: boolean
|
||||||
|
isMp3: boolean | undefined
|
||||||
|
hasLinkCard: boolean | undefined | string | null
|
||||||
|
isSelected: boolean
|
||||||
|
isLargeView: boolean
|
||||||
|
isPinned: boolean
|
||||||
|
isFavorite: boolean
|
||||||
|
detectedLanguage: string | null
|
||||||
|
setLargeViewItemId: (historyId: UniqueIdentifier | null) => void
|
||||||
|
setSavingItem: (historyId: UniqueIdentifier | null) => void
|
||||||
|
invalidateClipboardHistoryQuery?: () => void
|
||||||
|
generateLinkMetaData?: (
|
||||||
|
historyId: UniqueIdentifier,
|
||||||
|
url: string
|
||||||
|
) => Promise<LinkMetadata | void>
|
||||||
|
removeLinkMetaData?: (historyId: UniqueIdentifier) => Promise<void>
|
||||||
|
setSelectHistoryItem: (id: UniqueIdentifier) => void
|
||||||
|
setSelectedHistoryItems?: (ids: UniqueIdentifier[]) => void
|
||||||
|
selectedHistoryItems?: UniqueIdentifier[]
|
||||||
|
onCopyPaste: (id: UniqueIdentifier, delay?: number) => void
|
||||||
|
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
|
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
|
onDeleteConfirmationChange?: (
|
||||||
|
historyId: UniqueIdentifier | string | null,
|
||||||
|
isMultiSelect?: boolean
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextMenuTrigger = forwardRef<HTMLElement, ContextMenuTriggerProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
onOpenChange,
|
||||||
|
historyId,
|
||||||
|
value,
|
||||||
|
arrLinks,
|
||||||
|
isImage,
|
||||||
|
isText,
|
||||||
|
copiedFromApp,
|
||||||
|
isMasked,
|
||||||
|
isImageData,
|
||||||
|
isMp3,
|
||||||
|
hasLinkCard,
|
||||||
|
isSelected,
|
||||||
|
isLargeView,
|
||||||
|
isPinned,
|
||||||
|
isFavorite,
|
||||||
|
detectedLanguage,
|
||||||
|
setLargeViewItemId,
|
||||||
|
setSavingItem,
|
||||||
|
invalidateClipboardHistoryQuery,
|
||||||
|
generateLinkMetaData,
|
||||||
|
removeLinkMetaData,
|
||||||
|
setSelectHistoryItem,
|
||||||
|
setSelectedHistoryItems,
|
||||||
|
selectedHistoryItems,
|
||||||
|
onCopyPaste,
|
||||||
|
setHistoryFilters,
|
||||||
|
setAppFilters,
|
||||||
|
onDeleteConfirmationChange,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open)
|
||||||
|
onOpenChange?.(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu onOpenChange={handleOpenChange}>
|
||||||
|
<ContextMenuTriggerPrimitive ref={ref} asChild>
|
||||||
|
{children}
|
||||||
|
</ContextMenuTriggerPrimitive>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<ClipboardHistoryRowContextMenu
|
||||||
|
historyId={historyId}
|
||||||
|
value={value}
|
||||||
|
arrLinks={arrLinks}
|
||||||
|
isImage={isImage}
|
||||||
|
isText={isText}
|
||||||
|
copiedFromApp={copiedFromApp}
|
||||||
|
isMasked={isMasked}
|
||||||
|
isImageData={isImageData}
|
||||||
|
isMp3={isMp3}
|
||||||
|
hasLinkCard={hasLinkCard}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isLargeView={isLargeView}
|
||||||
|
isPinned={isPinned}
|
||||||
|
isFavorite={isFavorite}
|
||||||
|
detectedLanguage={detectedLanguage}
|
||||||
|
setLargeViewItemId={setLargeViewItemId}
|
||||||
|
setSavingItem={setSavingItem}
|
||||||
|
invalidateClipboardHistoryQuery={invalidateClipboardHistoryQuery}
|
||||||
|
generateLinkMetaData={generateLinkMetaData}
|
||||||
|
removeLinkMetaData={removeLinkMetaData}
|
||||||
|
setSelectHistoryItem={setSelectHistoryItem}
|
||||||
|
setSelectedHistoryItems={setSelectedHistoryItems}
|
||||||
|
selectedHistoryItems={selectedHistoryItems}
|
||||||
|
onCopyPaste={onCopyPaste}
|
||||||
|
setHistoryFilters={setHistoryFilters}
|
||||||
|
setAppFilters={setAppFilters}
|
||||||
|
onDeleteConfirmationChange={onDeleteConfirmationChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ContextMenuTrigger.displayName = 'ContextMenuTrigger'
|
||||||
|
|
||||||
|
export default ContextMenuTrigger
|
@ -25,6 +25,7 @@ import {
|
|||||||
settingsStoreAtom,
|
settingsStoreAtom,
|
||||||
showClipFindKeyPressed,
|
showClipFindKeyPressed,
|
||||||
showClipsMoveOnBoardId,
|
showClipsMoveOnBoardId,
|
||||||
|
showKeyboardNavContextMenuClipId,
|
||||||
showLargeViewClipId,
|
showLargeViewClipId,
|
||||||
showLinkedClipId,
|
showLinkedClipId,
|
||||||
} from '~/store'
|
} from '~/store'
|
||||||
@ -562,6 +563,30 @@ export function ClipCard({
|
|||||||
}
|
}
|
||||||
}, [isKeyboardSelected])
|
}, [isKeyboardSelected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showKeyboardNavContextMenuClipId.value === clip.id) {
|
||||||
|
if (contextMenuTriggerRef?.current) {
|
||||||
|
const targetElement = contextMenuTriggerRef.current
|
||||||
|
const rect = targetElement.getBoundingClientRect()
|
||||||
|
|
||||||
|
contextMenuClipId.value = clip.id
|
||||||
|
|
||||||
|
const contextMenuEvent = new MouseEvent('contextmenu', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
clientX: rect.x + 50,
|
||||||
|
clientY: rect.y + 20,
|
||||||
|
button: 2, // Right mouse button
|
||||||
|
})
|
||||||
|
|
||||||
|
targetElement.dispatchEvent(contextMenuEvent)
|
||||||
|
|
||||||
|
// Focus the first menu item after the context menu opens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [showKeyboardNavContextMenuClipId.value, clip.id, contextMenuTriggerRef?.current])
|
||||||
|
|
||||||
const isEditing = isClipNameEditing || isClipEdit
|
const isEditing = isClipNameEditing || isClipEdit
|
||||||
|
|
||||||
const copyDisabled =
|
const copyDisabled =
|
||||||
@ -602,10 +627,18 @@ export function ClipCard({
|
|||||||
<ContextMenu
|
<ContextMenu
|
||||||
onOpenChange={isOpen => {
|
onOpenChange={isOpen => {
|
||||||
contextMenuOpen.value = isOpen
|
contextMenuOpen.value = isOpen
|
||||||
|
if (!isOpen && showKeyboardNavContextMenuClipId.value === clip.id) {
|
||||||
|
showKeyboardNavContextMenuClipId.value = null
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContextMenuTrigger
|
<ContextMenuTrigger
|
||||||
disabled={(!isHover && !isSelected) || Boolean(globalSearchTerm)}
|
disabled={
|
||||||
|
(!isHover &&
|
||||||
|
!isSelected &&
|
||||||
|
showKeyboardNavContextMenuClipId.value !== clip.id) ||
|
||||||
|
Boolean(globalSearchTerm)
|
||||||
|
}
|
||||||
ref={contextMenuTriggerRef}
|
ref={contextMenuTriggerRef}
|
||||||
>
|
>
|
||||||
<Box className="relative">
|
<Box className="relative">
|
||||||
|
@ -123,7 +123,12 @@ const renderWithBadges = (
|
|||||||
: 'dark:!text-gray-300 text-gray-400 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
: 'dark:!text-gray-300 text-gray-400 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
||||||
} text-normal pr-2.5`}
|
} text-normal pr-2.5`}
|
||||||
>
|
>
|
||||||
<Check size={12} className={`mr-0.5 ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`} />
|
<Check
|
||||||
|
size={12}
|
||||||
|
className={`mr-0.5 ${
|
||||||
|
field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
{field.label}
|
{field.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
@ -163,7 +168,12 @@ const renderWithBadges = (
|
|||||||
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
||||||
} text-normal pr-2.5`}
|
} text-normal pr-2.5`}
|
||||||
>
|
>
|
||||||
<Check size={12} className={`mr-0.5 ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`} />
|
<Check
|
||||||
|
size={12}
|
||||||
|
className={`mr-0.5 ${
|
||||||
|
field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
{field.label}
|
{field.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
@ -196,7 +206,10 @@ const renderWithBadges = (
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
||||||
>
|
>
|
||||||
<Check size={12} className="mr-0.5 text-purple-600 dark:text-purple-400" />
|
<Check
|
||||||
|
size={12}
|
||||||
|
className="mr-0.5 text-purple-600 dark:text-purple-400"
|
||||||
|
/>
|
||||||
{field.label} (Global)
|
{field.label} (Global)
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
@ -223,7 +236,10 @@ const renderWithBadges = (
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
||||||
>
|
>
|
||||||
<Check size={12} className="mr-0.5 text-purple-600 dark:text-purple-400" />
|
<Check
|
||||||
|
size={12}
|
||||||
|
className="mr-0.5 text-purple-600 dark:text-purple-400"
|
||||||
|
/>
|
||||||
{field.label}
|
{field.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
@ -407,9 +423,10 @@ export function ClipViewTemplate({
|
|||||||
.filter(f => f.label !== undefined)
|
.filter(f => f.label !== undefined)
|
||||||
.map(({ label, isEnable, value, isValueMasked, isGlobal }) => {
|
.map(({ label, isEnable, value, isValueMasked, isGlobal }) => {
|
||||||
// For global templates, get the current value from globalTemplates
|
// For global templates, get the current value from globalTemplates
|
||||||
const actualValue = isGlobal && globalTemplatesEnabled
|
const actualValue =
|
||||||
|
isGlobal && globalTemplatesEnabled
|
||||||
? globalTemplates.find(gt => gt.isEnabled && gt.name === label)?.value || ''
|
? globalTemplates.find(gt => gt.isEnabled && gt.name === label)?.value || ''
|
||||||
: value;
|
: value
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
@ -417,7 +434,7 @@ export function ClipViewTemplate({
|
|||||||
value: actualValue,
|
value: actualValue,
|
||||||
isEnable,
|
isEnable,
|
||||||
isGlobal,
|
isGlobal,
|
||||||
};
|
}
|
||||||
}),
|
}),
|
||||||
clipboardValueSignal.value,
|
clipboardValueSignal.value,
|
||||||
templateShowFormat.value === 'values',
|
templateShowFormat.value === 'values',
|
||||||
@ -587,9 +604,7 @@ export function ClipViewTemplate({
|
|||||||
<span
|
<span
|
||||||
className={`whitespace-nowrap pr-1 min-w-[80px] overflow-hidden text-ellipsis block ${
|
className={`whitespace-nowrap pr-1 min-w-[80px] overflow-hidden text-ellipsis block ${
|
||||||
isLabelOnTop ? 'text-left' : 'text-right max-w-[160px]'
|
isLabelOnTop ? 'text-left' : 'text-right max-w-[160px]'
|
||||||
} ${
|
} ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`}
|
||||||
field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{field.label}
|
{field.label}
|
||||||
</span>
|
</span>
|
||||||
@ -781,7 +796,10 @@ export function ClipViewTemplate({
|
|||||||
title={`Global Template: ${field.label}`}
|
title={`Global Template: ${field.label}`}
|
||||||
/>
|
/>
|
||||||
<Badge className="inline-flex items-center gap-1 bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-700 cursor-default text-xs py-0.5 px-1.5">
|
<Badge className="inline-flex items-center gap-1 bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-700 cursor-default text-xs py-0.5 px-1.5">
|
||||||
<Check size={12} className="text-purple-600 dark:text-purple-400" />
|
<Check
|
||||||
|
size={12}
|
||||||
|
className="text-purple-600 dark:text-purple-400"
|
||||||
|
/>
|
||||||
{t('Global', { ns: 'templates' })}
|
{t('Global', { ns: 'templates' })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -394,18 +394,18 @@ export function GlobalSearch({ isDark }: { isDark: boolean }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Tab' && !e.shiftKey) {
|
} else if (e.key === 'Tab' && !e.shiftKey) {
|
||||||
|
if (availableTabs.length > 1) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (availableTabs.length > 1) {
|
|
||||||
const currentTabIndex = availableTabs.indexOf(filter)
|
const currentTabIndex = availableTabs.indexOf(filter)
|
||||||
const nextTabIndex = (currentTabIndex + 1) % availableTabs.length
|
const nextTabIndex = (currentTabIndex + 1) % availableTabs.length
|
||||||
setFilter(availableTabs[nextTabIndex])
|
setFilter(availableTabs[nextTabIndex])
|
||||||
setSelectedIndex(-1)
|
setSelectedIndex(-1)
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Tab' && e.shiftKey) {
|
} else if (e.key === 'Tab' && e.shiftKey) {
|
||||||
|
if (availableTabs.length > 1) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (availableTabs.length > 1) {
|
|
||||||
const currentTabIndex = availableTabs.indexOf(filter)
|
const currentTabIndex = availableTabs.indexOf(filter)
|
||||||
const prevTabIndex =
|
const prevTabIndex =
|
||||||
(currentTabIndex - 1 + availableTabs.length) % availableTabs.length
|
(currentTabIndex - 1 + availableTabs.length) % availableTabs.length
|
||||||
@ -422,6 +422,18 @@ export function GlobalSearch({ isDark }: { isDark: boolean }) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowSearchModal(false)
|
setShowSearchModal(false)
|
||||||
|
} else if (e.key === 'PageUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
} else if (e.key === 'PageDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
} else if (e.key === 'Home') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
} else if (e.altKey && e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[filter, selectedIndex, handleCopySelectedItem, flattenedItems]
|
[filter, selectedIndex, handleCopySelectedItem, flattenedItems]
|
||||||
|
@ -90,7 +90,11 @@ export function MenuCardViewBody({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isWrapText = useSignal(false)
|
const isWrapText = useSignal(false)
|
||||||
|
|
||||||
const { valuePreview, morePreviewLines, morePreviewChars } = getValuePreview(value, false, false)
|
const { valuePreview, morePreviewLines, morePreviewChars } = getValuePreview(
|
||||||
|
value,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
const textValue: string = value || ''
|
const textValue: string = value || ''
|
||||||
const isBrokenImage = useSignal(false)
|
const isBrokenImage = useSignal(false)
|
||||||
const pathTypeCheck = useSignal<string | null | undefined>('')
|
const pathTypeCheck = useSignal<string | null | undefined>('')
|
||||||
|
@ -35,6 +35,9 @@ import {
|
|||||||
showClipsMoveOnBoardId,
|
showClipsMoveOnBoardId,
|
||||||
showDetailsClipId,
|
showDetailsClipId,
|
||||||
showHistoryDeleteConfirmationId,
|
showHistoryDeleteConfirmationId,
|
||||||
|
showHistoryMultiDeleteConfirmationIds,
|
||||||
|
showKeyboardNavContextMenuClipId,
|
||||||
|
showKeyboardNavContextMenuHistoryId,
|
||||||
showLargeViewClipId,
|
showLargeViewClipId,
|
||||||
showLargeViewHistoryId,
|
showLargeViewHistoryId,
|
||||||
showOrganizeLayout,
|
showOrganizeLayout,
|
||||||
@ -120,7 +123,13 @@ import {
|
|||||||
} from '~/hooks/use-copypaste-history-item'
|
} from '~/hooks/use-copypaste-history-item'
|
||||||
import { useDebounce } from '~/hooks/use-debounce'
|
import { useDebounce } from '~/hooks/use-debounce'
|
||||||
import useDeleteConfirmationTimer from '~/hooks/use-delete-confirmation-items'
|
import useDeleteConfirmationTimer from '~/hooks/use-delete-confirmation-items'
|
||||||
|
import useKeyboardDeleteConfirmation from '~/hooks/use-keyboard-delete-confirmation'
|
||||||
import { useSignal } from '~/hooks/use-signal'
|
import { useSignal } from '~/hooks/use-signal'
|
||||||
|
import {
|
||||||
|
specialCopiedItem,
|
||||||
|
specialPastedItem,
|
||||||
|
specialPastedItemCountDown,
|
||||||
|
} from '~/hooks/use-special-copypaste-history-item'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardHistoryIconMenu,
|
ClipboardHistoryIconMenu,
|
||||||
@ -269,6 +278,38 @@ export default function ClipboardHistoryPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { showConfirmation: showConfirmationKeyboardDelete, keyboardItemIdDelete, resetTimer: resetKeyboardDeleteTimer } =
|
||||||
|
useKeyboardDeleteConfirmation({
|
||||||
|
keyboardSelectedItemId: keyboardSelectedItemId,
|
||||||
|
selectedHistoryItems,
|
||||||
|
onConfirmedDelete: async () => {
|
||||||
|
if (keyboardSelectedItemId.value) {
|
||||||
|
// Calculate next selection before deletion
|
||||||
|
const currentIndex = clipboardHistory.findIndex(
|
||||||
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
|
)
|
||||||
|
let nextSelectedId: UniqueIdentifier | null = null
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
if (currentIndex < clipboardHistory.length - 1) {
|
||||||
|
// Select next item
|
||||||
|
nextSelectedId = clipboardHistory[currentIndex + 1].historyId
|
||||||
|
} else if (currentIndex > 0) {
|
||||||
|
// Select previous item
|
||||||
|
nextSelectedId = clipboardHistory[currentIndex - 1].historyId
|
||||||
|
}
|
||||||
|
// If only one item, nextSelectedId remains null
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteClipboardHistoryByIds({
|
||||||
|
historyIds: [keyboardSelectedItemId.value],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update selection to the calculated next item
|
||||||
|
keyboardSelectedItemId.value = nextSelectedId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const isPinnedPanelHoverOpen = useMemo(() => {
|
const isPinnedPanelHoverOpen = useMemo(() => {
|
||||||
return isPinnedPanelKeepOpen.value || isPinnedPanelHovering.value
|
return isPinnedPanelKeepOpen.value || isPinnedPanelHovering.value
|
||||||
}, [isPinnedPanelHovering.value, isPinnedPanelKeepOpen.value])
|
}, [isPinnedPanelHovering.value, isPinnedPanelKeepOpen.value])
|
||||||
@ -339,6 +380,18 @@ export default function ClipboardHistoryPage() {
|
|||||||
|
|
||||||
const pastedItemValue = useMemo(() => pastedItem, [pastedItem])
|
const pastedItemValue = useMemo(() => pastedItem, [pastedItem])
|
||||||
const copiedItemValue = useMemo(() => copiedItem, [copiedItem])
|
const copiedItemValue = useMemo(() => copiedItem, [copiedItem])
|
||||||
|
const specialCopiedItemValue = useMemo(
|
||||||
|
() => specialCopiedItem.value,
|
||||||
|
[specialCopiedItem.value]
|
||||||
|
)
|
||||||
|
const specialPastedItemValue = useMemo(
|
||||||
|
() => specialPastedItem.value,
|
||||||
|
[specialPastedItem.value]
|
||||||
|
)
|
||||||
|
const specialPastingCountDown = useMemo(
|
||||||
|
() => specialPastedItemCountDown.value,
|
||||||
|
[specialPastedItemCountDown.value]
|
||||||
|
)
|
||||||
|
|
||||||
const clipboardHistory = hasSearchOrFilter ? foundClipboardHistory : allClipboardHistory
|
const clipboardHistory = hasSearchOrFilter ? foundClipboardHistory : allClipboardHistory
|
||||||
|
|
||||||
@ -436,13 +489,16 @@ export default function ClipboardHistoryPage() {
|
|||||||
currentNavigationContext.value === null) &&
|
currentNavigationContext.value === null) &&
|
||||||
keyboardSelectedItemId.value
|
keyboardSelectedItemId.value
|
||||||
) {
|
) {
|
||||||
|
// Reset keyboard delete confirmation when copying
|
||||||
|
resetKeyboardDeleteTimer()
|
||||||
setCopiedItem(keyboardSelectedItemId.value)
|
setCopiedItem(keyboardSelectedItemId.value)
|
||||||
} else if (
|
} else if (
|
||||||
(currentNavigationContext.value === 'history' ||
|
(currentNavigationContext.value === 'history' ||
|
||||||
currentNavigationContext.value === null) &&
|
currentNavigationContext.value === null) &&
|
||||||
clipboardHistory.length > 0
|
clipboardHistory.length > 0
|
||||||
) {
|
) {
|
||||||
setCopiedItem(clipboardHistory[0]?.historyId)
|
// TODO: Fix this
|
||||||
|
// setCopiedItem(clipboardHistory[0]?.historyId)
|
||||||
}
|
}
|
||||||
currentNavigationContext.value = null
|
currentNavigationContext.value = null
|
||||||
keyboardSelectedItemId.value = null
|
keyboardSelectedItemId.value = null
|
||||||
@ -465,6 +521,9 @@ export default function ClipboardHistoryPage() {
|
|||||||
e => {
|
e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Reset delete confirmation when navigating
|
||||||
|
showHistoryDeleteConfirmationId.value = null
|
||||||
|
|
||||||
if (keyboardSelectedBoardId.value) {
|
if (keyboardSelectedBoardId.value) {
|
||||||
const clipsOnBoard = clipItems
|
const clipsOnBoard = clipItems
|
||||||
.filter(
|
.filter(
|
||||||
@ -627,10 +686,46 @@ export default function ClipboardHistoryPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys(['control+s'], e => {
|
||||||
|
if (hoveringHistoryRowId.value) {
|
||||||
|
setSelectHistoryItem(hoveringHistoryRowId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
useHotkeys(['tab'], handleTabNavigation('forward'), {
|
useHotkeys(['tab'], handleTabNavigation('forward'), {
|
||||||
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['space'],
|
||||||
|
() => {
|
||||||
|
if (
|
||||||
|
currentNavigationContext.value === 'history' ||
|
||||||
|
currentNavigationContext.value === null
|
||||||
|
) {
|
||||||
|
if (keyboardSelectedItemId.value) {
|
||||||
|
// Reset keyboard delete confirmation when selecting
|
||||||
|
resetKeyboardDeleteTimer()
|
||||||
|
|
||||||
|
setSelectHistoryItem(keyboardSelectedItemId.value)
|
||||||
|
const currentItemIndex = clipboardHistory.findIndex(
|
||||||
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
|
)
|
||||||
|
const nextItem = clipboardHistory[currentItemIndex + 1]
|
||||||
|
if (nextItem) {
|
||||||
|
keyboardSelectedItemId.value = nextItem.historyId
|
||||||
|
if (showLargeViewHistoryId.value) {
|
||||||
|
showLargeViewHistoryId.value = nextItem.historyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
useHotkeys(['shift+tab'], handleTabNavigation('backward'), {
|
useHotkeys(['shift+tab'], handleTabNavigation('backward'), {
|
||||||
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
})
|
})
|
||||||
@ -638,7 +733,27 @@ export default function ClipboardHistoryPage() {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
'esc',
|
'esc',
|
||||||
() => {
|
() => {
|
||||||
|
// Clear any delete timeout when escaping
|
||||||
|
if (deleteTimeoutRef.current) {
|
||||||
|
clearTimeout(deleteTimeoutRef.current)
|
||||||
|
deleteTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset delete confirmation on escape
|
||||||
|
showHistoryDeleteConfirmationId.value = null
|
||||||
|
|
||||||
|
// Reset keyboard delete confirmation on escape
|
||||||
|
resetKeyboardDeleteTimer()
|
||||||
|
|
||||||
// Escape closes large view first, then performs normal escape behavior
|
// Escape closes large view first, then performs normal escape behavior
|
||||||
|
if (showKeyboardNavContextMenuHistoryId.value) {
|
||||||
|
showKeyboardNavContextMenuHistoryId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showKeyboardNavContextMenuClipId.value) {
|
||||||
|
showKeyboardNavContextMenuClipId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
if (showLargeViewHistoryId.value) {
|
if (showLargeViewHistoryId.value) {
|
||||||
showLargeViewHistoryId.value = null
|
showLargeViewHistoryId.value = null
|
||||||
} else {
|
} else {
|
||||||
@ -654,6 +769,19 @@ export default function ClipboardHistoryPage() {
|
|||||||
['arrowdown'],
|
['arrowdown'],
|
||||||
e => {
|
e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Clear any delete timeout when navigating away
|
||||||
|
if (deleteTimeoutRef.current) {
|
||||||
|
clearTimeout(deleteTimeoutRef.current)
|
||||||
|
deleteTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset delete confirmation when navigating to a different item
|
||||||
|
showHistoryDeleteConfirmationId.value = null
|
||||||
|
|
||||||
|
// Reset keyboard delete confirmation when navigating
|
||||||
|
resetKeyboardDeleteTimer()
|
||||||
|
|
||||||
const currentItemIndex = clipboardHistory.findIndex(
|
const currentItemIndex = clipboardHistory.findIndex(
|
||||||
item => item.historyId === keyboardSelectedItemId.value
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
)
|
)
|
||||||
@ -677,6 +805,19 @@ export default function ClipboardHistoryPage() {
|
|||||||
['arrowup'],
|
['arrowup'],
|
||||||
e => {
|
e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Clear any delete timeout when navigating away
|
||||||
|
if (deleteTimeoutRef.current) {
|
||||||
|
clearTimeout(deleteTimeoutRef.current)
|
||||||
|
deleteTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset delete confirmation when navigating to a different item
|
||||||
|
showHistoryDeleteConfirmationId.value = null
|
||||||
|
|
||||||
|
// Reset keyboard delete confirmation when navigating
|
||||||
|
resetKeyboardDeleteTimer()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentNavigationContext.value === 'history' ||
|
currentNavigationContext.value === 'history' ||
|
||||||
currentNavigationContext.value === null
|
currentNavigationContext.value === null
|
||||||
@ -768,6 +909,179 @@ export default function ClipboardHistoryPage() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['home'],
|
||||||
|
e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
currentNavigationContext.value === 'history' ||
|
||||||
|
currentNavigationContext.value === null
|
||||||
|
) {
|
||||||
|
// Navigate to first history item
|
||||||
|
if (clipboardHistory.length > 0) {
|
||||||
|
keyboardSelectedItemId.value = clipboardHistory[0].historyId
|
||||||
|
scrollToTopHistoryList()
|
||||||
|
if (showLargeViewHistoryId.value) {
|
||||||
|
showLargeViewHistoryId.value = clipboardHistory[0].historyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentNavigationContext.value === 'board') {
|
||||||
|
// Navigate to first clip in first board with clips
|
||||||
|
const boardsWithClips = clipItems
|
||||||
|
.filter(item => item.isBoard && item.tabId === currentTab)
|
||||||
|
.filter(board =>
|
||||||
|
clipItems.some(
|
||||||
|
clip =>
|
||||||
|
clip.isClip && clip.parentId === board.itemId && clip.tabId === currentTab
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.orderNumber - b.orderNumber)
|
||||||
|
|
||||||
|
if (boardsWithClips.length > 0) {
|
||||||
|
const firstBoard = boardsWithClips[0]
|
||||||
|
const firstBoardClips = clipItems
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.isClip &&
|
||||||
|
item.parentId === firstBoard.itemId &&
|
||||||
|
item.tabId === currentTab
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.orderNumber - b.orderNumber)
|
||||||
|
|
||||||
|
if (firstBoardClips.length > 0) {
|
||||||
|
keyboardSelectedBoardId.value = firstBoard.itemId
|
||||||
|
keyboardSelectedClipId.value = firstBoardClips[0].itemId
|
||||||
|
currentBoardIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['pageup'],
|
||||||
|
e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
currentNavigationContext.value === 'history' ||
|
||||||
|
currentNavigationContext.value === null
|
||||||
|
) {
|
||||||
|
// Move up by 5 items in history
|
||||||
|
const currentIndex = clipboardHistory.findIndex(
|
||||||
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
|
)
|
||||||
|
const newIndex = Math.max(0, currentIndex - 5)
|
||||||
|
if (clipboardHistory[newIndex]) {
|
||||||
|
keyboardSelectedItemId.value = clipboardHistory[newIndex].historyId
|
||||||
|
if (showLargeViewHistoryId.value) {
|
||||||
|
showLargeViewHistoryId.value = clipboardHistory[newIndex].historyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
currentNavigationContext.value === 'board' &&
|
||||||
|
keyboardSelectedBoardId.value
|
||||||
|
) {
|
||||||
|
// Move up by 5 clips in current board
|
||||||
|
const clipsOnBoard = clipItems
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.isClip &&
|
||||||
|
item.parentId === keyboardSelectedBoardId.value &&
|
||||||
|
item.tabId === currentTab
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.orderNumber - b.orderNumber)
|
||||||
|
|
||||||
|
const currentIndex = clipsOnBoard.findIndex(
|
||||||
|
clip => clip.itemId === keyboardSelectedClipId.value
|
||||||
|
)
|
||||||
|
const newIndex = Math.max(0, currentIndex - 5)
|
||||||
|
if (clipsOnBoard[newIndex]) {
|
||||||
|
keyboardSelectedClipId.value = clipsOnBoard[newIndex].itemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['pagedown'],
|
||||||
|
e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
currentNavigationContext.value === 'history' ||
|
||||||
|
currentNavigationContext.value === null
|
||||||
|
) {
|
||||||
|
// Move down by 5 items in history
|
||||||
|
const currentIndex = clipboardHistory.findIndex(
|
||||||
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
|
)
|
||||||
|
const newIndex = Math.min(clipboardHistory.length - 1, currentIndex + 5)
|
||||||
|
if (clipboardHistory[newIndex]) {
|
||||||
|
keyboardSelectedItemId.value = clipboardHistory[newIndex].historyId
|
||||||
|
if (showLargeViewHistoryId.value) {
|
||||||
|
showLargeViewHistoryId.value = clipboardHistory[newIndex].historyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
currentNavigationContext.value === 'board' &&
|
||||||
|
keyboardSelectedBoardId.value
|
||||||
|
) {
|
||||||
|
// Move down by 5 clips in current board
|
||||||
|
const clipsOnBoard = clipItems
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.isClip &&
|
||||||
|
item.parentId === keyboardSelectedBoardId.value &&
|
||||||
|
item.tabId === currentTab
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.orderNumber - b.orderNumber)
|
||||||
|
|
||||||
|
const currentIndex = clipsOnBoard.findIndex(
|
||||||
|
clip => clip.itemId === keyboardSelectedClipId.value
|
||||||
|
)
|
||||||
|
const newIndex = Math.min(clipsOnBoard.length - 1, currentIndex + 5)
|
||||||
|
if (clipsOnBoard[newIndex]) {
|
||||||
|
keyboardSelectedClipId.value = clipsOnBoard[newIndex].itemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['alt+arrowdown'],
|
||||||
|
e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
currentNavigationContext.value === 'history' ||
|
||||||
|
currentNavigationContext.value === null
|
||||||
|
) {
|
||||||
|
// Open context menu for selected history item
|
||||||
|
if (keyboardSelectedItemId.value) {
|
||||||
|
showKeyboardNavContextMenuHistoryId.value = keyboardSelectedItemId.value
|
||||||
|
}
|
||||||
|
} else if (currentNavigationContext.value === 'board') {
|
||||||
|
// Open context menu for selected clip item
|
||||||
|
if (keyboardSelectedClipId.value) {
|
||||||
|
showKeyboardNavContextMenuClipId.value = keyboardSelectedClipId.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !shouldKeyboardNavigationBeDisabled.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store timeout reference to clear it if needed
|
||||||
|
const deleteTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Control' || e.key === 'Meta') {
|
if (e.key === 'Control' || e.key === 'Meta') {
|
||||||
@ -777,6 +1091,11 @@ export default function ClipboardHistoryPage() {
|
|||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
// Clean up delete timeout on unmount
|
||||||
|
if (deleteTimeoutRef.current) {
|
||||||
|
clearTimeout(deleteTimeoutRef.current)
|
||||||
|
deleteTimeoutRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
clipboardHistory,
|
clipboardHistory,
|
||||||
@ -797,6 +1116,18 @@ export default function ClipboardHistoryPage() {
|
|||||||
}
|
}
|
||||||
}, [currentNavigationContext.value, keyboardSelectedItemId.value, clipboardHistory])
|
}, [currentNavigationContext.value, keyboardSelectedItemId.value, clipboardHistory])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (keyboardSelectedItemId.value && listRef.current) {
|
||||||
|
const selectedIndex = clipboardHistory.findIndex(
|
||||||
|
item => item.historyId === keyboardSelectedItemId.value
|
||||||
|
)
|
||||||
|
if (selectedIndex !== -1) {
|
||||||
|
// @ts-expect-error - scrollToItem is not in the types
|
||||||
|
listRef.current.scrollToItem?.(selectedIndex, 'smart')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [keyboardSelectedItemId.value, clipboardHistory])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listenToClipboardUnlisten = listen(
|
const listenToClipboardUnlisten = listen(
|
||||||
'clipboard://clipboard-monitor/update',
|
'clipboard://clipboard-monitor/update',
|
||||||
@ -1015,13 +1346,22 @@ export default function ClipboardHistoryPage() {
|
|||||||
|
|
||||||
const hasIsDeleting = (historyId: UniqueIdentifier) => {
|
const hasIsDeleting = (historyId: UniqueIdentifier) => {
|
||||||
return (
|
return (
|
||||||
(showConfirmation && selectedHistoryItems.includes(historyId)) ||
|
// Keyboard delete confirmation - only for the specific keyboard selected item
|
||||||
|
(showConfirmationKeyboardDelete &&
|
||||||
|
historyId === keyboardItemIdDelete &&
|
||||||
|
historyId === keyboardSelectedItemId.value) ||
|
||||||
|
// Mouse delete confirmation - only when keyboard delete is NOT active
|
||||||
|
(showConfirmation && !showConfirmationKeyboardDelete && selectedHistoryItems.includes(historyId)) ||
|
||||||
|
// Single item delete confirmation
|
||||||
historyId === showHistoryDeleteConfirmationId.value ||
|
historyId === showHistoryDeleteConfirmationId.value ||
|
||||||
(showConfirmation && historyId === hoveringHistoryIdDelete) ||
|
// Hovering delete confirmation - only when keyboard delete is NOT active
|
||||||
|
(showConfirmation && !showConfirmationKeyboardDelete && historyId === hoveringHistoryIdDelete) ||
|
||||||
|
// Drag over trash
|
||||||
historyId === dragOverTrashId ||
|
historyId === dragOverTrashId ||
|
||||||
(Boolean(dragOverTrashId) &&
|
(Boolean(dragOverTrashId) &&
|
||||||
Boolean(activeDragId) &&
|
Boolean(activeDragId) &&
|
||||||
selectedHistoryItems.includes(historyId)) ||
|
selectedHistoryItems.includes(historyId)) ||
|
||||||
|
// Menu deleting
|
||||||
(isMenuDeleting && selectedHistoryItems.includes(historyId))
|
(isMenuDeleting && selectedHistoryItems.includes(historyId))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1480,10 +1820,18 @@ export default function ClipboardHistoryPage() {
|
|||||||
pastingCountDown={
|
pastingCountDown={
|
||||||
historyId === pastedItemValue
|
historyId === pastedItemValue
|
||||||
? pastingCountDown
|
? pastingCountDown
|
||||||
|
: historyId === specialPastedItemValue
|
||||||
|
? specialPastingCountDown
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isPasted={historyId === pastedItemValue}
|
isPasted={
|
||||||
isCopied={historyId === copiedItemValue}
|
historyId === pastedItemValue ||
|
||||||
|
historyId === specialPastedItemValue
|
||||||
|
}
|
||||||
|
isCopied={
|
||||||
|
historyId === copiedItemValue ||
|
||||||
|
historyId === specialCopiedItemValue
|
||||||
|
}
|
||||||
isSaved={historyId === savingItem}
|
isSaved={historyId === savingItem}
|
||||||
setSavingItem={setSavingItem}
|
setSavingItem={setSavingItem}
|
||||||
isDeleting={hasIsDeleting(historyId)}
|
isDeleting={hasIsDeleting(historyId)}
|
||||||
@ -1523,7 +1871,9 @@ export default function ClipboardHistoryPage() {
|
|||||||
isSingleClickToCopyPaste={
|
isSingleClickToCopyPaste={
|
||||||
isSingleClickToCopyPaste
|
isSingleClickToCopyPaste
|
||||||
}
|
}
|
||||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
historyPreviewLineLimit={
|
||||||
|
historyPreviewLineLimit
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@ -1887,6 +2237,7 @@ export default function ClipboardHistoryPage() {
|
|||||||
className="pointer-events-auto rounded-full bg-slate-300 dark:bg-slate-600 hover:bg-slate-200 hover:dark:bg-slate-700"
|
className="pointer-events-auto rounded-full bg-slate-300 dark:bg-slate-600 hover:bg-slate-200 hover:dark:bg-slate-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scrollToTopHistoryList(true)
|
scrollToTopHistoryList(true)
|
||||||
|
resetKeyboardNavigation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="text-mute text-xs text-center px-3">
|
<Text className="text-mute text-xs text-center px-3">
|
||||||
@ -2090,9 +2441,14 @@ export default function ClipboardHistoryPage() {
|
|||||||
pastingCountDown={
|
pastingCountDown={
|
||||||
historyId === pastedItemValue
|
historyId === pastedItemValue
|
||||||
? pastingCountDown
|
? pastingCountDown
|
||||||
|
: historyId === specialPastedItemValue
|
||||||
|
? specialPastingCountDown
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isPasted={historyId === pastedItemValue}
|
isPasted={
|
||||||
|
historyId === pastedItemValue ||
|
||||||
|
historyId === specialPastedItemValue
|
||||||
|
}
|
||||||
isKeyboardSelected={
|
isKeyboardSelected={
|
||||||
(currentNavigationContext.value ===
|
(currentNavigationContext.value ===
|
||||||
'history' ||
|
'history' ||
|
||||||
@ -2100,7 +2456,10 @@ export default function ClipboardHistoryPage() {
|
|||||||
null) &&
|
null) &&
|
||||||
historyId === keyboardSelectedItemId.value
|
historyId === keyboardSelectedItemId.value
|
||||||
}
|
}
|
||||||
isCopied={historyId === copiedItemValue}
|
isCopied={
|
||||||
|
historyId === copiedItemValue ||
|
||||||
|
historyId === specialCopiedItemValue
|
||||||
|
}
|
||||||
isSaved={historyId === savingItem}
|
isSaved={historyId === savingItem}
|
||||||
setSavingItem={setSavingItem}
|
setSavingItem={setSavingItem}
|
||||||
key={historyId}
|
key={historyId}
|
||||||
@ -2150,7 +2509,9 @@ export default function ClipboardHistoryPage() {
|
|||||||
isSingleClickToCopyPaste={
|
isSingleClickToCopyPaste={
|
||||||
isSingleClickToCopyPaste
|
isSingleClickToCopyPaste
|
||||||
}
|
}
|
||||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
historyPreviewLineLimit={
|
||||||
|
historyPreviewLineLimit
|
||||||
|
}
|
||||||
index={index}
|
index={index}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
@ -2443,10 +2804,19 @@ export default function ClipboardHistoryPage() {
|
|||||||
pastingCountDown={
|
pastingCountDown={
|
||||||
inLargeViewItem.historyId === pastedItemValue
|
inLargeViewItem.historyId === pastedItemValue
|
||||||
? pastingCountDown
|
? pastingCountDown
|
||||||
|
: inLargeViewItem.historyId ===
|
||||||
|
specialPastedItemValue
|
||||||
|
? specialPastingCountDown
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
isPasted={inLargeViewItem.historyId === pastedItemValue}
|
isPasted={
|
||||||
isCopied={inLargeViewItem.historyId === copiedItemValue}
|
inLargeViewItem.historyId === pastedItemValue ||
|
||||||
|
inLargeViewItem.historyId === specialPastedItemValue
|
||||||
|
}
|
||||||
|
isCopied={
|
||||||
|
inLargeViewItem.historyId === copiedItemValue ||
|
||||||
|
inLargeViewItem.historyId === specialCopiedItemValue
|
||||||
|
}
|
||||||
isSaved={inLargeViewItem.historyId === savingItem}
|
isSaved={inLargeViewItem.historyId === savingItem}
|
||||||
isMp3={
|
isMp3={
|
||||||
inLargeViewItem.isLink &&
|
inLargeViewItem.isLink &&
|
||||||
|
@ -7,13 +7,19 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { settingsStoreAtom, uiStoreAtom } from '~/store'
|
import {
|
||||||
|
DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
|
DEFAULT_SPECIAL_PASTE_OPERATIONS,
|
||||||
|
settingsStoreAtom,
|
||||||
|
uiStoreAtom,
|
||||||
|
} from '~/store'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { Grip } from 'lucide-react'
|
import { ChevronDown, Grip } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import AutoSize from 'react-virtualized-auto-sizer'
|
import AutoSize from 'react-virtualized-auto-sizer'
|
||||||
|
|
||||||
|
import { TEXT_TRANSFORMS, TRANSFORM_CATEGORIES } from '~/lib/text-transforms'
|
||||||
import {
|
import {
|
||||||
arraysEqual,
|
arraysEqual,
|
||||||
isStringArrayEmpty,
|
isStringArrayEmpty,
|
||||||
@ -25,6 +31,7 @@ import Spacer from '~/components/atoms/spacer'
|
|||||||
import SimpleBar from '~/components/libs/simplebar-react'
|
import SimpleBar from '~/components/libs/simplebar-react'
|
||||||
import InputField from '~/components/molecules/input'
|
import InputField from '~/components/molecules/input'
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@ -32,6 +39,12 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CheckBoxFilter,
|
CheckBoxFilter,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
Flex,
|
Flex,
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -79,6 +92,224 @@ function SortableItem({ id, language }: SortableItemProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SortableCategoryItemProps {
|
||||||
|
categoryId: string
|
||||||
|
localCategoriesOrder: string[]
|
||||||
|
setLocalCategoriesOrder: (categories: string[]) => void
|
||||||
|
}
|
||||||
|
function SortableCategoryItem({
|
||||||
|
categoryId,
|
||||||
|
localCategoriesOrder,
|
||||||
|
setLocalCategoriesOrder,
|
||||||
|
}: SortableCategoryItemProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||||
|
useSortable({ id: categoryId })
|
||||||
|
|
||||||
|
const {
|
||||||
|
enabledSpecialPasteOperations,
|
||||||
|
setEnabledSpecialPasteOperations,
|
||||||
|
setSpecialPasteCategoriesOrder,
|
||||||
|
} = useAtomValue(settingsStoreAtom)
|
||||||
|
|
||||||
|
const category = TRANSFORM_CATEGORIES.find(c => c.id === categoryId)
|
||||||
|
if (!category) return null
|
||||||
|
|
||||||
|
const isCategoryEnabled = localCategoriesOrder.includes(category.id)
|
||||||
|
|
||||||
|
// Get all transforms in category (including from subcategories)
|
||||||
|
const allTransformsInCategory = category.subcategories
|
||||||
|
? category.subcategories.flatMap(subcategory => subcategory.transforms)
|
||||||
|
: category.transforms || []
|
||||||
|
|
||||||
|
const enabledTransformsInCategory = allTransformsInCategory.filter(transform =>
|
||||||
|
enabledSpecialPasteOperations.includes(transform.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
key={category.id}
|
||||||
|
{...attributes}
|
||||||
|
className={
|
||||||
|
isDragging
|
||||||
|
? 'z-100 opacity-70 bg-slate-50 dark:bg-slate-900'
|
||||||
|
: 'z-auto opacity-100'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className={`border rounded-lg p-4 ${
|
||||||
|
!isCategoryEnabled ? 'opacity-60 bg-gray-50 dark:bg-gray-900/50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Category Header */}
|
||||||
|
<Flex className="items-center justify-between">
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`opacity-40 hover:opacity-90 p-1 ${
|
||||||
|
isCategoryEnabled
|
||||||
|
? 'cursor-grab active:cursor-grabbing'
|
||||||
|
: 'cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
{...(isCategoryEnabled ? listeners : {})}
|
||||||
|
disabled={!isCategoryEnabled}
|
||||||
|
>
|
||||||
|
<Grip size={18} />
|
||||||
|
</Button>
|
||||||
|
<Text className="text-[14px] font-semibold">
|
||||||
|
{t(category.label, { ns: 'specialCopyPaste' })}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{enabledTransformsInCategory.length}/{allTransformsInCategory.length}
|
||||||
|
</Badge>
|
||||||
|
<Switch
|
||||||
|
className="scale-[.95]"
|
||||||
|
checked={isCategoryEnabled}
|
||||||
|
onCheckedChange={checked => {
|
||||||
|
if (checked) {
|
||||||
|
const newLocalOrder = localCategoriesOrder.includes(category.id)
|
||||||
|
? localCategoriesOrder
|
||||||
|
: [...localCategoriesOrder, category.id]
|
||||||
|
setLocalCategoriesOrder(newLocalOrder)
|
||||||
|
setSpecialPasteCategoriesOrder(newLocalOrder)
|
||||||
|
|
||||||
|
// Enable all transforms in the category (including from subcategories)
|
||||||
|
const allTransformIds = allTransformsInCategory.map(t => t.id)
|
||||||
|
const newOps = [
|
||||||
|
...new Set([...enabledSpecialPasteOperations, ...allTransformIds]),
|
||||||
|
]
|
||||||
|
setEnabledSpecialPasteOperations(newOps)
|
||||||
|
} else {
|
||||||
|
const newLocalOrder = localCategoriesOrder.filter(
|
||||||
|
id => id !== category.id
|
||||||
|
)
|
||||||
|
setLocalCategoriesOrder(newLocalOrder)
|
||||||
|
setSpecialPasteCategoriesOrder(newLocalOrder)
|
||||||
|
|
||||||
|
const transformIds = allTransformsInCategory.map(t => t.id)
|
||||||
|
const newOps = enabledSpecialPasteOperations.filter(
|
||||||
|
op => !transformIds.includes(op)
|
||||||
|
)
|
||||||
|
setEnabledSpecialPasteOperations(newOps)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Individual Transform Controls */}
|
||||||
|
{isCategoryEnabled && (
|
||||||
|
<Box className="mt-3">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-full justify-between">
|
||||||
|
{t('Select Operations', { ns: 'specialCopyPaste' })}
|
||||||
|
<ChevronDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width]">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{t(category.label, { ns: 'specialCopyPaste' })}{' '}
|
||||||
|
{t('Operations', { ns: 'specialCopyPaste' })}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<SimpleBar
|
||||||
|
className="code-filter"
|
||||||
|
style={{
|
||||||
|
height: 'auto',
|
||||||
|
maxHeight: '400px',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
}}
|
||||||
|
autoHide={false}
|
||||||
|
>
|
||||||
|
{category.subcategories
|
||||||
|
? // Handle categories with subcategories (like Format Converter)
|
||||||
|
category.subcategories.map(subcategory => (
|
||||||
|
<div key={subcategory.id}>
|
||||||
|
<DropdownMenuLabel className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t(subcategory.label, { ns: 'specialCopyPaste' })}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{subcategory.transforms.map(transform => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={transform.id}
|
||||||
|
checked={enabledSpecialPasteOperations.includes(
|
||||||
|
transform.id
|
||||||
|
)}
|
||||||
|
onSelect={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
onCheckedChange={checked => {
|
||||||
|
if (checked) {
|
||||||
|
setEnabledSpecialPasteOperations([
|
||||||
|
...enabledSpecialPasteOperations,
|
||||||
|
transform.id,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
setEnabledSpecialPasteOperations(
|
||||||
|
enabledSpecialPasteOperations.filter(
|
||||||
|
op => op !== transform.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{t(transform.label, { ns: 'specialCopyPaste' })}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
{category.subcategories &&
|
||||||
|
subcategory !==
|
||||||
|
category.subcategories[
|
||||||
|
category.subcategories.length - 1
|
||||||
|
] && <DropdownMenuSeparator />}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: // Handle categories with direct transforms
|
||||||
|
(category.transforms || []).map(transform => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={transform.id}
|
||||||
|
checked={enabledSpecialPasteOperations.includes(transform.id)}
|
||||||
|
onSelect={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
onCheckedChange={checked => {
|
||||||
|
if (checked) {
|
||||||
|
setEnabledSpecialPasteOperations([
|
||||||
|
...enabledSpecialPasteOperations,
|
||||||
|
transform.id,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
setEnabledSpecialPasteOperations(
|
||||||
|
enabledSpecialPasteOperations.filter(
|
||||||
|
op => op !== transform.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(transform.label, { ns: 'specialCopyPaste' })}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</SimpleBar>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ClipboardHistorySettings() {
|
export default function ClipboardHistorySettings() {
|
||||||
const {
|
const {
|
||||||
isHistoryEnabled,
|
isHistoryEnabled,
|
||||||
@ -131,6 +362,12 @@ export default function ClipboardHistorySettings() {
|
|||||||
setIsKeepPinnedOnClearEnabled,
|
setIsKeepPinnedOnClearEnabled,
|
||||||
isKeepStarredOnClearEnabled,
|
isKeepStarredOnClearEnabled,
|
||||||
setIsKeepStarredOnClearEnabled,
|
setIsKeepStarredOnClearEnabled,
|
||||||
|
isSpecialCopyPasteHistoryEnabled,
|
||||||
|
setIsSpecialCopyPasteHistoryEnabled,
|
||||||
|
enabledSpecialPasteOperations,
|
||||||
|
setEnabledSpecialPasteOperations,
|
||||||
|
specialPasteCategoriesOrder,
|
||||||
|
setSpecialPasteCategoriesOrder,
|
||||||
isAppReady,
|
isAppReady,
|
||||||
CONST: { APP_DETECT_LANGUAGES_SUPPORTED: languageList },
|
CONST: { APP_DETECT_LANGUAGES_SUPPORTED: languageList },
|
||||||
} = useAtomValue(settingsStoreAtom)
|
} = useAtomValue(settingsStoreAtom)
|
||||||
@ -138,6 +375,19 @@ export default function ClipboardHistorySettings() {
|
|||||||
const { returnRoute } = useAtomValue(uiStoreAtom)
|
const { returnRoute } = useAtomValue(uiStoreAtom)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = window.location.hash.substring(1)
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const releventDiv = document.getElementById(id)
|
||||||
|
releventDiv?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}, 400)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [exclusionListValue, setExclusionListValue] = useState('')
|
const [exclusionListValue, setExclusionListValue] = useState('')
|
||||||
const [exclusionAppListValue, setExclusionAppListValue] = useState('')
|
const [exclusionAppListValue, setExclusionAppListValue] = useState('')
|
||||||
const [autoMaskListValue, setAutoMaskListValue] = useState('')
|
const [autoMaskListValue, setAutoMaskListValue] = useState('')
|
||||||
@ -149,6 +399,7 @@ export default function ClipboardHistorySettings() {
|
|||||||
const debouncedAutoMaskListValue = useDebounce(autoMaskListValue, 300)
|
const debouncedAutoMaskListValue = useDebounce(autoMaskListValue, 300)
|
||||||
|
|
||||||
const [prioritizedLanguages, setPrioritizedLanguages] = useState<string[]>([])
|
const [prioritizedLanguages, setPrioritizedLanguages] = useState<string[]>([])
|
||||||
|
const [localCategoriesOrder, setLocalCategoriesOrder] = useState<string[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -161,6 +412,45 @@ export default function ClipboardHistorySettings() {
|
|||||||
}
|
}
|
||||||
}, [historyDetectLanguagesEnabledList, historyDetectLanguagesPrioritizedList])
|
}, [historyDetectLanguagesEnabledList, historyDetectLanguagesPrioritizedList])
|
||||||
|
|
||||||
|
// Initialize local categories order from store
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
Array.isArray(specialPasteCategoriesOrder) &&
|
||||||
|
specialPasteCategoriesOrder.length > 0
|
||||||
|
) {
|
||||||
|
setLocalCategoriesOrder(specialPasteCategoriesOrder)
|
||||||
|
} else {
|
||||||
|
setLocalCategoriesOrder([...DEFAULT_SPECIAL_PASTE_CATEGORIES])
|
||||||
|
}
|
||||||
|
}, [specialPasteCategoriesOrder])
|
||||||
|
|
||||||
|
// Show all categories, ordered by user preference with enabled ones first
|
||||||
|
const orderedCategories = (() => {
|
||||||
|
const enabled =
|
||||||
|
localCategoriesOrder.length > 0
|
||||||
|
? localCategoriesOrder
|
||||||
|
: [...DEFAULT_SPECIAL_PASTE_CATEGORIES]
|
||||||
|
|
||||||
|
// Get all categories that exist but aren't in the enabled list
|
||||||
|
const allCategoryIds = [...DEFAULT_SPECIAL_PASTE_CATEGORIES]
|
||||||
|
const disabled = allCategoryIds.filter(id => !enabled.includes(id))
|
||||||
|
|
||||||
|
// Return enabled categories first, then disabled ones
|
||||||
|
return [...enabled, ...disabled]
|
||||||
|
})()
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Component render - specialPasteCategoriesOrder:',
|
||||||
|
specialPasteCategoriesOrder,
|
||||||
|
typeof specialPasteCategoriesOrder,
|
||||||
|
Array.isArray(specialPasteCategoriesOrder)
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'Component render - orderedCategories:',
|
||||||
|
orderedCategories,
|
||||||
|
Array.isArray(orderedCategories)
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAppReady) {
|
if (isAppReady) {
|
||||||
setHistoryExclusionList(trimAndRemoveExtraNewlines(debouncedExclusionListValue))
|
setHistoryExclusionList(trimAndRemoveExtraNewlines(debouncedExclusionListValue))
|
||||||
@ -627,7 +917,7 @@ export default function ClipboardHistorySettings() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="max-w-xl animate-in fade-in mt-4">
|
<Box className="mt-4 max-w-xl animate-in fade-in">
|
||||||
<Card
|
<Card
|
||||||
className={`${
|
className={`${
|
||||||
!isExclusionAppListEnabled &&
|
!isExclusionAppListEnabled &&
|
||||||
@ -1129,6 +1419,156 @@ export default function ClipboardHistorySettings() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="mt-4 max-w-xl animate-in fade-in"
|
||||||
|
id="specialCopyPasteHistory"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`${
|
||||||
|
!isSpecialCopyPasteHistoryEnabled &&
|
||||||
|
'opacity-80 bg-gray-100 dark:bg-gray-900/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||||
|
<CardTitle className="animate-in fade-in text-md font-medium w-full">
|
||||||
|
{t('Special Copy/Paste Operations', { ns: 'specialCopyPaste' })}
|
||||||
|
</CardTitle>
|
||||||
|
<Switch
|
||||||
|
checked={isSpecialCopyPasteHistoryEnabled}
|
||||||
|
className="ml-auto"
|
||||||
|
onCheckedChange={() => {
|
||||||
|
setIsSpecialCopyPasteHistoryEnabled(
|
||||||
|
!isSpecialCopyPasteHistoryEnabled
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Text className="text-sm text-muted-foreground mb-2">
|
||||||
|
{t(
|
||||||
|
'Enable special text transformation options for clipboard history items',
|
||||||
|
{ ns: 'specialCopyPaste' }
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isSpecialCopyPasteHistoryEnabled && (
|
||||||
|
<>
|
||||||
|
<Text className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t(
|
||||||
|
'Drag and drop category to prioritize its order in the special copy/paste menu.',
|
||||||
|
{ ns: 'specialCopyPaste' }
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={event => {
|
||||||
|
const { active, over } = event
|
||||||
|
if (over?.id && active.id !== over?.id) {
|
||||||
|
setLocalCategoriesOrder(items => {
|
||||||
|
const activeId = active.id.toString()
|
||||||
|
const overId = over.id.toString()
|
||||||
|
|
||||||
|
// Check if both active and over items are in the enabled list
|
||||||
|
if (
|
||||||
|
items.includes(activeId) &&
|
||||||
|
items.includes(overId)
|
||||||
|
) {
|
||||||
|
const oldIndex = items.indexOf(activeId)
|
||||||
|
const newIndex = items.indexOf(overId)
|
||||||
|
const newArray = arrayMove(items, oldIndex, newIndex)
|
||||||
|
|
||||||
|
// Update the store if array changed
|
||||||
|
if (!arraysEqual(items, newArray)) {
|
||||||
|
setSpecialPasteCategoriesOrder(newArray)
|
||||||
|
}
|
||||||
|
return newArray
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={localCategoriesOrder}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<Box className="space-y-4">
|
||||||
|
{orderedCategories
|
||||||
|
.map(categoryId =>
|
||||||
|
TRANSFORM_CATEGORIES.find(c => c.id === categoryId)
|
||||||
|
)
|
||||||
|
.filter(category => category)
|
||||||
|
.map(category => {
|
||||||
|
if (!category) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableCategoryItem
|
||||||
|
key={category.id}
|
||||||
|
categoryId={category.id}
|
||||||
|
localCategoriesOrder={localCategoriesOrder}
|
||||||
|
setLocalCategoriesOrder={setLocalCategoriesOrder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<Box className="mt-4 pt-4">
|
||||||
|
<Text className="text-sm font-medium mb-2">
|
||||||
|
{t('Enabled Operations', { ns: 'specialCopyPaste' })} (
|
||||||
|
{enabledSpecialPasteOperations.length}):
|
||||||
|
</Text>
|
||||||
|
{enabledSpecialPasteOperations.length > 0 ? (
|
||||||
|
<Flex className="flex-wrap gap-1 justify-start">
|
||||||
|
{enabledSpecialPasteOperations.map(opId => {
|
||||||
|
const transform = TEXT_TRANSFORMS.find(
|
||||||
|
t => t.id === opId
|
||||||
|
)
|
||||||
|
return transform ? (
|
||||||
|
<Badge
|
||||||
|
key={opId}
|
||||||
|
variant="graySecondary"
|
||||||
|
className="font-normal text-xs"
|
||||||
|
>
|
||||||
|
{t(transform.label, { ns: 'specialCopyPaste' })}
|
||||||
|
</Badge>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Text className="text-sm text-muted-foreground">
|
||||||
|
{t('None', { ns: 'specialCopyPaste' })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{/* Reset Button */}
|
||||||
|
<Box className="mt-6">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// Enable all categories and operations using constants
|
||||||
|
const defaultCategories = [
|
||||||
|
...DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
|
]
|
||||||
|
setLocalCategoriesOrder(defaultCategories)
|
||||||
|
setSpecialPasteCategoriesOrder(defaultCategories)
|
||||||
|
setEnabledSpecialPasteOperations([
|
||||||
|
...DEFAULT_SPECIAL_PASTE_OPERATIONS,
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Enable All', { ns: 'specialCopyPaste' })}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
<Link to={returnRoute} replace>
|
<Link to={returnRoute} replace>
|
||||||
|
@ -61,6 +61,77 @@ export const SCREEN_AUTO_LOCK_TIMES_IN_MINUTES = [5, 10, 15, 20, 30, 45, 60]
|
|||||||
export const RESET_TIME_DELAY_SECONDS = 60
|
export const RESET_TIME_DELAY_SECONDS = 60
|
||||||
export const APP_NAME = 'PasteBar'
|
export const APP_NAME = 'PasteBar'
|
||||||
|
|
||||||
|
// Default special copy/paste settings
|
||||||
|
export const DEFAULT_SPECIAL_PASTE_OPERATIONS = [
|
||||||
|
// Text Case
|
||||||
|
'upperCase',
|
||||||
|
'lowerCase',
|
||||||
|
'titleCase',
|
||||||
|
'sentenceCase',
|
||||||
|
'invertCase',
|
||||||
|
// Code Formatting
|
||||||
|
'camelCase',
|
||||||
|
'snakeCase',
|
||||||
|
'kebabCase',
|
||||||
|
'pascalCase',
|
||||||
|
'jsonStringify',
|
||||||
|
// Whitespace & Lines
|
||||||
|
'trimWhiteSpace',
|
||||||
|
'removeLineFeeds',
|
||||||
|
'addOneLineFeed',
|
||||||
|
'addTwoLineFeeds',
|
||||||
|
'removeExtraSpaces',
|
||||||
|
'sortLinesAlphabetically',
|
||||||
|
'removeDuplicateLines',
|
||||||
|
'addLineNumbers',
|
||||||
|
// Encode/Decode
|
||||||
|
'base64Encode',
|
||||||
|
'base64Decode',
|
||||||
|
'urlEncode',
|
||||||
|
'urlDecode',
|
||||||
|
'htmlEncode',
|
||||||
|
'htmlDecode',
|
||||||
|
// Text Tools
|
||||||
|
'reverseText',
|
||||||
|
'addCurrentDateTime',
|
||||||
|
'countCharacters',
|
||||||
|
'countWords',
|
||||||
|
'countLines',
|
||||||
|
'countSentences',
|
||||||
|
// Format Converter - HTML
|
||||||
|
'htmlToMarkdown',
|
||||||
|
'htmlToReact',
|
||||||
|
'htmlToReactComponent',
|
||||||
|
'htmlToText',
|
||||||
|
// Format Converter - Markdown
|
||||||
|
'markdownToHtml',
|
||||||
|
'markdownToText',
|
||||||
|
// Format Converter - JSON
|
||||||
|
'jsonToCsv',
|
||||||
|
'jsonToYaml',
|
||||||
|
'jsonToXml',
|
||||||
|
'jsonToToml',
|
||||||
|
'jsonToTable',
|
||||||
|
// Format Converter - CSV
|
||||||
|
'csvToJson',
|
||||||
|
'csvToTable',
|
||||||
|
// Format Converter - YAML
|
||||||
|
'yamlToJson',
|
||||||
|
// Format Converter - XML
|
||||||
|
'xmlToJson',
|
||||||
|
// Format Converter - TOML
|
||||||
|
'tomlToJson',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const DEFAULT_SPECIAL_PASTE_CATEGORIES = [
|
||||||
|
'textCase',
|
||||||
|
'codeFormatting',
|
||||||
|
'whitespaceLines',
|
||||||
|
'encodingSecurity',
|
||||||
|
'textTools',
|
||||||
|
'formatConverter',
|
||||||
|
] as const
|
||||||
|
|
||||||
window['PasteBar'] = {
|
window['PasteBar'] = {
|
||||||
APP_UI_VERSION: APP_UI_VERSION,
|
APP_UI_VERSION: APP_UI_VERSION,
|
||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
|
@ -13,6 +13,10 @@ import { atomWithStore } from 'jotai-zustand'
|
|||||||
import { createStore } from 'zustand/vanilla'
|
import { createStore } from 'zustand/vanilla'
|
||||||
|
|
||||||
import DOMPurify from '../components/libs/dompurify'
|
import DOMPurify from '../components/libs/dompurify'
|
||||||
|
import {
|
||||||
|
DEFAULT_SPECIAL_PASTE_CATEGORIES,
|
||||||
|
DEFAULT_SPECIAL_PASTE_OPERATIONS,
|
||||||
|
} from './constants'
|
||||||
import {
|
import {
|
||||||
availableVersionBody,
|
availableVersionBody,
|
||||||
availableVersionDate,
|
availableVersionDate,
|
||||||
@ -111,6 +115,9 @@ type Settings = {
|
|||||||
isDoubleClickTrayToOpenEnabledOnWindows: boolean
|
isDoubleClickTrayToOpenEnabledOnWindows: boolean
|
||||||
isLeftClickTrayToOpenEnabledOnWindows: boolean
|
isLeftClickTrayToOpenEnabledOnWindows: boolean
|
||||||
isLeftClickTrayDisabledOnWindows: boolean
|
isLeftClickTrayDisabledOnWindows: boolean
|
||||||
|
isSpecialCopyPasteHistoryEnabled: boolean
|
||||||
|
enabledSpecialPasteOperations: string[]
|
||||||
|
specialPasteCategoriesOrder: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Constants = {
|
type Constants = {
|
||||||
@ -235,6 +242,9 @@ export interface SettingsStoreState {
|
|||||||
deleteGlobalTemplate: (templateId: string) => void
|
deleteGlobalTemplate: (templateId: string) => void
|
||||||
toggleGlobalTemplateEnabledState: (templateId: string) => void
|
toggleGlobalTemplateEnabledState: (templateId: string) => void
|
||||||
setIsDoubleClickTrayToOpenEnabledOnWindows: (isEnabled: boolean) => void
|
setIsDoubleClickTrayToOpenEnabledOnWindows: (isEnabled: boolean) => void
|
||||||
|
setIsSpecialCopyPasteHistoryEnabled: (isEnabled: boolean) => void
|
||||||
|
setEnabledSpecialPasteOperations: (operations: string[]) => void
|
||||||
|
setSpecialPasteCategoriesOrder: (categories: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsStoreState & Settings = {
|
const initialState: SettingsStoreState & Settings = {
|
||||||
@ -322,9 +332,15 @@ const initialState: SettingsStoreState & Settings = {
|
|||||||
isDoubleClickTrayToOpenEnabledOnWindows: false,
|
isDoubleClickTrayToOpenEnabledOnWindows: false,
|
||||||
isLeftClickTrayToOpenEnabledOnWindows: false,
|
isLeftClickTrayToOpenEnabledOnWindows: false,
|
||||||
isLeftClickTrayDisabledOnWindows: false,
|
isLeftClickTrayDisabledOnWindows: false,
|
||||||
|
isSpecialCopyPasteHistoryEnabled: true,
|
||||||
|
enabledSpecialPasteOperations: [...DEFAULT_SPECIAL_PASTE_OPERATIONS],
|
||||||
|
specialPasteCategoriesOrder: [...DEFAULT_SPECIAL_PASTE_CATEGORIES],
|
||||||
setIsDoubleClickTrayToOpenEnabledOnWindows: () => {},
|
setIsDoubleClickTrayToOpenEnabledOnWindows: () => {},
|
||||||
setIsLeftClickTrayToOpenEnabledOnWindows: () => {},
|
setIsLeftClickTrayToOpenEnabledOnWindows: () => {},
|
||||||
setIsLeftClickTrayDisabledOnWindows: () => {},
|
setIsLeftClickTrayDisabledOnWindows: () => {},
|
||||||
|
setIsSpecialCopyPasteHistoryEnabled: () => {},
|
||||||
|
setEnabledSpecialPasteOperations: () => {},
|
||||||
|
setSpecialPasteCategoriesOrder: () => {},
|
||||||
setHasPinProtectedCollections: async () => {},
|
setHasPinProtectedCollections: async () => {},
|
||||||
CONST: {
|
CONST: {
|
||||||
APP_DETECT_LANGUAGES_SUPPORTED: [],
|
APP_DETECT_LANGUAGES_SUPPORTED: [],
|
||||||
@ -524,6 +540,18 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'enabledSpecialPasteOperations' && typeof value === 'string') {
|
||||||
|
return set(() => ({
|
||||||
|
enabledSpecialPasteOperations: value.split(',').filter(Boolean),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'specialPasteCategoriesOrder' && typeof value === 'string') {
|
||||||
|
return set(() => ({
|
||||||
|
specialPasteCategoriesOrder: value.split(',').filter(Boolean),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
return set(() => ({ [name]: value }))
|
return set(() => ({ [name]: value }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@ -882,6 +910,15 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
|||||||
setIsDoubleClickTrayToOpenEnabledOnWindows: async (isEnabled: boolean) => {
|
setIsDoubleClickTrayToOpenEnabledOnWindows: async (isEnabled: boolean) => {
|
||||||
return get().updateSetting('isDoubleClickTrayToOpenEnabledOnWindows', isEnabled)
|
return get().updateSetting('isDoubleClickTrayToOpenEnabledOnWindows', isEnabled)
|
||||||
},
|
},
|
||||||
|
setIsSpecialCopyPasteHistoryEnabled: async (isEnabled: boolean) => {
|
||||||
|
return get().updateSetting('isSpecialCopyPasteHistoryEnabled', isEnabled)
|
||||||
|
},
|
||||||
|
setEnabledSpecialPasteOperations: async (operations: string[]) => {
|
||||||
|
return get().updateSetting('enabledSpecialPasteOperations', operations.join(','))
|
||||||
|
},
|
||||||
|
setSpecialPasteCategoriesOrder: async (categories: string[]) => {
|
||||||
|
return get().updateSetting('specialPasteCategoriesOrder', categories.join(','))
|
||||||
|
},
|
||||||
setIsLeftClickTrayToOpenEnabledOnWindows: async (isEnabled: boolean) => {
|
setIsLeftClickTrayToOpenEnabledOnWindows: async (isEnabled: boolean) => {
|
||||||
const result = await get().updateSetting(
|
const result = await get().updateSetting(
|
||||||
'isLeftClickTrayToOpenEnabledOnWindows',
|
'isLeftClickTrayToOpenEnabledOnWindows',
|
||||||
|
@ -37,8 +37,12 @@ export const resetTimeModalInterval = signal<NodeJS.Timeout | null>(null)
|
|||||||
|
|
||||||
// Clipboard History Signals
|
// Clipboard History Signals
|
||||||
export const showHistoryDeleteConfirmationId = signal<UniqueIdentifier | null>(null)
|
export const showHistoryDeleteConfirmationId = signal<UniqueIdentifier | null>(null)
|
||||||
|
export const showHistoryMultiDeleteConfirmationIds = signal<UniqueIdentifier[] | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
export const hoveringHistoryRowId = signal<UniqueIdentifier | null>(null)
|
export const hoveringHistoryRowId = signal<UniqueIdentifier | null>(null)
|
||||||
export const showLargeViewHistoryId = signal<UniqueIdentifier | null>(null)
|
export const showLargeViewHistoryId = signal<UniqueIdentifier | null>(null)
|
||||||
|
export const showKeyboardNavContextMenuHistoryId = signal<UniqueIdentifier | null>(null)
|
||||||
export const isHistoryCopyPasting = signal(false)
|
export const isHistoryCopyPasting = signal(false)
|
||||||
|
|
||||||
// Tabs Dashboard Signals
|
// Tabs Dashboard Signals
|
||||||
@ -55,6 +59,7 @@ export const forceSaveClipNameEditingError = signal(false)
|
|||||||
export const hoveringClipIdBoardId = signal<string | null>(null)
|
export const hoveringClipIdBoardId = signal<string | null>(null)
|
||||||
export const showDeleteClipConfirmationId = signal<UniqueIdentifier | null>(null)
|
export const showDeleteClipConfirmationId = signal<UniqueIdentifier | null>(null)
|
||||||
export const contextMenuClipId = signal<UniqueIdentifier | null>(null)
|
export const contextMenuClipId = signal<UniqueIdentifier | null>(null)
|
||||||
|
export const showKeyboardNavContextMenuClipId = signal<UniqueIdentifier | null>(null)
|
||||||
export const showDeleteImageClipConfirmationId = signal<UniqueIdentifier | null>(null)
|
export const showDeleteImageClipConfirmationId = signal<UniqueIdentifier | null>(null)
|
||||||
export const isDeletingSelectedClips = signal(false)
|
export const isDeletingSelectedClips = signal(false)
|
||||||
export const addSelectedTextToClipBoard = signal<string | null>(null)
|
export const addSelectedTextToClipBoard = signal<string | null>(null)
|
||||||
@ -145,7 +150,7 @@ export function resetMenuCreateOrEdit() {
|
|||||||
export function resetKeyboardNavigation() {
|
export function resetKeyboardNavigation() {
|
||||||
currentNavigationContext.value = null
|
currentNavigationContext.value = null
|
||||||
keyboardSelectedItemId.value = null
|
keyboardSelectedItemId.value = null
|
||||||
hoveringHistoryRowId.value = null
|
// hoveringHistoryRowId.value = null
|
||||||
keyboardSelectedBoardId.value = null
|
keyboardSelectedBoardId.value = null
|
||||||
keyboardSelectedClipId.value = null
|
keyboardSelectedClipId.value = null
|
||||||
currentBoardIndex.value = 0
|
currentBoardIndex.value = 0
|
||||||
@ -243,7 +248,10 @@ effect(() => {
|
|||||||
newMenuItemId.value ||
|
newMenuItemId.value ||
|
||||||
addSelectedTextToMenu.value ||
|
addSelectedTextToMenu.value ||
|
||||||
// Text selection states
|
// Text selection states
|
||||||
addSelectedTextToClipBoard.value
|
addSelectedTextToClipBoard.value ||
|
||||||
|
// Context menu states
|
||||||
|
showKeyboardNavContextMenuHistoryId.value ||
|
||||||
|
showKeyboardNavContextMenuClipId.value
|
||||||
) {
|
) {
|
||||||
// console.log('Disabling keyboard navigation due to edit or delete actions')
|
// console.log('Disabling keyboard navigation due to edit or delete actions')
|
||||||
// Disable keyboard navigation when editing
|
// Disable keyboard navigation when editing
|
||||||
|
160
src-tauri/Cargo.lock
generated
160
src-tauri/Cargo.lock
generated
@ -979,6 +979,12 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@ -1174,6 +1180,27 @@ dependencies = [
|
|||||||
"syn 2.0.102",
|
"syn 2.0.102",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
|
||||||
|
dependencies = [
|
||||||
|
"csv-core",
|
||||||
|
"itoa 1.0.15",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -1252,7 +1279,7 @@ version = "0.99.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case 0.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
@ -2035,7 +2062,7 @@ version = "0.2.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
|
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-width",
|
"unicode-width 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2370,6 +2397,43 @@ dependencies = [
|
|||||||
"utf8-width",
|
"utf8-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html-to-react"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "962bea78a5dac58a7e0f08853264d4e96aa0c2e562bb71436240a661631445b4"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case 0.5.0",
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html2md"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8cff9891f2e0d9048927fbdfc28b11bf378f6a93c7ba70b23d0fbee9af6071b4"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever 0.27.0",
|
||||||
|
"jni 0.19.0",
|
||||||
|
"lazy_static",
|
||||||
|
"markup5ever_rcdom",
|
||||||
|
"percent-encoding",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html2text"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74cda84f06c1cc83476f79ae8e2e892b626bdadafcb227baec54c918cadc18a0"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever 0.26.0",
|
||||||
|
"markup5ever 0.11.0",
|
||||||
|
"tendril",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
"xml5ever 0.17.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@ -2968,6 +3032,20 @@ dependencies = [
|
|||||||
"system-deps 5.0.0",
|
"system-deps 5.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
|
||||||
|
dependencies = [
|
||||||
|
"cesu8",
|
||||||
|
"combine",
|
||||||
|
"jni-sys",
|
||||||
|
"log",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@ -3349,6 +3427,18 @@ dependencies = [
|
|||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever_rcdom"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever 0.27.0",
|
||||||
|
"markup5ever 0.12.1",
|
||||||
|
"tendril",
|
||||||
|
"xml5ever 0.18.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -4180,6 +4270,7 @@ dependencies = [
|
|||||||
"clokwerk",
|
"clokwerk",
|
||||||
"cocoa 0.26.1",
|
"cocoa 0.26.1",
|
||||||
"colored_json",
|
"colored_json",
|
||||||
|
"csv",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@ -4187,6 +4278,9 @@ dependencies = [
|
|||||||
"fns",
|
"fns",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
|
"html-to-react",
|
||||||
|
"html2md",
|
||||||
|
"html2text",
|
||||||
"http-cache-mokadeser",
|
"http-cache-mokadeser",
|
||||||
"http-cache-reqwest",
|
"http-cache-reqwest",
|
||||||
"id3",
|
"id3",
|
||||||
@ -4211,6 +4305,8 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"opener",
|
"opener",
|
||||||
"platform-dirs",
|
"platform-dirs",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"quick-xml 0.31.0",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -4230,6 +4326,7 @@ dependencies = [
|
|||||||
"tl",
|
"tl",
|
||||||
"tld",
|
"tld",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml 0.8.23",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
@ -4668,6 +4765,25 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark-escape"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qoi"
|
name = "qoi"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -4695,6 +4811,16 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@ -5749,7 +5875,7 @@ dependencies = [
|
|||||||
"gtk",
|
"gtk",
|
||||||
"image 0.24.9",
|
"image 0.24.9",
|
||||||
"instant",
|
"instant",
|
||||||
"jni",
|
"jni 0.20.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libappindicator",
|
"libappindicator",
|
||||||
"libc",
|
"libc",
|
||||||
@ -6509,6 +6635,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -7783,6 +7915,28 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml5ever"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever 0.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml5ever"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever 0.12.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -29,6 +29,14 @@ http-cache-mokadeser = "0.1.3"
|
|||||||
# log = "0.4"
|
# log = "0.4"
|
||||||
serde_yaml = "0.9.0"
|
serde_yaml = "0.9.0"
|
||||||
scraper = "0.19.0"
|
scraper = "0.19.0"
|
||||||
|
# Format conversion dependencies
|
||||||
|
csv = "1.3"
|
||||||
|
html2text = "0.6"
|
||||||
|
html2md = "0.2"
|
||||||
|
html-to-react = "0.5.2"
|
||||||
|
pulldown-cmark = "0.10"
|
||||||
|
quick-xml = { version = "0.31", features = ["serialize"] }
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
jsonpath-rust = "0.4.0"
|
jsonpath-rust = "0.4.0"
|
||||||
ajson = "0.3.1"
|
ajson = "0.3.1"
|
||||||
|
375
src-tauri/src/commands/format_converter_commands.rs
Normal file
375
src-tauri/src/commands/format_converter_commands.rs
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
use csv::{Reader, Writer};
|
||||||
|
use html2text;
|
||||||
|
use pulldown_cmark::{html, Options, Parser};
|
||||||
|
use quick_xml::de::from_str as xml_from_str;
|
||||||
|
use quick_xml::se::to_string as xml_to_string;
|
||||||
|
use serde_json::{from_str as json_from_str, to_string_pretty, Value as JsonValue};
|
||||||
|
use serde_yaml::{from_str as yaml_from_str, to_string as yaml_to_string, Value as YamlValue};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use toml::{from_str as toml_from_str, to_string as toml_to_string, Value as TomlValue};
|
||||||
|
// Import html_to_react crate
|
||||||
|
extern crate html_to_react;
|
||||||
|
|
||||||
|
/// Convert CSV to JSON
|
||||||
|
fn csv_to_json(text: &str) -> Result<String, String> {
|
||||||
|
if !text.contains(',') && !text.contains('\t') && !text.contains(';') {
|
||||||
|
return Err("Input does not appear to be valid CSV format (no delimiters found)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = Reader::from_reader(text.as_bytes());
|
||||||
|
let mut records = Vec::new();
|
||||||
|
|
||||||
|
// Get headers
|
||||||
|
let headers = reader
|
||||||
|
.headers()
|
||||||
|
.map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Failed to read CSV headers - ensure the text is properly formatted CSV: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if headers.is_empty() {
|
||||||
|
return Err("CSV file appears to have no headers".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all records
|
||||||
|
for result in reader.records() {
|
||||||
|
let record = result.map_err(|e| format!("Failed to read CSV record: {}", e))?;
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
for (i, field) in record.iter().enumerate() {
|
||||||
|
if let Some(header) = headers.get(i) {
|
||||||
|
map.insert(header.to_string(), field.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records.push(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
to_string_pretty(&records).map_err(|e| format!("Failed to serialize to JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert JSON to CSV
|
||||||
|
fn json_to_csv(text: &str) -> Result<String, String> {
|
||||||
|
let json_data: JsonValue =
|
||||||
|
json_from_str(text).map_err(|e| format!("Invalid JSON format: {}", e))?;
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
{
|
||||||
|
let mut writer = Writer::from_writer(&mut output);
|
||||||
|
|
||||||
|
match json_data {
|
||||||
|
JsonValue::Array(ref array) => {
|
||||||
|
if array.is_empty() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract headers from first object
|
||||||
|
if let Some(JsonValue::Object(first_obj)) = array.first() {
|
||||||
|
let headers: Vec<String> = first_obj.keys().cloned().collect();
|
||||||
|
writer
|
||||||
|
.write_record(&headers)
|
||||||
|
.map_err(|e| format!("Failed to write CSV headers: {}", e))?;
|
||||||
|
|
||||||
|
// Write data rows
|
||||||
|
for item in array {
|
||||||
|
if let JsonValue::Object(obj) = item {
|
||||||
|
let row: Vec<String> = headers
|
||||||
|
.iter()
|
||||||
|
.map(|header| {
|
||||||
|
obj
|
||||||
|
.get(header)
|
||||||
|
.map(|v| match v {
|
||||||
|
JsonValue::String(s) => s.clone(),
|
||||||
|
JsonValue::Number(n) => n.to_string(),
|
||||||
|
JsonValue::Bool(b) => b.to_string(),
|
||||||
|
JsonValue::Null => String::new(),
|
||||||
|
_ => v.to_string(),
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
writer
|
||||||
|
.write_record(&row)
|
||||||
|
.map_err(|e| format!("Failed to write CSV row: {}", e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err("JSON must be an array of objects for CSV conversion".to_string()),
|
||||||
|
}
|
||||||
|
|
||||||
|
writer
|
||||||
|
.flush()
|
||||||
|
.map_err(|e| format!("Failed to flush CSV writer: {}", e))?;
|
||||||
|
} // writer is dropped here, releasing the borrow
|
||||||
|
|
||||||
|
String::from_utf8(output).map_err(|e| format!("Failed to convert CSV to string: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert YAML to JSON
|
||||||
|
fn yaml_to_json(text: &str) -> Result<String, String> {
|
||||||
|
let yaml_data: YamlValue = yaml_from_str(text).map_err(|e| format!("Invalid YAML: {}", e))?;
|
||||||
|
|
||||||
|
to_string_pretty(&yaml_data).map_err(|e| format!("Failed to serialize to JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert JSON to YAML
|
||||||
|
fn json_to_yaml(text: &str) -> Result<String, String> {
|
||||||
|
let json_data: JsonValue = json_from_str(text).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
yaml_to_string(&json_data).map_err(|e| format!("Failed to serialize to YAML: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Markdown to HTML
|
||||||
|
fn markdown_to_html(text: &str) -> Result<String, String> {
|
||||||
|
let mut options = Options::empty();
|
||||||
|
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||||
|
options.insert(Options::ENABLE_TABLES);
|
||||||
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
|
options.insert(Options::ENABLE_TASKLISTS);
|
||||||
|
|
||||||
|
let parser = Parser::new_ext(text, options);
|
||||||
|
let mut html_output = String::new();
|
||||||
|
html::push_html(&mut html_output, parser);
|
||||||
|
Ok(html_output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert HTML to Markdown
|
||||||
|
fn html_to_markdown(text: &str) -> Result<String, String> {
|
||||||
|
// Use html2md for better HTML to Markdown conversion
|
||||||
|
Ok(html2md::parse_html(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert HTML to plain text
|
||||||
|
fn html_to_text(text: &str) -> Result<String, String> {
|
||||||
|
Ok(html2text::from_read(text.as_bytes(), text.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Markdown to plain text
|
||||||
|
fn markdown_to_text(text: &str) -> Result<String, String> {
|
||||||
|
// First convert markdown to HTML, then HTML to text
|
||||||
|
let html = markdown_to_html(text)?;
|
||||||
|
html_to_text(&html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert HTML to React Component (JSX)
|
||||||
|
fn html_to_react_components(text: &str) -> Result<String, String> {
|
||||||
|
// Use html_to_react crate to convert HTML to React JSX
|
||||||
|
let component = html_to_react::convert_to_react(text.to_string(), "MyComponent".to_string());
|
||||||
|
Ok(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert HTML to React JSX with comprehensive HTML to JSX conversion
|
||||||
|
fn convert_html_to_react_jsx(text: &str) -> Result<String, String> {
|
||||||
|
// Comprehensive HTML to JSX converter with support for:
|
||||||
|
// - HTML attributes to JSX attributes (class -> className, for -> htmlFor, etc.)
|
||||||
|
// - Self-closing tags
|
||||||
|
// - HTML comments to JSX comments
|
||||||
|
// - Boolean attributes
|
||||||
|
// - CSS style properties to camelCase
|
||||||
|
let jsx = html_to_react::convert_props_react(text.to_string());
|
||||||
|
|
||||||
|
Ok(jsx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert XML to JSON
|
||||||
|
fn xml_to_json(text: &str) -> Result<String, String> {
|
||||||
|
let xml_data: JsonValue = xml_from_str(text).map_err(|e| format!("Invalid XML: {}", e))?;
|
||||||
|
|
||||||
|
to_string_pretty(&xml_data).map_err(|e| format!("Failed to serialize to JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert JSON to XML
|
||||||
|
fn json_to_xml(text: &str) -> Result<String, String> {
|
||||||
|
let json_data: JsonValue = json_from_str(text).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
xml_to_string(&json_data).map_err(|e| format!("Failed to serialize to XML: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert TOML to JSON
|
||||||
|
fn toml_to_json(text: &str) -> Result<String, String> {
|
||||||
|
let toml_data: TomlValue = toml_from_str(text).map_err(|e| format!("Invalid TOML: {}", e))?;
|
||||||
|
|
||||||
|
to_string_pretty(&toml_data).map_err(|e| format!("Failed to serialize to JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert JSON to TOML
|
||||||
|
fn json_to_toml(text: &str) -> Result<String, String> {
|
||||||
|
let json_data: JsonValue = json_from_str(text).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
// Convert JsonValue to TomlValue
|
||||||
|
let toml_data = json_to_toml_value(json_data)?;
|
||||||
|
toml_to_string(&toml_data).map_err(|e| format!("Failed to serialize to TOML: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to convert JsonValue to TomlValue
|
||||||
|
fn json_to_toml_value(json: JsonValue) -> Result<TomlValue, String> {
|
||||||
|
match json {
|
||||||
|
JsonValue::Null => Ok(TomlValue::String("".to_string())),
|
||||||
|
JsonValue::Bool(b) => Ok(TomlValue::Boolean(b)),
|
||||||
|
JsonValue::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
Ok(TomlValue::Integer(i))
|
||||||
|
} else if let Some(f) = n.as_f64() {
|
||||||
|
Ok(TomlValue::Float(f))
|
||||||
|
} else {
|
||||||
|
Err("Invalid number format".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonValue::String(s) => Ok(TomlValue::String(s)),
|
||||||
|
JsonValue::Array(arr) => {
|
||||||
|
let mut toml_array = Vec::new();
|
||||||
|
for item in arr {
|
||||||
|
toml_array.push(json_to_toml_value(item)?);
|
||||||
|
}
|
||||||
|
Ok(TomlValue::Array(toml_array))
|
||||||
|
}
|
||||||
|
JsonValue::Object(obj) => {
|
||||||
|
let mut toml_table = toml::value::Table::new();
|
||||||
|
for (key, value) in obj {
|
||||||
|
toml_table.insert(key, json_to_toml_value(value)?);
|
||||||
|
}
|
||||||
|
Ok(TomlValue::Table(toml_table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert CSV to Markdown table
|
||||||
|
fn csv_to_table(text: &str) -> Result<String, String> {
|
||||||
|
let mut reader = Reader::from_reader(text.as_bytes());
|
||||||
|
let mut markdown = String::new();
|
||||||
|
|
||||||
|
// Get headers
|
||||||
|
let headers = reader
|
||||||
|
.headers()
|
||||||
|
.map_err(|e| format!("Failed to read CSV headers: {}", e))?;
|
||||||
|
|
||||||
|
// Write header row
|
||||||
|
markdown.push('|');
|
||||||
|
for header in headers.iter() {
|
||||||
|
markdown.push(' ');
|
||||||
|
markdown.push_str(header);
|
||||||
|
markdown.push_str(" |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
|
||||||
|
// Write separator row
|
||||||
|
markdown.push('|');
|
||||||
|
for _ in headers.iter() {
|
||||||
|
markdown.push_str(" --- |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
|
||||||
|
// Write data rows
|
||||||
|
for result in reader.records() {
|
||||||
|
let record = result.map_err(|e| format!("Failed to read CSV record: {}", e))?;
|
||||||
|
markdown.push('|');
|
||||||
|
for field in record.iter() {
|
||||||
|
markdown.push(' ');
|
||||||
|
markdown.push_str(field);
|
||||||
|
markdown.push_str(" |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(markdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert JSON to Markdown table
|
||||||
|
fn json_to_table(text: &str) -> Result<String, String> {
|
||||||
|
let json_data: JsonValue = json_from_str(text).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
match json_data {
|
||||||
|
JsonValue::Array(ref array) => {
|
||||||
|
if array.is_empty() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut markdown = String::new();
|
||||||
|
|
||||||
|
// Extract headers from first object
|
||||||
|
if let Some(JsonValue::Object(first_obj)) = array.first() {
|
||||||
|
let headers: Vec<String> = first_obj.keys().cloned().collect();
|
||||||
|
|
||||||
|
// Write header row
|
||||||
|
markdown.push('|');
|
||||||
|
for header in &headers {
|
||||||
|
markdown.push(' ');
|
||||||
|
markdown.push_str(header);
|
||||||
|
markdown.push_str(" |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
|
||||||
|
// Write separator row
|
||||||
|
markdown.push('|');
|
||||||
|
for _ in &headers {
|
||||||
|
markdown.push_str(" --- |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
|
||||||
|
// Write data rows
|
||||||
|
for item in array {
|
||||||
|
if let JsonValue::Object(obj) = item {
|
||||||
|
markdown.push('|');
|
||||||
|
for header in &headers {
|
||||||
|
markdown.push(' ');
|
||||||
|
if let Some(value) = obj.get(header) {
|
||||||
|
let cell_value = match value {
|
||||||
|
JsonValue::String(s) => s.clone(),
|
||||||
|
JsonValue::Number(n) => n.to_string(),
|
||||||
|
JsonValue::Bool(b) => b.to_string(),
|
||||||
|
JsonValue::Null => String::new(),
|
||||||
|
_ => value.to_string(),
|
||||||
|
};
|
||||||
|
markdown.push_str(&cell_value);
|
||||||
|
}
|
||||||
|
markdown.push_str(" |");
|
||||||
|
}
|
||||||
|
markdown.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(markdown)
|
||||||
|
}
|
||||||
|
_ => Err("JSON must be an array of objects for table conversion".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main format converter command
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn format_convert(text: String, conversion_type: String) -> Result<String, String> {
|
||||||
|
// Validate input
|
||||||
|
if text.trim().is_empty() {
|
||||||
|
return Err("Input text cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the conversion attempt for debugging
|
||||||
|
// eprintln!(
|
||||||
|
// "Converting {} with type: {}",
|
||||||
|
// text.chars().take(50).collect::<String>(),
|
||||||
|
// conversion_type
|
||||||
|
// );
|
||||||
|
|
||||||
|
match conversion_type.as_str() {
|
||||||
|
"csv_to_json" => csv_to_json(&text).map_err(|e| format!("CSV to JSON conversion failed: {}", e)),
|
||||||
|
"json_to_csv" => json_to_csv(&text).map_err(|e| format!("JSON to CSV conversion failed: {}", e)),
|
||||||
|
"yaml_to_json" => yaml_to_json(&text).map_err(|e| format!("YAML to JSON conversion failed: {}", e)),
|
||||||
|
"json_to_yaml" => json_to_yaml(&text).map_err(|e| format!("JSON to YAML conversion failed: {}", e)),
|
||||||
|
"markdown_to_html" => markdown_to_html(&text).map_err(|e| format!("Markdown to HTML conversion failed: {}", e)),
|
||||||
|
"html_to_markdown" => html_to_markdown(&text).map_err(|e| format!("HTML to Markdown conversion failed: {}", e)),
|
||||||
|
"html_to_react_components" => html_to_react_components(&text).map_err(|e| format!("HTML to React Component conversion failed: {}", e)),
|
||||||
|
"html_to_text" => html_to_text(&text).map_err(|e| format!("HTML to Text conversion failed: {}", e)),
|
||||||
|
"markdown_to_text" => markdown_to_text(&text).map_err(|e| format!("Markdown to Text conversion failed: {}", e)),
|
||||||
|
"html_to_react" => convert_html_to_react_jsx(&text).map_err(|e| format!("HTML to React JSX conversion failed: {}", e)),
|
||||||
|
"xml_to_json" => xml_to_json(&text).map_err(|e| format!("XML to JSON conversion failed: {}", e)),
|
||||||
|
"json_to_xml" => json_to_xml(&text).map_err(|e| format!("JSON to XML conversion failed: {}", e)),
|
||||||
|
"toml_to_json" => toml_to_json(&text).map_err(|e| format!("TOML to JSON conversion failed: {}", e)),
|
||||||
|
"json_to_toml" => json_to_toml(&text).map_err(|e| format!("JSON to TOML conversion failed: {}", e)),
|
||||||
|
"csv_to_table" => csv_to_table(&text).map_err(|e| format!("CSV to Table conversion failed: {}", e)),
|
||||||
|
"json_to_table" => json_to_table(&text).map_err(|e| format!("JSON to Table conversion failed: {}", e)),
|
||||||
|
_ => Err(format!("Unsupported conversion type: '{}'. Available types: csv_to_json, json_to_csv, yaml_to_json, json_to_yaml, markdown_to_html, html_to_markdown, html_to_text, markdown_to_text, html_to_react, xml_to_json, json_to_xml, toml_to_json, json_to_toml, csv_to_table, json_to_table", conversion_type)),
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ pub(crate) mod backup_restore_commands;
|
|||||||
pub(crate) mod clipboard_commands;
|
pub(crate) mod clipboard_commands;
|
||||||
pub(crate) mod collections_commands;
|
pub(crate) mod collections_commands;
|
||||||
pub(crate) mod download_update;
|
pub(crate) mod download_update;
|
||||||
|
pub(crate) mod format_converter_commands;
|
||||||
pub(crate) mod history_commands;
|
pub(crate) mod history_commands;
|
||||||
pub(crate) mod items_commands;
|
pub(crate) mod items_commands;
|
||||||
pub(crate) mod link_metadata_commands;
|
pub(crate) mod link_metadata_commands;
|
||||||
|
@ -56,6 +56,7 @@ use commands::backup_restore_commands;
|
|||||||
use commands::clipboard_commands;
|
use commands::clipboard_commands;
|
||||||
use commands::collections_commands;
|
use commands::collections_commands;
|
||||||
use commands::download_update;
|
use commands::download_update;
|
||||||
|
use commands::format_converter_commands;
|
||||||
use commands::history_commands;
|
use commands::history_commands;
|
||||||
use commands::items_commands;
|
use commands::items_commands;
|
||||||
use commands::link_metadata_commands;
|
use commands::link_metadata_commands;
|
||||||
@ -188,6 +189,12 @@ fn update_left_click_tray_env(is_toggle_enabled: bool, is_disabled: bool) -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[tauri::command]
|
||||||
|
fn update_left_click_tray_env(is_toggle_enabled: bool, is_disabled: bool) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn is_autostart_enabled() -> Result<bool, bool> {
|
fn is_autostart_enabled() -> Result<bool, bool> {
|
||||||
let current_exe = current_exe().unwrap();
|
let current_exe = current_exe().unwrap();
|
||||||
@ -1380,6 +1387,7 @@ async fn main() {
|
|||||||
user_settings_command::cmd_get_setting,
|
user_settings_command::cmd_get_setting,
|
||||||
user_settings_command::cmd_set_setting,
|
user_settings_command::cmd_set_setting,
|
||||||
user_settings_command::cmd_remove_setting,
|
user_settings_command::cmd_remove_setting,
|
||||||
|
format_converter_commands::format_convert,
|
||||||
open_osx_accessibility_preferences,
|
open_osx_accessibility_preferences,
|
||||||
check_osx_accessibility_preferences,
|
check_osx_accessibility_preferences,
|
||||||
open_path_or_app,
|
open_path_or_app,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user