frontend: update openapi client generation
frontend: base repository and uploader views frontend: new components
This commit is contained in:
parent
1f2e1ec6e4
commit
dcdfcec05f
1
workspaces/frontend/.gitignore
vendored
1
workspaces/frontend/.gitignore
vendored
@ -11,4 +11,5 @@ node_modules/
|
||||
*.mjs
|
||||
*.log
|
||||
|
||||
openapi.json
|
||||
src/client
|
||||
|
481
workspaces/frontend/package-lock.json
generated
481
workspaces/frontend/package-lock.json
generated
@ -9,8 +9,10 @@
|
||||
"version": "0.0.5",
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^0.1.6",
|
||||
"@hey-api/client-axios": "^0.2.3",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"axios": "^1.6.8",
|
||||
"filesize": "^10.1.6",
|
||||
"pinia": "^2.1.7",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
@ -18,6 +20,7 @@
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "^0.53.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/node": "^18.19.3",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
@ -879,6 +882,44 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/client-axios": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-axios/-/client-axios-0.2.3.tgz",
|
||||
"integrity": "sha512-v1BoTozp8LQ9JawZF9atXdmaBdQEvoIf39pIYf/0WSdkZSv0rUJDVXxNWHnDDs+S1/6pOfbOhM/0VXD5YJqr8w==",
|
||||
"peerDependencies": {
|
||||
"axios": ">= 1.0.0 < 2"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts": {
|
||||
"version": "0.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.53.0.tgz",
|
||||
"integrity": "sha512-5pDd/s0yHJniruYyKYmEsAMbY10Nh/EwhHlgIrdpQ1KZWQdyTbH/tn8rVHT5Mopr1dMuYX0kq0TzpjcNlvrROQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "11.7.0",
|
||||
"c12": "1.11.1",
|
||||
"commander": "12.1.0",
|
||||
"handlebars": "4.7.8"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-ts": "bin/index.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@ -1422,6 +1463,18 @@
|
||||
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
@ -1587,6 +1640,34 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-1.11.1.tgz",
|
||||
"integrity": "sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"confbox": "^0.1.7",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"giget": "^1.2.3",
|
||||
"jiti": "^1.21.6",
|
||||
"mlly": "^1.7.1",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.1.1",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
@ -1674,6 +1755,24 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/citty": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -1714,6 +1813,21 @@
|
||||
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
|
||||
"integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz",
|
||||
"integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@ -1772,6 +1886,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -1780,6 +1900,12 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
|
||||
"integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@ -1790,6 +1916,18 @@
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -1876,6 +2014,29 @@
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
@ -1910,6 +2071,14 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "10.1.6",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
|
||||
"integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
|
||||
"engines": {
|
||||
"node": ">= 10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@ -1994,6 +2163,36 @@
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -2024,6 +2223,37 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz",
|
||||
"integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.3",
|
||||
"nypm": "^0.3.8",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"tar": "^6.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
|
||||
@ -2133,6 +2363,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@ -2190,6 +2429,18 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -2325,6 +2576,12 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -2364,6 +2621,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
@ -2395,6 +2664,61 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
|
||||
"integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.3",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.1.1",
|
||||
"ufo": "^1.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@ -2440,6 +2764,12 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
|
||||
"integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
@ -2507,6 +2837,53 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.11.tgz",
|
||||
"integrity": "sha512-E5GqaAYSnbb6n1qZyik2wjPDZON43FqOJO59+3OkWrnmQtjggrMOVnsyzfjxp/tS6nlYJBA4zRA5jSM2YaadMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.2.3",
|
||||
"execa": "^8.0.1",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.0",
|
||||
"ufo": "^1.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -2523,6 +2900,27 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz",
|
||||
"integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-typescript-codegen": {
|
||||
"version": "0.29.0",
|
||||
"resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
|
||||
@ -2590,6 +2988,18 @@
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@ -2684,6 +3094,17 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",
|
||||
"integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.7",
|
||||
"mlly": "^1.7.1",
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
@ -2849,6 +3270,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@ -3113,6 +3544,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@ -3199,6 +3642,38 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
@ -3256,6 +3731,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
|
@ -9,12 +9,15 @@
|
||||
"preview": "vite preview",
|
||||
"build": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"generate-client": "openapi --input ./openapi.json --output ./src/client/ --client axios"
|
||||
"generate-client": "openapi --input ./openapi.json --output ./src/client_old/ --client axios --name Client",
|
||||
"openapi-ts": "openapi-ts --input ./openapi.json --output ./src/client/ --client @hey-api/client-axios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^0.1.6",
|
||||
"@hey-api/client-axios": "^0.2.3",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"axios": "^1.6.8",
|
||||
"filesize": "^10.1.6",
|
||||
"pinia": "^2.1.7",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
@ -22,6 +25,7 @@
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "^0.53.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/node": "^18.19.3",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||
|
||||
export interface UserCredentials {
|
||||
name: string,
|
||||
password: string,
|
||||
email?: string
|
||||
}
|
||||
|
||||
export async function signup(body: UserCredentials): Promise<null | ResponseError> {
|
||||
return await api_client.post("/auth/signup", JSON.stringify(body))
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
export async function signin(body: UserCredentials): Promise<null | ResponseError> {
|
||||
return await api_client.post("/auth/signin", JSON.stringify(body))
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
export async function signout(): Promise<null | ResponseError> {
|
||||
return await api_client.get("/auth/signout")
|
||||
.catch(handle_error);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||
|
||||
export interface DirectoryInfo {
|
||||
id: number,
|
||||
repository_id: number,
|
||||
parent_id?: number,
|
||||
created: number,
|
||||
updated: number,
|
||||
name: string,
|
||||
path?: string,
|
||||
is_public: boolean,
|
||||
used?: number
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||
|
||||
export interface FileInfo {
|
||||
id: number,
|
||||
repository_id: number,
|
||||
parent_id?: number,
|
||||
created: number,
|
||||
updated: number,
|
||||
name: string,
|
||||
path?: string,
|
||||
is_public: boolean,
|
||||
size: number
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export * as auth from "@/api/auth";
|
||||
export * as user from "@/api/user";
|
||||
export * as repository from "@/api/repository";
|
||||
export * as directory from "@/api/directory";
|
||||
export * as file from "@/api/file";
|
@ -1,35 +0,0 @@
|
||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||
import { file, directory } from "@/api"
|
||||
|
||||
export interface RepositoryInfo {
|
||||
id: number,
|
||||
capacity: number,
|
||||
used?: number
|
||||
}
|
||||
|
||||
export interface RepositoryContent {
|
||||
files: file.FileInfo[],
|
||||
directories: directory.DirectoryInfo[]
|
||||
}
|
||||
|
||||
export async function info(): Promise<RepositoryInfo | ResponseError> {
|
||||
return await api_client.get("/repository")
|
||||
.then(async response => { return Promise.resolve<RepositoryInfo>(response.data); })
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
export async function create(): Promise<null | ResponseError> {
|
||||
return await api_client.post("/repository")
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
export async function remove(): Promise<null | ResponseError> {
|
||||
return await api_client.delete("/repository")
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
export async function content(): Promise<RepositoryContent | ResponseError> {
|
||||
return await api_client.get("/repository/content")
|
||||
.then(async response => { return Promise.resolve<RepositoryContent>(response.data); })
|
||||
.catch(handle_error);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||
|
||||
export interface UserCredentials {
|
||||
name: string,
|
||||
password: string,
|
||||
email?: string
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: string,
|
||||
name: string,
|
||||
lower_name: string,
|
||||
full_name?: string,
|
||||
email?: string,
|
||||
is_email_private: boolean,
|
||||
must_change_password: boolean,
|
||||
login_type: string,
|
||||
created: number,
|
||||
updated: number,
|
||||
last_login?: number,
|
||||
is_active: boolean,
|
||||
is_admin: boolean,
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
|
||||
export type Image = string | ArrayBuffer;
|
||||
|
||||
export async function info(): Promise<UserInfo | ResponseError> {
|
||||
return await api_client.get("/user")
|
||||
.then(async response => { return Promise.resolve<UserInfo>(response.data); })
|
||||
.catch(handle_error);
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,16 @@
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-ctp-green;
|
||||
@apply text-ctp-lavender;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply w-full pl-3 pr-3 pt-2 pb-2 rounded border bg-ctp-mantle border-ctp-overlay0 hover:border-ctp-overlay1 focus:border-ctp-green text-ctp-text outline-none;
|
||||
@apply w-full pl-3 pr-3 pt-2 pb-2 rounded border bg-ctp-mantle border-ctp-overlay0 hover:border-ctp-overlay1 focus:border-ctp-lavender text-ctp-text outline-none;
|
||||
}
|
||||
|
||||
.input-file {
|
||||
@apply block w-full border rounded cursor-pointer bg-ctp-base border-ctp-surface0 text-ctp-subtext0 focus:outline-none;
|
||||
@apply file:bg-ctp-mantle file:border-ctp-surface0 file:mr-5 file:py-2 file:px-3 file:h-full file:border-y-0 file:border-l-0 file:border-r file:text-ctp-blue hover:file:cursor-pointer hover:file:bg-ctp-base;
|
||||
}
|
||||
|
||||
label {
|
||||
@ -22,15 +27,15 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply pt-2 pb-2 pl-5 pr-5 rounded bg-ctp-mantle hover:bg-ctp-base text-ctp-blue cursor-pointer;
|
||||
@apply pt-1 pb-1 pl-3 pr-3 sm:pt-2 sm:pb-2 sm:pl-5 sm:pr-5 rounded bg-ctp-mantle border border-ctp-surface0 hover:bg-ctp-base text-ctp-blue cursor-pointer;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
@apply button text-ctp-green cursor-pointer;
|
||||
@apply button text-ctp-lavender cursor-pointer border-none;
|
||||
}
|
||||
|
||||
.hline {
|
||||
@apply border-t border-ctp-overlay0 ml-0 mr-0;
|
||||
@apply border-t border-ctp-surface0 ml-0 mr-0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -40,6 +45,10 @@
|
||||
label {
|
||||
@apply text-ctp-text;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply inline-block select-none text-center overflow-visible w-6 h-6 stroke-ctp-text;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
@ -1,45 +0,0 @@
|
||||
import axios, { type AxiosInstance, AxiosError } from "axios";
|
||||
|
||||
export class HttpError extends Error {
|
||||
status_code: number;
|
||||
|
||||
constructor(status_code: number, message: string) {
|
||||
super(JSON.stringify({ status_code: status_code, message: message }));
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
|
||||
this.name = Error.name;
|
||||
this.status_code = status_code;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResponseError {
|
||||
status: number | null,
|
||||
message: string | null
|
||||
}
|
||||
|
||||
export function handle_error(error: AxiosError): Promise<ResponseError> {
|
||||
let message = error.response?.data?.detail || error.response?.data;
|
||||
console.log(error);
|
||||
// extract pydantic error message
|
||||
if (error.response.status == 422) {
|
||||
message = error.response?.data?.detail[1].ctx.reason;
|
||||
}
|
||||
|
||||
return Promise.reject<ResponseError>({ status: error.response.status, message: message});
|
||||
}
|
||||
|
||||
const debug = import.meta.hot;
|
||||
|
||||
export const api_client: AxiosInstance = axios.create({
|
||||
baseURL: debug ? "http://localhost:54601/api" : "/api",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const resources_client: AxiosInstance = axios.create({
|
||||
baseURL: debug ? "http://localhost:54601/resources" : "/resources",
|
||||
responseType: "blob"
|
||||
});
|
||||
|
@ -1,12 +1,4 @@
|
||||
<template>
|
||||
<div class="fixed h-1/3 z-50 context-menu" :style="{ top: y + 'px', left: x + 'px' }">
|
||||
<div v-for="action in actions" :key="action.action" @click="emitAction(action.action)">
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
const { actions, x, y } = defineProps(['actions', 'x', 'y']);
|
||||
@ -17,11 +9,19 @@ const emitAction = (action) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute z-50 context-menu bg-ctp-mantle border rounded border-ctp-overlay0"
|
||||
:style="{ top: y + 'px', left: x + 'px' }">
|
||||
<div v-for="action in actions" :key="action.action" @click="emitAction(action.action)"
|
||||
class="hover:bg-ctp-base text-ctp-blue">
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
min-width: 150px;
|
||||
}
|
||||
@ -30,8 +30,4 @@ const emitAction = (action) => {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-menu div:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
38
workspaces/frontend/src/components/CtxMenu.vue
Normal file
38
workspaces/frontend/src/components/CtxMenu.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const { data, actions } = defineProps(["data", "actions"]);
|
||||
const isShow = ref(false);
|
||||
const posX = ref(0);
|
||||
const posY = ref(0);
|
||||
const anchor = ref(null);
|
||||
|
||||
const emit = defineEmits(["onEvent"]);
|
||||
|
||||
const showMenu = (event) => {
|
||||
event.preventDefault();
|
||||
console.log("pos", event);
|
||||
posX.value = event.pageX;
|
||||
posY.value = event.pageY;
|
||||
isShow.value = true;
|
||||
|
||||
emit("onEvent", event);
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="anchor" @contextmenu="showMenu($event)" style="display: contents" v-click-outside="closeMenu">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="isShow" class="absolute z-50 min-w-40 bg-ctp-mantle border rounded border-ctp-surface0"
|
||||
:style="{ top: posY + 'px', left: posX + 'px' }">
|
||||
<div v-for="action in actions" v-show="action.show()" :key="action.event" @click="action.event(data)"
|
||||
class="hover:bg-ctp-base text-ctp-blue select-none pl-4 pr-4 pt-2 pb-2">
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
84
workspaces/frontend/src/components/DataTable.vue
Normal file
84
workspaces/frontend/src/components/DataTable.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="header">
|
||||
<h2>
|
||||
{{ header.title }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ header.description }}
|
||||
</p>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="rowSelector">
|
||||
<div>
|
||||
<input id="contact-selectAll" type="checkbox" value="" @change="selectAll">
|
||||
</div>
|
||||
</th>
|
||||
<th v-for="(item, idx) in fields" :key="idx">
|
||||
{{ item.label }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in data" :key="index">
|
||||
<td v-if="rowSelector">
|
||||
<div>
|
||||
<input :id="`contact-${index}`" v-model="item.selected" type="checkbox">
|
||||
</div>
|
||||
</td>
|
||||
<td v-for="(field, idx) in fields" :key="idx" @click="rowSelected(item)">
|
||||
<span v-if="!hasNamedSlot(field.key)" :item="item">
|
||||
{{ item[field.key] }}
|
||||
</span>
|
||||
<slot v-else :name="field.key" :item="item" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="hasNamedSlot('footer')">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
header: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => null
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
rowSelector: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowSelected(item) {
|
||||
this.$emit('rowSelected', item)
|
||||
},
|
||||
selectAll(e) {
|
||||
const checked = e.target.checked
|
||||
this.data.forEach((item) => { item.selected = checked })
|
||||
this.$forceUpdate()
|
||||
},
|
||||
hasNamedSlot(slotName) {
|
||||
return Object.prototype.hasOwnProperty.call(this.$scopedSlots, slotName)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
15
workspaces/frontend/src/components/DragItem.vue
Normal file
15
workspaces/frontend/src/components/DragItem.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from "vue";
|
||||
|
||||
const { data } = defineProps(["data"]);
|
||||
|
||||
const onDragBeginEvent = (event) => {
|
||||
event.dataTransfer.setData("value", JSON.stringify(data));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr draggable="true" @dragstart="onDragBeginEvent">
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
18
workspaces/frontend/src/components/DropItem.vue
Normal file
18
workspaces/frontend/src/components/DropItem.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from "vue";
|
||||
|
||||
const { onDragOver, onDragLeave, onDrop } = defineProps(["onDragOver", "onDragLeave", "onDrop"]);
|
||||
|
||||
const onDragOverEvent = (event) => {
|
||||
event.preventDefault();
|
||||
if (onDragOver) {
|
||||
onDragOver(event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr @dragover="onDragOverEvent" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
@ -1,5 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
const { value } = defineProps(["value"]);
|
||||
|
||||
const handleError = (e) => {
|
||||
let entries = [];
|
||||
let titlecase = (str: string) => {
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
if (e?.response) {
|
||||
if (e.response.status == 422) {
|
||||
for (let detail of e.response.data.detail) {
|
||||
entries.push(titlecase(detail.msg));
|
||||
}
|
||||
} else {
|
||||
if (e.response.data?.detail) {
|
||||
entries.push(e.response.data.detail);
|
||||
} else {
|
||||
entries.push(e.response.data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.push(e);
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
|
||||
<slot></slot>
|
||||
</h1>
|
||||
<section v-if="value" class="mt-1 mb-1">
|
||||
<p v-for="entry in handleError(value)"
|
||||
class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
|
||||
{{ entry }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
48
workspaces/frontend/src/components/Modal.vue
Normal file
48
workspaces/frontend/src/components/Modal.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close-modal"]);
|
||||
|
||||
const closeModal = (action) => {
|
||||
emit("close-modal", action);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-mask">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<div class="flex mb-4">
|
||||
<slot name="header"></slot>
|
||||
<button class="mr-0 ml-auto button" @click="closeModal">x</button>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<div class="flex mt-4">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
@apply fixed z-[666] top-0 left-0 w-full h-full;
|
||||
@apply bg-ctp-crust bg-opacity-50;
|
||||
}
|
||||
|
||||
.modal-wrapper {}
|
||||
|
||||
.modal-container {
|
||||
@apply flex-grow w-full lg:w-[calc(100%-100px)] max-w-[1000px] ml-auto mr-auto lg:mt-20 lg:mb-20 p-5;
|
||||
@apply bg-ctp-crust shadow-ctp-overlay0 border rounded border-ctp-surface1 text-ctp-text;
|
||||
@apply h-screen lg:h-full;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="relative h-12">
|
||||
<nav
|
||||
class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-mantle">
|
||||
<nav class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-mantle">
|
||||
<div class="items-center m-0 flex">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
|
242
workspaces/frontend/src/components/RepositoryBrowser.vue
Normal file
242
workspaces/frontend/src/components/RepositoryBrowser.vue
Normal file
@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import IconDirectory from "@/components/icons/IconDirectory.vue";
|
||||
import IconFile from "@/components/icons/IconFile.vue";
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
import CtxMenu from "@/components/CtxMenu.vue";
|
||||
import Modal from "@/components/Modal.vue";
|
||||
import { defineProps, ref, defineModel } from "vue";
|
||||
import { filesize } from "filesize";
|
||||
import { store, router, api } from "@";
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const repoStore = store.useRepository();
|
||||
const route = useRoute();
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: "Copy",
|
||||
show: () => true,
|
||||
event: (data) => { repoStore.copyBufferSelected(); }
|
||||
},
|
||||
{
|
||||
label: "Paste",
|
||||
show: () => true,
|
||||
event: async (data) => { await repoStore.copyItems(repoStore.buffer, repoStore.currentPath); }
|
||||
},
|
||||
{
|
||||
label: "Move",
|
||||
show: () => true,
|
||||
event: async (data) => { await repoStore.moveItems(repoStore.buffer, repoStore.currentPath); }
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
show: () => true,
|
||||
event: async (data) => { await repoStore.deleteItem(data); }
|
||||
},
|
||||
{
|
||||
label: "Delete selected",
|
||||
show: () => { return repoStore.content.filter((item) => item.meta.selected).length > 1; },
|
||||
event: async (data) => { await repoStore.deleteSelectedItems(); }
|
||||
},
|
||||
];
|
||||
|
||||
// Properties
|
||||
const data = defineModel()
|
||||
const { onDragOver, onDragLeave, onDrop } = defineProps(["onDragOver", "onDragLeave", "onDrop"]);
|
||||
|
||||
// Drag and drop
|
||||
const onDragOverEvent = (event, value) => {
|
||||
event.preventDefault();
|
||||
console.log("drag over", JSON.stringify(value));
|
||||
|
||||
if (value.type === "directory") {
|
||||
value.meta.dragOvered = true;
|
||||
}
|
||||
|
||||
if (onDragOver) {
|
||||
onDragOver(event);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeaveEvent = (event, value) => {
|
||||
event.preventDefault();
|
||||
console.log("drag leave", JSON.stringify(value));
|
||||
|
||||
if (value.type === "directory") {
|
||||
value.meta.dragOvered = false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onDragBegin = (event, value) => {
|
||||
value.meta.selected = true;
|
||||
|
||||
let items = repoStore.content.filter((item) => item.meta.selected);
|
||||
|
||||
let elem = document.createElement("div");
|
||||
elem.id = "dragItem";
|
||||
elem.className = "min-w-16 h-8 absolute top-[-1000px] bg-ctp-mantle border rounded border-ctp-surface0 px-4 py-1";
|
||||
elem.appendChild(document.createTextNode("Move " + items.length + " items"));
|
||||
document.body.appendChild(elem);
|
||||
|
||||
event.dataTransfer.setDragImage(elem, 0, 0);
|
||||
event.dataTransfer.setData("value", JSON.stringify(items));
|
||||
};
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
let elem = document.getElementById("dragItem");
|
||||
if (elem.parentNode) {
|
||||
elem.parentNode.removeChild(elem);
|
||||
}
|
||||
deselectAll();
|
||||
};
|
||||
|
||||
const onDropEvent = async (event, item) => {
|
||||
if (item.type === "directory") {
|
||||
item.meta.dragOvered = false;
|
||||
let items = JSON.parse(event.dataTransfer.getData("value"));
|
||||
|
||||
await repoStore.moveItems(items, item.info.path);
|
||||
}
|
||||
console.log("drop data", JSON.parse(event.dataTransfer.getData("value")));
|
||||
};
|
||||
|
||||
const isDraggable = ref(false);
|
||||
|
||||
// Selection
|
||||
const deselectAll = () => {
|
||||
console.log("deselect", data.value);
|
||||
for (let item of data.value) {
|
||||
item.meta.selected = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectAll = (event) => {
|
||||
console.log("select all", event);
|
||||
for (let item of data.value) {
|
||||
item.meta.selected = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onSingleClick = (event: Event, item) => {
|
||||
console.log(event);
|
||||
if (!event.ctrlKey) {
|
||||
deselectAll();
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
selectAll();
|
||||
} else {
|
||||
item.meta.selected = !item.meta.selected;
|
||||
}
|
||||
};
|
||||
|
||||
const onDoubleClick = (item) => {
|
||||
if (item.type === "directory") {
|
||||
repoStore.changeDirectory(route.path, item.info);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickEvent = (event: Event, item) => {
|
||||
item.meta.clickCount++;
|
||||
if (item.meta.clickCount === 1) {
|
||||
onSingleClick(event, item);
|
||||
|
||||
item.meta.clickTimer = setTimeout(() => {
|
||||
item.meta.clickCount = 0;
|
||||
}, 300);
|
||||
} else {
|
||||
onDoubleClick(item);
|
||||
clearTimeout(item.meta.clickTimer);
|
||||
item.meta.clickCount = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onCtrlClickEvent = (event: Event, item) => {
|
||||
//console.log("ctrl click", event);
|
||||
//item.meta.selected = !item.meta.selected;
|
||||
};
|
||||
|
||||
// TODO: ctrl+a select all, ctrl-left-mouse select one more item, shift-left-mouse select range of items
|
||||
|
||||
// Misc
|
||||
|
||||
function timeAgo(timestamp: number) {
|
||||
const seconds = Math.floor((new Date().getTime() - new Date(timestamp * 1000).getTime()) / 1000)
|
||||
let interval = seconds / 31536000
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: 'auto' })
|
||||
if (interval > 1) { return rtf.format(-Math.floor(interval), 'year') }
|
||||
interval = seconds / 2592000
|
||||
if (interval > 1) { return rtf.format(-Math.floor(interval), 'month') }
|
||||
interval = seconds / 86400
|
||||
if (interval > 1) { return rtf.format(-Math.floor(interval), 'day') }
|
||||
interval = seconds / 3600
|
||||
if (interval > 1) { return rtf.format(-Math.floor(interval), 'hour') }
|
||||
interval = seconds / 60
|
||||
if (interval > 1) { return rtf.format(-Math.floor(interval), 'minute') }
|
||||
return rtf.format(-Math.floor(interval), 'second')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full border-y lg:border rounded border-ctp-surface0 select-none my-2"
|
||||
@contextmenu.prevent>
|
||||
<div class="flex w-full py-2 px-4">
|
||||
<span class="text-left w-1/2">Name</span>
|
||||
<span class="hidden lg:inline w-1/4 text-right">Size</span>
|
||||
<span class="w-1/2 lg:w-1/4 text-right">Created</span>
|
||||
</div>
|
||||
<div class="hline"></div>
|
||||
<div class="flex flex-col overflow-y-auto" v-click-outside="deselectAll">
|
||||
<div class="flex hover:bg-ctp-surface0 py-2 px-4" v-if="!repoStore.isRoot()"
|
||||
@click="repoStore.previousDirectory(route.path)">
|
||||
<div class="w-3/5 lg:w-1/2 flex items-center">
|
||||
<IconDirectory />
|
||||
|
||||
<span class="ml-4 text-ellipsis whitespace-nowrap overflow-auto">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden lg:flex w-1/4 items-center justify-end">
|
||||
<span>-</span>
|
||||
</div>
|
||||
<div class="w-2/5 lg:w-1/4 flex items-center justify-end">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex hover:bg-ctp-surface0 py-2 px-4"
|
||||
v-if="repoStore.isRoot() && repoStore.content.length === 0">
|
||||
<div class="flex items-center ml-auto mr-auto">
|
||||
Empty
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex hover:bg-ctp-surface1 py-2 px-4" v-for="(item, index) in repoStore.content"
|
||||
:class="{ 'bg-ctp-surface0': item.meta.selected, 'bg-ctp-lavender': item.meta.dragOvered }"
|
||||
@click="onClickEvent($event, item)" @keypress.ctrl.65="selectAll" draggable="true"
|
||||
@dragstart="onDragBegin($event, item)" @dragover="onDragOverEvent($event, item)"
|
||||
@dragend="onDragEnd($event)" @dragleave="onDragLeaveEvent($event, item)"
|
||||
@drop="onDropEvent($event, item)">
|
||||
<CtxMenu :data="item" :actions="actions" @onEvent="onSingleClick($event, item)">
|
||||
<div class="w-3/5 lg:w-1/2 flex items-center">
|
||||
<IconDirectory v-if="item.type == 'directory'" />
|
||||
<IconFile v-else />
|
||||
|
||||
<span class="ml-4 text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{{ item.info.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden lg:flex w-1/4 items-center justify-end">
|
||||
<span v-if="item.type == 'directory'">-</span>
|
||||
<span v-else>{{ filesize(item.info.size) }}</span>
|
||||
</div>
|
||||
<div class="w-2/5 lg:w-1/4 flex items-center justify-end">
|
||||
<span>{{ timeAgo(item.info.created) }}</span>
|
||||
</div>
|
||||
</CtxMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
45
workspaces/frontend/src/components/Tooltip.vue
Normal file
45
workspaces/frontend/src/components/Tooltip.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const { text } = defineProps(["text"]);
|
||||
const isShow = ref(false);
|
||||
const posX = ref(0);
|
||||
const posY = ref(0);
|
||||
const anchor = ref(null);
|
||||
|
||||
const showTooltip = (event, value) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isShow.value)
|
||||
return;
|
||||
|
||||
posX.value = anchor.value.getBoundingClientRect().left;
|
||||
posY.value = anchor.value.getBoundingClientRect().top;
|
||||
isShow.value = true;
|
||||
};
|
||||
|
||||
const closeTooltip = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="anchor" @mouseover="showTooltip" @mouseleave="closeTooltip" style="display: unset">
|
||||
<slot></slot>
|
||||
<div class="tooltip" :style="{ top: posY + 32 + 'px', left: posX + 16 + 'px' }" v-if="isShow">
|
||||
{{
|
||||
text
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tooltip {
|
||||
@apply hidden sm:inline z-[666] absolute bg-ctp-mantle rounded p-2 border border-ctp-surface0 max-w-[600px];
|
||||
overflow-wrap: break-all;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
90
workspaces/frontend/src/components/Uploader.vue
Normal file
90
workspaces/frontend/src/components/Uploader.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/Modal.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import IconDelete from "@/components/icons/IconDelete.vue";
|
||||
import IconRemove from "@/components/icons/IconRemove.vue";
|
||||
import IconUpload from "@/components/icons/IconUpload.vue";
|
||||
import IconAccept from "@/components/icons/IconAccept.vue";
|
||||
import { filesize } from "filesize";
|
||||
import { ref, shallowRef } from "vue";
|
||||
import { store } from "@";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close-uploader"]);
|
||||
|
||||
const closeUploader = (action) => {
|
||||
emit("close-uploader", action);
|
||||
};
|
||||
|
||||
const inputFileRef = shallowRef();
|
||||
const uploaderStore = store.useUploader();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :isOpen="isOpen" @close-modal="closeUploader">
|
||||
<template #header>
|
||||
<h2>Uploader</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex mb-8">
|
||||
<input type="file" multiple ref="inputFileRef" @change="uploaderStore.loadFiles($event)"
|
||||
class="input-file" />
|
||||
|
||||
<button class="button ml-2" :disabled="uploaderStore.files.length === 0"
|
||||
@click="uploaderStore.uploadFiles">Upload</button>
|
||||
<button class="button ml-2 text-ctp-red" @click="uploaderStore.removeFiles()">Clear</button>
|
||||
</div>
|
||||
<div v-if="uploaderStore.files.length > 0" class="flex flex-col h-full">
|
||||
<div class="flex w-full mb-2">
|
||||
<span class="text-left w-1/2">Name</span>
|
||||
<span class="hidden lg:inline w-1/4 text-right">Size</span>
|
||||
<span class="w-1/2 lg:w-1/4 text-right"></span>
|
||||
</div>
|
||||
<div class="hline"></div>
|
||||
<div class="flex flex-col overflow-y-auto h-[480px]">
|
||||
<div class="flex hover:bg-ctp-surface0 mt-1 mb-1" v-for="(item, index) in uploaderStore.files">
|
||||
<div class="w-3/5 lg:w-1/2 flex items-center text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<Tooltip :text="item.content.name">
|
||||
<span class="pl-2">{{ item.content.name }}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="hidden lg:flex w-1/4 items-center justify-end">
|
||||
<span>{{ filesize(item.content.size) }}</span>
|
||||
</div>
|
||||
<div class="w-2/5 lg:w-1/4 flex items-center justify-end pr-4">
|
||||
<Tooltip :text="'Stop'" v-if="item.status === 'transfer'">
|
||||
<button class="button" @click="item.cancel()">
|
||||
{{ Math.round(item.progress * 100) }}%
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button class="button" v-if="item.status === 'success'">
|
||||
<IconAccept class="stroke-ctp-green" />
|
||||
</button>
|
||||
<Tooltip :text="item.error" v-if="item.status === 'fail'">
|
||||
<button class="button" @click="uploaderStore.uploadFile(item)">
|
||||
<IconUpload class="stroke-ctp-red" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip :text="'Upload'" v-if="item.status === 'idle'">
|
||||
<button class="button" @click="uploaderStore.uploadFile(item)">
|
||||
<IconUpload class="stroke-ctp-blue" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip :text="'Remove'">
|
||||
<button class="button ml-2" @click="uploaderStore.removeFile(item)">
|
||||
<IconDelete class="stroke-ctp-peach" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hline"></div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
10
workspaces/frontend/src/components/icons/IconAccept.vue
Normal file
10
workspaces/frontend/src/components/icons/IconAccept.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="icon" viewBox="0 0 28 28" fill="none">
|
||||
<path
|
||||
d="M6.65263 14.0304C6.29251 13.6703 6.29251 13.0864 6.65263 12.7263C7.01276 12.3662 7.59663 12.3662 7.95676 12.7263L11.6602 16.4297L19.438 8.65183C19.7981 8.29171 20.382 8.29171 20.7421 8.65183C21.1023 9.01195 21.1023 9.59583 20.7421 9.95596L12.3667 18.3314C11.9762 18.7219 11.343 18.7219 10.9525 18.3314L6.65263 14.0304Z"
|
||||
stroke-width="2" />
|
||||
<path clip-rule="evenodd"
|
||||
d="M14 1C6.8203 1 1 6.8203 1 14C1 21.1797 6.8203 27 14 27C21.1797 27 27 21.1797 27 14C27 6.8203 21.1797 1 14 1ZM3 14C3 7.92487 7.92487 3 14 3C20.0751 3 25 7.92487 25 14C25 20.0751 20.0751 25 14 25C7.92487 25 3 20.0751 3 14Z"
|
||||
fill-rule="evenodd" stroke-width="2" />
|
||||
</svg>
|
||||
</template>
|
11
workspaces/frontend/src/components/icons/IconDelete.vue
Normal file
11
workspaces/frontend/src/components/icons/IconDelete.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="icon" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10 12V17" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 12V17" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M4 7H20" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</template>
|
8
workspaces/frontend/src/components/icons/IconRemove.vue
Normal file
8
workspaces/frontend/src/components/icons/IconRemove.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="icon" viewBox="0 0 1024 1024" fill="none">
|
||||
<path
|
||||
d="M905.92 237.76a32 32 0 0 0-52.48 36.48A416 416 0 1 1 96 512a418.56 418.56 0 0 1 297.28-398.72 32 32 0 1 0-18.24-61.44A480 480 0 1 0 992 512a477.12 477.12 0 0 0-86.08-274.24z" />
|
||||
<path
|
||||
d="M630.72 113.28A413.76 413.76 0 0 1 768 185.28a32 32 0 0 0 39.68-50.24 476.8 476.8 0 0 0-160-83.2 32 32 0 0 0-18.24 61.44zM489.28 86.72a36.8 36.8 0 0 0 10.56 6.72 30.08 30.08 0 0 0 24.32 0 37.12 37.12 0 0 0 10.56-6.72A32 32 0 0 0 544 64a33.6 33.6 0 0 0-9.28-22.72A32 32 0 0 0 505.6 32a20.8 20.8 0 0 0-5.76 1.92 23.68 23.68 0 0 0-5.76 2.88l-4.8 3.84a32 32 0 0 0-6.72 10.56A32 32 0 0 0 480 64a32 32 0 0 0 2.56 12.16 37.12 37.12 0 0 0 6.72 10.56zM726.72 297.28a32 32 0 0 0-45.12 0l-169.6 169.6-169.28-169.6A32 32 0 0 0 297.6 342.4l169.28 169.6-169.6 169.28a32 32 0 1 0 45.12 45.12l169.6-169.28 169.28 169.28a32 32 0 0 0 45.12-45.12L557.12 512l169.28-169.28a32 32 0 0 0 0.32-45.44z" />
|
||||
</svg>
|
||||
</template>
|
7
workspaces/frontend/src/components/icons/IconUpload.vue
Normal file
7
workspaces/frontend/src/components/icons/IconUpload.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="icon" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M12 19V12M12 12L9.75 14.3333M12 12L14.25 14.3333M6.6 17.8333C4.61178 17.8333 3 16.1917 3 14.1667C3 12.498 4.09438 11.0897 5.59198 10.6457C5.65562 10.6268 5.7 10.5675 5.7 10.5C5.7 7.46243 8.11766 5 11.1 5C14.0823 5 16.5 7.46243 16.5 10.5C16.5 10.5582 16.5536 10.6014 16.6094 10.5887C16.8638 10.5306 17.1284 10.5 17.4 10.5C19.3882 10.5 21 12.1416 21 14.1667C21 16.1917 19.3882 17.8333 17.4 17.8333"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.65263 14.0304C6.29251 13.6703 6.29251 13.0864 6.65263 12.7263C7.01276 12.3662 7.59663 12.3662 7.95676 12.7263L11.6602 16.4297L19.438 8.65183C19.7981 8.29171 20.382 8.29171 20.7421 8.65183C21.1023 9.01195 21.1023 9.59583 20.7421 9.95596L12.3667 18.3314C11.9762 18.7219 11.343 18.7219 10.9525 18.3314L6.65263 14.0304Z" fill="#000000"/><path clip-rule="evenodd" d="M14 1C6.8203 1 1 6.8203 1 14C1 21.1797 6.8203 27 14 27C21.1797 27 27 21.1797 27 14C27 6.8203 21.1797 1 14 1ZM3 14C3 7.92487 7.92487 3 14 3C20.0751 3 25 7.92487 25 14C25 20.0751 20.0751 25 14 25C7.92487 25 3 20.0751 3 14Z" fill="#000000" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 842 B |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 12V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 7H20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 862 B |
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M905.92 237.76a32 32 0 0 0-52.48 36.48A416 416 0 1 1 96 512a418.56 418.56 0 0 1 297.28-398.72 32 32 0 1 0-18.24-61.44A480 480 0 1 0 992 512a477.12 477.12 0 0 0-86.08-274.24z" fill="#231815" /><path d="M630.72 113.28A413.76 413.76 0 0 1 768 185.28a32 32 0 0 0 39.68-50.24 476.8 476.8 0 0 0-160-83.2 32 32 0 0 0-18.24 61.44zM489.28 86.72a36.8 36.8 0 0 0 10.56 6.72 30.08 30.08 0 0 0 24.32 0 37.12 37.12 0 0 0 10.56-6.72A32 32 0 0 0 544 64a33.6 33.6 0 0 0-9.28-22.72A32 32 0 0 0 505.6 32a20.8 20.8 0 0 0-5.76 1.92 23.68 23.68 0 0 0-5.76 2.88l-4.8 3.84a32 32 0 0 0-6.72 10.56A32 32 0 0 0 480 64a32 32 0 0 0 2.56 12.16 37.12 37.12 0 0 0 6.72 10.56zM726.72 297.28a32 32 0 0 0-45.12 0l-169.6 169.6-169.28-169.6A32 32 0 0 0 297.6 342.4l169.28 169.6-169.6 169.28a32 32 0 1 0 45.12 45.12l169.6-169.28 169.28 169.28a32 32 0 0 0 45.12-45.12L557.12 512l169.28-169.28a32 32 0 0 0 0.32-45.44z" fill="#231815" /></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 19V12M12 12L9.75 14.3333M12 12L14.25 14.3333M6.6 17.8333C4.61178 17.8333 3 16.1917 3 14.1667C3 12.498 4.09438 11.0897 5.59198 10.6457C5.65562 10.6268 5.7 10.5675 5.7 10.5C5.7 7.46243 8.11766 5 11.1 5C14.0823 5 16.5 7.46243 16.5 10.5C16.5 10.5582 16.5536 10.6014 16.6094 10.5887C16.8638 10.5306 17.1284 10.5 17.4 10.5C19.3882 10.5 21 12.1416 21 14.1667C21 16.1917 19.3882 17.8333 17.4 17.8333" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 702 B |
@ -1,14 +0,0 @@
|
||||
export const click_outside = {
|
||||
beforeMount: function(element: any, binding: any) {
|
||||
element.clickOutsideEvent = function(event: any) {
|
||||
if (!(element == event.target || element.contains(event.target))) {
|
||||
binding.value(event);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener("click", element.clickOutsideEvent);
|
||||
},
|
||||
unmounted: function(element: any) {
|
||||
document.body.removeEventListener("click", element.clickOutsideEvent);
|
||||
}
|
||||
}
|
@ -1,2 +1,9 @@
|
||||
export * as api from "@/api";
|
||||
export * as resources from "@/resources";
|
||||
export * as plugins from "@/plugins";
|
||||
export { router, check_auth } from "@/router";
|
||||
export { client } from "@/client";
|
||||
export * as api from "@/client/services.gen.ts";
|
||||
export * as schemas from "@/client/schemas.gen.ts";
|
||||
export * as api_types from "@/client/types.gen.ts";
|
||||
export * as store from "@/store";
|
||||
export * as style from "@/assets/style.css";
|
||||
export * as types from "@/types";
|
||||
|
@ -2,14 +2,20 @@ import App from "@/App.vue";
|
||||
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import { plugins, router, client, style } from "@";
|
||||
|
||||
import router from "@/router";
|
||||
import { click_outside } from "@/directives/click-outside";
|
||||
import "@/assets/style.css";
|
||||
|
||||
const debug = import.meta.hot;
|
||||
|
||||
client.setConfig({
|
||||
baseURL: debug ? "http://localhost:54601" : "/",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(router)
|
||||
.directive("click-outside", click_outside)
|
||||
.directive("click-outside", plugins.clickOutside)
|
||||
.directive("tooltip", plugins.tooltip)
|
||||
.mount('#app');
|
||||
|
||||
|
34
workspaces/frontend/src/plugins/directives.ts
Normal file
34
workspaces/frontend/src/plugins/directives.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export const clickOutside = {
|
||||
beforeMount: function(element: any, binding: any) {
|
||||
element.clickOutsideEvent = function(event: any) {
|
||||
if (!(element == event.target || element.contains(event.target))) {
|
||||
binding.value(event);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener("click", element.clickOutsideEvent);
|
||||
document.body.addEventListener("contextmenu", element.clickOutsideEvent);
|
||||
},
|
||||
unmounted: function(element: any) {
|
||||
document.body.removeEventListener("click", element.clickOutsideEvent);
|
||||
document.body.removeEventListener("contextmenu", element.clickOutsideEvent);
|
||||
}
|
||||
};
|
||||
|
||||
export const tooltip = {
|
||||
beforeMount: function (element: any, binding: any) {
|
||||
element.tooltip = function (event) {
|
||||
let target = event.target;
|
||||
if (target.offsetWidth < target.scrollWidth) {
|
||||
target.setAttribute('title', binding.value?.text ? binding.value.text : event.target.textContent);
|
||||
} else {
|
||||
target.hasAttribute('title') && target.removeAttribute('title');
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('mouseover', element.tooltip);
|
||||
},
|
||||
unmounted: function(element: any) {
|
||||
document.body.removeEventListener("mouseover", element.tooltip);
|
||||
}
|
||||
};
|
1
workspaces/frontend/src/plugins/index.ts
Normal file
1
workspaces/frontend/src/plugins/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "@/plugins/directives";
|
@ -1,23 +0,0 @@
|
||||
import { resources_client, type ResponseError, handle_error } from "@/client";
|
||||
|
||||
|
||||
export type Image = string | ArrayBuffer;
|
||||
|
||||
export async function avatars(avatar_id: string, format?: string): Promise<Image | null | ResponseError> {
|
||||
return await resources_client.get("/avatars/".concat(avatar_id), { params: { format: format ? format : "png" }})
|
||||
.then(async response => {
|
||||
return new Promise<Image | null>((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
reader.readAsDataURL(response.data);
|
||||
})
|
||||
|
||||
})
|
||||
.catch(handle_error);
|
||||
}
|
@ -1,43 +1,50 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { createRouter, createWebHistory, useRoute } from "vue-router";
|
||||
import { store, api, schemas } from "@";
|
||||
|
||||
import { useUserStore } from "@/stores";
|
||||
import { api, resources } from "@";
|
||||
|
||||
async function check_authorized(): Promise<boolean> {
|
||||
const userStore = useUserStore();
|
||||
|
||||
return await api.user.info()
|
||||
.then(async user_info => { userStore.info = user_info; })
|
||||
export const is_authorized = async (): Promise<boolean> => {
|
||||
const userStore = store.useUser();
|
||||
|
||||
return await api.userInfo({ throwOnError: true })
|
||||
.then(async res => { userStore.info = res.data; })
|
||||
.then(async () => {
|
||||
if (!userStore.avatar && userStore.info.avatar) {
|
||||
await resources.avatars(userStore.info.avatar)
|
||||
.then(async avatar => { userStore.avatar = avatar; })
|
||||
await api.resourcesAvatar(userStore.info.avatar)
|
||||
.then(async res => { userStore.avatar = res.data; })
|
||||
}
|
||||
})
|
||||
.then(async () => { return true; })
|
||||
.catch(() => {
|
||||
userStore.clear();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function bypass_auth(to: any, from: any) {
|
||||
if (await check_authorized() && (to.name === "signin" || to.name === "signup")) {
|
||||
const bypass_auth = async (to: any, from: any): any => {
|
||||
if (await is_authorized() && (to.name === "signin" || to.name === "signup")) {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function required_auth(to: any, from: any) {
|
||||
if (!await check_authorized()) {
|
||||
return { name: "signin" };
|
||||
const required_auth = async (to: any, from: any): any => {
|
||||
if (!await is_authorized()) {
|
||||
return { name: "signin", query: {redirect: to.path } };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function required_admin(to: any, from: any) {
|
||||
const userStore = useUserStore();
|
||||
const required_admin = async (to: any, from: any): boolean => {
|
||||
const userStore = store.useUser();
|
||||
return userStore.current.is_admin;
|
||||
}
|
||||
};
|
||||
|
||||
const router = createRouter({
|
||||
export const check_auth = async () => {
|
||||
const route = useRoute();
|
||||
if (!await is_authorized()) {
|
||||
router.push({ name: "signin", query: {redirect: route.path} });
|
||||
}
|
||||
};
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
@ -46,33 +53,33 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/auth/SignIn.vue")
|
||||
component: () => import("@/views/AuthSignIn.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth/signup", name: "signup", //beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/auth/SignUp.vue")
|
||||
path: "/auth/signup", name: "signup", beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/AuthSignUp.vue")
|
||||
},
|
||||
{
|
||||
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
|
||||
component: () => import("@/views/user/Preferencies.vue"),
|
||||
component: () => import("@/views/UserPrefs.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "profile", name: "prefs-profile", beforeEnter: [required_auth],
|
||||
component: () => import("@/views/user/preferencies/Profile.vue")
|
||||
component: () => import("@/views/UserPrefsProfile.vue")
|
||||
},
|
||||
{
|
||||
path: "account", name: "prefs-account", beforeEnter: [required_auth],
|
||||
component: () => import("@/views/user/preferencies/Account.vue")
|
||||
component: () => import("@/views/UserPrefsAccount.vue")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/:user/repository", name: "repository", beforeEnter: [required_auth],
|
||||
component: () => import("@/views/Repository.vue")
|
||||
path: "/:user/repository/:pathMatch(.*)*", name: "repository", beforeEnter: [required_auth],
|
||||
component: () => import("@/views/Repository.vue"),
|
||||
},
|
||||
{
|
||||
path: "/admin/settings", name: "settings", beforeEnter: [required_auth, required_admin],
|
||||
component: () => import("@/views/admin/Settings.vue")
|
||||
component: () => import("@/views/AdminSettings.vue")
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],
|
||||
|
363
workspaces/frontend/src/store/index.ts
Normal file
363
workspaces/frontend/src/store/index.ts
Normal file
@ -0,0 +1,363 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, type Ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import axios, { CancelToken } from "axios";
|
||||
|
||||
import { schemas, types, api, router, check_auth } from "@";
|
||||
|
||||
export const useUser = defineStore("user", () => {
|
||||
const info: Ref<schemas.UserInfoSchema | null> = ref(null);
|
||||
const avatar: Ref<string | ArrayBuffer | null> = ref(null);
|
||||
const isAccessTokenAlive: Ref<boolean> = ref(false);
|
||||
const isRefreshTokenAlive: Ref<boolean> = ref(false);
|
||||
const _accessTokenTimer: object | null = ref(null);
|
||||
const _refreshTokenTimer: object | null = ref(null);
|
||||
const _route = useRoute();
|
||||
|
||||
const clear = () => {
|
||||
info.value = null;
|
||||
avatar.value = null;
|
||||
isAccessTokenAlive.value = false;
|
||||
isRefreshTokenAlive.value = false;
|
||||
_accessTokenTimer.value = null;
|
||||
_refreshTokenTimer.value = null;
|
||||
};
|
||||
|
||||
const refreshInfo = async (error?: Ref<object> | null = null) => {
|
||||
await api.userInfo()
|
||||
.then(async res => {
|
||||
info.value = res.data;
|
||||
|
||||
})
|
||||
.then(async () => {
|
||||
if (!avatar.value && info.value.avatar) {
|
||||
await api.resourcesAvatar(info.value.avatar)
|
||||
.then(async res => { avatar.value = res.data; })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isAuthorized = async (): boolean => {
|
||||
if (!isAccessTokenAlive.value) {
|
||||
await refreshInfo();
|
||||
}
|
||||
if (!info.value) {
|
||||
clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isAdmin = (): boolean | null => {
|
||||
return info.value?.is_admin;
|
||||
};
|
||||
|
||||
const checkAuthorizationRedirect = async (path: string) => {
|
||||
if (!(await isAuthorized())) {
|
||||
router.push({ name: "signin", query: {redirect: path}});
|
||||
}
|
||||
};
|
||||
|
||||
return { info, avatar, clear, refreshInfo, isAuthorized, isAdmin, checkAuthorizationRedirect };
|
||||
});
|
||||
|
||||
export const useMisc = defineStore("misc", () => {
|
||||
// preferencies current tab
|
||||
const p_current_tab: Ref<number> = ref(0);
|
||||
|
||||
return { p_current_tab };
|
||||
});
|
||||
|
||||
export const useUploader = defineStore("uploader", () => {
|
||||
const files: Ref<types.UploadFile[]> = ref([]);
|
||||
const _repoStore = useRepository();
|
||||
|
||||
const loadFiles = (event: Event) => {
|
||||
for (let file of event.target.files) {
|
||||
let uploadFile: types.UploadFile ={
|
||||
content: file,
|
||||
status: "idle",
|
||||
progress: 0
|
||||
};
|
||||
|
||||
files.value.push(uploadFile);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (item: types.UploadFile) => {
|
||||
item.status === "transfer" && item.cancel();
|
||||
|
||||
const index = files.value.indexOf(item);
|
||||
files.value.splice(index, 1);
|
||||
};
|
||||
const removeFiles = () => {
|
||||
for (let file of files.value) {
|
||||
file.status === "transfer" && file.cancel();
|
||||
}
|
||||
files.value = [];
|
||||
};
|
||||
const _uploadFile = async (item: types.UploadFile) => {
|
||||
item.status = "transfer";
|
||||
|
||||
await api.fileCreate({
|
||||
body: {
|
||||
file: item.content,
|
||||
path: "/"
|
||||
},
|
||||
throwOnError: true,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
item.progress = progressEvent.progress;
|
||||
},
|
||||
cancelToken: new CancelToken((c) => {
|
||||
item.cancel = c;
|
||||
})
|
||||
})
|
||||
.then(async () => {
|
||||
item.status = "success";
|
||||
await _repoStore.refreshInfo();
|
||||
})
|
||||
.catch(err => {
|
||||
item.status = "fail";
|
||||
if (err.response?.data) {
|
||||
item.error = err.response.data?.detail || err.response.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
const uploadFile = async (item: types.UploadFile) => {
|
||||
await check_auth();
|
||||
await _uploadFile(item);
|
||||
};
|
||||
const uploadFiles = async () => {
|
||||
await check_auth();
|
||||
let promises = [];
|
||||
|
||||
for (let item of files.value) {
|
||||
if (item.status === "idle" || item.status === "fail") {
|
||||
promises.push(_uploadFile(item));
|
||||
}
|
||||
}
|
||||
|
||||
await axios.all(promises);
|
||||
};
|
||||
|
||||
return { files, loadFiles, removeFile, removeFiles, uploadFile, uploadFiles };
|
||||
});
|
||||
|
||||
export const useRepository = defineStore("repository", () => {
|
||||
const info: Ref<api_types.RepositoryInfo> = ref(null);
|
||||
const content: Ref<types.RepositoryContent> = ref([]);
|
||||
const isCreated: Ref<boolean> = ref(false);
|
||||
const currentPath: Ref<string> = ref("/");
|
||||
const buffer: Ref<types.RepositoryContent> = ref([]);
|
||||
|
||||
const clear = () => {
|
||||
info.value = null;
|
||||
content.value = [];
|
||||
isCreated.value = false;
|
||||
currentPath.value = "/";
|
||||
};
|
||||
|
||||
const refreshInfo = async (path: string | null = null, error?: Ref<object> | null) => {
|
||||
if (path) {
|
||||
currentPath.value = path;
|
||||
}
|
||||
|
||||
await api.repositoryInfo({ throwOnError: true })
|
||||
.then(async res => {
|
||||
isCreated.value = true;
|
||||
info.value = res.data;
|
||||
})
|
||||
.then(async () => {
|
||||
await refreshContent(error);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response.status === 404) {
|
||||
clear();
|
||||
}
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const _processContent = (_content: api_types.RepositoryContent | api_types.DirectoryContent) => {
|
||||
content.value = [];
|
||||
const item_meta: types.RepositoryItemMeta = {
|
||||
selected: false,
|
||||
clickCount: 0,
|
||||
clickTimer: null,
|
||||
dragOvered: false
|
||||
};
|
||||
|
||||
for (let directory of _content.directories) {
|
||||
content.value.push({ info: directory, meta: { ...item_meta }, type: "directory" });
|
||||
}
|
||||
|
||||
for (let file of _content.files) {
|
||||
content.value.push({ info: file, meta: { ...item_meta }, type: "file" });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const refreshContent = async (error?: Ref<object> | null = null) => {
|
||||
let promise = null;
|
||||
|
||||
if (isRoot()) {
|
||||
promise = api.repositoryContent({ throwOnError: true });
|
||||
} else {
|
||||
promise = api.directoryContent({ query: { path: currentPath.value }, throwOnError: true });
|
||||
}
|
||||
|
||||
await promise.then(async res => {
|
||||
_processContent(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const create = async () => {
|
||||
await api.repositoryCreate({ throwOnError: true })
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sizePercent = (): number => {
|
||||
return Math.round(info.value.used / info.value.capacity * 100);
|
||||
};
|
||||
|
||||
const isRoot = (): boolean => {
|
||||
return currentPath.value === "/";
|
||||
};
|
||||
|
||||
const changeDirectory = (current_path: string, directory: api_types.DirectoryInfo) => {
|
||||
router.push({ path: [current_path, directory.name].join("/") });
|
||||
};
|
||||
|
||||
const previousDirectory = (current_path: string) => {
|
||||
let path = current_path.split("/");
|
||||
path.splice(path.length - 1, 1);
|
||||
router.push({ path: path.join("/") });
|
||||
};
|
||||
|
||||
const makeDirectory = async (name: string, error?: Ref<object> | null = null) => {
|
||||
if (name === "") {
|
||||
return;
|
||||
}
|
||||
let path = currentPath.value;
|
||||
if (isRoot()) {
|
||||
path = path + name;
|
||||
} else {
|
||||
path = [path, name].join("/");
|
||||
}
|
||||
await api.directoryCreate({ body: { path: path }, throwOnError: true })
|
||||
.then(async () => {
|
||||
await refreshInfo(currentPath.value, error);
|
||||
})
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteItem = async (item: types.RepositoryItemInfo, error?: Ref<object> | null = null) => {
|
||||
let query = { query: { path: item.info.path }, throwOnError: true };
|
||||
let promise = null;
|
||||
|
||||
if (item.type === "directory") {
|
||||
promise = api.directoryRemove(query);
|
||||
}
|
||||
if (item.type === "file") {
|
||||
promise = api.fileRemove(query);
|
||||
}
|
||||
|
||||
await promise.then(async () => {
|
||||
await refreshInfo(null, error);
|
||||
})
|
||||
.catch(err => {
|
||||
if (error) {
|
||||
error.value = err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSelectedItems = async (error?: Ref<object> | null = null) => {
|
||||
let items = content.value.filter((item) => item.meta.selected);
|
||||
let promises = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
let query = { query: { path: item.info.path }, throwOnError: true };
|
||||
let callback = async () => {
|
||||
await refreshInfo(null, error);
|
||||
};
|
||||
if (item.type === "directory") {
|
||||
promises.push(api.directoryRemove(query).then(callback));
|
||||
}
|
||||
if (item.type === "file") {
|
||||
promises.push(api.fileRemove(query).then(callback));
|
||||
}
|
||||
});
|
||||
|
||||
await axios.all(promises);
|
||||
};
|
||||
|
||||
const moveItems = async (items: types.RepositoryContent, path: string, error?: Ref<object | null> = null) => {
|
||||
let promises = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
let query = { body: { path: item.info.path, target: path }, throwOnError: true };
|
||||
let callback = async () => {
|
||||
await refreshInfo(null, error);
|
||||
};
|
||||
if (item.type === "directory") {
|
||||
promises.push(api.directoryMove(query).then(callback));
|
||||
}
|
||||
if (item.type === "file") {
|
||||
promises.push(api.fileMove(query).then(callback));
|
||||
}
|
||||
});
|
||||
|
||||
await axios.all(promises);
|
||||
};
|
||||
|
||||
const copyBuffer = (items: types.RepositoryContent) => {
|
||||
buffer.value = items;
|
||||
};
|
||||
|
||||
const copyBufferSelected = () => {
|
||||
let items = content.value.filter((item) => item.meta.selected);
|
||||
copyBuffer(items);
|
||||
};
|
||||
|
||||
const copyItems = async (items: types.RepositoryContent, path: string, error?: Ref<object | null> = null) => {
|
||||
let promises = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
let query = { body: { path: item.info.path, target: path }, throwOnError: true };
|
||||
let callback = async () => {
|
||||
await refreshInfo(null, error);
|
||||
};
|
||||
if (item.type === "directory") {
|
||||
promises.push(api.directoryCopy(query).then(callback));
|
||||
}
|
||||
if (item.type === "file") {
|
||||
promises.push(api.fileCopy(query).then(callback));
|
||||
}
|
||||
});
|
||||
|
||||
await axios.all(promises);
|
||||
};
|
||||
|
||||
return { info, content, currentPath, buffer, isCreated, clear, refreshInfo, refreshContent, create, sizePercent, isRoot, changeDirectory, previousDirectory, makeDirectory, deleteItem, deleteSelectedItems, moveItems, copyBuffer, copyBufferSelected, copyItems };
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, type Ref } from "vue";
|
||||
|
||||
import { user } from "@/api";
|
||||
import { resources } from "@";
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
const info: Ref<user.UserInfo | null> = ref(null);
|
||||
const avatar: Ref<resources.Image | null> = ref(null);
|
||||
|
||||
function clear() {
|
||||
info.value = null;
|
||||
avatar.value = null;
|
||||
}
|
||||
|
||||
return { info, avatar, clear };
|
||||
});
|
||||
|
||||
export const useMiscStore = defineStore("misc", () => {
|
||||
// preferencies current tab
|
||||
const p_current_tab: Ref<number> = ref(0);
|
||||
|
||||
return { p_current_tab };
|
||||
});
|
30
workspaces/frontend/src/types.ts
Normal file
30
workspaces/frontend/src/types.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { api_types } from "@";
|
||||
import { CancelToken } from "axios";
|
||||
|
||||
export type Error = object;
|
||||
|
||||
export type RepositoryItemMeta = {
|
||||
selected: bool;
|
||||
clickCount: number;
|
||||
clickTimer: (object | null);
|
||||
};
|
||||
|
||||
export type RepositoryItemType = "directory" | "file";
|
||||
|
||||
export type RepositoryItemInfo = {
|
||||
info: (api_types.DirectoryInfo | api_types.FileInfo);
|
||||
meta: RepositoryItemMeta;
|
||||
type: RepositoryItemType;
|
||||
};
|
||||
|
||||
export type RepositoryContent = RepositoryItemInfo[];
|
||||
|
||||
export type UploadFileStatus = "success" | "fail" | "transfer" | "idle";
|
||||
|
||||
export type UploadFile = {
|
||||
content: File;
|
||||
status: UploadFileStatus;
|
||||
progress: number;
|
||||
cancel: CancelToken | null;
|
||||
error: string | null;
|
||||
};
|
@ -1,31 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { router, api, schemas, store } from "@";
|
||||
import { useRoute } from "vue-router";
|
||||
import Base from "@/views/Base.vue";
|
||||
import Error from "@/components/Error.vue";
|
||||
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
import router from "@/router";
|
||||
import { api } from "@";
|
||||
import { useUserStore } from "@/stores";
|
||||
|
||||
const email_or_username = defineModel("email_or_username");
|
||||
const password = defineModel("password");
|
||||
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
const userStore = store.useUser();
|
||||
const error = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (userStore.current) {
|
||||
router.replace({ path: "/" });
|
||||
router.push({ name: "home" });
|
||||
}
|
||||
});
|
||||
|
||||
async function signin() {
|
||||
const signin = async () => {
|
||||
if (!email_or_username.value || !password.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body: user.UserCredentials = {
|
||||
const body: schemas.UserCredentialsSchema = {
|
||||
name: null,
|
||||
password: password.value,
|
||||
email: null
|
||||
@ -38,12 +37,12 @@ async function signin() {
|
||||
body.name = email_or_username.value;
|
||||
}
|
||||
|
||||
await api.auth.signin(body)
|
||||
await api.authSignin({ body: body, throwOnError: true })
|
||||
.then(async () => {
|
||||
//userStore.info = user_info;
|
||||
router.push({ path: "/" });
|
||||
router.push(route.query.redirect ? { path: route.query.redirect } : { name: "home" });
|
||||
})
|
||||
.catch(err => { error.value = err.message; });
|
||||
.catch(err => { error.value = err; });
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -65,7 +64,7 @@ async function signin() {
|
||||
<button @click="$router.push('/auth/signup')" class="button">Sign Up</button>
|
||||
</div>
|
||||
</form>
|
||||
<Error v-if="error">{{ error }}</Error>
|
||||
<Error :value="error" />
|
||||
</div>
|
||||
</Base>
|
||||
</template>
|
@ -2,24 +2,30 @@
|
||||
import Base from "@/views/Base.vue";
|
||||
import Error from "@/components/Error.vue";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { router, api, schemas, store } from "@";
|
||||
|
||||
import router from "@/router";
|
||||
import { api } from "@";
|
||||
|
||||
const login = defineModel("login");
|
||||
const email = defineModel("email");
|
||||
const password = defineModel("password");
|
||||
|
||||
const userStore = store.useUser();
|
||||
const error = ref(null);
|
||||
|
||||
async function signup() {
|
||||
onMounted(async () => {
|
||||
if (userStore.current) {
|
||||
router.replace({ name: "home" });
|
||||
}
|
||||
});
|
||||
|
||||
const signup = async () => {
|
||||
if (!login.value || !email.value || !password.value) {
|
||||
return;
|
||||
}
|
||||
await api.auth.signup({ name: login.value, password: password.value, email: email.value })
|
||||
.then(async user => { router.push({ path: "/auth/signin" }); })
|
||||
.catch(err => { error.value = err.message; });
|
||||
await api.authSignup({ body: { name: login.value, password: password.value, email: email.value }, throwOnError: true })
|
||||
.then(async () => { router.replace({ name: "signin" }); })
|
||||
.catch(err => { error.value = err; });
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -44,7 +50,7 @@ async function signup() {
|
||||
<button @click="signup" class="button">Sign Up</button>
|
||||
</div>
|
||||
</form>
|
||||
<Error v-if="error">{{ error }}</Error>
|
||||
<Error :value="error" />
|
||||
</div>
|
||||
</Base>
|
||||
</template>
|
@ -1,30 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { router, api, store } from "@";
|
||||
import NavBar from "@/components/NavBar.vue";
|
||||
import DropdownMenu from "@/components/DropdownMenu.vue";
|
||||
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
import router from "@/router";
|
||||
import { user, auth } from "@/api";
|
||||
import { resources } from "@";
|
||||
import { useUserStore } from "@/stores";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userStore = store.useUser();
|
||||
const error = ref(null);
|
||||
|
||||
async function signout() {
|
||||
await auth.signout()
|
||||
const signout = async () => {
|
||||
await api.authSignout()
|
||||
.then(async () => {
|
||||
userStore.clear();
|
||||
router.push({ path: "/" });
|
||||
})
|
||||
.catch(error => { error.value = error; });
|
||||
}
|
||||
.catch(err => { error.value = err; });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow pb-20">
|
||||
<div class="flex-grow sm:pb-20">
|
||||
<NavBar>
|
||||
<template #left>
|
||||
<RouterLink class="link-button" to="/">Home</RouterLink>
|
||||
@ -62,13 +57,12 @@ async function signout() {
|
||||
</template>
|
||||
</NavBar>
|
||||
|
||||
<main class="w-[1000px] ml-auto mr-auto pt-5 pb-5">
|
||||
<main class="w-full max-w-[1000px] ml-auto mr-auto">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-hidden h-full ">
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||
<a href="/">Made with glove by Elnafo, 2024</a>
|
||||
<div>
|
||||
|
@ -4,157 +4,106 @@ import Error from "@/components/Error.vue";
|
||||
import IconDirectory from "@/components/icons/IconDirectory.vue";
|
||||
import IconFile from "@/components/icons/IconFile.vue";
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
import DragItem from "@/components/DragItem.vue";
|
||||
import DropItem from "@/components/DropItem.vue";
|
||||
import RepositoryBrowser from "@/components/RepositoryBrowser.vue";
|
||||
import Modal from "@/components/Modal.vue";
|
||||
import Uploader from "@/components/Uploader.vue";
|
||||
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ref, shallowRef, onMounted, watch } from "vue";
|
||||
import { onBeforeRouteUpdate, useRoute } from "vue-router"
|
||||
import { filesize } from "filesize";
|
||||
import { router, api, store, types, api_types, check_auth } from "@";
|
||||
|
||||
import { repository } from "@/api";
|
||||
import { useUserStore } from "@/stores";
|
||||
import router from "@/router";
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const error = ref<string>(null);
|
||||
|
||||
const repository_info = ref(null);
|
||||
const is_created = ref(null);
|
||||
const repository_content = ref(null);
|
||||
const userStore = store.useUser();
|
||||
const repoStore = store.useRepository();
|
||||
const error = ref<object>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await repository.info()
|
||||
.then(async _repository_info => {
|
||||
is_created.value = true;
|
||||
repository_info.value = _repository_info;
|
||||
})
|
||||
.catch(err => {
|
||||
is_created.value = false;
|
||||
});
|
||||
|
||||
if (is_created.value) {
|
||||
await repository.content()
|
||||
.then(async _repository_content => {
|
||||
repository_content.value = _repository_content;
|
||||
})
|
||||
.catch(err => {
|
||||
error.value = err;
|
||||
})
|
||||
}
|
||||
let path = route.params.pathMatch ? "/" + route.params.pathMatch.join("/") : "/";
|
||||
await repoStore.refreshInfo(path, error);
|
||||
console.log(route);
|
||||
});
|
||||
|
||||
async function create_repository() {
|
||||
await repository.create()
|
||||
.then(async () => {
|
||||
await repository.info()
|
||||
.then(async _repository_info => {
|
||||
repository_info.value = _repository_info;
|
||||
})
|
||||
.catch(err => {
|
||||
error.value = err;
|
||||
});
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
let path = to.params.pathMatch ? "/" + to.params.pathMatch.join("/") : "/";
|
||||
userStore.checkAuthorizationRedirect(to.path);
|
||||
await repoStore.refreshInfo(path, error);
|
||||
console.log("route", route);
|
||||
console.log("store", repoStore);
|
||||
});
|
||||
|
||||
is_created.value = true;
|
||||
})
|
||||
.catch(err => {
|
||||
error.value = err;
|
||||
})
|
||||
}
|
||||
|
||||
function round_size(size: number, mesure: string) {
|
||||
if (mesure == "GB") {
|
||||
return size / 8 / 1024 / 1024;
|
||||
} else if (mesure == "MB") {
|
||||
return size / 8 / 1024;
|
||||
}
|
||||
}
|
||||
const isUploaderOpen = ref(false);
|
||||
|
||||
function size_procent() {
|
||||
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100;
|
||||
}
|
||||
|
||||
function format_creation_time(timestamp: number): string {
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
return `${date.getDate()}-${date.getMonth()}-${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`;
|
||||
}
|
||||
|
||||
const showMenu = ref(false);
|
||||
const ctxMenuPosX = ref(0);
|
||||
const ctxMenuPosY = ref(0);
|
||||
const targetRow = ref({});
|
||||
const contextMenuActions = ref([
|
||||
{ label: 'Edit', action: 'edit' },
|
||||
{ label: 'Delete', action: 'delete' },
|
||||
]);
|
||||
|
||||
const showContextMenu = (event, user) => {
|
||||
event.preventDefault();
|
||||
showMenu.value = true;
|
||||
targetRow.value = user;
|
||||
ctxMenuPosX.value = event.clientX;
|
||||
ctxMenuPosY.value = event.clientY;
|
||||
const openUploader = () => {
|
||||
isUploaderOpen.value = true;
|
||||
};
|
||||
const closeUploader = () => {
|
||||
isUploaderOpen.value = false;
|
||||
};
|
||||
|
||||
const closeContextMenu = () => {
|
||||
showMenu.value = false;
|
||||
const isMakeDirectoryOpen = ref(false);
|
||||
const makeDirectoryName = ref(null);
|
||||
|
||||
const openMakeDirectory = () => {
|
||||
isMakeDirectoryOpen.value = true;
|
||||
};
|
||||
|
||||
function handleActionClick(action) {
|
||||
console.log(action);
|
||||
console.log(targetRow.value);
|
||||
}
|
||||
const closeMakeDirectory = () => {
|
||||
isMakeDirectoryOpen.value = false;
|
||||
makeDirectoryName.value = null;
|
||||
};
|
||||
|
||||
function close_menu() {
|
||||
showMenu.value = false;
|
||||
}
|
||||
const makeDirectory = async () => {
|
||||
await repoStore.makeDirectory(makeDirectoryName.value.value, error);
|
||||
closeMakeDirectory();
|
||||
};
|
||||
|
||||
// repoStore.makeDirectory('test', error)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Base>
|
||||
<Error v-if="error">{{ error }}</Error>
|
||||
<section v-if="is_created">
|
||||
<div class="flex items-center">
|
||||
<div class="w-full rounded-full h-2.5 bg-ctp-surface0">
|
||||
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: size_procent() + '%' }"></div>
|
||||
</div>
|
||||
<span class="min-w-48 text-center">{{ round_size(repository_info.used, "MB").toFixed(2) }} MB / {{
|
||||
round_size(repository_info.capacity, "GB") }} GB</span>
|
||||
<Error :value="error" />
|
||||
|
||||
<Uploader v-if="repoStore.isCreated" :isOpen="isUploaderOpen" @close-uploader="closeUploader()" />
|
||||
|
||||
<Modal :isOpen="isMakeDirectoryOpen" @close-modal="closeMakeDirectory()">
|
||||
<template #header>
|
||||
<h2>Create directory</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex mb-8">
|
||||
<input ref="makeDirectoryName" class="input mr-2">
|
||||
<button class="button" @click="makeDirectory()">Create</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<section v-if="repoStore.isCreated">
|
||||
<div
|
||||
class="flex items-center justify-center fixed bottom-4 bg-ctp-surface0 sm:bg-ctp-crust opacity-90 sm:opacity-100 w-full sm:w-auto sm:relative sm:bottom-auto sm:right-auto my-2">
|
||||
<span class="min-w-48 text-center">{{ filesize(repoStore.info.used)
|
||||
}}
|
||||
/ {{ filesize(repoStore.info.capacity)
|
||||
}}</span>
|
||||
<div class="hidden sm:inline ml-4 mr-4 w-full rounded-full h-2.5 bg-ctp-surface0">
|
||||
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: repoStore.sizePercent() + '%' }"></div>
|
||||
</div>
|
||||
|
||||
<button @click="openMakeDirectory" class="button justify-end mr-2">Create</button>
|
||||
<button @click="openUploader" class="button justify-end">Upload</button>
|
||||
</div>
|
||||
|
||||
<table v-if="repository_content" class="table-auto w-full mt-8 mb-8 pl-8 pr-8 text-ctp-text">
|
||||
<tbody>
|
||||
<tr class="hover:bg-ctp-surface0" v-for="directory in repository_content.directories"
|
||||
@contextmenu.prevent="showContextMenu($event, directory)">
|
||||
<td>
|
||||
<IconDirectory />
|
||||
<div class="inline ml-4">{{ directory.name }}</div>
|
||||
</td>
|
||||
<td class="text-right">-</td>
|
||||
<td class="text-right w-48">
|
||||
{{ format_creation_time(directory.created) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-ctp-surface0" v-for="file in repository_content.files">
|
||||
<td>
|
||||
<IconFile />
|
||||
<div class="inline ml-4">{{ file.name }}</div>
|
||||
</td>
|
||||
<td class="text-right">{{ round_size(file.size, "MB").toFixed(2) }} MB</td>
|
||||
<td class="text-right w-48">
|
||||
{{ format_creation_time(file.created) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<RepositoryBrowser v-if="!error" v-model="repoStore.content" />
|
||||
|
||||
<ContextMenu v-if="showMenu" :actions="contextMenuActions" @action-clicked="handleActionClick" :x="ctxMenuPosX"
|
||||
:y="ctxMenuPosY" v-click-outside="close_menu" />
|
||||
</section>
|
||||
<section v-else>
|
||||
<p>It looks like you don't have a repository yet...</p>
|
||||
<div class="flex justify-center mt-8">
|
||||
<button @click="create_repository" class="button">+ Create repository</button>
|
||||
<button @click="repoStore.create(error)" class="button">+ Create repository</button>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
@ -4,7 +4,7 @@ import Base from "@/views/Base.vue";
|
||||
import { ref, onMounted, watch, getCurrentInstance } from "vue";
|
||||
|
||||
import router from "@/router";
|
||||
import { user } from "@/api";
|
||||
import { api } from "@";
|
||||
import { useUserStore, useMiscStore } from "@/stores";
|
||||
|
||||
const error = ref(null);
|
||||
@ -22,7 +22,9 @@ const avatar_preview = ref(null);
|
||||
onMounted(async () => {
|
||||
miscStore.p_current_tab = 0;
|
||||
|
||||
login.value = userStore.current.login;
|
||||
login.value = userStore.info.name;
|
||||
name.value = userStore.info.full_name;
|
||||
email.value = userStore.info.email;
|
||||
});
|
||||
|
||||
function uploadFile(event) {
|
@ -5,7 +5,7 @@ import Error from "@/components/error/Error.vue";
|
||||
import { ref, onMounted, watch, getCurrentInstance } from "vue";
|
||||
import { onBeforeRouteUpdate, useRoute } from "vue-router"
|
||||
|
||||
import { user } from "@/api";
|
||||
import { api } from "@";
|
||||
import { useUserStore } from "@/stores";
|
||||
|
||||
const route = useRoute();
|
||||
@ -16,11 +16,11 @@ const person = ref<user.User>(null);
|
||||
const avatar = ref<user.Image>(null);
|
||||
|
||||
async function profile(login: string) {
|
||||
await user.profile(login)
|
||||
await api.user.userInfo()
|
||||
.then(async user => { person.value = user; })
|
||||
.then(async () => {
|
||||
if (person.value.avatar?.length) {
|
||||
await user.get_avatar(person.value.avatar)
|
||||
await api.resources.resourcesAvatars(userStore.info.avatar)
|
||||
.then(async _avatar => { avatar.value = _avatar; })
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user