Merge branch 'vue3-wip' into vue3
This commit is contained in:
commit
86f696e87f
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,3 +32,6 @@ build/
|
||||
|
||||
/frontend/dist/*
|
||||
!/frontend/dist/.gitkeep
|
||||
|
||||
default.nix
|
||||
Dockerfile.dev
|
||||
@ -3,18 +3,26 @@
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"parser": "vue-eslint-parser",
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"@vue/eslint-config-prettier"
|
||||
|
||||
],
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-reserved-component-names": "warn",
|
||||
"vue/no-mutating-props": "warn"
|
||||
"vue/no-mutating-props": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-undef": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
387
frontend/package-lock.json
generated
387
frontend/package-lock.json
generated
@ -33,6 +33,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.12.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue": "^4.3.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
@ -2488,11 +2489,312 @@
|
||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
|
||||
"integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz",
|
||||
"integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.6.0",
|
||||
"@typescript-eslint/type-utils": "6.6.0",
|
||||
"@typescript-eslint/utils": "6.6.0",
|
||||
"@typescript-eslint/visitor-keys": "6.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/parser": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz",
|
||||
"integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.6.0",
|
||||
"@typescript-eslint/types": "6.6.0",
|
||||
"@typescript-eslint/typescript-estree": "6.6.0",
|
||||
"@typescript-eslint/visitor-keys": "6.6.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz",
|
||||
"integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.6.0",
|
||||
"@typescript-eslint/visitor-keys": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz",
|
||||
"integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.6.0",
|
||||
"@typescript-eslint/utils": "6.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz",
|
||||
"integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz",
|
||||
"integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.6.0",
|
||||
"@typescript-eslint/visitor-keys": "6.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz",
|
||||
"integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.6.0",
|
||||
"@typescript-eslint/types": "6.6.0",
|
||||
"@typescript-eslint/typescript-estree": "6.6.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/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/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz",
|
||||
"integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.6.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-legacy": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-4.1.1.tgz",
|
||||
@ -2835,6 +3137,15 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@ -3367,6 +3678,18 @@
|
||||
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
||||
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-type": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -4131,6 +4454,26 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/good-listener": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
|
||||
@ -5083,6 +5426,15 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz",
|
||||
@ -5672,6 +6024,15 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -5875,6 +6236,18 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
|
||||
"integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
@ -5930,6 +6303,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz",
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||
"lint": "eslint --ext .vue,.js src/",
|
||||
"lint:fix": "eslint --ext .vue,.js --fix src/",
|
||||
"lint": "eslint --ext .vue,.ts src/",
|
||||
"lint:fix": "eslint --ext .vue,.ts --fix src/",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
@ -39,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.12.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue": "^4.3.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
|
||||
@ -179,7 +179,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
[{[ if .Theme -]}]
|
||||
<link
|
||||
|
||||
@ -4,20 +4,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "app",
|
||||
mounted() {
|
||||
const loading = document.getElementById("loading");
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
|
||||
onMounted(() => {
|
||||
const loading = document.getElementById("loading");
|
||||
if (loading !== null) {
|
||||
loading.classList.add("done");
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading);
|
||||
if (loading.parentNode !== null) {
|
||||
loading.parentNode.removeChild(loading);
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./css/styles.css";
|
||||
</style>
|
||||
|
||||
@ -5,13 +5,18 @@ import { useAuthStore } from "@/stores/auth";
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
||||
export default function command(url, command, onmessage, onclose) {
|
||||
export default function command(
|
||||
url: string,
|
||||
command: string,
|
||||
onmessage: WebSocket["onmessage"],
|
||||
onclose: WebSocket["onclose"]
|
||||
) {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
url = removePrefix(url);
|
||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
|
||||
|
||||
let conn = new window.WebSocket(url);
|
||||
const conn = new window.WebSocket(url);
|
||||
conn.onopen = () => conn.send(command);
|
||||
conn.onmessage = onmessage;
|
||||
conn.onclose = onclose;
|
||||
@ -3,17 +3,18 @@ import { baseURL } from "@/utils/constants";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { upload as postTus, useTus } from "./tus";
|
||||
|
||||
export async function fetch(url) {
|
||||
export async function fetch(url: ApiUrl) {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
const data = await res.json();
|
||||
data.url = `/files${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
// Perhaps change the any
|
||||
data.items = data.items.map((item: any, index: any) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
@ -28,10 +29,12 @@ export async function fetch(url) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function resourceAction(url, method, content) {
|
||||
async function resourceAction(url: ApiUrl, method: ApiMethod, content?: any) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let opts = { method };
|
||||
const opts: ApiOpts = {
|
||||
method,
|
||||
};
|
||||
|
||||
if (content) {
|
||||
opts.body = content;
|
||||
@ -42,15 +45,15 @@ async function resourceAction(url, method, content) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function remove(url) {
|
||||
export async function remove(url: ApiUrl) {
|
||||
return resourceAction(url, "DELETE");
|
||||
}
|
||||
|
||||
export async function put(url, content = "") {
|
||||
export async function put(url: ApiUrl, content = "") {
|
||||
return resourceAction(url, "PUT", content);
|
||||
}
|
||||
|
||||
export function download(format, ...files) {
|
||||
export function download(format: any, ...files: string[]) {
|
||||
let url = `${baseURL}/api/raw`;
|
||||
|
||||
if (files.length === 1) {
|
||||
@ -58,7 +61,7 @@ export function download(format, ...files) {
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
for (const file of files) {
|
||||
arg += removePrefix(file) + ",";
|
||||
}
|
||||
|
||||
@ -79,7 +82,12 @@ export function download(format, ...files) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
export async function post(url, content = "", overwrite = false, onupload) {
|
||||
export async function post(
|
||||
url: ApiUrl,
|
||||
content: ApiContent = "",
|
||||
overwrite = false,
|
||||
onupload: any = () => {}
|
||||
) {
|
||||
// Use the pre-existing API if:
|
||||
const useResourcesApi =
|
||||
// a folder is being created
|
||||
@ -94,10 +102,15 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
||||
: postTus(url, content, overwrite, onupload);
|
||||
}
|
||||
|
||||
async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
async function postResources(
|
||||
url: ApiUrl,
|
||||
content: ApiContent = "",
|
||||
overwrite = false,
|
||||
onupload: any
|
||||
) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let bufferContent;
|
||||
let bufferContent: ArrayBuffer;
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
!["http:", "https:"].includes(window.location.protocol)
|
||||
@ -107,7 +120,7 @@ async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
|
||||
const authStore = useAuthStore();
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest();
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||
@ -137,12 +150,17 @@ async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
});
|
||||
}
|
||||
|
||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
function moveCopy(
|
||||
items: any[],
|
||||
copy = false,
|
||||
overwrite = false,
|
||||
rename = false
|
||||
) {
|
||||
const promises = [];
|
||||
|
||||
for (let item of items) {
|
||||
for (const item of items) {
|
||||
const from = item.from;
|
||||
const to = encodeURIComponent(removePrefix(item.to));
|
||||
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
||||
const url = `${from}?action=${
|
||||
copy ? "copy" : "rename"
|
||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||
@ -152,20 +170,20 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
export function move(items, overwrite = false, rename = false) {
|
||||
export function move(items: any[], overwrite = false, rename = false) {
|
||||
return moveCopy(items, false, overwrite, rename);
|
||||
}
|
||||
|
||||
export function copy(items, overwrite = false, rename = false) {
|
||||
export function copy(items: any[], overwrite = false, rename = false) {
|
||||
return moveCopy(items, true, overwrite, rename);
|
||||
}
|
||||
|
||||
export async function checksum(url, algo) {
|
||||
export async function checksum(url: ApiUrl, algo: algo) {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
return (await data.json()).checksums[algo];
|
||||
}
|
||||
|
||||
export function getDownloadURL(file, inline) {
|
||||
export function getDownloadURL(file: IFile, inline: any) {
|
||||
const params = {
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
@ -173,7 +191,7 @@ export function getDownloadURL(file, inline) {
|
||||
return createURL("api/raw" + file.path, params);
|
||||
}
|
||||
|
||||
export function getPreviewURL(file, size) {
|
||||
export function getPreviewURL(file: IFile, size: string) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
key: Date.parse(file.modified),
|
||||
@ -182,7 +200,7 @@ export function getPreviewURL(file, size) {
|
||||
return createURL("api/preview/" + size + file.path, params);
|
||||
}
|
||||
|
||||
export function getSubtitlesURL(file) {
|
||||
export function getSubtitlesURL(file: IFile) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
};
|
||||
@ -195,7 +213,7 @@ export function getSubtitlesURL(file) {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
export async function usage(url) {
|
||||
export async function usage(url: ApiUrl) {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/usage${url}`, {});
|
||||
@ -1,7 +1,7 @@
|
||||
import { fetchURL, removePrefix, createURL } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
|
||||
export async function fetch(url, password = "") {
|
||||
export async function fetch(url: ApiUrl, password: string = "") {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(
|
||||
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
|
||||
false
|
||||
);
|
||||
|
||||
let data = await res.json();
|
||||
const data = await res.json();
|
||||
data.url = `/share${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
data.items = data.items.map((item: any, index: any) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
@ -32,7 +32,13 @@ export async function fetch(url, password = "") {
|
||||
return data;
|
||||
}
|
||||
|
||||
export function download(format, hash, token, ...files) {
|
||||
// Is this redundant code?
|
||||
export function download(
|
||||
format: any,
|
||||
hash: string,
|
||||
token: string,
|
||||
...files: any
|
||||
) {
|
||||
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||
|
||||
if (files.length === 1) {
|
||||
@ -40,7 +46,7 @@ export function download(format, hash, token, ...files) {
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
for (const file of files) {
|
||||
arg += encodeURIComponent(file) + ",";
|
||||
}
|
||||
|
||||
@ -60,7 +66,7 @@ export function download(format, hash, token, ...files) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
export function getDownloadURL(share, inline = false) {
|
||||
export function getDownloadURL(share: IFile, inline = false) {
|
||||
const params = {
|
||||
...(inline && { inline: "true" }),
|
||||
...(share.token && { token: share.token }),
|
||||
@ -1,7 +1,7 @@
|
||||
import { fetchURL, removePrefix } from "./utils";
|
||||
import url from "../utils/url";
|
||||
|
||||
export default async function search(base, query) {
|
||||
export default async function search(base: apiUrl, query: string) {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
@ -9,11 +9,11 @@ export default async function search(base, query) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item) => {
|
||||
data = data.map((item: item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
||||
@ -4,7 +4,7 @@ export function get() {
|
||||
return fetchJSON(`/api/settings`, {});
|
||||
}
|
||||
|
||||
export async function update(settings) {
|
||||
export async function update(settings: ISettings) {
|
||||
await fetchURL(`/api/settings`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
@ -4,18 +4,23 @@ export async function list() {
|
||||
return fetchJSON("/api/shares");
|
||||
}
|
||||
|
||||
export async function get(url) {
|
||||
export async function get(url: apiUrl) {
|
||||
url = removePrefix(url);
|
||||
return fetchJSON(`/api/share${url}`);
|
||||
}
|
||||
|
||||
export async function remove(hash) {
|
||||
export async function remove(hash: string) {
|
||||
await fetchURL(`/api/share/${hash}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function create(url, password = "", expires = "", unit = "hours") {
|
||||
export async function create(
|
||||
url: apiUrl,
|
||||
password = "",
|
||||
expires = "",
|
||||
unit = "hours"
|
||||
) {
|
||||
url = removePrefix(url);
|
||||
url = `/api/share${url}`;
|
||||
if (expires !== "") {
|
||||
@ -35,6 +40,6 @@ export async function create(url, password = "", expires = "", unit = "hours") {
|
||||
});
|
||||
}
|
||||
|
||||
export function getShareURL(share) {
|
||||
export function getShareURL(share: share) {
|
||||
return createURL("share/" + share.hash, {}, false);
|
||||
}
|
||||
@ -8,10 +8,10 @@ const RETRY_BASE_DELAY = 1000;
|
||||
const RETRY_MAX_DELAY = 20000;
|
||||
|
||||
export async function upload(
|
||||
filePath,
|
||||
content = "",
|
||||
filePath: string,
|
||||
content: ApiContent = "",
|
||||
overwrite = false,
|
||||
onupload
|
||||
onupload: any
|
||||
) {
|
||||
if (!tusSettings) {
|
||||
// Shouldn't happen as we check for tus support before calling this function
|
||||
@ -19,13 +19,18 @@ export async function upload(
|
||||
}
|
||||
|
||||
filePath = removePrefix(filePath);
|
||||
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||
|
||||
await createUpload(resourcePath);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
return new Promise((resolve, reject) => {
|
||||
let upload = new tus.Upload(content, {
|
||||
|
||||
// Exit early because of typescript, tus content can't be a string
|
||||
if (content === "") {
|
||||
return false;
|
||||
}
|
||||
return new Promise<void | string>((resolve, reject) => {
|
||||
const upload = new tus.Upload(content, {
|
||||
uploadUrl: `${baseURL}${resourcePath}`,
|
||||
chunkSize: tusSettings.chunkSize,
|
||||
retryDelays: computeRetryDelays(tusSettings),
|
||||
@ -52,8 +57,8 @@ export async function upload(
|
||||
});
|
||||
}
|
||||
|
||||
async function createUpload(resourcePath) {
|
||||
let headResp = await fetchURL(resourcePath, {
|
||||
async function createUpload(resourcePath: resourcePath) {
|
||||
const headResp = await fetchURL(resourcePath, {
|
||||
method: "POST",
|
||||
});
|
||||
if (headResp.status !== 201) {
|
||||
@ -63,10 +68,10 @@ async function createUpload(resourcePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function computeRetryDelays(tusSettings) {
|
||||
function computeRetryDelays(tusSettings: tusSettings): number[] | undefined {
|
||||
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||
// Disable retries altogether
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
// The tus client expects our retries as an array with computed backoffs
|
||||
// E.g.: [0, 3000, 5000, 10000, 20000]
|
||||
@ -82,7 +87,7 @@ function computeRetryDelays(tusSettings) {
|
||||
return retryDelays;
|
||||
}
|
||||
|
||||
export async function useTus(content) {
|
||||
export async function useTus(content: ApiContent) {
|
||||
return isTusSupported() && content instanceof Blob;
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@ export async function getAll() {
|
||||
return fetchJSON(`/api/users`, {});
|
||||
}
|
||||
|
||||
export async function get(id) {
|
||||
export async function get(id: number) {
|
||||
return fetchJSON(`/api/users/${id}`, {});
|
||||
}
|
||||
|
||||
export async function create(user) {
|
||||
export async function create(user: user) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@ -23,7 +23,7 @@ export async function create(user) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(user, which = ["all"]) {
|
||||
export async function update(user: user, which = ["all"]) {
|
||||
await fetchURL(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
@ -34,7 +34,7 @@ export async function update(user, which = ["all"]) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function remove(id) {
|
||||
export async function remove(id: number) {
|
||||
await fetchURL(`/api/users/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
@ -3,13 +3,13 @@ import { renew, logout } from "@/utils/auth";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { encodePath } from "@/utils/url";
|
||||
|
||||
export async function fetchURL(url, opts, auth = true) {
|
||||
export async function fetchURL(url: ApiUrl, opts: ApiOpts, auth = true) {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
|
||||
let { headers, ...rest } = opts;
|
||||
const { headers, ...rest } = opts;
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${baseURL}${url}`, {
|
||||
@ -21,6 +21,7 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
});
|
||||
} catch {
|
||||
const error = new Error("000 No connection");
|
||||
// @ts-ignore don't know yet how to solve
|
||||
error.status = 0;
|
||||
|
||||
throw error;
|
||||
@ -32,6 +33,7 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
|
||||
if (res.status < 200 || res.status > 299) {
|
||||
const error = new Error(await res.text());
|
||||
// @ts-ignore don't know yet how to solve
|
||||
error.status = res.status;
|
||||
|
||||
if (auth && res.status == 401) {
|
||||
@ -44,17 +46,17 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function fetchJSON(url, opts) {
|
||||
export async function fetchJSON(url: ApiUrl, opts?: any) {
|
||||
const res = await fetchURL(url, opts);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.json();
|
||||
} else {
|
||||
throw new Error(res.status);
|
||||
throw new Error(res.status.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export function removePrefix(url) {
|
||||
export function removePrefix(url: ApiUrl) {
|
||||
url = url.split("/").splice(2).join("/");
|
||||
|
||||
if (url === "") url = "/";
|
||||
@ -62,7 +64,7 @@ export function removePrefix(url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function createURL(endpoint, params = {}, auth = true) {
|
||||
export function createURL(endpoint: ApiUrl, params = {}, auth = true) {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
let prefix = baseURL;
|
||||
@ -71,7 +73,7 @@ export function createURL(endpoint, params = {}, auth = true) {
|
||||
}
|
||||
const url = new URL(prefix + encodePath(endpoint), origin);
|
||||
|
||||
const searchParams = {
|
||||
const searchParams: searchParams = {
|
||||
...(auth && { auth: authStore.jwt }),
|
||||
...params,
|
||||
};
|
||||
1
frontend/src/index.d.ts
vendored
Normal file
1
frontend/src/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "*.vue";
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { disableExternal } from "@/utils/constants";
|
||||
import { createApp } from "vue";
|
||||
import VueLazyload from "vue-lazyload";
|
||||
@ -12,6 +13,8 @@ import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import "./css/styles.css";
|
||||
|
||||
// register dayjs plugins globally
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { RouteLocation, createRouter, createWebHistory } from "vue-router";
|
||||
import Login from "@/views/Login.vue";
|
||||
import Layout from "@/views/Layout.vue";
|
||||
import Files from "@/views/Files.vue";
|
||||
@ -144,7 +144,8 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)*",
|
||||
redirect: (to) => `/files/${[...to.params.catchAll].join("/")}`,
|
||||
redirect: (to: RouteLocation) =>
|
||||
`/files/${[...to.params.catchAll].join("/")}`,
|
||||
},
|
||||
];
|
||||
|
||||
@ -156,7 +157,7 @@ async function initAuth() {
|
||||
}
|
||||
|
||||
if (recaptcha) {
|
||||
await new Promise((resolve) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
const check = () => {
|
||||
if (typeof window.grecaptcha === "undefined") {
|
||||
setTimeout(check, 100);
|
||||
@ -175,10 +176,11 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
router.beforeResolve(async (to: RouteLocation, from, next) => {
|
||||
let title;
|
||||
try {
|
||||
// this should not fail after we finished the migration
|
||||
// @ts-ignore
|
||||
title = i18n.global.t(titles[to.name]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -187,14 +189,14 @@ router.beforeResolve(async (to, from, next) => {
|
||||
document.title = title + " - " + name;
|
||||
|
||||
/*** RTL related settings per route ****/
|
||||
const rtlSet = document.querySelector("body").classList.contains("rtl");
|
||||
const rtlSet = document.querySelector("body")?.classList.contains("rtl");
|
||||
const shouldSetRtl = rtlLanguages.includes(i18n.global.locale);
|
||||
switch (true) {
|
||||
case shouldSetRtl && !rtlSet:
|
||||
document.querySelector("body").classList.add("rtl");
|
||||
document.querySelector("body")?.classList.add("rtl");
|
||||
break;
|
||||
case !shouldSetRtl && rtlSet:
|
||||
document.querySelector("body").classList.remove("rtl");
|
||||
document.querySelector("body")?.classList.remove("rtl");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -225,7 +227,7 @@ router.beforeResolve(async (to, from, next) => {
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.requiresAdmin)) {
|
||||
if (!authStore.user.perm.admin) {
|
||||
if (authStore.user === null || !authStore.user.perm.admin) {
|
||||
next({ path: "/403" });
|
||||
return;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { defineStore } from "pinia";
|
||||
import dayjs from "dayjs";
|
||||
import i18n, { detectLocale } from "@/i18n";
|
||||
@ -5,7 +6,10 @@ import { cloneDeep } from "lodash-es";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
// convert to a function
|
||||
state: () => ({
|
||||
state: (): {
|
||||
user: user | null;
|
||||
jwt: string;
|
||||
} => ({
|
||||
user: null,
|
||||
jwt: "",
|
||||
}),
|
||||
@ -15,7 +19,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
},
|
||||
actions: {
|
||||
// no context as first argument, use `this` instead
|
||||
setUser(value) {
|
||||
setUser(value: user) {
|
||||
if (value === null) {
|
||||
this.user = null;
|
||||
return;
|
||||
@ -23,19 +27,23 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
const locale = value.locale || detectLocale();
|
||||
dayjs.locale(locale);
|
||||
// @ts-ignore Don't know how to fix this yet
|
||||
i18n.global.locale.value = locale;
|
||||
this.user = value;
|
||||
},
|
||||
updateUser(value) {
|
||||
updateUser(value: user) {
|
||||
if (typeof value !== "object") return;
|
||||
|
||||
for (let field in value) {
|
||||
let field: userKey;
|
||||
for (field in value) {
|
||||
if (field === "locale") {
|
||||
const locale = value[field];
|
||||
dayjs.locale(locale);
|
||||
// @ts-ignore Don't know how to fix this yet
|
||||
i18n.global.locale.value = locale;
|
||||
}
|
||||
|
||||
// @ts-ignore to fix
|
||||
this.user[field] = cloneDeep(value[field]);
|
||||
}
|
||||
},
|
||||
@ -12,7 +12,7 @@ export const useClipboardStore = defineStore("clipboard", {
|
||||
},
|
||||
actions: {
|
||||
// no context as first argument, use `this` instead
|
||||
updateClipboard(value) {
|
||||
updateClipboard(value: any) {
|
||||
this.key = value.key;
|
||||
this.items = value.items;
|
||||
this.path = value.path;
|
||||
@ -2,9 +2,16 @@ import { defineStore } from "pinia";
|
||||
|
||||
export const useFileStore = defineStore("file", {
|
||||
// convert to a function
|
||||
state: () => ({
|
||||
req: {},
|
||||
oldReq: {},
|
||||
state: (): {
|
||||
req: IFile | null;
|
||||
oldReq: IFile | null;
|
||||
reload: boolean;
|
||||
selected: any[];
|
||||
multiple: boolean;
|
||||
isFiles: boolean;
|
||||
} => ({
|
||||
req: null,
|
||||
oldReq: null,
|
||||
reload: false,
|
||||
selected: [],
|
||||
multiple: false,
|
||||
@ -21,7 +28,7 @@ export const useFileStore = defineStore("file", {
|
||||
// return !layoutStore.loading && state.route._value.name === "Files";
|
||||
// },
|
||||
isListing: (state) => {
|
||||
return state.isFiles && state.req.isDir;
|
||||
return state.isFiles && state?.req?.isDir;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -29,12 +36,12 @@ export const useFileStore = defineStore("file", {
|
||||
toggleMultiple() {
|
||||
this.multiple = !this.multiple;
|
||||
},
|
||||
updateRequest(value) {
|
||||
updateRequest(value: IFile | null) {
|
||||
this.oldReq = this.req;
|
||||
this.req = value;
|
||||
},
|
||||
removeSelected(value) {
|
||||
let i = this.selected.indexOf(value);
|
||||
removeSelected(value: any) {
|
||||
const i = this.selected.indexOf(value);
|
||||
if (i === -1) return;
|
||||
this.selected.splice(i, 1);
|
||||
},
|
||||
@ -1,7 +1,8 @@
|
||||
import { createPinia as _createPinia } from "pinia";
|
||||
import { markRaw } from "vue";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
export default function createPinia(router) {
|
||||
export default function createPinia(router: Router) {
|
||||
const pinia = _createPinia();
|
||||
pinia.use(({ store }) => {
|
||||
store.router = markRaw(router);
|
||||
@ -4,7 +4,13 @@ import { defineStore } from "pinia";
|
||||
|
||||
export const useLayoutStore = defineStore("layout", {
|
||||
// convert to a function
|
||||
state: () => ({
|
||||
state: (): {
|
||||
loading: boolean;
|
||||
show: string | null | boolean;
|
||||
showConfirm: any;
|
||||
showAction: boolean | null;
|
||||
showShell: boolean | null;
|
||||
} => ({
|
||||
loading: false,
|
||||
show: null,
|
||||
showConfirm: null,
|
||||
@ -19,7 +25,7 @@ export const useLayoutStore = defineStore("layout", {
|
||||
toggleShell() {
|
||||
this.showShell = !this.showShell;
|
||||
},
|
||||
showHover(value) {
|
||||
showHover(value: LayoutValue | string) {
|
||||
if (typeof value !== "object") {
|
||||
this.show = value;
|
||||
return;
|
||||
@ -33,6 +39,7 @@ export const useLayoutStore = defineStore("layout", {
|
||||
},
|
||||
showError() {
|
||||
this.show = "error";
|
||||
console.error(' error')
|
||||
},
|
||||
showSuccess() {
|
||||
this.show = "success";
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { defineStore } from "pinia";
|
||||
import { useFileStore } from "./file";
|
||||
import { files as api } from "@/api";
|
||||
@ -6,14 +7,22 @@ import buttons from "@/utils/buttons";
|
||||
|
||||
const UPLOADS_LIMIT = 5;
|
||||
|
||||
const beforeUnload = (event) => {
|
||||
const beforeUnload = (event: Event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
// To remove >> is deprecated
|
||||
// event.returnValue = "";
|
||||
};
|
||||
|
||||
export const useUploadStore = defineStore("upload", {
|
||||
// convert to a function
|
||||
state: () => ({
|
||||
state: (): {
|
||||
id: number;
|
||||
sizes: any[];
|
||||
progress: any[];
|
||||
queue: any[];
|
||||
uploads: uploads;
|
||||
error: any;
|
||||
} => ({
|
||||
id: 0,
|
||||
sizes: [],
|
||||
progress: [],
|
||||
@ -30,7 +39,8 @@ export const useUploadStore = defineStore("upload", {
|
||||
|
||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
||||
|
||||
const sum = state.progress.reduce((acc, val) => acc + val);
|
||||
// @ts-ignore
|
||||
const sum: number = state.progress.reduce((acc, val) => acc + val);
|
||||
return Math.ceil((sum / totalSize) * 100);
|
||||
},
|
||||
filesInUploadCount: (state) => {
|
||||
@ -40,7 +50,7 @@ export const useUploadStore = defineStore("upload", {
|
||||
filesInUpload: (state) => {
|
||||
const files = [];
|
||||
|
||||
for (let index in state.uploads) {
|
||||
for (const index in state.uploads) {
|
||||
const upload = state.uploads[index];
|
||||
const id = upload.id;
|
||||
const type = upload.type;
|
||||
@ -65,8 +75,9 @@ export const useUploadStore = defineStore("upload", {
|
||||
},
|
||||
actions: {
|
||||
// no context as first argument, use `this` instead
|
||||
setProgress({ id, loaded }) {
|
||||
setProgress(obj: { id: number; loaded: boolean }) {
|
||||
// Vue.set(this.progress, id, loaded);
|
||||
const { id, loaded } = obj;
|
||||
this.progress[id] = loaded;
|
||||
},
|
||||
setError(error) {
|
||||
@ -77,7 +88,7 @@ export const useUploadStore = defineStore("upload", {
|
||||
this.sizes = [];
|
||||
this.progress = [];
|
||||
},
|
||||
addJob(item) {
|
||||
addJob(item: item) {
|
||||
this.queue.push(item);
|
||||
this.sizes[this.id] = item.file.size;
|
||||
this.id++;
|
||||
@ -88,15 +99,15 @@ export const useUploadStore = defineStore("upload", {
|
||||
// Vue.set(this.uploads, item.id, item);
|
||||
this.uploads[item.id] = item;
|
||||
},
|
||||
removeJob(id) {
|
||||
removeJob(id: number) {
|
||||
// Vue.delete(this.uploads, id);
|
||||
delete this.uploads[id];
|
||||
},
|
||||
upload(item) {
|
||||
let uploadsCount = Object.keys(this.uploads).length;
|
||||
upload(item: item) {
|
||||
const uploadsCount = Object.keys(this.uploads).length;
|
||||
|
||||
let isQueueEmpty = this.queue.length == 0;
|
||||
let isUploadsEmpty = uploadsCount == 0;
|
||||
const isQueueEmpty = this.queue.length == 0;
|
||||
const isUploadsEmpty = uploadsCount == 0;
|
||||
|
||||
if (isQueueEmpty && isUploadsEmpty) {
|
||||
window.addEventListener("beforeunload", beforeUnload);
|
||||
@ -106,8 +117,8 @@ export const useUploadStore = defineStore("upload", {
|
||||
this.addJob(item);
|
||||
this.processUploads();
|
||||
},
|
||||
finishUpload(item) {
|
||||
this.setProgress({ id: item.id, loaded: item.file.size });
|
||||
finishUpload(item: item) {
|
||||
this.setProgress({ id: item.id, loaded: item.file.size > 0 });
|
||||
this.removeJob(item.id);
|
||||
this.processUploads();
|
||||
},
|
||||
@ -136,7 +147,7 @@ export const useUploadStore = defineStore("upload", {
|
||||
if (item.file.isDir) {
|
||||
await api.post(item.path).catch(this.setError);
|
||||
} else {
|
||||
let onUpload = throttle(
|
||||
const onUpload = throttle(
|
||||
(event) =>
|
||||
this.setProgress({
|
||||
id: item.id,
|
||||
40
frontend/src/types/api.d.ts
vendored
Normal file
40
frontend/src/types/api.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
type ApiUrl = string; // Can also be set as a path eg: "path1" | "path2"
|
||||
|
||||
type resourcePath = string;
|
||||
|
||||
type ApiMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
|
||||
type ApiContent =
|
||||
| Blob
|
||||
| File
|
||||
| Pick<ReadableStreamDefaultReader<any>, "read">
|
||||
| "";
|
||||
|
||||
interface ApiOpts {
|
||||
method?: ApiMethod;
|
||||
headers?: object;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
interface tusSettings {
|
||||
retryCount: number;
|
||||
chunkSize: number;
|
||||
}
|
||||
|
||||
type algo = any;
|
||||
|
||||
type inline = any;
|
||||
|
||||
interface share {
|
||||
expire: any;
|
||||
hash: string;
|
||||
path: string;
|
||||
userID: number;
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface settings {
|
||||
any;
|
||||
}
|
||||
|
||||
type searchParams = any;
|
||||
48
frontend/src/types/file.d.ts
vendored
Normal file
48
frontend/src/types/file.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
interface IFile {
|
||||
index?: number;
|
||||
name: string;
|
||||
modified: string;
|
||||
path: string;
|
||||
subtitles: any[];
|
||||
isDir: boolean;
|
||||
size: number;
|
||||
fullPath: string;
|
||||
type: uploadType;
|
||||
items: IFile[];
|
||||
token?: string;
|
||||
hash: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
type uploadType =
|
||||
| "video"
|
||||
| "audio"
|
||||
| "image"
|
||||
| "pdf"
|
||||
| "text"
|
||||
| "blob"
|
||||
| "textImmutable";
|
||||
|
||||
type req = {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
modified: string;
|
||||
mode: number;
|
||||
isDir: boolean;
|
||||
isSymlink: boolean;
|
||||
type: string;
|
||||
url: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
interface uploads {
|
||||
[key: string]: upload;
|
||||
}
|
||||
|
||||
interface upload {
|
||||
id: number;
|
||||
file: file;
|
||||
type: string;
|
||||
}
|
||||
8
frontend/src/types/global.d.ts
vendored
Normal file
8
frontend/src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
FileBrowser: any;
|
||||
grecaptcha: any;
|
||||
}
|
||||
}
|
||||
5
frontend/src/types/layout.d.ts
vendored
Normal file
5
frontend/src/types/layout.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
interface LayoutValue {
|
||||
prompt: string;
|
||||
confirm: any;
|
||||
action?: boolean;
|
||||
}
|
||||
73
frontend/src/types/settings.d.ts
vendored
Normal file
73
frontend/src/types/settings.d.ts
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
interface ISettings {
|
||||
signup: boolean;
|
||||
createUserDir: boolean;
|
||||
userHomeBasePath: string;
|
||||
defaults: Defaults;
|
||||
rules: any[];
|
||||
branding: SettingsBranding;
|
||||
tus: SettingsTus;
|
||||
shell: string[];
|
||||
commands: SettingsCommand;
|
||||
}
|
||||
|
||||
interface SettingsDefaults {
|
||||
scope: string;
|
||||
locale: string;
|
||||
viewMode: string;
|
||||
singleClick: boolean;
|
||||
sorting: SettingsSorting;
|
||||
perm: SettingsPerm;
|
||||
commands: any[];
|
||||
hideDotfiles: boolean;
|
||||
dateFormat: boolean;
|
||||
}
|
||||
|
||||
interface SettingsSorting {
|
||||
by: string;
|
||||
asc: boolean;
|
||||
}
|
||||
|
||||
interface SettingsPerm {
|
||||
admin: boolean;
|
||||
execute: boolean;
|
||||
create: boolean;
|
||||
rename: boolean;
|
||||
modify: boolean;
|
||||
delete: boolean;
|
||||
share: boolean;
|
||||
download: boolean;
|
||||
}
|
||||
|
||||
interface SettingsBranding {
|
||||
name: string;
|
||||
disableExternal: boolean;
|
||||
disableUsedPercentage: boolean;
|
||||
files: string;
|
||||
theme: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface SettingsTus {
|
||||
chunkSize: number;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
interface SettingsCommand {
|
||||
after_copy?: string[];
|
||||
after_delete?: string[];
|
||||
after_rename?: string[];
|
||||
after_save?: string[];
|
||||
after_upload?: string[];
|
||||
before_copy?: string[];
|
||||
before_delete?: string[];
|
||||
before_rename?: string[];
|
||||
before_save?: string[];
|
||||
before_upload?: string[];
|
||||
}
|
||||
|
||||
interface SettingsUnit {
|
||||
KB: number;
|
||||
MB: number;
|
||||
GB: number;
|
||||
TB: number;
|
||||
}
|
||||
1
frontend/src/types/toast.d.ts
vendored
Normal file
1
frontend/src/types/toast.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
type TToast = (message: string) => void;
|
||||
7
frontend/src/types/user.d.ts
vendored
Normal file
7
frontend/src/types/user.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
interface user {
|
||||
id: number;
|
||||
locale: string;
|
||||
perm: any;
|
||||
}
|
||||
|
||||
type userKey = keyof user;
|
||||
1
frontend/src/types/utils.d.ts
vendored
Normal file
1
frontend/src/types/utils.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
type settings = any;
|
||||
@ -3,9 +3,9 @@ import router from "@/router";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import { baseURL } from "./constants";
|
||||
|
||||
export function parseToken(token) {
|
||||
export function parseToken(token: string) {
|
||||
// falsy or malformed jwt will throw InvalidTokenError
|
||||
const data = jwt_decode(token);
|
||||
const data = jwt_decode<{ [key: string]: any; user: user }>(token);
|
||||
|
||||
document.cookie = `auth=${token}; Path=/; SameSite=Strict;`;
|
||||
|
||||
@ -19,7 +19,7 @@ export function parseToken(token) {
|
||||
export async function validateLogin() {
|
||||
try {
|
||||
if (localStorage.getItem("jwt")) {
|
||||
await renew(localStorage.getItem("jwt"));
|
||||
await renew(<string>localStorage.getItem("jwt"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Invalid JWT token in storage"); // eslint-disable-line
|
||||
@ -27,7 +27,11 @@ export async function validateLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(username, password, recaptcha) {
|
||||
export async function login(
|
||||
username: string,
|
||||
password: string,
|
||||
recaptcha: string
|
||||
) {
|
||||
const data = { username, password, recaptcha };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/login`, {
|
||||
@ -47,7 +51,7 @@ export async function login(username, password, recaptcha) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function renew(jwt) {
|
||||
export async function renew(jwt: string) {
|
||||
const res = await fetch(`${baseURL}/api/renew`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -64,7 +68,7 @@ export async function renew(jwt) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(username, password) {
|
||||
export async function signup(username: string, password: string) {
|
||||
const data = { username, password };
|
||||
|
||||
const res = await fetch(`${baseURL}/api/signup`, {
|
||||
@ -76,6 +80,7 @@ export async function signup(username, password) {
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
// @ts-ignore still need to fix these errors
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
@ -86,6 +91,6 @@ export function logout() {
|
||||
const authStore = useAuthStore();
|
||||
authStore.clearUser();
|
||||
|
||||
localStorage.setItem("jwt", null);
|
||||
localStorage.setItem("jwt", "");
|
||||
router.push({ path: "/login" });
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
function loading(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.icon = el.innerHTML;
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.add("spin");
|
||||
el.innerHTML = "autorenew";
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function done(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = el.dataset.icon;
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function success(button) {
|
||||
let el = document.querySelector(`#${button}-button > i`);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = "done";
|
||||
el.style.opacity = 1;
|
||||
|
||||
setTimeout(() => {
|
||||
el.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
el.innerHTML = el.dataset.icon;
|
||||
el.style.opacity = 1;
|
||||
}, 100);
|
||||
}, 500);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export default {
|
||||
loading,
|
||||
done,
|
||||
success,
|
||||
};
|
||||
83
frontend/src/utils/buttons.ts
Normal file
83
frontend/src/utils/buttons.ts
Normal file
@ -0,0 +1,83 @@
|
||||
function loading(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.icon = el.innerHTML;
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el) {
|
||||
el.classList.add("spin");
|
||||
el.innerHTML = "autorenew";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function done(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = el?.dataset?.icon || "";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function success(button: string) {
|
||||
const el: HTMLButtonElement | null = document.querySelector(
|
||||
`#${button}-button > i`
|
||||
);
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log("Error getting button " + button); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.classList.remove("spin");
|
||||
el.innerHTML = "done";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (el) el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.innerHTML = el?.dataset?.icon || "";
|
||||
el.style.opacity = "1";
|
||||
}
|
||||
}, 100);
|
||||
}, 500);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export default {
|
||||
loading,
|
||||
done,
|
||||
success,
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
export default function (name) {
|
||||
let re = new RegExp(
|
||||
export default function (name: string) {
|
||||
const re = new RegExp(
|
||||
"(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"
|
||||
);
|
||||
return document.cookie.replace(re, "$1");
|
||||
@ -1,10 +1,10 @@
|
||||
export default function getRule(rules) {
|
||||
export default function getRule(rules: any) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
rules[i] = rules[i].toLowerCase();
|
||||
}
|
||||
|
||||
let result = null;
|
||||
let find = Array.prototype.find;
|
||||
const find = Array.prototype.find;
|
||||
|
||||
find.call(document.styleSheets, (styleSheet) => {
|
||||
result = find.call(styleSheet.cssRules, (cssRule) => {
|
||||
@ -1 +0,0 @@
|
||||
export * from "./funcs";
|
||||
1
frontend/src/utils/index.ts
Normal file
1
frontend/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// export * from "./funcs";
|
||||
@ -1,26 +1,27 @@
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
export function checkConflict(files: file[], items: item[]) {
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
items = [];
|
||||
}
|
||||
|
||||
let folder_upload = files[0].fullPath !== undefined;
|
||||
const folder_upload = files[0].fullPath !== undefined;
|
||||
|
||||
let conflict = false;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
const file = files[i];
|
||||
let name = file.name;
|
||||
|
||||
if (folder_upload) {
|
||||
let dirs = file.fullPath.split("/");
|
||||
const dirs = file.fullPath.split("/");
|
||||
if (dirs.length > 1) {
|
||||
name = dirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
let res = items.findIndex(function hasConflict(element) {
|
||||
const res = items.findIndex(function hasConflict(element) {
|
||||
// @ts-ignore Don't know what this does
|
||||
return element.name === this;
|
||||
}, name);
|
||||
|
||||
@ -33,13 +34,13 @@ export function checkConflict(files, items) {
|
||||
return conflict;
|
||||
}
|
||||
|
||||
export function scanFiles(dt) {
|
||||
export function scanFiles(dt: { [key: string]: any; item: item }) {
|
||||
return new Promise((resolve) => {
|
||||
let reading = 0;
|
||||
const contents = [];
|
||||
const contents: any[] = [];
|
||||
|
||||
if (dt.items !== undefined) {
|
||||
for (let item of dt.items) {
|
||||
for (const item of dt.items) {
|
||||
if (
|
||||
item.kind === "file" &&
|
||||
typeof item.webkitGetAsEntry === "function"
|
||||
@ -52,10 +53,10 @@ export function scanFiles(dt) {
|
||||
resolve(dt.files);
|
||||
}
|
||||
|
||||
function readEntry(entry, directory = "") {
|
||||
function readEntry(entry: any, directory = "") {
|
||||
if (entry.isFile) {
|
||||
reading++;
|
||||
entry.file((file) => {
|
||||
entry.file((file: file) => {
|
||||
reading--;
|
||||
|
||||
file.fullPath = `${directory}${file.name}`;
|
||||
@ -79,10 +80,10 @@ export function scanFiles(dt) {
|
||||
}
|
||||
}
|
||||
|
||||
function readReaderContent(reader, directory) {
|
||||
function readReaderContent(reader: any, directory: string) {
|
||||
reading++;
|
||||
|
||||
reader.readEntries(function (entries) {
|
||||
reader.readEntries(function (entries: any[]) {
|
||||
reading--;
|
||||
if (entries.length > 0) {
|
||||
for (const entry of entries) {
|
||||
@ -100,7 +101,7 @@ export function scanFiles(dt) {
|
||||
});
|
||||
}
|
||||
|
||||
function detectType(mimetype) {
|
||||
function detectType(mimetype: string): uploadType {
|
||||
if (mimetype.startsWith("video")) return "video";
|
||||
if (mimetype.startsWith("audio")) return "audio";
|
||||
if (mimetype.startsWith("image")) return "image";
|
||||
@ -109,13 +110,13 @@ function detectType(mimetype) {
|
||||
return "blob";
|
||||
}
|
||||
|
||||
export function handleFiles(files, base, overwrite = false) {
|
||||
export function handleFiles(files: file[], base: string, overwrite = false) {
|
||||
const uploadStore = useUploadStore();
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let id = uploadStore.id;
|
||||
const id = uploadStore.id;
|
||||
let path = base;
|
||||
let file = files[i];
|
||||
const file = files[i];
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
path += url.encodePath(file.fullPath);
|
||||
@ -1,5 +1,5 @@
|
||||
export function removeLastDir(url) {
|
||||
var arr = url.split("/");
|
||||
export function removeLastDir(url: string) {
|
||||
const arr = url.split("/");
|
||||
if (arr.pop() === "") {
|
||||
arr.pop();
|
||||
}
|
||||
@ -9,7 +9,7 @@ export function removeLastDir(url) {
|
||||
|
||||
// this function is taken from mozilla
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
|
||||
export function encodeRFC5987ValueChars(str) {
|
||||
export function encodeRFC5987ValueChars(str: string) {
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
// The following creates the sequences %27 %28 %29 %2A (Note that
|
||||
@ -28,7 +28,7 @@ export function encodeRFC5987ValueChars(str) {
|
||||
);
|
||||
}
|
||||
|
||||
export function encodePath(str) {
|
||||
export function encodePath(str: string) {
|
||||
return str
|
||||
.split("/")
|
||||
.map((v) => encodeURIComponent(v))
|
||||
67
frontend/src/utils/vue.cjs
Normal file
67
frontend/src/utils/vue.cjs
Normal file
@ -0,0 +1,67 @@
|
||||
import Vue from "vue";
|
||||
import Noty from "noty";
|
||||
import VueLazyload from "vue-lazyload";
|
||||
// @ts-ignore
|
||||
import i18n from "@/i18n";
|
||||
import { disableExternal } from "@/utils/constants";
|
||||
|
||||
Vue.use(VueLazyload);
|
||||
|
||||
Vue.config.productionTip = true;
|
||||
|
||||
const notyDefault = {
|
||||
type: "info",
|
||||
layout: "bottomRight",
|
||||
timeout: 1000,
|
||||
progressBar: true,
|
||||
};
|
||||
|
||||
Vue.prototype.$noty = (opts) => {
|
||||
new Noty(Object.assign({}, notyDefault, opts)).show();
|
||||
};
|
||||
|
||||
Vue.prototype.$showSuccess = (message) => {
|
||||
new Noty(
|
||||
Object.assign({}, notyDefault, {
|
||||
text: message,
|
||||
type: "success",
|
||||
})
|
||||
).show();
|
||||
};
|
||||
|
||||
Vue.prototype.$showError = (error, displayReport = true) => {
|
||||
let btns = [
|
||||
Noty.button(i18n.t("buttons.close"), "", function () {
|
||||
n.close();
|
||||
}),
|
||||
];
|
||||
|
||||
if (!disableExternal && displayReport) {
|
||||
btns.unshift(
|
||||
Noty.button(i18n.t("buttons.reportIssue"), "", function () {
|
||||
window.open(
|
||||
"https://github.com/filebrowser/filebrowser/issues/new/choose"
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let n = new Noty(
|
||||
Object.assign({}, notyDefault, {
|
||||
text: error.message || error,
|
||||
type: "error",
|
||||
timeout: null,
|
||||
buttons: btns,
|
||||
})
|
||||
);
|
||||
|
||||
n.show();
|
||||
};
|
||||
|
||||
Vue.directive("focus", {
|
||||
inserted: function (el) {
|
||||
el.focus();
|
||||
},
|
||||
});
|
||||
|
||||
export default Vue;
|
||||
@ -4,15 +4,24 @@
|
||||
|
||||
<h2 class="message">
|
||||
<i class="material-icons">{{ info.icon }}</i>
|
||||
<span>{{ $t(info.message) }}</span>
|
||||
<span>{{ t(info.message) }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const errors = {
|
||||
const { t } = useI18n({});
|
||||
|
||||
const errors: {
|
||||
[key: string]: {
|
||||
icon: string;
|
||||
message: string;
|
||||
};
|
||||
} = {
|
||||
0: {
|
||||
icon: "cloud_off",
|
||||
message: "errors.connection",
|
||||
@ -31,16 +40,9 @@ const errors = {
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "errors",
|
||||
components: {
|
||||
HeaderBar,
|
||||
},
|
||||
props: ["errorCode", "showHeader"],
|
||||
computed: {
|
||||
info() {
|
||||
return errors[this.errorCode] ? errors[this.errorCode] : errors[500];
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = defineProps(["errorCode", "showHeader"]);
|
||||
|
||||
const info = computed(() => {
|
||||
return errors[props.errorCode] ? errors[props.errorCode] : errors[500];
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<header-bar v-if="error || req.type == null" showMenu showLogo />
|
||||
<header-bar
|
||||
v-if="error || fileStore.req?.type === null"
|
||||
showMenu
|
||||
showLogo
|
||||
/>
|
||||
|
||||
<breadcrumbs base="/files" />
|
||||
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<Errors v-if="error" :errorCode="error?.status" />
|
||||
<component v-else-if="currentView" :is="currentView"></component>
|
||||
<div v-else>
|
||||
<h2 class="message delayed">
|
||||
@ -13,16 +17,23 @@
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
<span>{{ $t("files.loading") }}</span>
|
||||
<span>{{ t("files.loading") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { files as api } from "@/api";
|
||||
import { mapState, mapActions, mapWritableState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
@ -30,130 +41,108 @@ import { useUploadStore } from "@/stores/upload";
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import Preview from "@/views/files/Preview.vue";
|
||||
import FileListing from "@/views/files/FileListing.vue";
|
||||
const Editor = defineAsyncComponent(() => import("@/views/files/Editor.vue"));
|
||||
|
||||
function clean(path) {
|
||||
const layoutStore = useLayoutStore();
|
||||
const fileStore = useFileStore();
|
||||
const uploadStore = useUploadStore();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { t } = useI18n({});
|
||||
|
||||
const clean = (path: string) => {
|
||||
return path.endsWith("/") ? path.slice(0, -1) : path;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "files",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Breadcrumbs,
|
||||
Errors,
|
||||
Preview,
|
||||
FileListing,
|
||||
Editor: defineAsyncComponent(() => import("@/views/files/Editor.vue")),
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
error: null,
|
||||
width: window.innerWidth,
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapWritableState(useFileStore, [
|
||||
"req",
|
||||
"reload",
|
||||
"selected",
|
||||
"multiple",
|
||||
"isFiles",
|
||||
]),
|
||||
...mapState(useLayoutStore, ["show", "showShell"]),
|
||||
...mapWritableState(useLayoutStore, ["loading"]),
|
||||
...mapState(useUploadStore, {
|
||||
uploadError: "error",
|
||||
}),
|
||||
currentView() {
|
||||
if (this.req.type == undefined) {
|
||||
return null;
|
||||
}
|
||||
const error = ref<any | null>(null);
|
||||
|
||||
if (this.req.isDir) {
|
||||
return "file-listing";
|
||||
} else if (
|
||||
this.req.type === "text" ||
|
||||
this.req.type === "textImmutable"
|
||||
) {
|
||||
return "editor";
|
||||
} else {
|
||||
return "preview";
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
watch: {
|
||||
$route: "fetchData",
|
||||
reload: function (value) {
|
||||
if (value === true) {
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
uploadError(newValue, oldValue) {
|
||||
newValue && newValue !== oldValue && this.$showError(this.uploadError);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isFiles = true;
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
unmounted() {
|
||||
this.isFiles = false;
|
||||
if (this.showShell) {
|
||||
this.toggleShell();
|
||||
const currentView = computed(() => {
|
||||
if (fileStore.req?.type === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fileStore.req.isDir) {
|
||||
return FileListing;
|
||||
} else if (
|
||||
fileStore.req.type === "text" ||
|
||||
fileStore.req.type === "textImmutable"
|
||||
) {
|
||||
return Editor;
|
||||
} else {
|
||||
return Preview;
|
||||
}
|
||||
});
|
||||
|
||||
// Define hooks
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
fileStore.isFiles = true;
|
||||
window.addEventListener("keydown", keyEvent);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", keyEvent);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
fileStore.isFiles = false;
|
||||
if (layoutStore.showShell) {
|
||||
layoutStore.toggleShell();
|
||||
}
|
||||
fileStore.updateRequest(null);
|
||||
});
|
||||
|
||||
watch(route, () => fetchData());
|
||||
// @ts-ignore
|
||||
watch(fileStore.reload, (val) => {
|
||||
if (val) {
|
||||
fetchData();
|
||||
}
|
||||
});
|
||||
watch(uploadStore.error, (newValue, oldValue) => {
|
||||
newValue && newValue !== oldValue && layoutStore.showError();
|
||||
});
|
||||
|
||||
// Define functions
|
||||
|
||||
const fetchData = async () => {
|
||||
// Reset view information.
|
||||
fileStore.reload = false;
|
||||
fileStore.selected = [];
|
||||
fileStore.multiple = false;
|
||||
layoutStore.closeHovers();
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
layoutStore.loading = true;
|
||||
error.value = null;
|
||||
|
||||
let url = route.path;
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
try {
|
||||
const res = await api.fetch(url);
|
||||
|
||||
if (clean(res.path) !== clean(`/${[...route.params.path].join("/")}`)) {
|
||||
throw new Error("Data Mismatch!");
|
||||
}
|
||||
this.updateRequest({});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["toggleShell", "showHover", "closeHovers"]),
|
||||
...mapActions(useFileStore, ["updateRequest"]),
|
||||
async fetchData() {
|
||||
// Reset view information.
|
||||
this.reload = false;
|
||||
this.selected = [];
|
||||
this.multiple = false;
|
||||
this.closeHovers();
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
let url = this.$route.path;
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
|
||||
try {
|
||||
const res = await api.fetch(url);
|
||||
|
||||
if (
|
||||
clean(res.path) !==
|
||||
clean(`/${[...this.$route.params.path].join("/")}`)
|
||||
) {
|
||||
throw new Error("Data Mismatch!");
|
||||
}
|
||||
|
||||
this.updateRequest(res);
|
||||
document.title = `${res.name} - ${document.title}`;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
if (event.key === "F1") {
|
||||
event.preventDefault();
|
||||
this.showHover("help");
|
||||
}
|
||||
},
|
||||
},
|
||||
fileStore.updateRequest(res);
|
||||
document.title = `${res.name} - ${document.title}`;
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
} finally {
|
||||
layoutStore.loading = false;
|
||||
}
|
||||
};
|
||||
const keyEvent = (event: KeyboardEvent) => {
|
||||
if (event.key === "F1") {
|
||||
event.preventDefault();
|
||||
layoutStore.showHover("help");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,56 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="progress" class="progress">
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
<div v-if="uploadStore.getProgress" class="progress">
|
||||
<div v-bind:style="{ width: uploadStore.getProgress + '%' }"></div>
|
||||
</div>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
<shell v-if="isExecEnabled && isLoggedIn && user.perm.execute" />
|
||||
<shell
|
||||
v-if="
|
||||
enableExec && authStore.isLoggedIn && authStore.user?.perm.execute
|
||||
"
|
||||
/>
|
||||
</main>
|
||||
<prompts></prompts>
|
||||
<upload-files></upload-files>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
import Prompts from "@/components/prompts/Prompts.vue";
|
||||
import Shell from "@/components/Shell.vue";
|
||||
import UploadFiles from "@/components/prompts/UploadFiles.vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import { watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "layout",
|
||||
components: {
|
||||
Sidebar,
|
||||
Prompts,
|
||||
Shell,
|
||||
UploadFiles,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["isLoggedIn", "user"]),
|
||||
...mapState(useLayoutStore, ["show"]),
|
||||
...mapState(useUploadStore, { progress: "getProgress" }),
|
||||
...mapWritableState(useFileStore, ["selected", "multiple"]),
|
||||
isExecEnabled: () => enableExec,
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.selected = [];
|
||||
this.multiple = false;
|
||||
if (this.show !== "success") {
|
||||
this.closeHovers();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const layoutStore = useLayoutStore();
|
||||
const authStore = useAuthStore();
|
||||
const fileStore = useFileStore();
|
||||
const uploadStore = useUploadStore();
|
||||
const route = useRoute();
|
||||
|
||||
watch(route, () => {
|
||||
fileStore.selected = [];
|
||||
fileStore.multiple = false;
|
||||
if (layoutStore.show !== "success") {
|
||||
layoutStore.closeHovers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -11,39 +11,37 @@
|
||||
type="text"
|
||||
autocapitalize="off"
|
||||
v-model="username"
|
||||
:placeholder="$t('login.username')"
|
||||
:placeholder="t('login.username')"
|
||||
/>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="password"
|
||||
v-model="password"
|
||||
:placeholder="$t('login.password')"
|
||||
:placeholder="t('login.password')"
|
||||
/>
|
||||
<input
|
||||
class="input input--block"
|
||||
v-if="createMode"
|
||||
type="password"
|
||||
v-model="passwordConfirm"
|
||||
:placeholder="$t('login.passwordConfirm')"
|
||||
:placeholder="t('login.passwordConfirm')"
|
||||
/>
|
||||
|
||||
<div v-if="recaptcha" id="recaptcha"></div>
|
||||
<input
|
||||
class="button button--block"
|
||||
type="submit"
|
||||
:value="createMode ? $t('login.signup') : $t('login.submit')"
|
||||
:value="createMode ? t('login.signup') : t('login.submit')"
|
||||
/>
|
||||
|
||||
<p @click="toggleMode" v-if="signup">
|
||||
{{
|
||||
createMode ? $t("login.loginInstead") : $t("login.createAnAccount")
|
||||
}}
|
||||
{{ createMode ? t("login.loginInstead") : t("login.createAnAccount") }}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import * as auth from "@/utils/auth";
|
||||
import {
|
||||
name,
|
||||
@ -52,79 +50,75 @@ import {
|
||||
recaptchaKey,
|
||||
signup,
|
||||
} from "@/utils/constants";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "login",
|
||||
computed: {
|
||||
signup: () => signup,
|
||||
name: () => name,
|
||||
logoURL: () => logoURL,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
createMode: false,
|
||||
error: "",
|
||||
username: "",
|
||||
password: "",
|
||||
recaptcha: recaptcha,
|
||||
passwordConfirm: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!recaptcha) return;
|
||||
// Define refs
|
||||
const createMode = ref<boolean>(false);
|
||||
const error = ref<string>("");
|
||||
const username = ref<string>("");
|
||||
const password = ref<string>("");
|
||||
const passwordConfirm = ref<string>("");
|
||||
|
||||
window.grecaptcha.ready(function () {
|
||||
window.grecaptcha.render("recaptcha", {
|
||||
sitekey: recaptchaKey,
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
toggleMode() {
|
||||
this.createMode = !this.createMode;
|
||||
},
|
||||
async submit(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n({});
|
||||
// Define functions
|
||||
const toggleMode = () => (createMode.value = !createMode.value);
|
||||
|
||||
let redirect = this.$route.query.redirect;
|
||||
if (redirect === "" || redirect === undefined || redirect === null) {
|
||||
redirect = "/files/";
|
||||
}
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let captcha = "";
|
||||
if (recaptcha) {
|
||||
captcha = window.grecaptcha.getResponse();
|
||||
let redirect = route.query.redirect;
|
||||
if (redirect === "" || redirect === undefined || redirect === null) {
|
||||
redirect = "/files/";
|
||||
}
|
||||
|
||||
if (captcha === "") {
|
||||
this.error = this.$t("login.wrongCredentials");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let captcha = "";
|
||||
if (recaptcha) {
|
||||
captcha = window.grecaptcha.getResponse();
|
||||
|
||||
if (this.createMode) {
|
||||
if (this.password !== this.passwordConfirm) {
|
||||
this.error = this.$t("login.passwordsDontMatch");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (captcha === "") {
|
||||
error.value = t("login.wrongCredentials");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.createMode) {
|
||||
await auth.signup(this.username, this.password);
|
||||
}
|
||||
if (createMode.value) {
|
||||
if (password.value !== passwordConfirm.value) {
|
||||
error.value = t("login.passwordsDontMatch");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await auth.login(this.username, this.password, captcha);
|
||||
this.$router.push({ path: redirect });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.message == 409) {
|
||||
this.error = this.$t("login.usernameTaken");
|
||||
} else {
|
||||
this.error = this.$t("login.wrongCredentials");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
try {
|
||||
if (createMode.value) {
|
||||
await auth.signup(username.value, password.value);
|
||||
}
|
||||
|
||||
await auth.login(username.value, password.value, captcha);
|
||||
// @ts-ignore
|
||||
router.push({ path: redirect });
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (e.message == 409) {
|
||||
error.value = t("login.usernameTaken");
|
||||
} else {
|
||||
error.value = t("login.wrongCredentials");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Run hooks
|
||||
onMounted(() => {
|
||||
if (!recaptcha) return;
|
||||
|
||||
window.grecaptcha.ready(function () {
|
||||
window.grecaptcha.render("recaptcha", {
|
||||
sitekey: recaptchaKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -7,27 +7,27 @@
|
||||
<ul>
|
||||
<router-link to="/settings/profile"
|
||||
><li :class="{ active: $route.path === '/settings/profile' }">
|
||||
{{ $t("settings.profileSettings") }}
|
||||
{{ t("settings.profileSettings") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/shares" v-if="user.perm.share"
|
||||
<router-link to="/settings/shares" v-if="user?.perm.share"
|
||||
><li :class="{ active: $route.path === '/settings/shares' }">
|
||||
{{ $t("settings.shareManagement") }}
|
||||
{{ t("settings.shareManagement") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/global" v-if="user.perm.admin"
|
||||
<router-link to="/settings/global" v-if="user?.perm.admin"
|
||||
><li :class="{ active: $route.path === '/settings/global' }">
|
||||
{{ $t("settings.globalSettings") }}
|
||||
{{ t("settings.globalSettings") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/users" v-if="user.perm.admin"
|
||||
<router-link to="/settings/users" v-if="user?.perm.admin"
|
||||
><li
|
||||
:class="{
|
||||
active:
|
||||
$route.path === '/settings/users' || $route.name === 'User',
|
||||
}"
|
||||
>
|
||||
{{ $t("settings.userManagement") }}
|
||||
{{ t("settings.userManagement") }}
|
||||
</li></router-link
|
||||
>
|
||||
</ul>
|
||||
@ -41,7 +41,7 @@
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
<span>{{ $t("files.loading") }}</span>
|
||||
<span>{{ t("files.loading") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -49,20 +49,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
HeaderBar,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapState(useLayoutStore, ["loading"]),
|
||||
},
|
||||
};
|
||||
const { t } = useI18n();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const user = computed(() => authStore.user);
|
||||
const loading = computed(() => layoutStore.loading);
|
||||
</script>
|
||||
|
||||
@ -4,55 +4,55 @@
|
||||
<title />
|
||||
|
||||
<action
|
||||
v-if="selectedCount"
|
||||
v-if="fileStore.selectedCount"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
:label="t('buttons.download')"
|
||||
@action="download"
|
||||
:counter="selectedCount"
|
||||
:counter="fileStore.selectedCount"
|
||||
/>
|
||||
<button
|
||||
v-if="isSingleFile()"
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="linkSelected()"
|
||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
:aria-label="t('buttons.copyDownloadLinkToClipboard')"
|
||||
:data-title="t('buttons.copyDownloadLinkToClipboard')"
|
||||
>
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
<action
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
:label="t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
</header-bar>
|
||||
|
||||
<breadcrumbs :base="'/share/' + hash" />
|
||||
|
||||
<div v-if="loading">
|
||||
<div v-if="layoutStore.loading">
|
||||
<h2 class="message delayed">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
<span>{{ $t("files.loading") }}</span>
|
||||
<span>{{ t("files.loading") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<div v-if="error.status === 401">
|
||||
<div class="card floating" id="password">
|
||||
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
|
||||
{{ $t("login.wrongCredentials") }}
|
||||
{{ t("login.wrongCredentials") }}
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("login.password") }}</h2>
|
||||
<h2>{{ t("login.password") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<input
|
||||
v-focus
|
||||
type="password"
|
||||
:placeholder="$t('login.password')"
|
||||
:placeholder="t('login.password')"
|
||||
v-model="password"
|
||||
@keyup.enter="fetchData"
|
||||
/>
|
||||
@ -61,43 +61,43 @@
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="fetchData"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')"
|
||||
:aria-label="t('buttons.submit')"
|
||||
:data-title="t('buttons.submit')"
|
||||
>
|
||||
{{ $t("buttons.submit") }}
|
||||
{{ t("buttons.submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<errors v-else :errorCode="error.status" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else-if="req !== null">
|
||||
<div class="share">
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{
|
||||
req.isDir
|
||||
? $t("download.downloadFolder")
|
||||
: $t("download.downloadFile")
|
||||
? t("download.downloadFolder")
|
||||
: t("download.downloadFile")
|
||||
}}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
|
||||
<strong>{{ t("prompts.displayName") }}</strong> {{ req.name }}
|
||||
</div>
|
||||
<div class="share__box__element" :title="modTime">
|
||||
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
|
||||
<div class="share__box__element" :data-title="modTime">
|
||||
<strong>{{ t("prompts.lastModified") }}:</strong> {{ humanTime }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }}
|
||||
<strong>{{ t("prompts.size") }}:</strong> {{ humanSize }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<a target="_blank" :href="link" class="button button--flat">
|
||||
<div>
|
||||
<i class="material-icons">file_download</i
|
||||
>{{ $t("buttons.download") }}
|
||||
>{{ t("buttons.download") }}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@ -108,7 +108,7 @@
|
||||
>
|
||||
<div>
|
||||
<i class="material-icons">open_in_new</i
|
||||
>{{ $t("buttons.openFile") }}
|
||||
>{{ t("buttons.openFile") }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@ -121,11 +121,11 @@
|
||||
class="share__box share__box__items"
|
||||
>
|
||||
<div class="share__box__header" v-if="req.isDir">
|
||||
{{ $t("files.files") }}
|
||||
{{ t("files.files") }}
|
||||
</div>
|
||||
<div id="listing" class="list file-icons">
|
||||
<item
|
||||
v-for="item in req.items.slice(0, this.showLimit)"
|
||||
v-for="item in req.items.slice(0, showLimit)"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
@ -147,14 +147,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{ active: multiple }" id="multiple-selection">
|
||||
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
|
||||
<div
|
||||
:class="{ active: fileStore.multiple }"
|
||||
id="multiple-selection"
|
||||
>
|
||||
<p>{{ t("files.multipleSelectionEnabled") }}</p>
|
||||
<div
|
||||
@click="() => (multiple = false)"
|
||||
@click="() => (fileStore.multiple = false)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:title="$t('files.clear')"
|
||||
:aria-label="$t('files.clear')"
|
||||
:data-title="t('files.clear')"
|
||||
:aria-label="t('files.clear')"
|
||||
class="action"
|
||||
>
|
||||
<i class="material-icons">clear</i>
|
||||
@ -168,7 +171,7 @@
|
||||
>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
<span>{{ t("files.lonely") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,8 +179,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { pub as api } from "@/api";
|
||||
import { filesize } from "filesize";
|
||||
import dayjs from "dayjs";
|
||||
@ -192,171 +194,166 @@ import Item from "@/components/files/ListingItem.vue";
|
||||
import Clipboard from "clipboard";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "share",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Breadcrumbs,
|
||||
Item,
|
||||
QrcodeVue,
|
||||
Errors,
|
||||
},
|
||||
data: () => ({
|
||||
error: null,
|
||||
showLimit: 100,
|
||||
password: "",
|
||||
attemptedPasswordLogin: false,
|
||||
hash: null,
|
||||
token: null,
|
||||
clip: null,
|
||||
}),
|
||||
inject: ["$showSuccess"],
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.showLimit = 100;
|
||||
const error = ref<null | any>(null);
|
||||
const showLimit = ref<number>(100);
|
||||
const password = ref<string>("");
|
||||
const attemptedPasswordLogin = ref<boolean>(false);
|
||||
const hash = ref<any>(null);
|
||||
const token = ref<any>(null);
|
||||
const clip = ref<any>(null);
|
||||
|
||||
this.fetchData();
|
||||
},
|
||||
},
|
||||
created: async function () {
|
||||
const hash = this.$route.params.path[0];
|
||||
this.hash = hash;
|
||||
await this.fetchData();
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
this.clip.destroy();
|
||||
},
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selectedCount"]),
|
||||
...mapWritableState(useFileStore, ["reload", "multiple", "selected"]),
|
||||
...mapWritableState(useLayoutStore, ["loading"]),
|
||||
icon: function () {
|
||||
if (this.req.isDir) return "folder";
|
||||
if (this.req.type === "image") return "insert_photo";
|
||||
if (this.req.type === "audio") return "volume_up";
|
||||
if (this.req.type === "video") return "movie";
|
||||
return "insert_drive_file";
|
||||
},
|
||||
link: function () {
|
||||
return api.getDownloadURL(this.req);
|
||||
},
|
||||
inlineLink: function () {
|
||||
return api.getDownloadURL(this.req, true);
|
||||
},
|
||||
humanSize: function () {
|
||||
if (this.req.isDir) {
|
||||
return this.req.items.length;
|
||||
}
|
||||
const { t } = useI18n({});
|
||||
|
||||
return filesize(this.req.size);
|
||||
},
|
||||
humanTime: function () {
|
||||
return dayjs(this.req.modified).fromNow();
|
||||
},
|
||||
modTime: function () {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useFileStore, ["updateRequest", "toggleMultiple"]),
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
base64: function (name) {
|
||||
return Base64.encodeURI(name);
|
||||
},
|
||||
fetchData: async function () {
|
||||
// Reset view information.
|
||||
this.reload = false;
|
||||
this.selected = [];
|
||||
this.multiple = false;
|
||||
this.closeHovers();
|
||||
const route = useRoute();
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
watch(route, () => {
|
||||
showLimit.value = 100;
|
||||
fetchData();
|
||||
});
|
||||
|
||||
if (this.password !== "") {
|
||||
this.attemptedPasswordLogin = true;
|
||||
}
|
||||
const req = computed(() => fileStore.req);
|
||||
|
||||
let url = this.$route.path;
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
// Define computes
|
||||
|
||||
try {
|
||||
let file = await api.fetch(url, this.password);
|
||||
file.hash = this.hash;
|
||||
const icon = computed(() => {
|
||||
if (req.value === null) return "insert_drive_file";
|
||||
if (req.value.isDir) return "folder";
|
||||
if (req.value.type === "image") return "insert_photo";
|
||||
if (req.value.type === "audio") return "volume_up";
|
||||
if (req.value.type === "video") return "movie";
|
||||
return "insert_drive_file";
|
||||
});
|
||||
|
||||
this.token = file.token || "";
|
||||
const link = computed(() => (req.value ? api.getDownloadURL(req.value) : ""));
|
||||
const inlineLink = computed(() =>
|
||||
req.value ? api.getDownloadURL(req.value, true) : ""
|
||||
);
|
||||
const humanSize = computed(() => {
|
||||
if (req.value) {
|
||||
return req.value.isDir
|
||||
? req.value.items.length
|
||||
: filesize(req.value.size ?? 0);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const humanTime = computed(() => dayjs(req.value?.modified).fromNow());
|
||||
const modTime = computed(() =>
|
||||
req.value
|
||||
? new Date(Date.parse(req.value.modified)).toLocaleString()
|
||||
: new Date()
|
||||
);
|
||||
|
||||
this.updateRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
if (event.key === "Escape") {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (this.selectedCount > 0) {
|
||||
this.selected = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleMultipleSelection() {
|
||||
this.toggleMultiple();
|
||||
},
|
||||
isSingleFile: function () {
|
||||
return (
|
||||
this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir
|
||||
);
|
||||
},
|
||||
download() {
|
||||
if (this.isSingleFile()) {
|
||||
api.download(
|
||||
null,
|
||||
this.hash,
|
||||
this.token,
|
||||
this.req.items[this.selected[0]].path
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Functions
|
||||
const base64 = (name: any) => Base64.encodeURI(name);
|
||||
const fetchData = async () => {
|
||||
fileStore.reload = false;
|
||||
fileStore.selected = [];
|
||||
fileStore.multiple = false;
|
||||
// fileStore.closeHovers();
|
||||
|
||||
this.showHover({
|
||||
prompt: "download",
|
||||
confirm: (format) => {
|
||||
this.closeHovers();
|
||||
// Set loading to true and reset the error.
|
||||
layoutStore.loading = true;
|
||||
error.value = null;
|
||||
if (password.value !== "") {
|
||||
attemptedPasswordLogin.value = true;
|
||||
}
|
||||
|
||||
let files = [];
|
||||
let url = route.path;
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].path);
|
||||
}
|
||||
try {
|
||||
let file = await api.fetch(url, password.value);
|
||||
file.hash = hash.value;
|
||||
|
||||
api.download(format, this.hash, this.token, ...files);
|
||||
},
|
||||
});
|
||||
},
|
||||
linkSelected: function () {
|
||||
return this.isSingleFile()
|
||||
? api.getDownloadURL({
|
||||
hash: this.hash,
|
||||
path: this.req.items[this.selected[0]].path,
|
||||
})
|
||||
: "";
|
||||
},
|
||||
},
|
||||
token.value = file.token || "";
|
||||
|
||||
fileStore.updateRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (e) {
|
||||
error.value = e;
|
||||
} finally {
|
||||
layoutStore.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const keyEvent = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (fileStore.selectedCount > 0) {
|
||||
fileStore.selected = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMultipleSelection = () => {
|
||||
// toggle
|
||||
};
|
||||
|
||||
const isSingleFile = () =>
|
||||
fileStore.selectedCount === 1 &&
|
||||
!req.value?.items[fileStore.selected[0]].isDir;
|
||||
|
||||
const download = () => {
|
||||
if (isSingleFile()) {
|
||||
api.download(
|
||||
null,
|
||||
hash.value,
|
||||
token.value,
|
||||
req.value?.items[fileStore.selected[0]].path
|
||||
);
|
||||
return;
|
||||
}
|
||||
layoutStore.showHover({
|
||||
prompt: "download",
|
||||
confirm: (format: any) => {
|
||||
if (req.value === null) return false;
|
||||
layoutStore.closeHovers();
|
||||
|
||||
let files: string[] = [];
|
||||
|
||||
for (let i of fileStore.selected) {
|
||||
files.push(req.value.items[i].path);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
api.download(format, hash.value, token.value, ...files);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const linkSelected = () => {
|
||||
return isSingleFile() && req.value
|
||||
? // @ts-ignore
|
||||
api.getDownloadURL({
|
||||
hash: hash.value,
|
||||
path: req.value.items[fileStore.selected[0]].path,
|
||||
})
|
||||
: "";
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Created
|
||||
hash.value = route.params.path[0];
|
||||
await fetchData();
|
||||
|
||||
window.addEventListener("keydown", keyEvent);
|
||||
clip.value = new Clipboard(".copy-clipboard");
|
||||
clip.value.on("success", () => {
|
||||
// $showSuccess(this.t("success.linkCopied"));
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", keyEvent);
|
||||
clip.value.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -616,7 +616,7 @@ export default {
|
||||
if (!items) return;
|
||||
|
||||
let columns = Math.floor(
|
||||
document.querySelector("main").offsetWidth / this.columnWidth
|
||||
document.querySelector("main")?.offsetWidth / this.columnWidth
|
||||
);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<div class="row" v-else-if="!loading">
|
||||
<div class="row" v-else-if="!layoutStore.loading && settings !== null">
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.globalSettings") }}</h2>
|
||||
<h2>{{ t("settings.globalSettings") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.signup" />
|
||||
{{ $t("settings.allowSignup") }}
|
||||
{{ t("settings.allowSignup") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.createUserDir" />
|
||||
{{ $t("settings.createUserDir") }}
|
||||
{{ t("settings.createUserDir") }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="small">{{ $t("settings.userHomeBasePath") }}</p>
|
||||
<p class="small">{{ t("settings.userHomeBasePath") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@ -27,22 +27,22 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.globalRules") }}</p>
|
||||
<h3>{{ t("settings.rules") }}</h3>
|
||||
<p class="small">{{ t("settings.globalRules") }}</p>
|
||||
<rules v-model:rules="settings.rules" />
|
||||
|
||||
<div v-if="isExecEnabled">
|
||||
<h3>{{ $t("settings.executeOnShell") }}</h3>
|
||||
<p class="small">{{ $t("settings.executeOnShellDescription") }}</p>
|
||||
<div v-if="enableExec">
|
||||
<h3>{{ t("settings.executeOnShell") }}</h3>
|
||||
<p class="small">{{ t("settings.executeOnShellDescription") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
placeholder="bash -c, cmd /c, ..."
|
||||
v-model="settings.shell"
|
||||
v-model="shellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t("settings.branding") }}</h3>
|
||||
<h3>{{ t("settings.branding") }}</h3>
|
||||
|
||||
<i18n-t
|
||||
keypath="settings.brandingHelp"
|
||||
@ -54,7 +54,7 @@
|
||||
class="link"
|
||||
target="_blank"
|
||||
href="https://filebrowser.org/configuration/custom-branding"
|
||||
>{{ $t("settings.documentation") }}</a
|
||||
>{{ t("settings.documentation") }}</a
|
||||
>
|
||||
</i18n-t>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
v-model="settings.branding.disableExternal"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableExternalLinks") }}
|
||||
{{ t("settings.disableExternalLinks") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@ -73,11 +73,11 @@
|
||||
v-model="settings.branding.disableUsedPercentage"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||
{{ t("settings.disableUsedDiskPercentage") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="theme">{{ $t("settings.themes.title") }}</label>
|
||||
<label for="theme">{{ t("settings.themes.title") }}</label>
|
||||
<themes
|
||||
class="input input--block"
|
||||
v-model:theme="settings.branding.theme"
|
||||
@ -86,7 +86,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
||||
<label for="branding-name">{{ t("settings.instanceName") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@ -97,7 +97,7 @@
|
||||
|
||||
<p>
|
||||
<label for="branding-files">{{
|
||||
$t("settings.brandingDirectoryPath")
|
||||
t("settings.brandingDirectoryPath")
|
||||
}}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
@ -107,14 +107,14 @@
|
||||
/>
|
||||
</p>
|
||||
|
||||
<h3>{{ $t("settings.tusUploads") }}</h3>
|
||||
<h3>{{ t("settings.tusUploads") }}</h3>
|
||||
|
||||
<p class="small">{{ $t("settings.tusUploadsHelp") }}</p>
|
||||
<p class="small">{{ t("settings.tusUploadsHelp") }}</p>
|
||||
|
||||
<div class="tusConditionalSettings">
|
||||
<p>
|
||||
<label for="tus-chunkSize">{{
|
||||
$t("settings.tusUploadsChunkSize")
|
||||
t("settings.tusUploadsChunkSize")
|
||||
}}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
@ -126,7 +126,7 @@
|
||||
|
||||
<p>
|
||||
<label for="tus-retryCount">{{
|
||||
$t("settings.tusUploadsRetryCount")
|
||||
t("settings.tusUploadsRetryCount")
|
||||
}}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
@ -143,7 +143,7 @@
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
:value="t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@ -152,11 +152,11 @@
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.userDefaults") }}</h2>
|
||||
<h2>{{ t("settings.userDefaults") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="small">{{ $t("settings.defaultUserDescription") }}</p>
|
||||
<p class="small">{{ t("settings.defaultUserDescription") }}</p>
|
||||
|
||||
<user-form
|
||||
:isNew="false"
|
||||
@ -169,16 +169,16 @@
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
:value="t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
|
||||
<form v-if="enableExec" class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.commandRunner") }}</h2>
|
||||
<h2>{{ t("settings.commandRunner") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
@ -194,24 +194,24 @@
|
||||
class="link"
|
||||
target="_blank"
|
||||
href="https://filebrowser.org/configuration/command-runner"
|
||||
>{{ $t("settings.documentation") }}</a
|
||||
>{{ t("settings.documentation") }}</a
|
||||
>
|
||||
</i18n-t>
|
||||
|
||||
<div
|
||||
v-for="command in settings.commands"
|
||||
:key="command.name"
|
||||
v-for="(command, key) in settings.commands"
|
||||
:key="key"
|
||||
class="collapsible"
|
||||
>
|
||||
<input :id="command.name" type="checkbox" />
|
||||
<label :for="command.name">
|
||||
<p>{{ capitalize(command.name) }}</p>
|
||||
<input :id="key" type="checkbox" />
|
||||
<label :for="key">
|
||||
<p>{{ capitalize(key) }}</p>
|
||||
<i class="material-icons">arrow_drop_down</i>
|
||||
</label>
|
||||
<div class="collapse">
|
||||
<textarea
|
||||
class="input input--block input--textarea"
|
||||
v-model.trim="command.value"
|
||||
v-model.trim="commandObject[key]"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,7 +221,7 @@
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
:value="t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@ -229,9 +229,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapWritableState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { settings as api } from "@/api";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
@ -239,143 +237,162 @@ import UserForm from "@/components/settings/UserForm.vue";
|
||||
import Rules from "@/components/settings/Rules.vue";
|
||||
import Themes from "@/components/settings/Themes.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
Themes,
|
||||
UserForm,
|
||||
Rules,
|
||||
Errors,
|
||||
const error = ref<any>(null);
|
||||
const originalSettings = ref<ISettings | null>(null);
|
||||
const settings = ref<ISettings | null>(null);
|
||||
const debounceTimeout = ref<number | null>(null);
|
||||
|
||||
const commandObject = ref<{
|
||||
[key in keyof SettingsCommand]: string;
|
||||
}>({});
|
||||
const shellValue = ref<string>("")
|
||||
|
||||
const $showError = inject<TToast>("$showError") as TToast;
|
||||
const $showSuccess = inject<TToast>("$showSuccess") as TToast;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const formattedChunkSize = computed({
|
||||
get() {
|
||||
return settings?.value?.tus?.chunkSize
|
||||
? formatBytes(settings?.value?.tus?.chunkSize)
|
||||
: "";
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
error: null,
|
||||
originalSettings: null,
|
||||
settings: null,
|
||||
debounceTimeout: null,
|
||||
};
|
||||
},
|
||||
inject: ["$showError", "$showSuccess"],
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapWritableState(useLayoutStore, ["loading"]),
|
||||
isExecEnabled: () => enableExec,
|
||||
formattedChunkSize: {
|
||||
get() {
|
||||
return this.formatBytes(this.settings.tus.chunkSize);
|
||||
},
|
||||
set(value) {
|
||||
// Use debouncing to allow the user to type freely without
|
||||
// interruption by the formatter
|
||||
// Clear the previous timeout if it exists
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
// Set a new timeout to apply the format after a short delay
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.settings.tus.chunkSize = this.parseBytes(value);
|
||||
}, 1500);
|
||||
},
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
const original = await api.get();
|
||||
let settings = { ...original, commands: [] };
|
||||
|
||||
for (const key in original.commands) {
|
||||
settings.commands.push({
|
||||
name: key,
|
||||
value: original.commands[key].join("\n"),
|
||||
});
|
||||
}
|
||||
|
||||
settings.shell = settings.shell.join(" ");
|
||||
|
||||
this.originalSettings = original;
|
||||
this.settings = settings;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
set(value: any) {
|
||||
// Use debouncing to allow the user to type freely without
|
||||
// interruption by the formatter
|
||||
// Clear the previous timeout if it exists
|
||||
if (debounceTimeout.value) {
|
||||
clearTimeout(debounceTimeout.value);
|
||||
}
|
||||
|
||||
// Set a new timeout to apply the format after a short delay
|
||||
debounceTimeout.value = setTimeout(() => {
|
||||
if (settings.value) settings.value.tus.chunkSize = parseBytes(value);
|
||||
}, 1500);
|
||||
},
|
||||
methods: {
|
||||
capitalize(name, where = "_") {
|
||||
if (where === "caps") where = /(?=[A-Z])/;
|
||||
let splitted = name.split(where);
|
||||
name = "";
|
||||
});
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name +=
|
||||
splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
|
||||
}
|
||||
// Define funcs
|
||||
const capitalize = (name: string, where: string | RegExp = "_") => {
|
||||
if (where === "caps") where = /(?=[A-Z])/;
|
||||
let splitted = name.split(where);
|
||||
name = "";
|
||||
|
||||
return name.slice(0, -1);
|
||||
},
|
||||
async save() {
|
||||
let settings = {
|
||||
...this.settings,
|
||||
shell: this.settings.shell
|
||||
.trim()
|
||||
.split(" ")
|
||||
.filter((s) => s !== ""),
|
||||
commands: {},
|
||||
};
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
|
||||
}
|
||||
|
||||
for (const { name, value } of this.settings.commands) {
|
||||
settings.commands[name] = value.split("\n").filter((cmd) => cmd !== "");
|
||||
}
|
||||
|
||||
try {
|
||||
await api.update(settings);
|
||||
this.$showSuccess(this.$t("settings.settingsUpdated"));
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
// Parse the user-friendly input (e.g., "20M" or "1T") to bytes
|
||||
parseBytes(input) {
|
||||
const regex = /^(\d+)(\.\d+)?(B|K|KB|M|MB|G|GB|T|TB)?$/i;
|
||||
const matches = input.match(regex);
|
||||
if (matches) {
|
||||
const size = parseFloat(matches[1].concat(matches[2] || ""));
|
||||
let unit = matches[3].toUpperCase();
|
||||
if (!unit.endsWith("B")) {
|
||||
unit += "B";
|
||||
}
|
||||
const units = {
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
};
|
||||
return size * (units[unit] || 1);
|
||||
} else {
|
||||
return 1024 ** 2;
|
||||
}
|
||||
},
|
||||
// Format the chunk size in bytes to user-friendly format
|
||||
formatBytes(bytes) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size}${units[unitIndex]}`;
|
||||
},
|
||||
// Clear the debounce timeout when the component is destroyed
|
||||
beforeUnmount() {
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
},
|
||||
},
|
||||
return name.slice(0, -1);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (settings.value === null) return false;
|
||||
let newSettings: ISettings = {
|
||||
...settings.value,
|
||||
shell:
|
||||
settings.value?.shell
|
||||
.join(" ")
|
||||
.trim()
|
||||
.split(" ")
|
||||
.filter((s: string) => s !== "") ?? [],
|
||||
commands: {},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
for (const name of Object.keys(settings.value.commands)) {
|
||||
// @ts-ignore
|
||||
const newValue = commandObject.value[name]
|
||||
// @ts-ignore
|
||||
if(name in commandObject.value && !Array.isArray(newValue)) {
|
||||
// @ts-ignore
|
||||
newSettings.commands[name] = newValue
|
||||
.split("\n")
|
||||
.filter((cmd: string) => cmd !== "");
|
||||
} else {
|
||||
// @ts-ignore
|
||||
newSettings.commands[name] = newValue
|
||||
}
|
||||
}
|
||||
newSettings.shell = shellValue.value.split("\n");
|
||||
|
||||
try {
|
||||
await api.update(newSettings);
|
||||
$showSuccess(t("settings.settingsUpdated"));
|
||||
} catch (e: any) {
|
||||
$showError(e);
|
||||
}
|
||||
};
|
||||
// Parse the user-friendly input (e.g., "20M" or "1T") to bytes
|
||||
const parseBytes = (input: string) => {
|
||||
const regex = /^(\d+)(\.\d+)?(B|K|KB|M|MB|G|GB|T|TB)?$/i;
|
||||
const matches = input.match(regex);
|
||||
if (matches) {
|
||||
const size = parseFloat(matches[1].concat(matches[2] || ""));
|
||||
let unit: keyof SettingsUnit =
|
||||
matches[3].toUpperCase() as keyof SettingsUnit;
|
||||
if (!unit.endsWith("B")) {
|
||||
unit += "B";
|
||||
}
|
||||
const units: SettingsUnit = {
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
};
|
||||
return size * (units[unit as keyof SettingsUnit] || 1);
|
||||
} else {
|
||||
return 1024 ** 2;
|
||||
}
|
||||
};
|
||||
// Format the chunk size in bytes to user-friendly format
|
||||
const formatBytes = (bytes: number) => {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size}${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
// Define Hooks
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
layoutStore.loading = true;
|
||||
|
||||
const original: ISettings = await api.get();
|
||||
let newSettings: ISettings = { ...original, commands: {} };
|
||||
|
||||
for (const key in original.commands) {
|
||||
// @ts-ignore
|
||||
newSettings.commands[key] = original.commands[key];
|
||||
// @ts-ignore
|
||||
commandObject.value[key] = original.commands[key].join("\n");
|
||||
}
|
||||
|
||||
originalSettings.value = original;
|
||||
settings.value = newSettings;
|
||||
// @ts-ignore
|
||||
shellValue.value = newSettings.shell.join("\n")
|
||||
} catch (e) {
|
||||
error.value = e;
|
||||
} finally {
|
||||
layoutStore.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the debounce timeout when the component is destroyed
|
||||
onBeforeUnmount(() => {
|
||||
if (debounceTimeout.value) {
|
||||
clearTimeout(debounceTimeout.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
30
frontend/tsconfig.json
Normal file
30
frontend/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"outDir": "../dist/frontend",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"],
|
||||
"typeRoots": ["./node_modules/@types", "./some-custom-lib"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.vue",
|
||||
],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@ -8,7 +8,7 @@ import pluginRewriteAll from "vite-plugin-rewrite-all";
|
||||
|
||||
const plugins = [
|
||||
vue(),
|
||||
VueI18nPlugin(),
|
||||
VueI18nPlugin({}),
|
||||
legacy({
|
||||
// defaults already drop IE support
|
||||
targets: ["defaults"],
|
||||
1632
package-lock.json
generated
Normal file
1632
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
package.json
Normal file
4
package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user