fix(gui): add missing @lucide/svelte dependency to fix runtime error
- commit
- 3ef94184479a5a4930dda0af542e3f93bf7a7037
- author
- bamen · · 2025-09-15 14:03 UTC
- parent
- 3aefd86
- signature
- unsigned
9 files · +228 -35
M gui/package-lock.json +12 -18
| @@ -7,6 +7,9 @@ | |||
| 7 | 7 | "": { | |
| 8 | 8 | "name": "@dumber/gui", | |
| 9 | 9 | "version": "0.1.0", | |
| 10 | + | "dependencies": { | |
| 11 | + | "@lucide/svelte": "^0.544.0" | |
| 12 | + | }, | |
| 10 | 13 | "devDependencies": { | |
| 11 | 14 | "@sveltejs/vite-plugin-svelte": "^4.0.0", | |
| 12 | 15 | "@tailwindcss/cli": "^4.0.0", | |
| @@ -204,7 +207,6 @@ | |||
| 204 | 207 | "version": "0.3.13", | |
| 205 | 208 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | |
| 206 | 209 | "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | |
| 207 | - | "dev": true, | |
| 208 | 210 | "license": "MIT", | |
| 209 | 211 | "dependencies": { | |
| 210 | 212 | "@jridgewell/sourcemap-codec": "^1.5.0", | |
| @@ -215,7 +217,6 @@ | |||
| 215 | 217 | "version": "2.3.5", | |
| 216 | 218 | "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", | |
| 217 | 219 | "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | |
| 218 | - | "dev": true, | |
| 219 | 220 | "license": "MIT", | |
| 220 | 221 | "dependencies": { | |
| 221 | 222 | "@jridgewell/gen-mapping": "^0.3.5", | |
| @@ -226,7 +227,6 @@ | |||
| 226 | 227 | "version": "3.1.2", | |
| 227 | 228 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | |
| 228 | 229 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | |
| 229 | - | "dev": true, | |
| 230 | 230 | "license": "MIT", | |
| 231 | 231 | "engines": { | |
| 232 | 232 | "node": ">=6.0.0" | |
| @@ -236,20 +236,27 @@ | |||
| 236 | 236 | "version": "1.5.5", | |
| 237 | 237 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | |
| 238 | 238 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | |
| 239 | - | "dev": true, | |
| 240 | 239 | "license": "MIT" | |
| 241 | 240 | }, | |
| 242 | 241 | "node_modules/@jridgewell/trace-mapping": { | |
| 243 | 242 | "version": "0.3.30", | |
| 244 | 243 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", | |
| 245 | 244 | "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", | |
| 246 | - | "dev": true, | |
| 247 | 245 | "license": "MIT", | |
| 248 | 246 | "dependencies": { | |
| 249 | 247 | "@jridgewell/resolve-uri": "^3.1.0", | |
| 250 | 248 | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 251 | 249 | } | |
| 252 | 250 | }, | |
| 251 | + | "node_modules/@lucide/svelte": { | |
| 252 | + | "version": "0.544.0", | |
| 253 | + | "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.544.0.tgz", | |
| 254 | + | "integrity": "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw==", | |
| 255 | + | "license": "ISC", | |
| 256 | + | "peerDependencies": { | |
| 257 | + | "svelte": "^5" | |
| 258 | + | } | |
| 259 | + | }, | |
| 253 | 260 | "node_modules/@nodelib/fs.scandir": { | |
| 254 | 261 | "version": "2.1.5", | |
| 255 | 262 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | |
| @@ -895,7 +902,6 @@ | |||
| 895 | 902 | "version": "1.0.5", | |
| 896 | 903 | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", | |
| 897 | 904 | "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", | |
| 898 | - | "dev": true, | |
| 899 | 905 | "license": "MIT", | |
| 900 | 906 | "peerDependencies": { | |
| 901 | 907 | "acorn": "^8.9.0" | |
| @@ -1250,7 +1256,6 @@ | |||
| 1250 | 1256 | "version": "1.0.8", | |
| 1251 | 1257 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", | |
| 1252 | 1258 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | |
| 1253 | - | "dev": true, | |
| 1254 | 1259 | "license": "MIT" | |
| 1255 | 1260 | }, | |
| 1256 | 1261 | "node_modules/@types/node": { | |
| @@ -1520,7 +1525,6 @@ | |||
| 1520 | 1525 | "version": "8.15.0", | |
| 1521 | 1526 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", | |
| 1522 | 1527 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", | |
| 1523 | - | "dev": true, | |
| 1524 | 1528 | "license": "MIT", | |
| 1525 | 1529 | "bin": { | |
| 1526 | 1530 | "acorn": "bin/acorn" | |
| @@ -1593,7 +1597,6 @@ | |||
| 1593 | 1597 | "version": "5.3.2", | |
| 1594 | 1598 | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", | |
| 1595 | 1599 | "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", | |
| 1596 | - | "dev": true, | |
| 1597 | 1600 | "license": "Apache-2.0", | |
| 1598 | 1601 | "engines": { | |
| 1599 | 1602 | "node": ">= 0.4" | |
| @@ -1651,7 +1654,6 @@ | |||
| 1651 | 1654 | "version": "4.1.0", | |
| 1652 | 1655 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", | |
| 1653 | 1656 | "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", | |
| 1654 | - | "dev": true, | |
| 1655 | 1657 | "license": "Apache-2.0", | |
| 1656 | 1658 | "engines": { | |
| 1657 | 1659 | "node": ">= 0.4" | |
| @@ -1809,7 +1811,6 @@ | |||
| 1809 | 1811 | "version": "2.1.1", | |
| 1810 | 1812 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | |
| 1811 | 1813 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | |
| 1812 | - | "dev": true, | |
| 1813 | 1814 | "license": "MIT", | |
| 1814 | 1815 | "engines": { | |
| 1815 | 1816 | "node": ">=6" | |
| @@ -2090,7 +2091,6 @@ | |||
| 2090 | 2091 | "version": "1.2.2", | |
| 2091 | 2092 | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", | |
| 2092 | 2093 | "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", | |
| 2093 | - | "dev": true, | |
| 2094 | 2094 | "license": "MIT" | |
| 2095 | 2095 | }, | |
| 2096 | 2096 | "node_modules/espree": { | |
| @@ -2128,7 +2128,6 @@ | |||
| 2128 | 2128 | "version": "2.1.0", | |
| 2129 | 2129 | "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", | |
| 2130 | 2130 | "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", | |
| 2131 | - | "dev": true, | |
| 2132 | 2131 | "license": "MIT", | |
| 2133 | 2132 | "dependencies": { | |
| 2134 | 2133 | "@jridgewell/sourcemap-codec": "^1.4.15" | |
| @@ -2552,7 +2551,6 @@ | |||
| 2552 | 2551 | "version": "3.0.3", | |
| 2553 | 2552 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", | |
| 2554 | 2553 | "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", | |
| 2555 | - | "dev": true, | |
| 2556 | 2554 | "license": "MIT", | |
| 2557 | 2555 | "dependencies": { | |
| 2558 | 2556 | "@types/estree": "^1.0.6" | |
| @@ -2896,7 +2894,6 @@ | |||
| 2896 | 2894 | "version": "3.0.0", | |
| 2897 | 2895 | "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", | |
| 2898 | 2896 | "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", | |
| 2899 | - | "dev": true, | |
| 2900 | 2897 | "license": "MIT" | |
| 2901 | 2898 | }, | |
| 2902 | 2899 | "node_modules/locate-path": { | |
| @@ -2926,7 +2923,6 @@ | |||
| 2926 | 2923 | "version": "0.30.19", | |
| 2927 | 2924 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", | |
| 2928 | 2925 | "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", | |
| 2929 | - | "dev": true, | |
| 2930 | 2926 | "license": "MIT", | |
| 2931 | 2927 | "dependencies": { | |
| 2932 | 2928 | "@jridgewell/sourcemap-codec": "^1.5.5" | |
| @@ -3517,7 +3513,6 @@ | |||
| 3517 | 3513 | "version": "5.38.10", | |
| 3518 | 3514 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.10.tgz", | |
| 3519 | 3515 | "integrity": "sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg==", | |
| 3520 | - | "dev": true, | |
| 3521 | 3516 | "license": "MIT", | |
| 3522 | 3517 | "dependencies": { | |
| 3523 | 3518 | "@jridgewell/remapping": "^2.3.4", | |
| @@ -4569,7 +4564,6 @@ | |||
| 4569 | 4564 | "version": "1.1.4", | |
| 4570 | 4565 | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | |
| 4571 | 4566 | "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", | |
| 4572 | - | "dev": true, | |
| 4573 | 4567 | "license": "MIT" | |
| 4574 | 4568 | } | |
| 4575 | 4569 | } | |
| @@ -7,6 +7,9 @@ | |||||
| 7 | "": { | 7 | "": { | ||
| 8 | "name": "@dumber/gui", | 8 | "name": "@dumber/gui", | ||
| 9 | "version": "0.1.0", | 9 | "version": "0.1.0", | ||
| 10 | + | "dependencies": { | |||
| 11 | + | "@lucide/svelte": "^0.544.0" | |||
| 12 | + | }, | |||
| 10 | "devDependencies": { | 13 | "devDependencies": { | ||
| 11 | "@sveltejs/vite-plugin-svelte": "^4.0.0", | 14 | "@sveltejs/vite-plugin-svelte": "^4.0.0", | ||
| 12 | "@tailwindcss/cli": "^4.0.0", | 15 | "@tailwindcss/cli": "^4.0.0", | ||
| @@ -204,7 +207,6 @@ | |||||
| 204 | "version": "0.3.13", | 207 | "version": "0.3.13", | ||
| 205 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | 208 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | ||
| 206 | "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | 209 | "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | ||
| 207 | - | "dev": true, | |||
| 208 | "license": "MIT", | 210 | "license": "MIT", | ||
| 209 | "dependencies": { | 211 | "dependencies": { | ||
| 210 | "@jridgewell/sourcemap-codec": "^1.5.0", | 212 | "@jridgewell/sourcemap-codec": "^1.5.0", | ||
| @@ -215,7 +217,6 @@ | |||||
| 215 | "version": "2.3.5", | 217 | "version": "2.3.5", | ||
| 216 | "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", | 218 | "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", | ||
| 217 | "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | 219 | "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | ||
| 218 | - | "dev": true, | |||
| 219 | "license": "MIT", | 220 | "license": "MIT", | ||
| 220 | "dependencies": { | 221 | "dependencies": { | ||
| 221 | "@jridgewell/gen-mapping": "^0.3.5", | 222 | "@jridgewell/gen-mapping": "^0.3.5", | ||
| @@ -226,7 +227,6 @@ | |||||
| 226 | "version": "3.1.2", | 227 | "version": "3.1.2", | ||
| 227 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | 228 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | ||
| 228 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | 229 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | ||
| 229 | - | "dev": true, | |||
| 230 | "license": "MIT", | 230 | "license": "MIT", | ||
| 231 | "engines": { | 231 | "engines": { | ||
| 232 | "node": ">=6.0.0" | 232 | "node": ">=6.0.0" | ||
| @@ -236,20 +236,27 @@ | |||||
| 236 | "version": "1.5.5", | 236 | "version": "1.5.5", | ||
| 237 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | 237 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | ||
| 238 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | 238 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | ||
| 239 | - | "dev": true, | |||
| 240 | "license": "MIT" | 239 | "license": "MIT" | ||
| 241 | }, | 240 | }, | ||
| 242 | "node_modules/@jridgewell/trace-mapping": { | 241 | "node_modules/@jridgewell/trace-mapping": { | ||
| 243 | "version": "0.3.30", | 242 | "version": "0.3.30", | ||
| 244 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", | 243 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", | ||
| 245 | "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", | 244 | "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", | ||
| 246 | - | "dev": true, | |||
| 247 | "license": "MIT", | 245 | "license": "MIT", | ||
| 248 | "dependencies": { | 246 | "dependencies": { | ||
| 249 | "@jridgewell/resolve-uri": "^3.1.0", | 247 | "@jridgewell/resolve-uri": "^3.1.0", | ||
| 250 | "@jridgewell/sourcemap-codec": "^1.4.14" | 248 | "@jridgewell/sourcemap-codec": "^1.4.14" | ||
| 251 | } | 249 | } | ||
| 252 | }, | 250 | }, | ||
| 251 | + | "node_modules/@lucide/svelte": { | |||
| 252 | + | "version": "0.544.0", | |||
| 253 | + | "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.544.0.tgz", | |||
| 254 | + | "integrity": "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw==", | |||
| 255 | + | "license": "ISC", | |||
| 256 | + | "peerDependencies": { | |||
| 257 | + | "svelte": "^5" | |||
| 258 | + | } | |||
| 259 | + | }, | |||
| 253 | "node_modules/@nodelib/fs.scandir": { | 260 | "node_modules/@nodelib/fs.scandir": { | ||
| 254 | "version": "2.1.5", | 261 | "version": "2.1.5", | ||
| 255 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | 262 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||
| @@ -895,7 +902,6 @@ | |||||
| 895 | "version": "1.0.5", | 902 | "version": "1.0.5", | ||
| 896 | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", | 903 | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", | ||
| 897 | "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", | 904 | "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", | ||
| 898 | - | "dev": true, | |||
| 899 | "license": "MIT", | 905 | "license": "MIT", | ||
| 900 | "peerDependencies": { | 906 | "peerDependencies": { | ||
| 901 | "acorn": "^8.9.0" | 907 | "acorn": "^8.9.0" | ||
| @@ -1250,7 +1256,6 @@ | |||||
| 1250 | "version": "1.0.8", | 1256 | "version": "1.0.8", | ||
| 1251 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", | 1257 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", | ||
| 1252 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | 1258 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | ||
| 1253 | - | "dev": true, | |||
| 1254 | "license": "MIT" | 1259 | "license": "MIT" | ||
| 1255 | }, | 1260 | }, | ||
| 1256 | "node_modules/@types/node": { | 1261 | "node_modules/@types/node": { | ||
| @@ -1520,7 +1525,6 @@ | |||||
| 1520 | "version": "8.15.0", | 1525 | "version": "8.15.0", | ||
| 1521 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", | 1526 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", | ||
| 1522 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", | 1527 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", | ||
| 1523 | - | "dev": true, | |||
| 1524 | "license": "MIT", | 1528 | "license": "MIT", | ||
| 1525 | "bin": { | 1529 | "bin": { | ||
| 1526 | "acorn": "bin/acorn" | 1530 | "acorn": "bin/acorn" | ||
| @@ -1593,7 +1597,6 @@ | |||||
| 1593 | "version": "5.3.2", | 1597 | "version": "5.3.2", | ||
| 1594 | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", | 1598 | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", | ||
| 1595 | "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", | 1599 | "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", | ||
| 1596 | - | "dev": true, | |||
| 1597 | "license": "Apache-2.0", | 1600 | "license": "Apache-2.0", | ||
| 1598 | "engines": { | 1601 | "engines": { | ||
| 1599 | "node": ">= 0.4" | 1602 | "node": ">= 0.4" | ||
| @@ -1651,7 +1654,6 @@ | |||||
| 1651 | "version": "4.1.0", | 1654 | "version": "4.1.0", | ||
| 1652 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", | 1655 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", | ||
| 1653 | "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", | 1656 | "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", | ||
| 1654 | - | "dev": true, | |||
| 1655 | "license": "Apache-2.0", | 1657 | "license": "Apache-2.0", | ||
| 1656 | "engines": { | 1658 | "engines": { | ||
| 1657 | "node": ">= 0.4" | 1659 | "node": ">= 0.4" | ||
| @@ -1809,7 +1811,6 @@ | |||||
| 1809 | "version": "2.1.1", | 1811 | "version": "2.1.1", | ||
| 1810 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | 1812 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | ||
| 1811 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | 1813 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | ||
| 1812 | - | "dev": true, | |||
| 1813 | "license": "MIT", | 1814 | "license": "MIT", | ||
| 1814 | "engines": { | 1815 | "engines": { | ||
| 1815 | "node": ">=6" | 1816 | "node": ">=6" | ||
| @@ -2090,7 +2091,6 @@ | |||||
| 2090 | "version": "1.2.2", | 2091 | "version": "1.2.2", | ||
| 2091 | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", | 2092 | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", | ||
| 2092 | "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", | 2093 | "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", | ||
| 2093 | - | "dev": true, | |||
| 2094 | "license": "MIT" | 2094 | "license": "MIT" | ||
| 2095 | }, | 2095 | }, | ||
| 2096 | "node_modules/espree": { | 2096 | "node_modules/espree": { | ||
| @@ -2128,7 +2128,6 @@ | |||||
| 2128 | "version": "2.1.0", | 2128 | "version": "2.1.0", | ||
| 2129 | "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", | 2129 | "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", | ||
| 2130 | "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", | 2130 | "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", | ||
| 2131 | - | "dev": true, | |||
| 2132 | "license": "MIT", | 2131 | "license": "MIT", | ||
| 2133 | "dependencies": { | 2132 | "dependencies": { | ||
| 2134 | "@jridgewell/sourcemap-codec": "^1.4.15" | 2133 | "@jridgewell/sourcemap-codec": "^1.4.15" | ||
| @@ -2552,7 +2551,6 @@ | |||||
| 2552 | "version": "3.0.3", | 2551 | "version": "3.0.3", | ||
| 2553 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", | 2552 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", | ||
| 2554 | "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", | 2553 | "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", | ||
| 2555 | - | "dev": true, | |||
| 2556 | "license": "MIT", | 2554 | "license": "MIT", | ||
| 2557 | "dependencies": { | 2555 | "dependencies": { | ||
| 2558 | "@types/estree": "^1.0.6" | 2556 | "@types/estree": "^1.0.6" | ||
| @@ -2896,7 +2894,6 @@ | |||||
| 2896 | "version": "3.0.0", | 2894 | "version": "3.0.0", | ||
| 2897 | "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", | 2895 | "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", | ||
| 2898 | "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", | 2896 | "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", | ||
| 2899 | - | "dev": true, | |||
| 2900 | "license": "MIT" | 2897 | "license": "MIT" | ||
| 2901 | }, | 2898 | }, | ||
| 2902 | "node_modules/locate-path": { | 2899 | "node_modules/locate-path": { | ||
| @@ -2926,7 +2923,6 @@ | |||||
| 2926 | "version": "0.30.19", | 2923 | "version": "0.30.19", | ||
| 2927 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", | 2924 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", | ||
| 2928 | "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", | 2925 | "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", | ||
| 2929 | - | "dev": true, | |||
| 2930 | "license": "MIT", | 2926 | "license": "MIT", | ||
| 2931 | "dependencies": { | 2927 | "dependencies": { | ||
| 2932 | "@jridgewell/sourcemap-codec": "^1.5.5" | 2928 | "@jridgewell/sourcemap-codec": "^1.5.5" | ||
| @@ -3517,7 +3513,6 @@ | |||||
| 3517 | "version": "5.38.10", | 3513 | "version": "5.38.10", | ||
| 3518 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.10.tgz", | 3514 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.10.tgz", | ||
| 3519 | "integrity": "sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg==", | 3515 | "integrity": "sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg==", | ||
| 3520 | - | "dev": true, | |||
| 3521 | "license": "MIT", | 3516 | "license": "MIT", | ||
| 3522 | "dependencies": { | 3517 | "dependencies": { | ||
| 3523 | "@jridgewell/remapping": "^2.3.4", | 3518 | "@jridgewell/remapping": "^2.3.4", | ||
| @@ -4569,7 +4564,6 @@ | |||||
| 4569 | "version": "1.1.4", | 4564 | "version": "1.1.4", | ||
| 4570 | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | 4565 | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | ||
| 4571 | "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", | 4566 | "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", | ||
| 4572 | - | "dev": true, | |||
| 4573 | "license": "MIT" | 4567 | "license": "MIT" | ||
| 4574 | } | 4568 | } | ||
| 4575 | } | 4569 | } | ||
M gui/package.json +3 -0
| @@ -29,5 +29,8 @@ | |||
| 29 | 29 | "typescript": "^5.3.0", | |
| 30 | 30 | "typescript-eslint": "^8.43.0", | |
| 31 | 31 | "vite": "^5.0.0" | |
| 32 | + | }, | |
| 33 | + | "dependencies": { | |
| 34 | + | "@lucide/svelte": "^0.544.0" | |
| 32 | 35 | } | |
| 33 | 36 | } | |
| @@ -29,5 +29,8 @@ | |||||
| 29 | "typescript": "^5.3.0", | 29 | "typescript": "^5.3.0", | ||
| 30 | "typescript-eslint": "^8.43.0", | 30 | "typescript-eslint": "^8.43.0", | ||
| 31 | "vite": "^5.0.0" | 31 | "vite": "^5.0.0" | ||
| 32 | + | }, | |||
| 33 | + | "dependencies": { | |||
| 34 | + | "@lucide/svelte": "^0.544.0" | |||
| 32 | } | 35 | } | ||
| 33 | } | 36 | } | ||
M gui/src/pages/Homepage.svelte +101 -12
| @@ -33,7 +33,13 @@ | |||
| 33 | 33 | let topVisitedLoading = $state(true); | |
| 34 | 34 | let shortcutsLoading = $state(true); | |
| 35 | 35 | let statsLoading = $state(true); | |
| 36 | - | let deletingIds = $state<Set<number>>(new Set()); | |
| 36 | + | let deletingIds = $state<number[]>([]); | |
| 37 | + | let historyOffset = $state(0); | |
| 38 | + | let hasMoreHistory = $state(true); | |
| 39 | + | let loadingMoreHistory = $state(false); | |
| 40 | + | ||
| 41 | + | // Function for checking if an item is being deleted | |
| 42 | + | const isDeleting = (id: number) => deletingIds.includes(id); | |
| 37 | 43 | ||
| 38 | 44 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | |
| 39 | 45 | const initializeShortcuts = () => { | |
| @@ -109,18 +115,50 @@ | |||
| 109 | 115 | if (response.ok) { | |
| 110 | 116 | const data = await response.json(); | |
| 111 | 117 | historyItems = Array.isArray(data) ? data : []; | |
| 118 | + | historyOffset = historyItems.length; | |
| 119 | + | hasMoreHistory = data.length === 20; // If we got full batch, there might be more | |
| 112 | 120 | } else { | |
| 113 | 121 | console.warn('Failed to fetch history:', response.status); | |
| 114 | 122 | historyItems = []; | |
| 123 | + | hasMoreHistory = false; | |
| 115 | 124 | } | |
| 116 | 125 | } catch (error) { | |
| 117 | 126 | console.error('Error fetching history:', error); | |
| 118 | 127 | historyItems = []; | |
| 128 | + | hasMoreHistory = false; | |
| 119 | 129 | } finally { | |
| 120 | 130 | historyLoading = false; | |
| 121 | 131 | } | |
| 122 | 132 | }; | |
| 123 | 133 | ||
| 134 | + | // Load more history items for infinite scroll | |
| 135 | + | const loadMoreHistory = async () => { | |
| 136 | + | if (loadingMoreHistory || !hasMoreHistory) return; | |
| 137 | + | ||
| 138 | + | loadingMoreHistory = true; | |
| 139 | + | try { | |
| 140 | + | const response = await fetch(`dumb://homepage/api/history/recent?limit=20&offset=${historyOffset}`); | |
| 141 | + | if (response.ok) { | |
| 142 | + | const data = await response.json(); | |
| 143 | + | if (Array.isArray(data) && data.length > 0) { | |
| 144 | + | historyItems = [...historyItems, ...data]; | |
| 145 | + | historyOffset += data.length; | |
| 146 | + | hasMoreHistory = data.length === 20; // If we got full batch, there might be more | |
| 147 | + | } else { | |
| 148 | + | hasMoreHistory = false; | |
| 149 | + | } | |
| 150 | + | } else { | |
| 151 | + | console.warn('Failed to fetch more history:', response.status); | |
| 152 | + | hasMoreHistory = false; | |
| 153 | + | } | |
| 154 | + | } catch (error) { | |
| 155 | + | console.error('Error fetching more history:', error); | |
| 156 | + | hasMoreHistory = false; | |
| 157 | + | } finally { | |
| 158 | + | loadingMoreHistory = false; | |
| 159 | + | } | |
| 160 | + | }; | |
| 161 | + | ||
| 124 | 162 | // Navigate to a URL when history item is clicked | |
| 125 | 163 | const navigateTo = (url: string) => { | |
| 126 | 164 | window.location.href = url; | |
| @@ -130,8 +168,10 @@ | |||
| 130 | 168 | const deleteHistoryEntry = async (id: number, event: Event) => { | |
| 131 | 169 | event.stopPropagation(); // Prevent navigation | |
| 132 | 170 | ||
| 133 | - | // Add to deleting set for animation | |
| 134 | - | deletingIds = new Set([...deletingIds, id]); | |
| 171 | + | // Add to deleting array for animation | |
| 172 | + | if (!deletingIds.includes(id)) { | |
| 173 | + | deletingIds = [...deletingIds, id]; | |
| 174 | + | } | |
| 135 | 175 | ||
| 136 | 176 | try { | |
| 137 | 177 | const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`); | |
| @@ -139,23 +179,25 @@ | |||
| 139 | 179 | // Wait a bit for the fade animation to complete | |
| 140 | 180 | await new Promise(resolve => setTimeout(resolve, 200)); | |
| 141 | 181 | ||
| 142 | - | // Refresh history after successful deletion | |
| 182 | + | // Reset pagination and refresh history after successful deletion | |
| 183 | + | historyOffset = 0; | |
| 184 | + | hasMoreHistory = true; | |
| 143 | 185 | await fetchHistory(); | |
| 144 | 186 | await fetchStats(); | |
| 145 | 187 | await fetchTopVisited(); | |
| 146 | 188 | } else { | |
| 147 | 189 | console.error('Failed to delete history entry:', response.status); | |
| 148 | - | // Remove from deleting set if failed | |
| 149 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | |
| 190 | + | // Remove from deleting array if failed | |
| 191 | + | deletingIds = deletingIds.filter(i => i !== id); | |
| 150 | 192 | } | |
| 151 | 193 | } catch (error) { | |
| 152 | 194 | console.error('Error deleting history entry:', error); | |
| 153 | - | // Remove from deleting set if failed | |
| 154 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | |
| 195 | + | // Remove from deleting array if failed | |
| 196 | + | deletingIds = deletingIds.filter(i => i !== id); | |
| 155 | 197 | } | |
| 156 | 198 | ||
| 157 | - | // Remove from deleting set after refresh | |
| 158 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | |
| 199 | + | // Remove from deleting array after refresh | |
| 200 | + | deletingIds = deletingIds.filter(i => i !== id); | |
| 159 | 201 | }; | |
| 160 | 202 | ||
| 161 | 203 | // Get domain from URL | |
| @@ -227,9 +269,36 @@ | |||
| 227 | 269 | } | |
| 228 | 270 | }; | |
| 229 | 271 | ||
| 272 | + | // Intersection Observer for infinite scroll | |
| 273 | + | let historyScrollSentinel = $state<HTMLElement | null>(null); | |
| 274 | + | ||
| 275 | + | const setupInfiniteScroll = () => { | |
| 276 | + | if (!historyScrollSentinel) return; | |
| 277 | + | ||
| 278 | + | const observer = new IntersectionObserver( | |
| 279 | + | (entries) => { | |
| 280 | + | const entry = entries[0]; | |
| 281 | + | if (entry && entry.isIntersecting && hasMoreHistory && !loadingMoreHistory) { | |
| 282 | + | loadMoreHistory(); | |
| 283 | + | } | |
| 284 | + | }, | |
| 285 | + | { | |
| 286 | + | threshold: 0.1, | |
| 287 | + | rootMargin: '50px' | |
| 288 | + | } | |
| 289 | + | ); | |
| 290 | + | ||
| 291 | + | observer.observe(historyScrollSentinel); | |
| 292 | + | ||
| 293 | + | return () => observer.disconnect(); | |
| 294 | + | }; | |
| 295 | + | ||
| 230 | 296 | onMount(() => { | |
| 231 | 297 | initializeShortcuts(); | |
| 232 | - | fetchHistory(); | |
| 298 | + | fetchHistory().then(() => { | |
| 299 | + | // Setup infinite scroll after initial history is loaded | |
| 300 | + | setTimeout(setupInfiniteScroll, 100); | |
| 301 | + | }); | |
| 233 | 302 | fetchStats(); | |
| 234 | 303 | fetchTopVisited(); | |
| 235 | 304 | }); | |
| @@ -252,7 +321,7 @@ | |||
| 252 | 321 | {#each historyItems as item (item.url)} | |
| 253 | 322 | <div | |
| 254 | 323 | class="history-item" | |
| 255 | - | class:deleting={deletingIds.has(item.id)} | |
| 324 | + | class:deleting={isDeleting(item.id)} | |
| 256 | 325 | role="button" | |
| 257 | 326 | tabindex="0" | |
| 258 | 327 | onclick={() => navigateTo(item.url)} | |
| @@ -309,6 +378,14 @@ | |||
| 309 | 378 | </div> | |
| 310 | 379 | </div> | |
| 311 | 380 | {/each} | |
| 381 | + | ||
| 382 | + | <!-- Infinite scroll sentinel and loading indicator --> | |
| 383 | + | {#if hasMoreHistory} | |
| 384 | + | <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div> | |
| 385 | + | {#if loadingMoreHistory} | |
| 386 | + | <div class="loading-more">Loading more history...</div> | |
| 387 | + | {/if} | |
| 388 | + | {/if} | |
| 312 | 389 | {/if} | |
| 313 | 390 | </div> | |
| 314 | 391 | </div> | |
| @@ -725,6 +802,18 @@ | |||
| 725 | 802 | color: #888; | |
| 726 | 803 | } | |
| 727 | 804 | ||
| 805 | + | .scroll-sentinel { | |
| 806 | + | height: 1px; | |
| 807 | + | width: 100%; | |
| 808 | + | } | |
| 809 | + | ||
| 810 | + | .loading-more { | |
| 811 | + | padding: 1rem; | |
| 812 | + | text-align: center; | |
| 813 | + | color: #888; | |
| 814 | + | font-size: 0.9rem; | |
| 815 | + | } | |
| 816 | + | ||
| 728 | 817 | .empty-state { | |
| 729 | 818 | padding: 3rem 2rem; | |
| 730 | 819 | text-align: center; | |
| @@ -33,7 +33,13 @@ | |||||
| 33 | let topVisitedLoading = $state(true); | 33 | let topVisitedLoading = $state(true); | ||
| 34 | let shortcutsLoading = $state(true); | 34 | let shortcutsLoading = $state(true); | ||
| 35 | let statsLoading = $state(true); | 35 | let statsLoading = $state(true); | ||
| 36 | - | let deletingIds = $state<Set<number>>(new Set()); | 36 | + | let deletingIds = $state<number[]>([]); |
| 37 | + | let historyOffset = $state(0); | |||
| 38 | + | let hasMoreHistory = $state(true); | |||
| 39 | + | let loadingMoreHistory = $state(false); | |||
| 40 | + | ||||
| 41 | + | // Function for checking if an item is being deleted | |||
| 42 | + | const isDeleting = (id: number) => deletingIds.includes(id); | |||
| 37 | 43 | ||||
| 38 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | 44 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | ||
| 39 | const initializeShortcuts = () => { | 45 | const initializeShortcuts = () => { | ||
| @@ -109,18 +115,50 @@ | |||||
| 109 | if (response.ok) { | 115 | if (response.ok) { | ||
| 110 | const data = await response.json(); | 116 | const data = await response.json(); | ||
| 111 | historyItems = Array.isArray(data) ? data : []; | 117 | historyItems = Array.isArray(data) ? data : []; | ||
| 118 | + | historyOffset = historyItems.length; | |||
| 119 | + | hasMoreHistory = data.length === 20; // If we got full batch, there might be more | |||
| 112 | } else { | 120 | } else { | ||
| 113 | console.warn('Failed to fetch history:', response.status); | 121 | console.warn('Failed to fetch history:', response.status); | ||
| 114 | historyItems = []; | 122 | historyItems = []; | ||
| 123 | + | hasMoreHistory = false; | |||
| 115 | } | 124 | } | ||
| 116 | } catch (error) { | 125 | } catch (error) { | ||
| 117 | console.error('Error fetching history:', error); | 126 | console.error('Error fetching history:', error); | ||
| 118 | historyItems = []; | 127 | historyItems = []; | ||
| 128 | + | hasMoreHistory = false; | |||
| 119 | } finally { | 129 | } finally { | ||
| 120 | historyLoading = false; | 130 | historyLoading = false; | ||
| 121 | } | 131 | } | ||
| 122 | }; | 132 | }; | ||
| 123 | 133 | ||||
| 134 | + | // Load more history items for infinite scroll | |||
| 135 | + | const loadMoreHistory = async () => { | |||
| 136 | + | if (loadingMoreHistory || !hasMoreHistory) return; | |||
| 137 | + | ||||
| 138 | + | loadingMoreHistory = true; | |||
| 139 | + | try { | |||
| 140 | + | const response = await fetch(`dumb://homepage/api/history/recent?limit=20&offset=${historyOffset}`); | |||
| 141 | + | if (response.ok) { | |||
| 142 | + | const data = await response.json(); | |||
| 143 | + | if (Array.isArray(data) && data.length > 0) { | |||
| 144 | + | historyItems = [...historyItems, ...data]; | |||
| 145 | + | historyOffset += data.length; | |||
| 146 | + | hasMoreHistory = data.length === 20; // If we got full batch, there might be more | |||
| 147 | + | } else { | |||
| 148 | + | hasMoreHistory = false; | |||
| 149 | + | } | |||
| 150 | + | } else { | |||
| 151 | + | console.warn('Failed to fetch more history:', response.status); | |||
| 152 | + | hasMoreHistory = false; | |||
| 153 | + | } | |||
| 154 | + | } catch (error) { | |||
| 155 | + | console.error('Error fetching more history:', error); | |||
| 156 | + | hasMoreHistory = false; | |||
| 157 | + | } finally { | |||
| 158 | + | loadingMoreHistory = false; | |||
| 159 | + | } | |||
| 160 | + | }; | |||
| 161 | + | ||||
| 124 | // Navigate to a URL when history item is clicked | 162 | // Navigate to a URL when history item is clicked | ||
| 125 | const navigateTo = (url: string) => { | 163 | const navigateTo = (url: string) => { | ||
| 126 | window.location.href = url; | 164 | window.location.href = url; | ||
| @@ -130,8 +168,10 @@ | |||||
| 130 | const deleteHistoryEntry = async (id: number, event: Event) => { | 168 | const deleteHistoryEntry = async (id: number, event: Event) => { | ||
| 131 | event.stopPropagation(); // Prevent navigation | 169 | event.stopPropagation(); // Prevent navigation | ||
| 132 | 170 | ||||
| 133 | - | // Add to deleting set for animation | 171 | + | // Add to deleting array for animation |
| 134 | - | deletingIds = new Set([...deletingIds, id]); | 172 | + | if (!deletingIds.includes(id)) { |
| 173 | + | deletingIds = [...deletingIds, id]; | |||
| 174 | + | } | |||
| 135 | 175 | ||||
| 136 | try { | 176 | try { | ||
| 137 | const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`); | 177 | const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`); | ||
| @@ -139,23 +179,25 @@ | |||||
| 139 | // Wait a bit for the fade animation to complete | 179 | // Wait a bit for the fade animation to complete | ||
| 140 | await new Promise(resolve => setTimeout(resolve, 200)); | 180 | await new Promise(resolve => setTimeout(resolve, 200)); | ||
| 141 | 181 | ||||
| 142 | - | // Refresh history after successful deletion | 182 | + | // Reset pagination and refresh history after successful deletion |
| 183 | + | historyOffset = 0; | |||
| 184 | + | hasMoreHistory = true; | |||
| 143 | await fetchHistory(); | 185 | await fetchHistory(); | ||
| 144 | await fetchStats(); | 186 | await fetchStats(); | ||
| 145 | await fetchTopVisited(); | 187 | await fetchTopVisited(); | ||
| 146 | } else { | 188 | } else { | ||
| 147 | console.error('Failed to delete history entry:', response.status); | 189 | console.error('Failed to delete history entry:', response.status); | ||
| 148 | - | // Remove from deleting set if failed | 190 | + | // Remove from deleting array if failed |
| 149 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | 191 | + | deletingIds = deletingIds.filter(i => i !== id); |
| 150 | } | 192 | } | ||
| 151 | } catch (error) { | 193 | } catch (error) { | ||
| 152 | console.error('Error deleting history entry:', error); | 194 | console.error('Error deleting history entry:', error); | ||
| 153 | - | // Remove from deleting set if failed | 195 | + | // Remove from deleting array if failed |
| 154 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | 196 | + | deletingIds = deletingIds.filter(i => i !== id); |
| 155 | } | 197 | } | ||
| 156 | 198 | ||||
| 157 | - | // Remove from deleting set after refresh | 199 | + | // Remove from deleting array after refresh |
| 158 | - | deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id)); | 200 | + | deletingIds = deletingIds.filter(i => i !== id); |
| 159 | }; | 201 | }; | ||
| 160 | 202 | ||||
| 161 | // Get domain from URL | 203 | // Get domain from URL | ||
| @@ -227,9 +269,36 @@ | |||||
| 227 | } | 269 | } | ||
| 228 | }; | 270 | }; | ||
| 229 | 271 | ||||
| 272 | + | // Intersection Observer for infinite scroll | |||
| 273 | + | let historyScrollSentinel = $state<HTMLElement | null>(null); | |||
| 274 | + | ||||
| 275 | + | const setupInfiniteScroll = () => { | |||
| 276 | + | if (!historyScrollSentinel) return; | |||
| 277 | + | ||||
| 278 | + | const observer = new IntersectionObserver( | |||
| 279 | + | (entries) => { | |||
| 280 | + | const entry = entries[0]; | |||
| 281 | + | if (entry && entry.isIntersecting && hasMoreHistory && !loadingMoreHistory) { | |||
| 282 | + | loadMoreHistory(); | |||
| 283 | + | } | |||
| 284 | + | }, | |||
| 285 | + | { | |||
| 286 | + | threshold: 0.1, | |||
| 287 | + | rootMargin: '50px' | |||
| 288 | + | } | |||
| 289 | + | ); | |||
| 290 | + | ||||
| 291 | + | observer.observe(historyScrollSentinel); | |||
| 292 | + | ||||
| 293 | + | return () => observer.disconnect(); | |||
| 294 | + | }; | |||
| 295 | + | ||||
| 230 | onMount(() => { | 296 | onMount(() => { | ||
| 231 | initializeShortcuts(); | 297 | initializeShortcuts(); | ||
| 232 | - | fetchHistory(); | 298 | + | fetchHistory().then(() => { |
| 299 | + | // Setup infinite scroll after initial history is loaded | |||
| 300 | + | setTimeout(setupInfiniteScroll, 100); | |||
| 301 | + | }); | |||
| 233 | fetchStats(); | 302 | fetchStats(); | ||
| 234 | fetchTopVisited(); | 303 | fetchTopVisited(); | ||
| 235 | }); | 304 | }); | ||
| @@ -252,7 +321,7 @@ | |||||
| 252 | {#each historyItems as item (item.url)} | 321 | {#each historyItems as item (item.url)} | ||
| 253 | <div | 322 | <div | ||
| 254 | class="history-item" | 323 | class="history-item" | ||
| 255 | - | class:deleting={deletingIds.has(item.id)} | 324 | + | class:deleting={isDeleting(item.id)} |
| 256 | role="button" | 325 | role="button" | ||
| 257 | tabindex="0" | 326 | tabindex="0" | ||
| 258 | onclick={() => navigateTo(item.url)} | 327 | onclick={() => navigateTo(item.url)} | ||
| @@ -309,6 +378,14 @@ | |||||
| 309 | </div> | 378 | </div> | ||
| 310 | </div> | 379 | </div> | ||
| 311 | {/each} | 380 | {/each} | ||
| 381 | + | ||||
| 382 | + | <!-- Infinite scroll sentinel and loading indicator --> | |||
| 383 | + | {#if hasMoreHistory} | |||
| 384 | + | <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div> | |||
| 385 | + | {#if loadingMoreHistory} | |||
| 386 | + | <div class="loading-more">Loading more history...</div> | |||
| 387 | + | {/if} | |||
| 388 | + | {/if} | |||
| 312 | {/if} | 389 | {/if} | ||
| 313 | </div> | 390 | </div> | ||
| 314 | </div> | 391 | </div> | ||
| @@ -725,6 +802,18 @@ | |||||
| 725 | color: #888; | 802 | color: #888; | ||
| 726 | } | 803 | } | ||
| 727 | 804 | ||||
| 805 | + | .scroll-sentinel { | |||
| 806 | + | height: 1px; | |||
| 807 | + | width: 100%; | |||
| 808 | + | } | |||
| 809 | + | ||||
| 810 | + | .loading-more { | |||
| 811 | + | padding: 1rem; | |||
| 812 | + | text-align: center; | |||
| 813 | + | color: #888; | |||
| 814 | + | font-size: 0.9rem; | |||
| 815 | + | } | |||
| 816 | + | ||||
| 728 | .empty-state { | 817 | .empty-state { | ||
| 729 | padding: 3rem 2rem; | 818 | padding: 3rem 2rem; | ||
| 730 | text-align: center; | 819 | text-align: center; | ||
M internal/app/api/routes.go +34 -5
| @@ -58,19 +58,48 @@ func (h *Handler) HandleConfig(cfg *config.Config) (string, []byte, bool) { | |||
| 58 | 58 | // HandleHistoryRecent handles GET /history/recent endpoint | |
| 59 | 59 | func (h *Handler) HandleHistoryRecent(u *neturl.URL) (string, []byte, bool) { | |
| 60 | 60 | log.Printf("[api] GET /history/recent%s", u.RawQuery) | |
| 61 | - | // Parse limit | |
| 61 | + | // Parse limit and offset | |
| 62 | 62 | q := u.Query() | |
| 63 | - | limit := 50 | |
| 63 | + | limit := 20 | |
| 64 | 64 | if l := q.Get("limit"); l != "" { | |
| 65 | 65 | if n, err := strconv.Atoi(l); err == nil && n > 0 { | |
| 66 | 66 | limit = n | |
| 67 | 67 | } | |
| 68 | 68 | } | |
| 69 | + | offset := 0 | |
| 70 | + | if o := q.Get("offset"); o != "" { | |
| 71 | + | if n, err := strconv.Atoi(o); err == nil && n >= 0 { | |
| 72 | + | offset = n | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 69 | 76 | ctx := context.Background() | |
| 70 | - | entries, err := h.browserService.GetRecentHistory(ctx, limit) | |
| 71 | - | if err != nil { | |
| 72 | - | return constants.ContentTypeJSON, []byte("[]"), true | |
| 77 | + | var entries []interface{} | |
| 78 | + | ||
| 79 | + | // Use paginated version if offset is provided | |
| 80 | + | if offset > 0 || q.Has("offset") { | |
| 81 | + | histEntries, err := h.browserService.GetRecentHistoryWithOffset(ctx, limit, offset) | |
| 82 | + | if err != nil { | |
| 83 | + | return constants.ContentTypeJSON, []byte("[]"), true | |
| 84 | + | } | |
| 85 | + | // Convert to interface slice for JSON marshaling | |
| 86 | + | entries = make([]interface{}, len(histEntries)) | |
| 87 | + | for i, entry := range histEntries { | |
| 88 | + | entries[i] = entry | |
| 89 | + | } | |
| 90 | + | } else { | |
| 91 | + | // Use original method for backward compatibility | |
| 92 | + | histEntries, err := h.browserService.GetRecentHistory(ctx, limit) | |
| 93 | + | if err != nil { | |
| 94 | + | return constants.ContentTypeJSON, []byte("[]"), true | |
| 95 | + | } | |
| 96 | + | // Convert to interface slice for JSON marshaling | |
| 97 | + | entries = make([]interface{}, len(histEntries)) | |
| 98 | + | for i, entry := range histEntries { | |
| 99 | + | entries[i] = entry | |
| 100 | + | } | |
| 73 | 101 | } | |
| 102 | + | ||
| 74 | 103 | b, _ := json.Marshal(entries) | |
| 75 | 104 | return constants.ContentTypeJSON, b, true | |
| 76 | 105 | } | |
| @@ -58,19 +58,48 @@ func (h *Handler) HandleConfig(cfg *config.Config) (string, []byte, bool) { | |||||
| 58 | // HandleHistoryRecent handles GET /history/recent endpoint | 58 | // HandleHistoryRecent handles GET /history/recent endpoint | ||
| 59 | func (h *Handler) HandleHistoryRecent(u *neturl.URL) (string, []byte, bool) { | 59 | func (h *Handler) HandleHistoryRecent(u *neturl.URL) (string, []byte, bool) { | ||
| 60 | log.Printf("[api] GET /history/recent%s", u.RawQuery) | 60 | log.Printf("[api] GET /history/recent%s", u.RawQuery) | ||
| 61 | - | // Parse limit | 61 | + | // Parse limit and offset |
| 62 | q := u.Query() | 62 | q := u.Query() | ||
| 63 | - | limit := 50 | 63 | + | limit := 20 |
| 64 | if l := q.Get("limit"); l != "" { | 64 | if l := q.Get("limit"); l != "" { | ||
| 65 | if n, err := strconv.Atoi(l); err == nil && n > 0 { | 65 | if n, err := strconv.Atoi(l); err == nil && n > 0 { | ||
| 66 | limit = n | 66 | limit = n | ||
| 67 | } | 67 | } | ||
| 68 | } | 68 | } | ||
| 69 | + | offset := 0 | |||
| 70 | + | if o := q.Get("offset"); o != "" { | |||
| 71 | + | if n, err := strconv.Atoi(o); err == nil && n >= 0 { | |||
| 72 | + | offset = n | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 69 | ctx := context.Background() | 76 | ctx := context.Background() | ||
| 70 | - | entries, err := h.browserService.GetRecentHistory(ctx, limit) | 77 | + | var entries []interface{} |
| 71 | - | if err != nil { | 78 | + | |
| 72 | - | return constants.ContentTypeJSON, []byte("[]"), true | 79 | + | // Use paginated version if offset is provided |
| 80 | + | if offset > 0 || q.Has("offset") { | |||
| 81 | + | histEntries, err := h.browserService.GetRecentHistoryWithOffset(ctx, limit, offset) | |||
| 82 | + | if err != nil { | |||
| 83 | + | return constants.ContentTypeJSON, []byte("[]"), true | |||
| 84 | + | } | |||
| 85 | + | // Convert to interface slice for JSON marshaling | |||
| 86 | + | entries = make([]interface{}, len(histEntries)) | |||
| 87 | + | for i, entry := range histEntries { | |||
| 88 | + | entries[i] = entry | |||
| 89 | + | } | |||
| 90 | + | } else { | |||
| 91 | + | // Use original method for backward compatibility | |||
| 92 | + | histEntries, err := h.browserService.GetRecentHistory(ctx, limit) | |||
| 93 | + | if err != nil { | |||
| 94 | + | return constants.ContentTypeJSON, []byte("[]"), true | |||
| 95 | + | } | |||
| 96 | + | // Convert to interface slice for JSON marshaling | |||
| 97 | + | entries = make([]interface{}, len(histEntries)) | |||
| 98 | + | for i, entry := range histEntries { | |||
| 99 | + | entries[i] = entry | |||
| 100 | + | } | |||
| 73 | } | 101 | } | ||
| 102 | + | ||||
| 74 | b, _ := json.Marshal(entries) | 103 | b, _ := json.Marshal(entries) | ||
| 75 | return constants.ContentTypeJSON, b, true | 104 | return constants.ContentTypeJSON, b, true | ||
| 76 | } | 105 | } | ||
M internal/db/history.sql.go +38 -0
| @@ -73,6 +73,44 @@ func (q *Queries) GetHistory(ctx context.Context, limit int64) ([]History, error | |||
| 73 | 73 | return items, nil | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | const GetHistoryWithOffset = `-- name: GetHistoryWithOffset :many | |
| 77 | + | SELECT id, url, title, visit_count, last_visited, created_at, favicon_url | |
| 78 | + | FROM history | |
| 79 | + | ORDER BY last_visited DESC | |
| 80 | + | LIMIT ? OFFSET ? | |
| 81 | + | ` | |
| 82 | + | ||
| 83 | + | func (q *Queries) GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) { | |
| 84 | + | rows, err := q.db.QueryContext(ctx, GetHistoryWithOffset, limit, offset) | |
| 85 | + | if err != nil { | |
| 86 | + | return nil, err | |
| 87 | + | } | |
| 88 | + | defer rows.Close() | |
| 89 | + | items := []History{} | |
| 90 | + | for rows.Next() { | |
| 91 | + | var i History | |
| 92 | + | if err := rows.Scan( | |
| 93 | + | &i.ID, | |
| 94 | + | &i.Url, | |
| 95 | + | &i.Title, | |
| 96 | + | &i.VisitCount, | |
| 97 | + | &i.LastVisited, | |
| 98 | + | &i.CreatedAt, | |
| 99 | + | &i.FaviconUrl, | |
| 100 | + | ); err != nil { | |
| 101 | + | return nil, err | |
| 102 | + | } | |
| 103 | + | items = append(items, i) | |
| 104 | + | } | |
| 105 | + | if err := rows.Close(); err != nil { | |
| 106 | + | return nil, err | |
| 107 | + | } | |
| 108 | + | if err := rows.Err(); err != nil { | |
| 109 | + | return nil, err | |
| 110 | + | } | |
| 111 | + | return items, nil | |
| 112 | + | } | |
| 113 | + | ||
| 76 | 114 | const GetShortcuts = `-- name: GetShortcuts :many | |
| 77 | 115 | SELECT id, shortcut, url_template, description, created_at | |
| 78 | 116 | FROM shortcuts | |
| @@ -73,6 +73,44 @@ func (q *Queries) GetHistory(ctx context.Context, limit int64) ([]History, error | |||||
| 73 | return items, nil | 73 | return items, nil | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | + | const GetHistoryWithOffset = `-- name: GetHistoryWithOffset :many | |||
| 77 | + | SELECT id, url, title, visit_count, last_visited, created_at, favicon_url | |||
| 78 | + | FROM history | |||
| 79 | + | ORDER BY last_visited DESC | |||
| 80 | + | LIMIT ? OFFSET ? | |||
| 81 | + | ` | |||
| 82 | + | ||||
| 83 | + | func (q *Queries) GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) { | |||
| 84 | + | rows, err := q.db.QueryContext(ctx, GetHistoryWithOffset, limit, offset) | |||
| 85 | + | if err != nil { | |||
| 86 | + | return nil, err | |||
| 87 | + | } | |||
| 88 | + | defer rows.Close() | |||
| 89 | + | items := []History{} | |||
| 90 | + | for rows.Next() { | |||
| 91 | + | var i History | |||
| 92 | + | if err := rows.Scan( | |||
| 93 | + | &i.ID, | |||
| 94 | + | &i.Url, | |||
| 95 | + | &i.Title, | |||
| 96 | + | &i.VisitCount, | |||
| 97 | + | &i.LastVisited, | |||
| 98 | + | &i.CreatedAt, | |||
| 99 | + | &i.FaviconUrl, | |||
| 100 | + | ); err != nil { | |||
| 101 | + | return nil, err | |||
| 102 | + | } | |||
| 103 | + | items = append(items, i) | |||
| 104 | + | } | |||
| 105 | + | if err := rows.Close(); err != nil { | |||
| 106 | + | return nil, err | |||
| 107 | + | } | |||
| 108 | + | if err := rows.Err(); err != nil { | |||
| 109 | + | return nil, err | |||
| 110 | + | } | |||
| 111 | + | return items, nil | |||
| 112 | + | } | |||
| 113 | + | ||||
| 76 | const GetShortcuts = `-- name: GetShortcuts :many | 114 | const GetShortcuts = `-- name: GetShortcuts :many | ||
| 77 | SELECT id, shortcut, url_template, description, created_at | 115 | SELECT id, shortcut, url_template, description, created_at | ||
| 78 | FROM shortcuts | 116 | FROM shortcuts | ||
M internal/db/interfaces.go +1 -0
| @@ -17,6 +17,7 @@ type ZoomQuerier interface { | |||
| 17 | 17 | // HistoryQuerier defines the interface for history-related database operations | |
| 18 | 18 | type HistoryQuerier interface { | |
| 19 | 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | |
| 20 | + | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | |
| 20 | 21 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | |
| 21 | 22 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | |
| 22 | 23 | UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error | |
| @@ -17,6 +17,7 @@ type ZoomQuerier interface { | |||||
| 17 | // HistoryQuerier defines the interface for history-related database operations | 17 | // HistoryQuerier defines the interface for history-related database operations | ||
| 18 | type HistoryQuerier interface { | 18 | type HistoryQuerier interface { | ||
| 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | ||
| 20 | + | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | |||
| 20 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | 21 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | ||
| 21 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | 22 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | ||
| 22 | UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error | 23 | UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error | ||
M internal/db/querier.go +1 -0
| @@ -20,6 +20,7 @@ type Querier interface { | |||
| 20 | 20 | DeleteZoomLevel(ctx context.Context, domain string) error | |
| 21 | 21 | GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) | |
| 22 | 22 | GetHistory(ctx context.Context, limit int64) ([]History, error) | |
| 23 | + | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | |
| 23 | 24 | GetShortcuts(ctx context.Context) ([]Shortcut, error) | |
| 24 | 25 | // Get zoom level for a specific domain | |
| 25 | 26 | GetZoomLevel(ctx context.Context, domain string) (float64, error) | |
| @@ -20,6 +20,7 @@ type Querier interface { | |||||
| 20 | DeleteZoomLevel(ctx context.Context, domain string) error | 20 | DeleteZoomLevel(ctx context.Context, domain string) error | ||
| 21 | GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) | 21 | GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) | ||
| 22 | GetHistory(ctx context.Context, limit int64) ([]History, error) | 22 | GetHistory(ctx context.Context, limit int64) ([]History, error) | ||
| 23 | + | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | |||
| 23 | GetShortcuts(ctx context.Context) ([]Shortcut, error) | 24 | GetShortcuts(ctx context.Context) ([]Shortcut, error) | ||
| 24 | // Get zoom level for a specific domain | 25 | // Get zoom level for a specific domain | ||
| 25 | GetZoomLevel(ctx context.Context, domain string) (float64, error) | 26 | GetZoomLevel(ctx context.Context, domain string) (float64, error) | ||
M internal/db/queries/history.sql +6 -0
| @@ -4,6 +4,12 @@ FROM history | |||
| 4 | 4 | ORDER BY last_visited DESC | |
| 5 | 5 | LIMIT ?; | |
| 6 | 6 | ||
| 7 | + | -- name: GetHistoryWithOffset :many | |
| 8 | + | SELECT * | |
| 9 | + | FROM history | |
| 10 | + | ORDER BY last_visited DESC | |
| 11 | + | LIMIT ? OFFSET ?; | |
| 12 | + | ||
| 7 | 13 | -- name: AddOrUpdateHistory :exec | |
| 8 | 14 | INSERT INTO history (url, title) | |
| 9 | 15 | VALUES (?, ?) | |
| @@ -4,6 +4,12 @@ FROM history | |||||
| 4 | ORDER BY last_visited DESC | 4 | ORDER BY last_visited DESC | ||
| 5 | LIMIT ?; | 5 | LIMIT ?; | ||
| 6 | 6 | ||||
| 7 | + | -- name: GetHistoryWithOffset :many | |||
| 8 | + | SELECT * | |||
| 9 | + | FROM history | |||
| 10 | + | ORDER BY last_visited DESC | |||
| 11 | + | LIMIT ? OFFSET ?; | |||
| 12 | + | ||||
| 7 | -- name: AddOrUpdateHistory :exec | 13 | -- name: AddOrUpdateHistory :exec | ||
| 8 | INSERT INTO history (url, title) | 14 | INSERT INTO history (url, title) | ||
| 9 | VALUES (?, ?) | 15 | VALUES (?, ?) | ||
M services/browser_service.go +32 -0
| @@ -250,6 +250,38 @@ func (s *BrowserService) GetRecentHistory(ctx context.Context, limit int) ([]His | |||
| 250 | 250 | return result, nil | |
| 251 | 251 | } | |
| 252 | 252 | ||
| 253 | + | // GetRecentHistoryWithOffset returns recent browser history entries with pagination support. | |
| 254 | + | func (s *BrowserService) GetRecentHistoryWithOffset(ctx context.Context, limit, offset int) ([]HistoryEntry, error) { | |
| 255 | + | if limit <= 0 { | |
| 256 | + | limit = 20 // Default limit for pagination | |
| 257 | + | } | |
| 258 | + | if offset < 0 { | |
| 259 | + | offset = 0 | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | entries, err := s.dbQueries.GetHistoryWithOffset(ctx, int64(limit), int64(offset)) | |
| 263 | + | if err != nil { | |
| 264 | + | return nil, err | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | result := make([]HistoryEntry, len(entries)) | |
| 268 | + | for i, entry := range entries { | |
| 269 | + | // Defensive cast to int32 to prevent overflow | |
| 270 | + | vc := clampToInt32(entry.VisitCount.Int64) | |
| 271 | + | result[i] = HistoryEntry{ | |
| 272 | + | ID: entry.ID, | |
| 273 | + | URL: entry.Url, | |
| 274 | + | Title: entry.Title.String, | |
| 275 | + | FaviconURL: entry.FaviconUrl.String, | |
| 276 | + | VisitCount: vc, | |
| 277 | + | LastVisited: entry.LastVisited.Time, | |
| 278 | + | CreatedAt: entry.CreatedAt.Time, | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | return result, nil | |
| 283 | + | } | |
| 284 | + | ||
| 253 | 285 | // SearchHistory searches browser history. | |
| 254 | 286 | func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit int) ([]HistoryEntry, error) { | |
| 255 | 287 | if query == "" { | |
| @@ -250,6 +250,38 @@ func (s *BrowserService) GetRecentHistory(ctx context.Context, limit int) ([]His | |||||
| 250 | return result, nil | 250 | return result, nil | ||
| 251 | } | 251 | } | ||
| 252 | 252 | ||||
| 253 | + | // GetRecentHistoryWithOffset returns recent browser history entries with pagination support. | |||
| 254 | + | func (s *BrowserService) GetRecentHistoryWithOffset(ctx context.Context, limit, offset int) ([]HistoryEntry, error) { | |||
| 255 | + | if limit <= 0 { | |||
| 256 | + | limit = 20 // Default limit for pagination | |||
| 257 | + | } | |||
| 258 | + | if offset < 0 { | |||
| 259 | + | offset = 0 | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | entries, err := s.dbQueries.GetHistoryWithOffset(ctx, int64(limit), int64(offset)) | |||
| 263 | + | if err != nil { | |||
| 264 | + | return nil, err | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | result := make([]HistoryEntry, len(entries)) | |||
| 268 | + | for i, entry := range entries { | |||
| 269 | + | // Defensive cast to int32 to prevent overflow | |||
| 270 | + | vc := clampToInt32(entry.VisitCount.Int64) | |||
| 271 | + | result[i] = HistoryEntry{ | |||
| 272 | + | ID: entry.ID, | |||
| 273 | + | URL: entry.Url, | |||
| 274 | + | Title: entry.Title.String, | |||
| 275 | + | FaviconURL: entry.FaviconUrl.String, | |||
| 276 | + | VisitCount: vc, | |||
| 277 | + | LastVisited: entry.LastVisited.Time, | |||
| 278 | + | CreatedAt: entry.CreatedAt.Time, | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | return result, nil | |||
| 283 | + | } | |||
| 284 | + | ||||
| 253 | // SearchHistory searches browser history. | 285 | // SearchHistory searches browser history. | ||
| 254 | func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit int) ([]HistoryEntry, error) { | 286 | func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit int) ([]HistoryEntry, error) { | ||
| 255 | if query == "" { | 287 | if query == "" { | ||