commit 8ac20d8b457c79aa88a0690d7434a0baffed91c0 Author: root Date: Sun Dec 21 09:57:09 2025 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..812e4cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Frontend development +node_modules/ +*.local +.env.local +.env.*.local +.cache/ +.vite/ +.temp/ +.tmp/ + +# Frontend build +/static/ + +# Backend development +/lib/vendor/ +coverage/ +phpunit.xml.cache +.phpunit.result.cache +.php-cs-fixer.cache +.phpstan.cache +.phpactor/ + +# Editors +.DS_Store +.vscode/ +.idea/ + +# Logs +*.log diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6aaa1c5 --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "name": "ktrix/files", + "description": "File browser interface module", + "type": "ktrix-module", + "license": "AGPL-3.0-or-later", + "autoload": { + "psr-4": { + "KTXM\\Files\\": "lib/" + } + }, + "require": {} +} diff --git a/lib/Module.php b/lib/Module.php new file mode 100644 index 0000000..9a245ef --- /dev/null +++ b/lib/Module.php @@ -0,0 +1,65 @@ + [ + 'label' => 'Access Files', + 'description' => 'View and access the file browser module', + 'group' => 'File Management' + ], + ]; + } + + public function registerBI(): array + { + return [ + 'handle' => $this->handle(), + 'namespace' => 'Files', + 'version' => $this->version(), + 'label' => $this->label(), + 'author' => $this->author(), + 'description' => $this->description(), + 'boot' => 'static/module.mjs', + ]; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c54acab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1527 @@ +{ + "name": "files", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "files", + "version": "0.0.1", + "license": "AGPL-3.0-or-later", + "dependencies": { + "pinia": "^2.3.1", + "vue": "^3.5.18", + "vue-router": "^4.5.1", + "vuetify": "^3.10.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", + "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz", + "integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.50" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.5.tgz", + "integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz", + "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", + "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", + "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.5.tgz", + "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vuetify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.0.tgz", + "integrity": "sha512-ITGeT3uaTIwI2SdyTvtE45tY6FlS2oWklfLU47s2K0ZHnu1it35p9lz8oE15Id8ThtKyQojQGobMkN+korheEw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=2.1.0", + "vue": "^3.5.0", + "webpack-plugin-vuetify": ">=3.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f287af --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "files", + "version": "0.0.1", + "private": true, + "license": "AGPL-3.0-or-later", + "author": "Ktrix", + "type": "module", + "scripts": { + "build": "vite build --mode production --config vite.config.ts", + "dev": "vite build --mode development --config vite.config.ts", + "watch": "vite build --mode development --watch --config vite.config.ts", + "typecheck": "vue-tsc --noEmit", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "pinia": "^2.3.1", + "vue": "^3.5.18", + "vue-router": "^4.5.1", + "vuetify": "^3.10.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } +} diff --git a/src/components/FileActionsMenu.vue b/src/components/FileActionsMenu.vue new file mode 100644 index 0000000..ffaf80b --- /dev/null +++ b/src/components/FileActionsMenu.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/components/FilesBreadcrumbs.vue b/src/components/FilesBreadcrumbs.vue new file mode 100644 index 0000000..08c39d7 --- /dev/null +++ b/src/components/FilesBreadcrumbs.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/components/FilesDragOverlay.vue b/src/components/FilesDragOverlay.vue new file mode 100644 index 0000000..09cb661 --- /dev/null +++ b/src/components/FilesDragOverlay.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/components/FilesEmptyState.vue b/src/components/FilesEmptyState.vue new file mode 100644 index 0000000..c79bd44 --- /dev/null +++ b/src/components/FilesEmptyState.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/components/FilesInfoPanel.vue b/src/components/FilesInfoPanel.vue new file mode 100644 index 0000000..fed381a --- /dev/null +++ b/src/components/FilesInfoPanel.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/src/components/FilesSelectionToolbar.vue b/src/components/FilesSelectionToolbar.vue new file mode 100644 index 0000000..adef1dd --- /dev/null +++ b/src/components/FilesSelectionToolbar.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/FilesSidebar.vue b/src/components/FilesSidebar.vue new file mode 100644 index 0000000..a6e7c56 --- /dev/null +++ b/src/components/FilesSidebar.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/components/FilesToolbar.vue b/src/components/FilesToolbar.vue new file mode 100644 index 0000000..f35cf99 --- /dev/null +++ b/src/components/FilesToolbar.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/components/dialogs/DeleteConfirmDialog.vue b/src/components/dialogs/DeleteConfirmDialog.vue new file mode 100644 index 0000000..7e7fee4 --- /dev/null +++ b/src/components/dialogs/DeleteConfirmDialog.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/dialogs/NewFolderDialog.vue b/src/components/dialogs/NewFolderDialog.vue new file mode 100644 index 0000000..8b43d51 --- /dev/null +++ b/src/components/dialogs/NewFolderDialog.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/dialogs/RenameDialog.vue b/src/components/dialogs/RenameDialog.vue new file mode 100644 index 0000000..2631da4 --- /dev/null +++ b/src/components/dialogs/RenameDialog.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/dialogs/UploadDialog.vue b/src/components/dialogs/UploadDialog.vue new file mode 100644 index 0000000..37d9087 --- /dev/null +++ b/src/components/dialogs/UploadDialog.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/components/dialogs/index.ts b/src/components/dialogs/index.ts new file mode 100644 index 0000000..395a374 --- /dev/null +++ b/src/components/dialogs/index.ts @@ -0,0 +1,4 @@ +export { default as NewFolderDialog } from './NewFolderDialog.vue' +export { default as RenameDialog } from './RenameDialog.vue' +export { default as DeleteConfirmDialog } from './DeleteConfirmDialog.vue' +export { default as UploadDialog } from './UploadDialog.vue' diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..c802ce0 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,14 @@ +// Layout components +export { default as FilesToolbar } from './FilesToolbar.vue' +export { default as FilesSidebar } from './FilesSidebar.vue' +export { default as FilesBreadcrumbs } from './FilesBreadcrumbs.vue' +export { default as FilesEmptyState } from './FilesEmptyState.vue' +export { default as FilesDragOverlay } from './FilesDragOverlay.vue' +export { default as FilesInfoPanel } from './FilesInfoPanel.vue' +export { default as FileActionsMenu } from './FileActionsMenu.vue' + +// View components +export * from './views' + +// Dialog components +export * from './dialogs' diff --git a/src/components/views/FilesDetailsView.vue b/src/components/views/FilesDetailsView.vue new file mode 100644 index 0000000..1f02ef9 --- /dev/null +++ b/src/components/views/FilesDetailsView.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/components/views/FilesGridView.vue b/src/components/views/FilesGridView.vue new file mode 100644 index 0000000..1f3b983 --- /dev/null +++ b/src/components/views/FilesGridView.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/components/views/FilesListView.vue b/src/components/views/FilesListView.vue new file mode 100644 index 0000000..a80f7fe --- /dev/null +++ b/src/components/views/FilesListView.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/views/index.ts b/src/components/views/index.ts new file mode 100644 index 0000000..df10af5 --- /dev/null +++ b/src/components/views/index.ts @@ -0,0 +1,3 @@ +export { default as FilesGridView } from './FilesGridView.vue' +export { default as FilesListView } from './FilesListView.vue' +export { default as FilesDetailsView } from './FilesDetailsView.vue' diff --git a/src/composables/index.ts b/src/composables/index.ts new file mode 100644 index 0000000..b19b582 --- /dev/null +++ b/src/composables/index.ts @@ -0,0 +1,11 @@ +/** + * Central export point for all Files module composables + */ + +export { useFileManager } from './useFileManager' +export { useFileSelection } from './useFileSelection' +export { useFileUpload } from './useFileUpload' + +export type { UseFileManagerOptions } from './useFileManager' +export type { UseFileSelectionOptions } from './useFileSelection' +export type { UseFileUploadOptions, FileUploadProgress } from './useFileUpload' diff --git a/src/composables/useFileManager.ts b/src/composables/useFileManager.ts new file mode 100644 index 0000000..51dc82b --- /dev/null +++ b/src/composables/useFileManager.ts @@ -0,0 +1,303 @@ +/** + * File Manager composable for convenient file/folder operations + * Provides reactive access to file manager state and actions + */ + +import { computed, ref } from 'vue' +import type { Ref, ComputedRef } from 'vue' +import { useProvidersStore } from '@FileManager/stores/providersStore' +import { useServicesStore } from '@FileManager/stores/servicesStore' +import { useNodesStore, ROOT_ID } from '@FileManager/stores/nodesStore' +import type { FilterCondition, SortCondition, RangeCondition } from '@FileManager/types/common' +import { FileCollectionObject } from '@FileManager/models/collection' +import { FileEntityObject } from '@FileManager/models/entity' + +// Base URL for file manager transfer endpoints +const TRANSFER_BASE_URL = '/m/file_manager' + +export interface UseFileManagerOptions { + providerId: string + serviceId: string + autoFetch?: boolean +} + +export function useFileManager(options: UseFileManagerOptions) { + const providersStore = useProvidersStore() + const servicesStore = useServicesStore() + const nodesStore = useNodesStore() + + const { providerId, serviceId, autoFetch = false } = options + + // Current location (folder being viewed) + const currentLocation: Ref = ref(ROOT_ID) + + // Loading/error state + const isLoading = computed(() => nodesStore.loading) + const error = computed(() => nodesStore.error) + + // Provider and service + const provider = computed(() => providersStore.getProvider(providerId)) + const service = computed(() => servicesStore.getService(providerId, serviceId)) + const rootId = computed(() => servicesStore.getRootId(providerId, serviceId) || ROOT_ID) + + // Current children + const currentChildren = computed(() => + nodesStore.getChildren(providerId, serviceId, currentLocation.value) + ) + + const currentCollections: ComputedRef = computed(() => + nodesStore.getChildCollections(providerId, serviceId, currentLocation.value) + ) + + const currentEntities: ComputedRef = computed(() => + nodesStore.getChildEntities(providerId, serviceId, currentLocation.value) + ) + + // Breadcrumb path + const breadcrumbs = computed(() => { + if (currentLocation.value === ROOT_ID) { + return [] + } + return nodesStore.getPath(providerId, serviceId, currentLocation.value) + }) + + // Is at root? + const isAtRoot = computed(() => currentLocation.value === ROOT_ID) + + // Navigate to a folder + const navigateTo = async (collectionId: string | null) => { + currentLocation.value = collectionId || ROOT_ID + await refresh() + } + + // Navigate up one level + const navigateUp = async () => { + if (currentLocation.value === ROOT_ID) { + return + } + const currentNode = nodesStore.getNode(providerId, serviceId, currentLocation.value) + if (currentNode) { + await navigateTo(currentNode.in || ROOT_ID) + } + } + + // Navigate to root + const navigateToRoot = async () => { + await navigateTo(ROOT_ID) + } + + // Refresh current location + const refresh = async ( + filter?: FilterCondition[] | null, + sort?: SortCondition[] | null, + range?: RangeCondition | null + ) => { + await nodesStore.fetchNodes( + providerId, + serviceId, + currentLocation.value === ROOT_ID ? null : currentLocation.value, + false, + filter, + sort, + range + ) + } + + // Create a new folder + const createFolder = async (label: string): Promise => { + return await nodesStore.createCollection( + providerId, + serviceId, + currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value, + { label } + ) + } + + // Create a new file + const createFile = async ( + label: string, + mime: string = 'application/octet-stream' + ): Promise => { + return await nodesStore.createEntity( + providerId, + serviceId, + currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value, + { label, mime } + ) + } + + // Rename a node + const renameNode = async (nodeId: string, newLabel: string) => { + const node = nodesStore.getNode(providerId, serviceId, nodeId) + if (!node) { + throw new Error('Node not found') + } + + if (node['@type'] === 'files.collection') { + return await nodesStore.modifyCollection(providerId, serviceId, nodeId, { label: newLabel }) + } else { + return await nodesStore.modifyEntity(providerId, serviceId, node.in, nodeId, { label: newLabel }) + } + } + + // Delete a node + const deleteNode = async (nodeId: string): Promise => { + const node = nodesStore.getNode(providerId, serviceId, nodeId) + if (!node) { + throw new Error('Node not found') + } + + if (node['@type'] === 'files.collection') { + return await nodesStore.destroyCollection(providerId, serviceId, nodeId) + } else { + return await nodesStore.destroyEntity(providerId, serviceId, node.in, nodeId) + } + } + + // Copy a node + const copyNode = async (nodeId: string, destinationId?: string | null) => { + const node = nodesStore.getNode(providerId, serviceId, nodeId) + if (!node) { + throw new Error('Node not found') + } + + const destination = destinationId ?? currentLocation.value + + if (node['@type'] === 'files.collection') { + return await nodesStore.copyCollection(providerId, serviceId, nodeId, destination) + } else { + return await nodesStore.copyEntity(providerId, serviceId, node.in, nodeId, destination) + } + } + + // Move a node + const moveNode = async (nodeId: string, destinationId?: string | null) => { + const node = nodesStore.getNode(providerId, serviceId, nodeId) + if (!node) { + throw new Error('Node not found') + } + + const destination = destinationId ?? currentLocation.value + + if (node['@type'] === 'files.collection') { + return await nodesStore.moveCollection(providerId, serviceId, nodeId, destination) + } else { + return await nodesStore.moveEntity(providerId, serviceId, node.in, nodeId, destination) + } + } + + // Read file content + const readFile = async (entityId: string): Promise => { + const node = nodesStore.getNode(providerId, serviceId, entityId) + if (!node || node['@type'] !== 'files.entity') { + throw new Error('Entity not found') + } + return await nodesStore.readEntity(providerId, serviceId, node.in || ROOT_ID, entityId) + } + + // Write file content + const writeFile = async (entityId: string, content: string): Promise => { + const node = nodesStore.getNode(providerId, serviceId, entityId) + if (!node || node['@type'] !== 'files.entity') { + throw new Error('Entity not found') + } + return await nodesStore.writeEntity(providerId, serviceId, node.in, entityId, content) + } + + // Download a single file + const downloadEntity = (entityId: string, collectionId?: string | null): void => { + const collection = collectionId ?? currentLocation.value + // Use path parameters: /download/entity/{provider}/{service}/{collection}/{identifier} + const url = `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}` + + // Trigger download by opening URL (browser handles it) + window.open(url, '_blank') + } + + // Download a collection (folder) as ZIP + const downloadCollection = (collectionId: string): void => { + // Use path parameters: /download/collection/{provider}/{service}/{identifier} + const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collectionId)}` + + window.open(url, '_blank') + } + + // Download multiple items as ZIP archive + const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => { + const collection = collectionId ?? currentLocation.value + const params = new URLSearchParams({ + provider: providerId, + service: serviceId, + }) + ids.forEach(id => params.append('ids[]', id)) + if (name) { + params.append('name', name) + } + if (collection && collection !== ROOT_ID) { + params.append('collection', collection) + } + + const url = `${TRANSFER_BASE_URL}/download/archive?${params.toString()}` + + window.open(url, '_blank') + } + + // Initialize - fetch providers, services, and initial nodes if autoFetch + const initialize = async () => { + if (!providersStore.initialized) { + await providersStore.fetchProviders() + } + if (!servicesStore.initialized) { + await servicesStore.fetchServices() + } + if (autoFetch) { + await refresh() + } + } + + return { + // State + currentLocation, + isLoading, + error, + + // Provider/Service + provider, + service, + rootId, + + // Current view + currentChildren, + currentCollections, + currentEntities, + breadcrumbs, + isAtRoot, + + // Navigation + navigateTo, + navigateUp, + navigateToRoot, + refresh, + + // Operations + createFolder, + createFile, + renameNode, + deleteNode, + copyNode, + moveNode, + readFile, + writeFile, + downloadEntity, + downloadCollection, + downloadArchive, + + // Initialize + initialize, + + // Constants + ROOT_ID, + } +} + +export default useFileManager diff --git a/src/composables/useFileSelection.ts b/src/composables/useFileSelection.ts new file mode 100644 index 0000000..72b4ef7 --- /dev/null +++ b/src/composables/useFileSelection.ts @@ -0,0 +1,173 @@ +/** + * File selection composable + * Provides reactive selection management for file manager + */ + +import { ref, computed } from 'vue' +import type { Ref, ComputedRef } from 'vue' +import { FileCollectionObject } from '@FileManager/models/collection' +import { FileEntityObject } from '@FileManager/models/entity' + +type NodeRecord = FileCollectionObject | FileEntityObject + +export interface UseFileSelectionOptions { + multiple?: boolean + allowFolders?: boolean + allowFiles?: boolean +} + +export function useFileSelection(options: UseFileSelectionOptions = {}) { + const { + multiple = true, + allowFolders = true, + allowFiles = true + } = options + + const selectedIds: Ref> = ref(new Set()) + const selectedNodes: Ref> = ref(new Map()) + + // Get selected count + const count: ComputedRef = computed(() => selectedIds.value.size) + + // Check if any selected + const hasSelection: ComputedRef = computed(() => selectedIds.value.size > 0) + + // Get selected IDs as array + const selectedIdArray: ComputedRef = computed(() => + Array.from(selectedIds.value) + ) + + // Get selected nodes as array + const selectedNodeArray: ComputedRef = computed(() => + Array.from(selectedNodes.value.values()) + ) + + // Get selected collections only + const selectedCollections: ComputedRef = computed(() => + selectedNodeArray.value.filter( + (node): node is FileCollectionObject => node['@type'] === 'files.collection' + ) + ) + + // Get selected entities only + const selectedEntities: ComputedRef = computed(() => + selectedNodeArray.value.filter( + (node): node is FileEntityObject => node['@type'] === 'files.entity' + ) + ) + + // Check if a node is selected + const isSelected = (nodeId: string): boolean => { + return selectedIds.value.has(nodeId) + } + + // Check if node type is allowed + const isTypeAllowed = (node: NodeRecord): boolean => { + if (node['@type'] === 'files.collection' && !allowFolders) { + return false + } + if (node['@type'] === 'files.entity' && !allowFiles) { + return false + } + return true + } + + // Select a node + const select = (node: NodeRecord) => { + if (!isTypeAllowed(node)) { + return + } + + if (!multiple) { + // Clear previous selection for single select + selectedIds.value.clear() + selectedNodes.value.clear() + } + + selectedIds.value.add(node.id) + selectedNodes.value.set(node.id, node) + } + + // Deselect a node + const deselect = (nodeId: string) => { + selectedIds.value.delete(nodeId) + selectedNodes.value.delete(nodeId) + } + + // Toggle selection + const toggle = (node: NodeRecord) => { + if (isSelected(node.id)) { + deselect(node.id) + } else { + select(node) + } + } + + // Select multiple nodes + const selectMultiple = (nodes: NodeRecord[]) => { + if (!multiple) { + // For single select, only select the last one + const lastNode = nodes[nodes.length - 1] + if (lastNode && isTypeAllowed(lastNode)) { + selectedIds.value.clear() + selectedNodes.value.clear() + selectedIds.value.add(lastNode.id) + selectedNodes.value.set(lastNode.id, lastNode) + } + return + } + + for (const node of nodes) { + if (isTypeAllowed(node)) { + selectedIds.value.add(node.id) + selectedNodes.value.set(node.id, node) + } + } + } + + // Select all from a list + const selectAll = (nodes: NodeRecord[]) => { + if (!multiple) { + return + } + selectMultiple(nodes) + } + + // Clear selection + const clear = () => { + selectedIds.value.clear() + selectedNodes.value.clear() + } + + // Set selection (replace current) + const setSelection = (nodes: NodeRecord[]) => { + clear() + selectMultiple(nodes) + } + + return { + // State + selectedIds, + selectedNodes, + + // Computed + count, + hasSelection, + selectedIdArray, + selectedNodeArray, + selectedCollections, + selectedEntities, + + // Methods + isSelected, + select, + deselect, + toggle, + selectMultiple, + selectAll, + clear, + setSelection, + } +} + +export default useFileSelection diff --git a/src/composables/useFileUpload.ts b/src/composables/useFileUpload.ts new file mode 100644 index 0000000..4f7c07f --- /dev/null +++ b/src/composables/useFileUpload.ts @@ -0,0 +1,425 @@ +/** + * File upload composable + * Handles file upload operations for file manager + * Supports individual files and entire folder uploads with path preservation + */ + +import { ref, computed } from 'vue' +import type { Ref, ComputedRef } from 'vue' +import { useNodesStore, ROOT_ID } from '@FileManager/stores/nodesStore' +import { FileEntityObject } from '@FileManager/models/entity' + +export interface FileUploadProgress { + file: File + progress: number + status: 'pending' | 'uploading' | 'completed' | 'error' + error?: string + entity?: FileEntityObject + /** Relative path within folder upload (e.g., "folder/subfolder/file.txt") */ + relativePath?: string +} + +export interface FileWithPath { + file: File + relativePath: string +} + +export interface UseFileUploadOptions { + providerId: string + serviceId: string + collectionId?: string | null + maxFileSize?: number + allowedTypes?: string[] +} + +export function useFileUpload(options: UseFileUploadOptions) { + const nodesStore = useNodesStore() + + const { + providerId, + serviceId, + collectionId = ROOT_ID, + maxFileSize, + allowedTypes + } = options + + const uploads: Ref> = ref(new Map()) + const isUploading = ref(false) + + // Get current collection ID (reactive) + const currentCollection: Ref = ref(collectionId) + + // Total upload progress + const totalProgress: ComputedRef = computed(() => { + const items = Array.from(uploads.value.values()) + if (items.length === 0) return 0 + const total = items.reduce((sum, item) => sum + item.progress, 0) + return Math.round(total / items.length) + }) + + // Pending uploads + const pendingUploads: ComputedRef = computed(() => + Array.from(uploads.value.values()).filter(u => u.status === 'pending') + ) + + // Active uploads + const activeUploads: ComputedRef = computed(() => + Array.from(uploads.value.values()).filter(u => u.status === 'uploading') + ) + + // Completed uploads + const completedUploads: ComputedRef = computed(() => + Array.from(uploads.value.values()).filter(u => u.status === 'completed') + ) + + // Failed uploads + const failedUploads: ComputedRef = computed(() => + Array.from(uploads.value.values()).filter(u => u.status === 'error') + ) + + // Validate a file + const validateFile = (file: File): string | null => { + if (maxFileSize && file.size > maxFileSize) { + return `File size exceeds maximum allowed (${formatSize(maxFileSize)})` + } + if (allowedTypes && allowedTypes.length > 0) { + const isAllowed = allowedTypes.some(type => { + if (type.endsWith('/*')) { + // Wildcard match (e.g., "image/*") + return file.type.startsWith(type.slice(0, -1)) + } + return file.type === type + }) + if (!isAllowed) { + return `File type ${file.type} is not allowed` + } + } + return null + } + + // Format file size + const formatSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + // Generate unique upload ID + const generateUploadId = (file: File, relativePath?: string): string => { + const pathPart = relativePath || file.name + return `${pathPart}-${file.size}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + } + + // Add files to upload queue + const addFiles = (files: FileList | File[]): FileUploadProgress[] => { + const added: FileUploadProgress[] = [] + + for (const file of files) { + const error = validateFile(file) + const uploadId = generateUploadId(file) + + const progress: FileUploadProgress = { + file, + progress: 0, + status: error ? 'error' : 'pending', + error: error || undefined + } + + uploads.value.set(uploadId, progress) + added.push(progress) + } + + return added + } + + // Add files with relative paths (for folder uploads) + const addFilesWithPaths = ( + filesOrList: FileList | File[] | FileWithPath[] + ): FileUploadProgress[] => { + const added: FileUploadProgress[] = [] + + // Handle FileList from webkitdirectory input + if (filesOrList instanceof FileList || (Array.isArray(filesOrList) && filesOrList[0] instanceof File && !('relativePath' in filesOrList[0]))) { + const fileList = filesOrList as FileList | File[] + for (const file of fileList) { + // webkitRelativePath is set on files from folder input + const relativePath = (file as any).webkitRelativePath || file.name + const error = validateFile(file) + const uploadId = generateUploadId(file, relativePath) + + const progress: FileUploadProgress = { + file, + progress: 0, + status: error ? 'error' : 'pending', + error: error || undefined, + relativePath + } + + uploads.value.set(uploadId, progress) + added.push(progress) + } + } else { + // Handle FileWithPath array (from drag & drop folder processing) + const filesWithPaths = filesOrList as FileWithPath[] + for (const { file, relativePath } of filesWithPaths) { + const error = validateFile(file) + const uploadId = generateUploadId(file, relativePath) + + const progress: FileUploadProgress = { + file, + progress: 0, + status: error ? 'error' : 'pending', + error: error || undefined, + relativePath + } + + uploads.value.set(uploadId, progress) + added.push(progress) + } + } + + return added + } + + // Extract unique folder paths from pending uploads + const extractFolderPaths = (): string[] => { + const folders = new Set() + + for (const upload of uploads.value.values()) { + if (upload.relativePath && upload.status === 'pending') { + // Get all parent directories + const parts = upload.relativePath.split('/') + parts.pop() // Remove filename + + let currentPath = '' + for (const part of parts) { + currentPath = currentPath ? `${currentPath}/${part}` : part + folders.add(currentPath) + } + } + } + + // Sort by depth (shortest paths first) to ensure parent folders are created first + return Array.from(folders).sort((a, b) => { + const depthA = a.split('/').length + const depthB = b.split('/').length + return depthA - depthB + }) + } + + // Create folder structure for uploads + const createFolderStructure = async (): Promise> => { + const folderPaths = extractFolderPaths() + const folderIdMap = new Map() // path -> collection ID + + for (const folderPath of folderPaths) { + const parts = folderPath.split('/') + const folderName = parts[parts.length - 1] + const parentPath = parts.slice(0, -1).join('/') + + // Determine parent collection ID + const parentId = parentPath + ? (folderIdMap.get(parentPath) ?? currentCollection.value) + : currentCollection.value + + try { + // Create the folder + const collection = await nodesStore.createCollection( + providerId, + serviceId, + parentId, + { label: folderName } + ) + folderIdMap.set(folderPath, collection.id) + } catch (e) { + console.error(`Failed to create folder: ${folderPath}`, e) + // Try to continue with other folders + } + } + + return folderIdMap + } + + // Upload a single file + const uploadFile = async ( + uploadId: string, + folderIdMap?: Map + ): Promise => { + const upload = uploads.value.get(uploadId) + if (!upload || upload.status !== 'pending') { + return null + } + + upload.status = 'uploading' + upload.progress = 0 + + try { + // Determine target collection based on relative path + let targetCollection = currentCollection.value + + if (upload.relativePath && folderIdMap) { + const parts = upload.relativePath.split('/') + parts.pop() // Remove filename + const parentPath = parts.join('/') + + if (parentPath && folderIdMap.has(parentPath)) { + targetCollection = folderIdMap.get(parentPath)! + } + } + + // Convert file to base64 + const content = await fileToBase64(upload.file) + + upload.progress = 50 + + // Create the entity + const entity = await nodesStore.createEntity( + providerId, + serviceId, + targetCollection, + { + label: upload.file.name, + mime: upload.file.type || 'application/octet-stream', + size: upload.file.size, + } + ) + + upload.progress = 75 + + // Write the content + await nodesStore.writeEntity( + providerId, + serviceId, + targetCollection, + entity.id, + content + ) + + upload.progress = 100 + upload.status = 'completed' + upload.entity = entity + + return entity + } catch (e) { + upload.status = 'error' + upload.error = e instanceof Error ? e.message : 'Upload failed' + return null + } + } + + // Convert file to base64 + const fileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const result = reader.result as string + // Remove data URL prefix (e.g., "data:image/png;base64,") + const base64 = result.split(',')[1] || result + resolve(base64) + } + reader.onerror = reject + reader.readAsDataURL(file) + }) + } + + // Upload all pending files + const uploadAll = async (): Promise => { + isUploading.value = true + const entities: FileEntityObject[] = [] + + try { + // Check if any uploads have relative paths (folder upload) + const hasFolderUploads = Array.from(uploads.value.values()).some( + u => u.relativePath && u.relativePath.includes('/') && u.status === 'pending' + ) + + // Create folder structure first if needed + let folderIdMap: Map | undefined + if (hasFolderUploads) { + folderIdMap = await createFolderStructure() + } + + // Upload all files + for (const [uploadId, upload] of uploads.value) { + if (upload.status === 'pending') { + const entity = await uploadFile(uploadId, folderIdMap) + if (entity) { + entities.push(entity) + } + } + } + } finally { + isUploading.value = false + } + + return entities + } + + // Remove an upload from the queue + const removeUpload = (uploadId: string) => { + uploads.value.delete(uploadId) + } + + // Clear completed uploads + const clearCompleted = () => { + for (const [id, upload] of uploads.value) { + if (upload.status === 'completed') { + uploads.value.delete(id) + } + } + } + + // Clear all uploads + const clearAll = () => { + uploads.value.clear() + } + + // Retry a failed upload + const retryUpload = async (uploadId: string): Promise => { + const upload = uploads.value.get(uploadId) + if (!upload || upload.status !== 'error') { + return null + } + + upload.status = 'pending' + upload.error = undefined + upload.progress = 0 + + return await uploadFile(uploadId) + } + + // Set current collection + const setCollection = (collectionId: string | null) => { + currentCollection.value = collectionId + } + + return { + // State + uploads, + isUploading, + currentCollection, + + // Computed + totalProgress, + pendingUploads, + activeUploads, + completedUploads, + failedUploads, + + // Methods + validateFile, + addFiles, + addFilesWithPaths, + uploadFile, + uploadAll, + removeUpload, + clearCompleted, + clearAll, + retryUpload, + setCollection, + } +} + +export default useFileUpload diff --git a/src/integrations.ts b/src/integrations.ts new file mode 100644 index 0000000..490d6da --- /dev/null +++ b/src/integrations.ts @@ -0,0 +1,15 @@ +import type { ModuleIntegrations } from "@KTXC/types/moduleTypes"; + +const integrations: ModuleIntegrations = { + app_menu: [ + { + id: 'files', + label: 'Files', + path: '/files', + icon: 'mdi-folder-outline', + priority: 30, + }, + ], +}; + +export default integrations; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..abdab84 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,8 @@ +import routes from '@/routes' +import integrations from '@/integrations' + +// CSS filename is injected by the vite plugin at build time +// The placeholder gets replaced with the actual hashed filename +export const css = ['__CSS_FILENAME_PLACEHOLDER__'] + +export { routes, integrations } diff --git a/src/pages/FilesPage.vue b/src/pages/FilesPage.vue new file mode 100644 index 0000000..64d36b6 --- /dev/null +++ b/src/pages/FilesPage.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/src/routes.ts b/src/routes.ts new file mode 100644 index 0000000..426f25e --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,11 @@ +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + { + name: 'files', + path: '/files', + component: () => import('@/pages/FilesPage.vue') + }, +] + +export default routes diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..627d1e0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,30 @@ +/** + * Files module types + */ + +export type ViewMode = 'grid' | 'list' | 'details' + +export type SortField = 'label' | 'size' | 'modifiedOn' | 'createdOn' | 'mime' + +export type SortOrder = 'asc' | 'desc' + +export interface ViewSettings { + mode: ViewMode + sortField: SortField + sortOrder: SortOrder + showHidden: boolean +} + +export interface BreadcrumbItem { + id: string + label: string + isRoot: boolean +} + +export interface ContextMenuAction { + id: string + label: string + icon: string + disabled?: boolean + divider?: boolean +} diff --git a/src/utils/fileHelpers.ts b/src/utils/fileHelpers.ts new file mode 100644 index 0000000..59c95be --- /dev/null +++ b/src/utils/fileHelpers.ts @@ -0,0 +1,68 @@ +import { FileEntityObject } from '@FileManager/models/entity' + +/** + * Get the appropriate icon for a file based on its MIME type + */ +export function getFileIcon(entity: FileEntityObject): string { + const mime = entity.mime || '' + if (!mime) return 'mdi-file' + if (mime.startsWith('image/')) return 'mdi-file-image' + if (mime.startsWith('video/')) return 'mdi-file-video' + if (mime.startsWith('audio/')) return 'mdi-file-music' + if (mime.startsWith('text/')) return 'mdi-file-document' + if (mime === 'application/pdf') return 'mdi-file-pdf-box' + if (mime.includes('zip') || mime.includes('tar') || mime.includes('compressed')) return 'mdi-folder-zip' + if (mime.includes('word') || mime.includes('document')) return 'mdi-file-word' + if (mime.includes('excel') || mime.includes('spreadsheet')) return 'mdi-file-excel' + if (mime.includes('powerpoint') || mime.includes('presentation')) return 'mdi-file-powerpoint' + return 'mdi-file' +} + +/** + * Format file size in human readable format + */ +export function formatSize(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * Format date string to localized format + */ +export function formatDate(dateStr: string): string { + if (!dateStr) return '—' + return new Date(dateStr).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) +} + +/** + * Get upload status icon + */ +export function getUploadStatusIcon(status: string): string { + switch (status) { + case 'pending': return 'mdi-clock-outline' + case 'uploading': return 'mdi-loading mdi-spin' + case 'completed': return 'mdi-check-circle' + case 'error': return 'mdi-alert-circle' + default: return 'mdi-file' + } +} + +/** + * Get upload status color + */ +export function getUploadStatusColor(status: string): string { + switch (status) { + case 'pending': return 'grey' + case 'uploading': return 'primary' + case 'completed': return 'success' + case 'error': return 'error' + default: return 'grey' + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..83a671f --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './fileHelpers' diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..509cd17 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.app.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@KTXC/*": ["../../core/src/*"], + "@FileManager/*": ["../file_manager/src/*"] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "../file_manager/src/**/*.ts", + "../../core/src/**/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..0ae4817 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.node.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2e210a0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,70 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + { + name: 'inject-css-filename', + enforce: 'post', + generateBundle(_options, bundle) { + // Find the CSS file in the bundle + const cssFile = Object.keys(bundle).find(name => name.endsWith('.css')) + + if (!cssFile) { + console.warn('No CSS file found in bundle') + return + } + + // Find and update all JS chunks + // Prefix with static/ to match nginx location pattern: /modules/{handle}/static/{file} + for (const fileName of Object.keys(bundle)) { + const chunk = bundle[fileName] + if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) { + chunk.code = chunk.code.replace( + /__CSS_FILENAME_PLACEHOLDER__/g, + `static/${cssFile}` + ) + console.log(`Injected CSS filename "static/${cssFile}" into ${fileName}`) + } + } + } + } + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@KTXC': fileURLToPath(new URL('../../core/src', import.meta.url)), + '@FileManager': fileURLToPath(new URL('../file_manager/src', import.meta.url)), + }, + }, + build: { + outDir: 'static', + emptyOutDir: true, + sourcemap: false, + lib: { + entry: fileURLToPath(new URL('./src/main.ts', import.meta.url)), + name: 'Files', + formats: ['es'], + fileName: () => 'module.mjs', + }, + rollupOptions: { + external: [ + 'vue', + 'vue-router', + 'pinia', + ], + output: { + // Use content hash for CSS files + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith('.css')) { + return 'files-[hash].css' + } + return '[name]-[hash][extname]' + } + } + }, + }, +})