frontend: update openapi client generation

frontend: base repository and uploader views
frontend: new components
This commit is contained in:
L-Nafaryus 2024-09-20 23:15:35 +05:00
parent 1f2e1ec6e4
commit dcdfcec05f
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
49 changed files with 1782 additions and 455 deletions

View File

@ -11,4 +11,5 @@ node_modules/
*.mjs *.mjs
*.log *.log
openapi.json
src/client src/client

View File

@ -9,8 +9,10 @@
"version": "0.0.5", "version": "0.0.5",
"dependencies": { "dependencies": {
"@catppuccin/tailwindcss": "^0.1.6", "@catppuccin/tailwindcss": "^0.1.6",
"@hey-api/client-axios": "^0.2.3",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"axios": "^1.6.8", "axios": "^1.6.8",
"filesize": "^10.1.6",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
@ -18,6 +20,7 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "^0.53.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.19.3", "@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^4.5.2",
@ -879,6 +882,44 @@
"node": ">=12" "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": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1422,6 +1463,18 @@
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
"dev": true "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": { "node_modules/ansi-regex": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "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": "^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": { "node_modules/camelcase": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@ -1674,6 +1755,24 @@
"node": ">= 6" "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": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -1714,6 +1813,21 @@
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
"dev": true "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": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1780,6 +1900,12 @@
"node": ">=0.4.0" "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": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "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", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" "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": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" "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": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -1910,6 +2071,14 @@
"reusify": "^1.0.4" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -1994,6 +2163,36 @@
"node": ">=14.14" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2024,6 +2223,37 @@
"node": ">=6.9.0" "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": { "node_modules/glob": {
"version": "10.3.10", "version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
@ -2133,6 +2363,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -2190,6 +2429,18 @@
"node": ">=0.12.0" "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": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -2325,6 +2576,12 @@
"node": ">= 0.10.0" "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": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -2364,6 +2621,18 @@
"node": ">= 0.6" "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": { "node_modules/minimatch": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@ -2395,6 +2664,61 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2440,6 +2764,12 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "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": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "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" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -2523,6 +2900,27 @@
"node": ">= 6" "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": { "node_modules/openapi-typescript-codegen": {
"version": "0.29.0", "version": "0.29.0",
"resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz", "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
@ -2590,6 +2988,18 @@
"node": "14 || >=16.14" "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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -2684,6 +3094,17 @@
"node": ">= 6" "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": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -3113,6 +3544,18 @@
"node": ">=8" "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": { "node_modules/sucrase": {
"version": "3.35.0", "version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@ -3199,6 +3642,38 @@
"node": ">=14.0.0" "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": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -3256,6 +3731,12 @@
"node": ">=14.17" "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": { "node_modules/uglify-js": {
"version": "3.19.3", "version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",

View File

@ -9,12 +9,15 @@
"preview": "vite preview", "preview": "vite preview",
"build": "vite build", "build": "vite build",
"type-check": "vue-tsc --build --force", "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": { "dependencies": {
"@catppuccin/tailwindcss": "^0.1.6", "@catppuccin/tailwindcss": "^0.1.6",
"@hey-api/client-axios": "^0.2.3",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"axios": "^1.6.8", "axios": "^1.6.8",
"filesize": "^10.1.6",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
@ -22,6 +25,7 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "^0.53.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.19.3", "@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^4.5.2",

View File

@ -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);
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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";

View 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);
}

View File

@ -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);
}

View File

@ -10,11 +10,16 @@
} }
a { a {
@apply text-ctp-green; @apply text-ctp-lavender;
} }
.input { .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 { label {
@ -22,15 +27,15 @@
} }
.button { .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 { .link-button {
@apply button text-ctp-green cursor-pointer; @apply button text-ctp-lavender cursor-pointer border-none;
} }
.hline { .hline {
@apply border-t border-ctp-overlay0 ml-0 mr-0; @apply border-t border-ctp-surface0 ml-0 mr-0;
} }
h1 { h1 {
@ -40,6 +45,10 @@
label { label {
@apply text-ctp-text; @apply text-ctp-text;
} }
.icon {
@apply inline-block select-none text-center overflow-visible w-6 h-6 stroke-ctp-text;
}
} }
@layer utilities { @layer utilities {

View File

@ -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"
});

View File

@ -1,12 +1,4 @@
<template> <script setup lang="ts">
<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>
import { ref, defineProps, defineEmits } from 'vue'; import { ref, defineProps, defineEmits } from 'vue';
const { actions, x, y } = defineProps(['actions', 'x', 'y']); const { actions, x, y } = defineProps(['actions', 'x', 'y']);
@ -17,11 +9,19 @@ const emitAction = (action) => {
}; };
</script> </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> <style scoped>
.context-menu { .context-menu {
position: absolute; position: absolute;
background: white;
border: 1px solid #ccc;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
min-width: 150px; min-width: 150px;
} }
@ -30,8 +30,4 @@ const emitAction = (action) => {
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
} }
.context-menu div:hover {
background-color: #f0f0f0;
}
</style> </style>

View 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>

View 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>

View 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>

View 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>

View File

@ -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> <template>
<h1 class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red"> <section v-if="value" class="mt-1 mb-1">
<slot></slot> <p v-for="entry in handleError(value)"
</h1> class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
{{ entry }}
</p>
</section>
</template> </template>

View 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>

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="relative h-12"> <div class="relative h-12">
<nav <nav class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-mantle">
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"> <div class="items-center m-0 flex">
<slot name="left"></slot> <slot name="left"></slot>
</div> </div>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -1,2 +1,9 @@
export * as api from "@/api"; export * as plugins from "@/plugins";
export * as resources from "@/resources"; 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";

View File

@ -2,14 +2,20 @@ import App from "@/App.vue";
import { createApp } from "vue"; import { createApp } from "vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import { plugins, router, client, style } from "@";
import router from "@/router";
import { click_outside } from "@/directives/click-outside"; const debug = import.meta.hot;
import "@/assets/style.css";
client.setConfig({
baseURL: debug ? "http://localhost:54601" : "/",
withCredentials: true,
});
createApp(App) createApp(App)
.use(createPinia()) .use(createPinia())
.use(router) .use(router)
.directive("click-outside", click_outside) .directive("click-outside", plugins.clickOutside)
.directive("tooltip", plugins.tooltip)
.mount('#app'); .mount('#app');

View 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);
}
};

View File

@ -0,0 +1 @@
export * from "@/plugins/directives";

View File

@ -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);
}

View File

@ -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> { export const is_authorized = async (): Promise<boolean> => {
const userStore = useUserStore(); const userStore = store.useUser();
return await api.user.info() return await api.userInfo({ throwOnError: true })
.then(async user_info => { userStore.info = user_info; }) .then(async res => { userStore.info = res.data; })
.then(async () => { .then(async () => {
if (!userStore.avatar && userStore.info.avatar) { if (!userStore.avatar && userStore.info.avatar) {
await resources.avatars(userStore.info.avatar) await api.resourcesAvatar(userStore.info.avatar)
.then(async avatar => { userStore.avatar = avatar; }) .then(async res => { userStore.avatar = res.data; })
} }
}) })
.then(async () => { return true; }) .then(async () => { return true; })
.catch(() => { .catch(() => {
userStore.clear();
return false; return false;
}); });
} };
async function bypass_auth(to: any, from: any) { const bypass_auth = async (to: any, from: any): any => {
if (await check_authorized() && (to.name === "signin" || to.name === "signup")) { if (await is_authorized() && (to.name === "signin" || to.name === "signup")) {
return from; return from;
} }
} };
async function required_auth(to: any, from: any) { const required_auth = async (to: any, from: any): any => {
if (!await check_authorized()) { if (!await is_authorized()) {
return { name: "signin" }; return { name: "signin", query: {redirect: to.path } };
} }
} };
async function required_admin(to: any, from: any) { const required_admin = async (to: any, from: any): boolean => {
const userStore = useUserStore(); const userStore = store.useUser();
return userStore.current.is_admin; 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(), history: createWebHistory(),
routes: [ routes: [
{ {
@ -46,33 +53,33 @@ const router = createRouter({
}, },
{ {
path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth], 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], path: "/auth/signup", name: "signup", beforeEnter: [bypass_auth],
component: () => import("@/views/auth/SignUp.vue") component: () => import("@/views/AuthSignUp.vue")
}, },
{ {
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth], path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
component: () => import("@/views/user/Preferencies.vue"), component: () => import("@/views/UserPrefs.vue"),
children: [ children: [
{ {
path: "profile", name: "prefs-profile", beforeEnter: [required_auth], 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], 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], path: "/:user/repository/:pathMatch(.*)*", name: "repository", beforeEnter: [required_auth],
component: () => import("@/views/Repository.vue") component: () => import("@/views/Repository.vue"),
}, },
{ {
path: "/admin/settings", name: "settings", beforeEnter: [required_auth, required_admin], 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], path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],

View 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 };
});

View File

@ -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 };
});

View 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;
};

View File

@ -1,31 +1,30 @@
<script setup lang="ts"> <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 Base from "@/views/Base.vue";
import Error from "@/components/Error.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 email_or_username = defineModel("email_or_username");
const password = defineModel("password"); const password = defineModel("password");
const userStore = useUserStore(); const route = useRoute();
const userStore = store.useUser();
const error = ref(null); const error = ref(null);
onMounted(async () => { onMounted(async () => {
if (userStore.current) { if (userStore.current) {
router.replace({ path: "/" }); router.push({ name: "home" });
} }
}); });
async function signin() { const signin = async () => {
if (!email_or_username.value || !password.value) { if (!email_or_username.value || !password.value) {
return; return;
} }
const body: user.UserCredentials = { const body: schemas.UserCredentialsSchema = {
name: null, name: null,
password: password.value, password: password.value,
email: null email: null
@ -38,12 +37,12 @@ async function signin() {
body.name = email_or_username.value; body.name = email_or_username.value;
} }
await api.auth.signin(body) await api.authSignin({ body: body, throwOnError: true })
.then(async () => { .then(async () => {
//userStore.info = user_info; //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> </script>
@ -65,7 +64,7 @@ async function signin() {
<button @click="$router.push('/auth/signup')" class="button">Sign Up</button> <button @click="$router.push('/auth/signup')" class="button">Sign Up</button>
</div> </div>
</form> </form>
<Error v-if="error">{{ error }}</Error> <Error :value="error" />
</div> </div>
</Base> </Base>
</template> </template>

View File

@ -2,24 +2,30 @@
import Base from "@/views/Base.vue"; import Base from "@/views/Base.vue";
import Error from "@/components/Error.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 login = defineModel("login");
const email = defineModel("email"); const email = defineModel("email");
const password = defineModel("password"); const password = defineModel("password");
const userStore = store.useUser();
const error = ref(null); 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) { if (!login.value || !email.value || !password.value) {
return; return;
} }
await api.auth.signup({ name: login.value, password: password.value, email: email.value }) await api.authSignup({ body: { name: login.value, password: password.value, email: email.value }, throwOnError: true })
.then(async user => { router.push({ path: "/auth/signin" }); }) .then(async () => { router.replace({ name: "signin" }); })
.catch(err => { error.value = err.message; }); .catch(err => { error.value = err; });
}; };
</script> </script>
@ -44,7 +50,7 @@ async function signup() {
<button @click="signup" class="button">Sign Up</button> <button @click="signup" class="button">Sign Up</button>
</div> </div>
</form> </form>
<Error v-if="error">{{ error }}</Error> <Error :value="error" />
</div> </div>
</Base> </Base>
</template> </template>

View File

@ -1,30 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue";
import { router, api, store } from "@";
import NavBar from "@/components/NavBar.vue"; import NavBar from "@/components/NavBar.vue";
import DropdownMenu from "@/components/DropdownMenu.vue"; import DropdownMenu from "@/components/DropdownMenu.vue";
import { ref, onMounted } from "vue"; const userStore = store.useUser();
import router from "@/router";
import { user, auth } from "@/api";
import { resources } from "@";
import { useUserStore } from "@/stores";
const userStore = useUserStore();
const error = ref(null); const error = ref(null);
async function signout() { const signout = async () => {
await auth.signout() await api.authSignout()
.then(async () => { .then(async () => {
userStore.clear(); userStore.clear();
router.push({ path: "/" }); router.push({ path: "/" });
}) })
.catch(error => { error.value = error; }); .catch(err => { error.value = err; });
} };
</script> </script>
<template> <template>
<div class="flex-grow pb-20"> <div class="flex-grow sm:pb-20">
<NavBar> <NavBar>
<template #left> <template #left>
<RouterLink class="link-button" to="/">Home</RouterLink> <RouterLink class="link-button" to="/">Home</RouterLink>
@ -62,13 +57,12 @@ async function signout() {
</template> </template>
</NavBar> </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> <slot></slot>
</main> </main>
</div> </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"> <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> <a href="/">Made with glove by Elnafo, 2024</a>
<div> <div>

View File

@ -4,157 +4,106 @@ import Error from "@/components/Error.vue";
import IconDirectory from "@/components/icons/IconDirectory.vue"; import IconDirectory from "@/components/icons/IconDirectory.vue";
import IconFile from "@/components/icons/IconFile.vue"; import IconFile from "@/components/icons/IconFile.vue";
import ContextMenu from "@/components/ContextMenu.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 { 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 route = useRoute();
const userStore = useUserStore(); const userStore = store.useUser();
const error = ref<string>(null); const repoStore = store.useRepository();
const error = ref<object>(null);
const repository_info = ref(null);
const is_created = ref(null);
const repository_content = ref(null);
onMounted(async () => { onMounted(async () => {
await repository.info() let path = route.params.pathMatch ? "/" + route.params.pathMatch.join("/") : "/";
.then(async _repository_info => { await repoStore.refreshInfo(path, error);
is_created.value = true; console.log(route);
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;
})
}
}); });
async function create_repository() { onBeforeRouteUpdate(async (to, from) => {
await repository.create() let path = to.params.pathMatch ? "/" + to.params.pathMatch.join("/") : "/";
.then(async () => { userStore.checkAuthorizationRedirect(to.path);
await repository.info() await repoStore.refreshInfo(path, error);
.then(async _repository_info => { console.log("route", route);
repository_info.value = _repository_info; console.log("store", repoStore);
}) });
.catch(err => {
error.value = err;
});
is_created.value = true;
})
.catch(err => {
error.value = err;
})
}
function round_size(size: number, mesure: string) { const isUploaderOpen = ref(false);
if (mesure == "GB") {
return size / 8 / 1024 / 1024;
} else if (mesure == "MB") {
return size / 8 / 1024;
}
}
function size_procent() { const openUploader = () => {
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100; isUploaderOpen.value = true;
} };
const closeUploader = () => {
function format_creation_time(timestamp: number): string { isUploaderOpen.value = false;
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 closeContextMenu = () => { const isMakeDirectoryOpen = ref(false);
showMenu.value = false; const makeDirectoryName = ref(null);
const openMakeDirectory = () => {
isMakeDirectoryOpen.value = true;
}; };
function handleActionClick(action) { const closeMakeDirectory = () => {
console.log(action); isMakeDirectoryOpen.value = false;
console.log(targetRow.value); makeDirectoryName.value = null;
} };
function close_menu() { const makeDirectory = async () => {
showMenu.value = false; await repoStore.makeDirectory(makeDirectoryName.value.value, error);
} closeMakeDirectory();
};
// repoStore.makeDirectory('test', error)
</script> </script>
<template> <template>
<Base> <Base>
<Error v-if="error">{{ error }}</Error> <Error :value="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>
<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> </div>
<table v-if="repository_content" class="table-auto w-full mt-8 mb-8 pl-8 pr-8 text-ctp-text"> <RepositoryBrowser v-if="!error" v-model="repoStore.content" />
<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>
<ContextMenu v-if="showMenu" :actions="contextMenuActions" @action-clicked="handleActionClick" :x="ctxMenuPosX"
:y="ctxMenuPosY" v-click-outside="close_menu" />
</section> </section>
<section v-else> <section v-else>
<p>It looks like you don't have a repository yet...</p> <p>It looks like you don't have a repository yet...</p>
<div class="flex justify-center mt-8"> <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> </div>
</section> </section>
</Base> </Base>

View File

@ -4,7 +4,7 @@ import Base from "@/views/Base.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue"; import { ref, onMounted, watch, getCurrentInstance } from "vue";
import router from "@/router"; import router from "@/router";
import { user } from "@/api"; import { api } from "@";
import { useUserStore, useMiscStore } from "@/stores"; import { useUserStore, useMiscStore } from "@/stores";
const error = ref(null); const error = ref(null);
@ -22,7 +22,9 @@ const avatar_preview = ref(null);
onMounted(async () => { onMounted(async () => {
miscStore.p_current_tab = 0; 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) { function uploadFile(event) {

View File

@ -5,7 +5,7 @@ import Error from "@/components/error/Error.vue";
import { ref, onMounted, watch, getCurrentInstance } from "vue"; import { ref, onMounted, watch, getCurrentInstance } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router" import { onBeforeRouteUpdate, useRoute } from "vue-router"
import { user } from "@/api"; import { api } from "@";
import { useUserStore } from "@/stores"; import { useUserStore } from "@/stores";
const route = useRoute(); const route = useRoute();
@ -16,11 +16,11 @@ const person = ref<user.User>(null);
const avatar = ref<user.Image>(null); const avatar = ref<user.Image>(null);
async function profile(login: string) { async function profile(login: string) {
await user.profile(login) await api.user.userInfo()
.then(async user => { person.value = user; }) .then(async user => { person.value = user; })
.then(async () => { .then(async () => {
if (person.value.avatar?.length) { 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; }) .then(async _avatar => { avatar.value = _avatar; })
} }
}) })