replace el-form

This commit is contained in:
pa
2026-01-12 22:42:54 +09:00
committed by Natsumi
parent 82bd985142
commit c814f8f60c
34 changed files with 1419 additions and 736 deletions
+94 -32
View File
@@ -8,8 +8,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hazardous": "^0.3.0", "hazardous": "^0.3.0",
"node-api-dotnet": "^0.9.18", "node-api-dotnet": "^0.9.18"
"vue-sonner": "^2.0.9"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^4.0.2", "@electron/rebuild": "^4.0.2",
@@ -28,6 +27,7 @@
"@tanstack/vue-virtual": "^3.13.18", "@tanstack/vue-virtual": "^3.13.18",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.0.6", "@types/node": "^25.0.6",
"@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.3",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
@@ -61,6 +61,7 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vee-validate": "^4.15.1",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.8", "vue-i18n": "^11.2.8",
@@ -68,8 +69,10 @@
"vue-marquee-text-component": "^2.0.1", "vue-marquee-text-component": "^2.0.1",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-showdown": "^4.2.0", "vue-showdown": "^4.2.0",
"vue-sonner": "^2.0.9",
"worker-timers": "^8.0.28", "worker-timers": "^8.0.28",
"yargs": "^18.0.0" "yargs": "^18.0.0",
"zod": "^4.3.5"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -103,6 +106,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -1260,7 +1264,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1282,7 +1285,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -1299,7 +1301,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -1314,7 +1315,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -5300,6 +5300,7 @@
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@@ -5690,6 +5691,33 @@
"win32" "win32"
] ]
}, },
"node_modules/@vee-validate/zod": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/@vee-validate/zod/-/zod-4.15.1.tgz",
"integrity": "sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^4.8.3",
"vee-validate": "4.15.1"
},
"peerDependencies": {
"zod": "^3.24.0"
}
},
"node_modules/@vee-validate/zod/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
@@ -6017,6 +6045,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -6050,6 +6079,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -6814,6 +6844,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -7762,8 +7793,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "10.1.0", "version": "10.1.0",
@@ -8064,6 +8094,7 @@
"integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==", "integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.4.0", "app-builder-lib": "26.4.0",
"builder-util": "26.3.4", "builder-util": "26.3.4",
@@ -8468,7 +8499,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -8489,7 +8519,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -8647,7 +8676,8 @@
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/embla-carousel-reactive-utils": { "node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0", "version": "8.6.0",
@@ -8693,17 +8723,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -8832,6 +8851,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@@ -8911,6 +8931,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8971,6 +8992,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -13440,14 +13462,16 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
@@ -14673,6 +14697,7 @@
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/devtools-api": "^7.7.7" "@vue/devtools-api": "^7.7.7"
}, },
@@ -14843,7 +14868,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -14861,7 +14885,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -14882,6 +14905,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -15325,7 +15349,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -15340,7 +15363,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@@ -15362,7 +15384,6 @@
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -17200,7 +17221,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -17264,7 +17284,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -17383,6 +17402,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -17892,6 +17912,33 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/vee-validate": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.15.1.tgz",
"integrity": "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.5.2",
"type-fest": "^4.8.3"
},
"peerDependencies": {
"vue": "^3.4.26"
}
},
"node_modules/vee-validate/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/verror": { "node_modules/verror": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -17914,6 +17961,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -18007,6 +18055,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -18020,6 +18069,7 @@
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.26", "@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26", "@vue/compiler-sfc": "3.5.26",
@@ -18216,6 +18266,7 @@
"version": "2.0.9", "version": "2.0.9",
"resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-2.0.9.tgz", "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-2.0.9.tgz",
"integrity": "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==", "integrity": "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@nuxt/kit": "^4.0.3", "@nuxt/kit": "^4.0.3",
@@ -18633,6 +18684,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/zod": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zrender": { "node_modules/zrender": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
+6 -3
View File
@@ -48,6 +48,7 @@
"@tanstack/vue-virtual": "^3.13.18", "@tanstack/vue-virtual": "^3.13.18",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.0.6", "@types/node": "^25.0.6",
"@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.3",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
@@ -81,6 +82,7 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vee-validate": "^4.15.1",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.8", "vue-i18n": "^11.2.8",
@@ -88,8 +90,10 @@
"vue-marquee-text-component": "^2.0.1", "vue-marquee-text-component": "^2.0.1",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-showdown": "^4.2.0", "vue-showdown": "^4.2.0",
"vue-sonner": "^2.0.9",
"worker-timers": "^8.0.28", "worker-timers": "^8.0.28",
"yargs": "^18.0.0" "yargs": "^18.0.0",
"zod": "^4.3.5"
}, },
"build": { "build": {
"appId": "app.vrcx", "appId": "app.vrcx",
@@ -176,7 +180,6 @@
}, },
"dependencies": { "dependencies": {
"hazardous": "^0.3.0", "hazardous": "^0.3.0",
"node-api-dotnet": "^0.9.18", "node-api-dotnet": "^0.9.18"
"vue-sonner": "^2.0.9"
} }
} }
@@ -145,7 +145,7 @@
side="top" side="top"
:content="t('dialog.group.actions.unrepresent_tooltip')"> :content="t('dialog.group.actions.unrepresent_tooltip')">
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="secondary" variant="secondary"
size="icon-lg" size="icon-lg"
style="margin-left: 5px" style="margin-left: 5px"
@@ -156,10 +156,9 @@
<TooltipWrapper v-else side="top" :content="t('dialog.group.actions.represent_tooltip')"> <TooltipWrapper v-else side="top" :content="t('dialog.group.actions.represent_tooltip')">
<span> <span>
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="outline" variant="outline"
size="icon-lg" size="icon-lg"
style="margin-left: 5px"
:disabled="groupDialog.ref.privacy === 'private'" :disabled="groupDialog.ref.privacy === 'private'"
@click="setGroupRepresentation(groupDialog.id)"> @click="setGroupRepresentation(groupDialog.id)">
<StarFilled /> <StarFilled />
@@ -171,10 +170,9 @@
<TooltipWrapper side="top" :content="t('dialog.group.actions.cancel_join_request_tooltip')"> <TooltipWrapper side="top" :content="t('dialog.group.actions.cancel_join_request_tooltip')">
<span> <span>
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="outline" variant="outline"
size="icon-lg" size="icon-lg"
style="margin-left: 5px"
@click="cancelGroupRequest(groupDialog.id)"> @click="cancelGroupRequest(groupDialog.id)">
<Close /> <Close />
</Button> </Button>
@@ -185,10 +183,9 @@
<TooltipWrapper side="top" :content="t('dialog.group.actions.pending_request_tooltip')"> <TooltipWrapper side="top" :content="t('dialog.group.actions.pending_request_tooltip')">
<span> <span>
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="outline" variant="outline"
size="icon-lg" size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)"> @click="joinGroup(groupDialog.id)">
<Check /> <Check />
</Button> </Button>
@@ -201,10 +198,9 @@
side="top" side="top"
:content="t('dialog.group.actions.request_join_tooltip')"> :content="t('dialog.group.actions.request_join_tooltip')">
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="outline" variant="outline"
size="icon-lg" size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)"> @click="joinGroup(groupDialog.id)">
<Message /> <Message />
</Button> </Button>
@@ -214,12 +210,7 @@
side="top" side="top"
:content="t('dialog.group.actions.invite_required_tooltip')"> :content="t('dialog.group.actions.invite_required_tooltip')">
<span> <span>
<Button <Button class="rounded-full mr-2" variant="outline" size="icon-lg" disabled>
class="rounded-full"
variant="outline"
size="icon-lg"
disabled
style="margin-left: 5px">
<Message /> <Message />
</Button> </Button>
</span> </span>
@@ -229,10 +220,9 @@
side="top" side="top"
:content="t('dialog.group.actions.join_group_tooltip')"> :content="t('dialog.group.actions.join_group_tooltip')">
<Button <Button
class="rounded-full" class="rounded-full mr-2"
variant="outline" variant="outline"
size="icon-lg" size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)"> @click="joinGroup(groupDialog.id)">
<Check /> <Check />
</Button> </Button>
@@ -245,8 +235,7 @@
:variant=" :variant="
groupDialog.ref.membershipStatus === 'userblocked' ? 'destructive' : 'outline' groupDialog.ref.membershipStatus === 'userblocked' ? 'destructive' : 'outline'
" "
size="icon-lg" size="icon-lg">
style="margin-left: 5px">
<MoreFilled /> <MoreFilled />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -5,88 +5,105 @@
width="650px" width="650px"
append-to-body> append-to-body>
<div v-if="groupPostEditDialog.visible"> <div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3> <FieldGroup class="gap-4">
<el-form :model="groupPostEditDialog" label-width="150px"> <Field>
<el-form-item :label="t('dialog.group_post_edit.title')"> <FieldLabel>{{ t('dialog.group_post_edit.title') }}</FieldLabel>
<InputGroupField v-model="groupPostEditDialog.title" size="sm" /> <FieldContent>
</el-form-item> <InputGroupField v-model="groupPostEditDialog.title" size="sm" />
<el-form-item :label="t('dialog.group_post_edit.message')"> </FieldContent>
<InputGroupTextareaField </Field>
v-model="groupPostEditDialog.text" <Field>
:rows="4" <FieldLabel>{{ t('dialog.group_post_edit.message') }}</FieldLabel>
style="margin-top: 10px" <FieldContent>
input-class="resize-none" /> <InputGroupTextareaField
</el-form-item> v-model="groupPostEditDialog.text"
<el-form-item> :rows="4"
<label v-if="!groupPostEditDialog.postId" class="inline-flex items-center gap-2"> style="margin-top: 10px"
<Checkbox v-model="groupPostEditDialog.sendNotification" /> input-class="resize-none" />
<span>{{ t('dialog.group_post_edit.send_notification') }}</span> </FieldContent>
</label> </Field>
</el-form-item> <Field v-if="!groupPostEditDialog.postId">
<el-form-item :label="t('dialog.group_post_edit.post_visibility')"> <FieldLabel class="sr-only">{{ t('dialog.group_post_edit.send_notification') }}</FieldLabel>
<RadioGroup v-model="groupPostEditDialog.visibility" class="flex items-center gap-4"> <FieldContent>
<div class="flex items-center space-x-2"> <label class="inline-flex items-center gap-2">
<RadioGroupItem id="groupPostVisibility-public" value="public" /> <Checkbox v-model="groupPostEditDialog.sendNotification" />
<label for="groupPostVisibility-public"> <span>{{ t('dialog.group_post_edit.send_notification') }}</span>
{{ t('dialog.group_post_edit.visibility_public') }} </label>
</label> </FieldContent>
</div> </Field>
<div class="flex items-center space-x-2"> <Field>
<RadioGroupItem id="groupPostVisibility-group" value="group" /> <FieldLabel>{{ t('dialog.group_post_edit.post_visibility') }}</FieldLabel>
<label for="groupPostVisibility-group"> <FieldContent>
{{ t('dialog.group_post_edit.visibility_group') }} <RadioGroup v-model="groupPostEditDialog.visibility" class="flex items-center gap-4">
</label> <div class="flex items-center space-x-2">
</div> <RadioGroupItem id="groupPostVisibility-public" value="public" />
</RadioGroup> <label for="groupPostVisibility-public">
</el-form-item> {{ t('dialog.group_post_edit.visibility_public') }}
<el-form-item v-if="groupPostEditDialog.visibility === 'group'" :label="t('dialog.new_instance.roles')"> </label>
<Select </div>
multiple <div class="flex items-center space-x-2">
:model-value="Array.isArray(groupPostEditDialog.roleIds) ? groupPostEditDialog.roleIds : []" <RadioGroupItem id="groupPostVisibility-group" value="group" />
@update:modelValue="handleRoleIdsChange"> <label for="groupPostVisibility-group">
<SelectTrigger size="sm" class="w-full"> {{ t('dialog.group_post_edit.visibility_group') }}
<SelectValue> </label>
<span class="truncate"> </div>
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }} </RadioGroup>
</span> </FieldContent>
</SelectValue> </Field>
</SelectTrigger> <Field v-if="groupPostEditDialog.visibility === 'group'">
<SelectContent> <FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<SelectGroup> <FieldContent>
<SelectItem <Select
v-for="role in groupPostEditDialog.groupRef?.roles ?? []" multiple
:key="role.id" :model-value="Array.isArray(groupPostEditDialog.roleIds) ? groupPostEditDialog.roleIds : []"
:value="role.id"> @update:modelValue="handleRoleIdsChange">
{{ role.name }} <SelectTrigger size="sm" class="w-full">
</SelectItem> <SelectValue>
</SelectGroup> <span class="truncate">
</SelectContent> {{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</Select> </span>
</el-form-item> </SelectValue>
<el-form-item :label="t('dialog.group_post_edit.image')"> </SelectTrigger>
<template v-if="gallerySelectDialog.selectedFileId"> <SelectContent>
<div style="display: inline-block; flex: none; margin-right: 5px"> <SelectGroup>
<img <SelectItem
:src="gallerySelectDialog.selectedImageUrl" v-for="role in groupPostEditDialog.groupRef?.roles ?? []"
style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover" :key="role.id"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" :value="role.id">
loading="lazy" /> {{ role.name }}
<Button </SelectItem>
size="sm" </SelectGroup>
variant="outline" </SelectContent>
style="vertical-align: top" </Select>
@click="clearImageGallerySelect"> </FieldContent>
{{ t('dialog.invite_message.clear_selected_image') }} </Field>
<Field>
<FieldLabel>{{ t('dialog.group_post_edit.image') }}</FieldLabel>
<FieldContent>
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<img
:src="gallerySelectDialog.selectedImageUrl"
style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)"
loading="lazy" />
<Button
size="sm"
variant="outline"
style="vertical-align: top"
@click="clearImageGallerySelect">
{{ t('dialog.invite_message.clear_selected_image') }}
</Button>
</div>
</template>
<template v-else>
<Button size="sm" variant="outline" @click="showGallerySelectDialog">
{{ t('dialog.invite_message.select_image') }}
</Button> </Button>
</div> </template>
</template> </FieldContent>
<template v-else> </Field>
<Button size="sm" variant="outline" @click="showGallerySelectDialog"> </FieldGroup>
{{ t('dialog.invite_message.select_image') }}
</Button>
</template>
</el-form-item>
</el-form>
</div> </div>
<template #footer> <template #footer>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -109,6 +126,7 @@
</template> </template>
<script setup> <script setup>
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group'; import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
+61 -55
View File
@@ -1,63 +1,68 @@
<template> <template>
<el-dialog :z-index="launchDialogIndex" v-model="isVisible" :title="t('dialog.launch.header')" width="450px"> <el-dialog :z-index="launchDialogIndex" v-model="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-form :model="launchDialog" label-width="100px"> <FieldGroup class="gap-4">
<el-form-item :label="t('dialog.launch.url')"> <Field>
<InputGroupField <FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
v-model="launchDialog.url" <FieldContent class="flex-row items-center gap-2">
size="sm" <InputGroupField
style="width: 230px" v-model="launchDialog.url"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" /> size="sm"
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')"> @click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<Button <TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
class="rounded-full ml-1" <Button
size="icon-sm" class="rounded-full"
variant="ghost" size="icon-sm"
@click="copyInstanceMessage(launchDialog.url)" variant="ghost"
><Copy @click="copyInstanceMessage(launchDialog.url)"
/></Button> ><Copy
</TooltipWrapper> /></Button>
</el-form-item> </TooltipWrapper>
<el-form-item v-if="launchDialog.shortUrl"> </FieldContent>
<template #label> </Field>
<div class="flex items-center"> <Field v-if="launchDialog.shortUrl">
<FieldLabel>
<span class="flex items-center gap-1">
<span>{{ t('dialog.launch.short_url') }}</span> <span>{{ t('dialog.launch.short_url') }}</span>
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')"> <TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
<el-icon style="display: inline-block; margin-left: 5px"><Warning /></el-icon> <el-icon><Warning /></el-icon>
</TooltipWrapper> </TooltipWrapper>
</div> </span>
</template> </FieldLabel>
<InputGroupField <FieldContent class="flex-row items-center gap-2">
v-model="launchDialog.shortUrl" <InputGroupField
size="sm" v-model="launchDialog.shortUrl"
style="width: 230px" size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" /> @click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')"> <TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button <Button
class="rounded-full ml-1" class="rounded-full"
size="icon-sm" size="icon-sm"
variant="ghost" variant="ghost"
@click="copyInstanceMessage(launchDialog.shortUrl)" @click="copyInstanceMessage(launchDialog.shortUrl)"
><Copy ><Copy
/></Button> /></Button>
</TooltipWrapper> </TooltipWrapper>
</el-form-item> </FieldContent>
<el-form-item :label="t('dialog.launch.location')"> </Field>
<InputGroupField <Field>
v-model="launchDialog.location" <FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
size="sm" <FieldContent class="flex-row items-center gap-2">
style="width: 230px" <InputGroupField
@click="$event.target.tagName === 'INPUT' && $event.target.select()" /> v-model="launchDialog.location"
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')"> size="sm"
<Button @click="$event.target.tagName === 'INPUT' && $event.target.select()" />
class="rounded-full ml-1" <TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
size="icon-sm" <Button
variant="ghost" class="rounded-full"
@click="copyInstanceMessage(launchDialog.location)" size="icon-sm"
><Copy variant="ghost"
/></Button> @click="copyInstanceMessage(launchDialog.location)"
</TooltipWrapper> ><Copy
</el-form-item> /></Button>
</el-form> </TooltipWrapper>
</FieldContent>
</Field>
</FieldGroup>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<Button <Button
@@ -126,6 +131,7 @@
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group'; import { ButtonGroup } from '@/components/ui/button-group';
import { Copy } from 'lucide-vue-next'; import { Copy } from 'lucide-vue-next';
+406 -338
View File
@@ -7,361 +7,428 @@
append-to-body> append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick"> <el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick">
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')"> <el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px"> <FieldGroup class="gap-4">
<el-form-item :label="t('dialog.new_instance.access_type')"> <Field>
<ToggleGroup <FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
type="single" <FieldContent>
required <ToggleGroup
variant="outline" type="single"
size="sm" required
:model-value="newInstanceDialog.accessType" variant="outline"
@update:model-value=" size="sm"
(value) => { :model-value="newInstanceDialog.accessType"
newInstanceDialog.accessType = value; @update:model-value="
buildInstance(); (value) => {
} newInstanceDialog.accessType = value;
"> buildInstance();
<ToggleGroupItem value="public">{{ }
t('dialog.new_instance.access_type_public') ">
}}</ToggleGroupItem> <ToggleGroupItem value="public">{{
<ToggleGroupItem value="group">{{ t('dialog.new_instance.access_type_public')
t('dialog.new_instance.access_type_group') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="group">{{
<ToggleGroupItem value="friends+">{{ t('dialog.new_instance.access_type_group')
t('dialog.new_instance.access_type_friend_plus') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="friends+">{{
<ToggleGroupItem value="friends">{{ t('dialog.new_instance.access_type_friend_plus')
t('dialog.new_instance.access_type_friend') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="friends">{{
<ToggleGroupItem value="invite+">{{ t('dialog.new_instance.access_type_friend')
t('dialog.new_instance.access_type_invite_plus') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="invite+">{{
<ToggleGroupItem value="invite">{{ t('dialog.new_instance.access_type_invite_plus')
t('dialog.new_instance.access_type_invite') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="invite">{{
</ToggleGroup> t('dialog.new_instance.access_type_invite')
</el-form-item> }}</ToggleGroupItem>
<el-form-item </ToggleGroup>
v-if="newInstanceDialog.accessType === 'group'" </FieldContent>
:label="t('dialog.new_instance.group_access_type')"> </Field>
<ToggleGroup <Field v-if="newInstanceDialog.accessType === 'group'">
type="single" <FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
required <FieldContent>
variant="outline" <ToggleGroup
size="sm" type="single"
:model-value="newInstanceDialog.groupAccessType" required
@update:model-value=" variant="outline"
(value) => { size="sm"
newInstanceDialog.groupAccessType = value; :model-value="newInstanceDialog.groupAccessType"
buildInstance(); @update:model-value="
} (value) => {
"> newInstanceDialog.groupAccessType = value;
<ToggleGroupItem buildInstance();
value="members" }
">
<ToggleGroupItem
value="members"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
"
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
>
<ToggleGroupItem
value="plus"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
"
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
>
<ToggleGroupItem
value="public"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-public-create'
) || newInstanceDialog.groupRef.privacy === 'private'
"
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.queueEnabled') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled=" :disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create') !hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
" "
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem @update:modelValue="buildInstance" />
> </FieldContent>
<ToggleGroupItem </Field>
value="plus" <Field>
:disabled=" <FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create') <FieldContent>
" <InputGroupField
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem :disabled="!isLocalUserVrcPlusSupporter"
> v-model="newInstanceDialog.displayName"
<ToggleGroupItem size="sm"
value="public" @click="$event.target.tagName === 'INPUT' && $event.target.select()"
:disabled=" @change="buildInstance" />
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-public-create') || </FieldContent>
newInstanceDialog.groupRef.privacy === 'private' </Field>
" <Field v-if="newInstanceDialog.accessType === 'group'">
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem <FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
> <FieldContent>
</ToggleGroup> <VirtualCombobox
</el-form-item> v-model="newInstanceDialog.groupId"
<el-form-item :label="t('dialog.new_instance.region')"> :groups="normalGroupPickerGroups"
<ToggleGroup :placeholder="t('dialog.new_instance.group_placeholder')"
type="single" :search-placeholder="t('dialog.new_instance.group_placeholder')"
required :clearable="true"
variant="outline" :close-on-select="true"
size="sm" :deselect-on-reselect="true"
:model-value="newInstanceDialog.region" @change="buildInstance">
@update:model-value=" <template #item="{ item, selected }">
(value) => { <div class="x-friend-item flex w-full items-center">
newInstanceDialog.region = value; <div class="avatar">
buildInstance(); <img :src="item.iconUrl" loading="lazy" />
} </div>
"> <div class="detail">
<ToggleGroupItem value="US West">{{ t('dialog.new_instance.region_usw') }}</ToggleGroupItem> <span class="name" v-text="item.label"></span>
<ToggleGroupItem value="US East">{{ t('dialog.new_instance.region_use') }}</ToggleGroupItem> </div>
<ToggleGroupItem value="Europe">{{ t('dialog.new_instance.region_eu') }}</ToggleGroupItem> <CheckIcon
<ToggleGroupItem value="Japan">{{ t('dialog.new_instance.region_jp') }}</ToggleGroupItem> :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</ToggleGroup>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.queueEnabled')">
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.ageGate')">
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
"
@update:modelValue="buildInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.display_name')">
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance" />
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_id')">
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="normalGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div> </div>
<div class="detail"> </template>
<span class="name" v-text="item.label"></span> </VirtualCombobox>
</div> </FieldContent>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" /> </Field>
</div> <Field
</template>
</VirtualCombobox>
</el-form-item>
<el-form-item
v-if=" v-if="
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members' newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'
" "
:label="t('dialog.new_instance.roles')"> class="items-start">
<Select <FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
multiple <FieldContent>
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []" <Select
@update:modelValue="handleRoleIdsChange"> multiple
<SelectTrigger size="sm" class="w-full"> :model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []"
<SelectValue> @update:modelValue="handleRoleIdsChange">
<span class="truncate"> <SelectTrigger size="sm" class="w-full">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }} <SelectValue>
</span> <span class="truncate">
</SelectValue> {{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</SelectTrigger> </span>
<SelectContent> </SelectValue>
<SelectGroup> </SelectTrigger>
<SelectItem <SelectContent>
v-for="role in newInstanceDialog.selectedGroupRoles" <SelectGroup>
:key="role.id" <SelectItem
:value="role.id"> v-for="role in newInstanceDialog.selectedGroupRoles"
{{ role.name }} :key="role.id"
</SelectItem> :value="role.id">
</SelectGroup> {{ role.name }}
</SelectContent> </SelectItem>
</Select> </SelectGroup>
</el-form-item> </SelectContent>
</Select>
</FieldContent>
</Field>
<template v-if="newInstanceDialog.instanceCreated"> <template v-if="newInstanceDialog.instanceCreated">
<el-form-item :label="t('dialog.new_instance.location')"> <Field>
<InputGroupField <FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
v-model="newInstanceDialog.location" <FieldContent>
size="sm" <InputGroupField
readonly v-model="newInstanceDialog.location"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" /> size="sm"
</el-form-item> readonly
<el-form-item :label="t('dialog.new_instance.url')"> @click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly /> </FieldContent>
</el-form-item> </Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</template> </template>
</el-form> </FieldGroup>
</el-tab-pane> </el-tab-pane>
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')"> <el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')">
<el-form :model="newInstanceDialog" label-width="150px"> <FieldGroup class="gap-4">
<el-form-item :label="t('dialog.new_instance.access_type')"> <Field>
<ToggleGroup <FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
type="single" <FieldContent>
required <ToggleGroup
variant="outline" type="single"
size="sm" required
:model-value="newInstanceDialog.accessType" variant="outline"
@update:model-value=" size="sm"
(value) => { :model-value="newInstanceDialog.accessType"
newInstanceDialog.accessType = value; @update:model-value="
buildLegacyInstance(); (value) => {
} newInstanceDialog.accessType = value;
"> buildLegacyInstance();
<ToggleGroupItem value="public">{{ }
t('dialog.new_instance.access_type_public') ">
}}</ToggleGroupItem> <ToggleGroupItem value="public">{{
<ToggleGroupItem value="group">{{ t('dialog.new_instance.access_type_public')
t('dialog.new_instance.access_type_group') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="group">{{
<ToggleGroupItem value="friends+">{{ t('dialog.new_instance.access_type_group')
t('dialog.new_instance.access_type_friend_plus') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="friends+">{{
<ToggleGroupItem value="friends">{{ t('dialog.new_instance.access_type_friend_plus')
t('dialog.new_instance.access_type_friend') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="friends">{{
<ToggleGroupItem value="invite+">{{ t('dialog.new_instance.access_type_friend')
t('dialog.new_instance.access_type_invite_plus') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="invite+">{{
<ToggleGroupItem value="invite">{{ t('dialog.new_instance.access_type_invite_plus')
t('dialog.new_instance.access_type_invite') }}</ToggleGroupItem>
}}</ToggleGroupItem> <ToggleGroupItem value="invite">{{
</ToggleGroup> t('dialog.new_instance.access_type_invite')
</el-form-item> }}</ToggleGroupItem>
<el-form-item </ToggleGroup>
v-if="newInstanceDialog.accessType === 'group'" </FieldContent>
:label="t('dialog.new_instance.group_access_type')"> </Field>
<ToggleGroup <Field v-if="newInstanceDialog.accessType === 'group'">
type="single" <FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
required <FieldContent>
variant="outline" <ToggleGroup
size="sm" type="single"
:model-value="newInstanceDialog.groupAccessType" required
@update:model-value=" variant="outline"
(value) => { size="sm"
newInstanceDialog.groupAccessType = value; :model-value="newInstanceDialog.groupAccessType"
buildLegacyInstance(); @update:model-value="
} (value) => {
"> newInstanceDialog.groupAccessType = value;
<ToggleGroupItem value="members">{{ buildLegacyInstance();
t('dialog.new_instance.group_access_type_members') }
}}</ToggleGroupItem> ">
<ToggleGroupItem value="plus">{{ <ToggleGroupItem value="members">{{
t('dialog.new_instance.group_access_type_plus') t('dialog.new_instance.group_access_type_members')
}}</ToggleGroupItem> }}</ToggleGroupItem>
<ToggleGroupItem value="public">{{ <ToggleGroupItem value="plus">{{
t('dialog.new_instance.group_access_type_public') t('dialog.new_instance.group_access_type_plus')
}}</ToggleGroupItem> }}</ToggleGroupItem>
</ToggleGroup> <ToggleGroupItem value="public">{{
</el-form-item> t('dialog.new_instance.group_access_type_public')
<el-form-item :label="t('dialog.new_instance.region')"> }}</ToggleGroupItem>
<ToggleGroup </ToggleGroup>
type="single" </FieldContent>
required </Field>
variant="outline" <Field>
size="sm" <FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
:model-value="newInstanceDialog.region" <FieldContent>
@update:model-value=" <ToggleGroup
(value) => { type="single"
newInstanceDialog.region = value; required
buildLegacyInstance(); variant="outline"
} size="sm"
"> :model-value="newInstanceDialog.region"
<ToggleGroupItem value="US West">{{ t('dialog.new_instance.region_usw') }}</ToggleGroupItem> @update:model-value="
<ToggleGroupItem value="US East">{{ t('dialog.new_instance.region_use') }}</ToggleGroupItem> (value) => {
<ToggleGroupItem value="Europe">{{ t('dialog.new_instance.region_eu') }}</ToggleGroupItem> newInstanceDialog.region = value;
<ToggleGroupItem value="Japan">{{ t('dialog.new_instance.region_jp') }}</ToggleGroupItem> buildLegacyInstance();
</ToggleGroup> }
</el-form-item> ">
<el-form-item <ToggleGroupItem value="US West">{{
v-if="newInstanceDialog.accessType === 'group'" t('dialog.new_instance.region_usw')
:label="t('dialog.new_instance.ageGate')"> }}</ToggleGroupItem>
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" /> <ToggleGroupItem value="US East">{{
</el-form-item> t('dialog.new_instance.region_use')
<el-form-item :label="t('dialog.new_instance.world_id')"> }}</ToggleGroupItem>
<InputGroupField <ToggleGroupItem value="Europe">{{
v-model="newInstanceDialog.worldId" t('dialog.new_instance.region_eu')
size="sm" }}</ToggleGroupItem>
@click="$event.target.tagName === 'INPUT' && $event.target.select()" <ToggleGroupItem value="Japan">{{
@change="buildLegacyInstance" /> t('dialog.new_instance.region_jp')
</el-form-item> }}</ToggleGroupItem>
<el-form-item :label="t('dialog.new_instance.instance_id')"> </ToggleGroup>
<InputGroupField </FieldContent>
v-model="newInstanceDialog.instanceName" </Field>
:placeholder="t('dialog.new_instance.instance_id_placeholder')" <Field v-if="newInstanceDialog.accessType === 'group'">
size="sm" <FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
@change="buildLegacyInstance" /> <FieldContent>
</el-form-item> <Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
<el-form-item </FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.world_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.worldId"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.instance_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="sm"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field
v-if=" v-if="
newInstanceDialog.selectedTab === 'Legacy' && newInstanceDialog.selectedTab === 'Legacy' &&
newInstanceDialog.accessType !== 'public' && newInstanceDialog.accessType !== 'public' &&
newInstanceDialog.accessType !== 'group' newInstanceDialog.accessType !== 'group'
" "
:label="t('dialog.new_instance.instance_creator')"> class="items-start">
<VirtualCombobox <FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
v-model="newInstanceDialog.userId" <FieldContent>
:groups="creatorPickerGroups" <VirtualCombobox
:placeholder="t('dialog.new_instance.instance_creator_placeholder')" v-model="newInstanceDialog.userId"
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')" :groups="creatorPickerGroups"
:clearable="true" :placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:close-on-select="true" :search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:deselect-on-reselect="true" :clearable="true"
@change="buildLegacyInstance"> :close-on-select="true"
<template #item="{ item, selected }"> :deselect-on-reselect="true"
<div class="x-friend-item flex w-full items-center"> @change="buildLegacyInstance">
<template v-if="item.user"> <template #item="{ item, selected }">
<div class="avatar" :class="userStatusClass(item.user)"> <div class="x-friend-item flex w-full items-center">
<img :src="userImage(item.user)" loading="lazy" /> <template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div> </div>
<div class="detail"> <div class="detail">
<span <span class="name" v-text="item.label"></span>
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div> </div>
</template> <CheckIcon
<template v-else> :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
<span v-text="item.label"></span>
</template>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_id')">
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div> </div>
<div class="detail"> </template>
<span class="name" v-text="item.label"></span> </VirtualCombobox>
</div> </FieldContent>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" /> </Field>
</div> <Field>
</template> <FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
</VirtualCombobox> <FieldContent>
</el-form-item> <InputGroupField
<el-form-item :label="t('dialog.new_instance.location')"> v-model="newInstanceDialog.location"
<InputGroupField size="sm"
v-model="newInstanceDialog.location" readonly
size="sm" @click="$event.target.tagName === 'INPUT' && $event.target.select()" />
readonly </FieldContent>
@click="$event.target.tagName === 'INPUT' && $event.target.select()" /> </Field>
</el-form-item> <Field>
<el-form-item :label="t('dialog.new_instance.url')"> <FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly /> <FieldContent>
</el-form-item> <InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</el-form> </FieldContent>
</Field>
</FieldGroup>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer> <template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
@@ -441,11 +508,12 @@
</template> </template>
<script setup> <script setup>
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { Checkbox } from '@/components/ui/checkbox';
import { Check as CheckIcon } from 'lucide-vue-next'; import { Check as CheckIcon } from 'lucide-vue-next';
import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -766,8 +766,8 @@
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue'; import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
import { Ellipsis, RefreshCcw, Star, Trash2 } from 'lucide-vue-next'; import { Ellipsis, RefreshCcw, Star, Trash2 } from 'lucide-vue-next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
import { fieldVariants } from '.';
const props = defineProps({
class: { type: null, required: false },
orientation: { type: null, required: false }
});
</script>
<template>
<div
role="group"
data-slot="field"
:data-orientation="orientation"
:class="cn(fieldVariants({ orientation }), props.class)">
<slot />
</div>
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-content"
:class="cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,22 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<p
data-slot="field-description"
:class="
cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class
)
">
<slot />
</p>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup>
import { cn } from '@/lib/utils';
import { computed } from 'vue';
const props = defineProps({
class: { type: null, required: false },
errors: { type: Array, required: false }
});
const content = computed(() => {
if (!props.errors || props.errors.length === 0) return null;
const uniqueErrors = [
...new Map(
props.errors.filter(Boolean).map((error) => {
const message = typeof error === 'string' ? error : error?.message;
return [message, error];
})
).values()
];
if (uniqueErrors.length === 1 && uniqueErrors[0]) {
return typeof uniqueErrors[0] === 'string' ? uniqueErrors[0] : uniqueErrors[0].message;
}
return uniqueErrors.map((error) => (typeof error === 'string' ? error : error?.message));
});
</script>
<template>
<div
v-if="$slots.default || content"
role="alert"
data-slot="field-error"
:class="cn('text-destructive text-sm font-normal', props.class)">
<slot v-if="$slots.default" />
<template v-else-if="typeof content === 'string'">
{{ content }}
</template>
<ul v-else-if="Array.isArray(content)" class="ml-4 flex list-disc flex-col gap-1">
<li v-for="(error, index) in content" :key="index">
{{ error }}
</li>
</ul>
</div>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-group"
:class="
cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
props.class
)
">
<slot />
</div>
</template>
+23
View File
@@ -0,0 +1,23 @@
<script setup>
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<Label
data-slot="field-label"
:class="
cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
props.class
)
">
<slot />
</Label>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false },
variant: { type: String, required: false }
});
</script>
<template>
<legend
data-slot="field-legend"
:data-variant="variant"
:class="cn('mb-3 font-medium', 'data-[variant=legend]:text-base', 'data-[variant=label]:text-sm', props.class)">
<slot />
</legend>
</template>
@@ -0,0 +1,23 @@
<script setup>
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-separator"
:data-content="!!$slots.default"
:class="cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', props.class)">
<Separator class="absolute inset-0 top-1/2" />
<span
v-if="$slots.default"
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content">
<slot />
</span>
</div>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<fieldset
data-slot="field-set"
:class="
cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
props.class
)
">
<slot />
</fieldset>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-label"
:class="
cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
props.class
)
">
<slot />
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
import { cva } from 'class-variance-authority';
export const fieldVariants = cva(
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
{
variants: {
orientation: {
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
],
responsive: [
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
]
}
},
defaultVariants: {
orientation: 'vertical'
}
}
);
export { default as Field } from './Field.vue';
export { default as FieldContent } from './FieldContent.vue';
export { default as FieldDescription } from './FieldDescription.vue';
export { default as FieldError } from './FieldError.vue';
export { default as FieldGroup } from './FieldGroup.vue';
export { default as FieldLabel } from './FieldLabel.vue';
export { default as FieldLegend } from './FieldLegend.vue';
export { default as FieldSeparator } from './FieldSeparator.vue';
export { default as FieldSet } from './FieldSet.vue';
export { default as FieldTitle } from './FieldTitle.vue';
+17
View File
@@ -0,0 +1,17 @@
<script setup>
import { Slot } from 'reka-ui';
import { useFormField } from './useFormField';
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
</script>
<template>
<Slot
:id="formItemId"
data-slot="form-control"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error">
<slot />
</Slot>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { cn } from '@/lib/utils';
import { useFormField } from './useFormField';
const props = defineProps({
class: { type: null, required: false }
});
const { formDescriptionId } = useFormField();
</script>
<template>
<p :id="formDescriptionId" data-slot="form-description" :class="cn('text-muted-foreground text-sm', props.class)">
<slot />
</p>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
import { provide } from 'vue';
import { useId } from 'reka-ui';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
const props = defineProps({
class: { type: null, required: false }
});
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);
</script>
<template>
<div data-slot="form-item" :class="cn('grid gap-2', props.class)">
<slot />
</div>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script setup>
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import { useFormField } from './useFormField';
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const { error, formItemId } = useFormField();
</script>
<template>
<Label
data-slot="form-label"
:data-error="!!error"
:class="cn('data-[error=true]:text-destructive', props.class)"
:for="formItemId">
<slot />
</Label>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
import { ErrorMessage } from 'vee-validate';
import { cn } from '@/lib/utils';
import { toValue } from 'vue';
import { useFormField } from './useFormField';
const props = defineProps({
class: { type: null, required: false }
});
const { name, formMessageId } = useFormField();
</script>
<template>
<ErrorMessage
:id="formMessageId"
data-slot="form-message"
as="p"
:name="toValue(name)"
:class="cn('text-destructive text-sm', props.class)" />
</template>
+11
View File
@@ -0,0 +1,11 @@
export { default as FormControl } from './FormControl.vue';
export { default as FormDescription } from './FormDescription.vue';
export { default as FormItem } from './FormItem.vue';
export { default as FormLabel } from './FormLabel.vue';
export { default as FormMessage } from './FormMessage.vue';
export { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export {
Form,
Field as FormField,
FieldArray as FormFieldArray
} from 'vee-validate';
+1
View File
@@ -0,0 +1 @@
export const FORM_ITEM_INJECTION_KEY = Symbol();
+31
View File
@@ -0,0 +1,31 @@
import { computed, inject } from 'vue';
import { FieldContextKey } from 'vee-validate';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export function useFormField() {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>');
const { name, errorMessage: error, meta } = fieldContext;
const id = fieldItemContext;
const fieldState = {
valid: computed(() => meta.valid),
isDirty: computed(() => meta.dirty),
isTouched: computed(() => meta.touched),
error
};
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
};
}
@@ -13,6 +13,7 @@
<template> <template>
<Button <Button
type="button"
:data-size="props.size" :data-size="props.size"
:variant="props.variant" :variant="props.variant"
:class="cn(inputGroupButtonVariants({ size: props.size }), props.class)"> :class="cn(inputGroupButtonVariants({ size: props.size }), props.class)">
+28
View File
@@ -0,0 +1,28 @@
<script setup>
import { Label } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Label
data-slot="label"
v-bind="delegatedProps"
:class="
cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class
)
">
<slot />
</Label>
</template>
+1
View File
@@ -0,0 +1 @@
export { default as Label } from './Label.vue';
+1 -15
View File
@@ -42,21 +42,7 @@ export const useAuthStore = defineStore('Auth', () => {
endpoint: '', endpoint: '',
websocket: '', websocket: '',
saveCredentials: false, saveCredentials: false,
lastUserLoggedIn: '', lastUserLoggedIn: ''
rules: {
username: [
{
required: true,
trigger: 'blur'
}
],
password: [
{
required: true,
trigger: 'blur'
}
]
}
}); });
const enablePrimaryPasswordDialog = ref({ const enablePrimaryPasswordDialog = ref({
+72 -59
View File
@@ -51,65 +51,77 @@
<p class="mutual-graph__force-description"> <p class="mutual-graph__force-description">
{{ t('view.charts.mutual_friend.force_dialog.description') }} {{ t('view.charts.mutual_friend.force_dialog.description') }}
</p> </p>
<el-form label-position="top" size="small" class="mutual-graph__force-form"> <FieldGroup class="mutual-graph__force-form">
<el-form-item :label="t('view.charts.mutual_friend.force_dialog.repulsion')"> <Field>
<NumberField <FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.repulsion') }}</FieldLabel>
v-model="forceForm.repulsion" <FieldContent>
:step="1" <NumberField
:format-options="{ maximumFractionDigits: 0 }" v-model="forceForm.repulsion"
class="mutual-graph__number-input"> :step="1"
<NumberFieldContent> :format-options="{ maximumFractionDigits: 0 }"
<NumberFieldInput /> class="mutual-graph__number-input">
</NumberFieldContent> <NumberFieldContent>
</NumberField> <NumberFieldInput />
<div class="mutual-graph__helper"> </NumberFieldContent>
{{ t('view.charts.mutual_friend.force_dialog.repulsion_help') }} </NumberField>
</div> <FieldDescription class="mutual-graph__helper">
</el-form-item> {{ t('view.charts.mutual_friend.force_dialog.repulsion_help') }}
<el-form-item :label="t('view.charts.mutual_friend.force_dialog.edge_length_min')"> </FieldDescription>
<NumberField </FieldContent>
v-model="forceForm.edgeLengthMin" </Field>
:step="1" <Field>
:format-options="{ maximumFractionDigits: 0 }" <FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_min') }}</FieldLabel>
class="mutual-graph__number-input"> <FieldContent>
<NumberFieldContent> <NumberField
<NumberFieldInput /> v-model="forceForm.edgeLengthMin"
</NumberFieldContent> :step="1"
</NumberField> :format-options="{ maximumFractionDigits: 0 }"
<div class="mutual-graph__helper"> class="mutual-graph__number-input">
{{ t('view.charts.mutual_friend.force_dialog.edge_length_min_help') }} <NumberFieldContent>
</div> <NumberFieldInput />
</el-form-item> </NumberFieldContent>
<el-form-item :label="t('view.charts.mutual_friend.force_dialog.edge_length_max')"> </NumberField>
<NumberField <FieldDescription class="mutual-graph__helper">
v-model="forceForm.edgeLengthMax" {{ t('view.charts.mutual_friend.force_dialog.edge_length_min_help') }}
:step="1" </FieldDescription>
:format-options="{ maximumFractionDigits: 0 }" </FieldContent>
class="mutual-graph__number-input"> </Field>
<NumberFieldContent> <Field>
<NumberFieldInput /> <FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_max') }}</FieldLabel>
</NumberFieldContent> <FieldContent>
</NumberField> <NumberField
<div class="mutual-graph__helper"> v-model="forceForm.edgeLengthMax"
{{ t('view.charts.mutual_friend.force_dialog.edge_length_max_help') }} :step="1"
</div> :format-options="{ maximumFractionDigits: 0 }"
</el-form-item> class="mutual-graph__number-input">
<el-form-item :label="t('view.charts.mutual_friend.force_dialog.gravity')"> <NumberFieldContent>
<NumberField <NumberFieldInput />
v-model="forceForm.gravity" </NumberFieldContent>
:max="1" </NumberField>
:step="0.1" <FieldDescription class="mutual-graph__helper">
:format-options="{ maximumFractionDigits: 1 }" {{ t('view.charts.mutual_friend.force_dialog.edge_length_max_help') }}
class="mutual-graph__number-input"> </FieldDescription>
<NumberFieldContent> </FieldContent>
<NumberFieldInput /> </Field>
</NumberFieldContent> <Field>
</NumberField> <FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.gravity') }}</FieldLabel>
<div class="mutual-graph__helper"> <FieldContent>
{{ t('view.charts.mutual_friend.force_dialog.gravity_help') }} <NumberField
</div> v-model="forceForm.gravity"
</el-form-item> :max="1"
</el-form> :step="0.1"
:format-options="{ maximumFractionDigits: 1 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.gravity_help') }}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<template #footer> <template #footer>
<div class="mutual-graph__dialog-footer"> <div class="mutual-graph__dialog-footer">
@@ -127,6 +139,7 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field';
import { NumberField, NumberFieldContent, NumberFieldInput } from '@/components/ui/number-field'; import { NumberField, NumberFieldContent, NumberFieldInput } from '@/components/ui/number-field';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
+152 -72
View File
@@ -16,71 +16,101 @@
<div class="x-login-form-container"> <div class="x-login-form-container">
<div> <div>
<h2 style="font-weight: bold; text-align: center; margin: 0">{{ t('view.login.login') }}</h2> <h2 style="font-weight: bold; text-align: center; margin: 0">{{ t('view.login.login') }}</h2>
<el-form <form id="login-form" @submit.prevent="onSubmit">
ref="loginFormRef" <FieldGroup class="gap-3">
:model="loginForm" <VeeField v-slot="{ field, errors }" name="username">
:rules="loginForm.rules" <Field :data-invalid="!!errors.length">
@submit.prevent="handleLogin()"> <FieldLabel for="login-form-username">
<el-form-item {{ t('view.login.field.username') }}
:label="t('view.login.field.username')" </FieldLabel>
prop="username" <FieldContent>
required <InputGroupField
style="display: block"> id="login-form-username"
<InputGroupField :model-value="field.value"
v-model="loginForm.username" name="username"
name="username" :placeholder="t('view.login.field.username')"
:placeholder="t('view.login.field.username')" :aria-invalid="!!errors.length"
clearable /> clearable
</el-form-item> @update:modelValue="field.onChange"
<el-form-item @blur="field.onBlur" />
:label="t('view.login.field.password')" <FieldError v-if="errors.length" :errors="errors" />
prop="password" </FieldContent>
required </Field>
style="display: block; margin-top: 10px"> </VeeField>
<InputGroupField <VeeField v-slot="{ field, errors }" name="password">
v-model="loginForm.password" <Field :data-invalid="!!errors.length">
type="password" <FieldLabel for="login-form-password">
name="password" {{ t('view.login.field.password') }}
:placeholder="t('view.login.field.password')" </FieldLabel>
clearable <FieldContent>
show-password /> <InputGroupField
</el-form-item> id="login-form-password"
:model-value="field.value"
type="password"
name="password"
:placeholder="t('view.login.field.password')"
:aria-invalid="!!errors.length"
clearable
show-password
@update:modelValue="field.onChange"
@blur="field.onBlur" />
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
</Field>
</VeeField>
</FieldGroup>
<label class="inline-flex items-center gap-2 mr-2"> <label class="inline-flex items-center gap-2 mr-2">
<Checkbox v-model="loginForm.saveCredentials" /> <Checkbox v-model="loginForm.saveCredentials" />
<span>{{ t('view.login.field.saveCredentials') }}</span> <span>{{ t('view.login.field.saveCredentials') }}</span>
</label> </label>
<label class="inline-flex items-center gap-2" style="margin-top: 10px"> <label class="inline-flex items-center gap-2" style="margin-top: 10px">
<Checkbox v-model="enableCustomEndpoint" @update:modelValue="toggleCustomEndpoint" /> <Checkbox v-model="enableCustomEndpoint" @update:modelValue="handleCustomEndpointToggle" />
<span>{{ t('view.login.field.devEndpoint') }}</span> <span>{{ t('view.login.field.devEndpoint') }}</span>
</label> </label>
<el-form-item <FieldGroup v-if="enableCustomEndpoint" class="mt-3 gap-3">
v-if="enableCustomEndpoint" <VeeField v-slot="{ field, errors }" name="endpoint">
:label="t('view.login.field.endpoint')" <Field :data-invalid="!!errors.length">
prop="endpoint" <FieldLabel for="login-form-endpoint">
style="margin-top: 10px"> {{ t('view.login.field.endpoint') }}
<InputGroupField </FieldLabel>
v-model="loginForm.endpoint" <FieldContent>
name="endpoint" <InputGroupField
:placeholder="AppDebug.endpointDomainVrchat" id="login-form-endpoint"
clearable /> :model-value="field.value"
</el-form-item> name="endpoint"
<el-form-item :placeholder="AppDebug.endpointDomainVrchat"
v-if="enableCustomEndpoint" :aria-invalid="!!errors.length"
:label="t('view.login.field.websocket')" clearable
prop="websocket" @update:modelValue="field.onChange"
style="margin-top: 10px"> @blur="field.onBlur" />
<InputGroupField <FieldError v-if="errors.length" :errors="errors" />
v-model="loginForm.websocket" </FieldContent>
name="websocket" </Field>
:placeholder="AppDebug.websocketDomainVrchat" </VeeField>
clearable /> <VeeField v-slot="{ field, errors }" name="websocket">
</el-form-item> <Field :data-invalid="!!errors.length">
<el-form-item> <FieldLabel for="login-form-websocket">
<Button class="mt-2" type="submit" size="lg" style="width: 100%">{{ {{ t('view.login.field.websocket') }}
t('view.login.login') </FieldLabel>
}}</Button> <FieldContent>
</el-form-item> <InputGroupField
</el-form> id="login-form-websocket"
:model-value="field.value"
name="websocket"
:placeholder="AppDebug.websocketDomainVrchat"
:aria-invalid="!!errors.length"
clearable
@update:modelValue="field.onChange"
@blur="field.onBlur" />
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
</Field>
</VeeField>
</FieldGroup>
<Field class="mt-2">
<Button type="submit" size="lg" style="width: 100%">{{ t('view.login.login') }}</Button>
</Field>
</form>
<Button <Button
variant="Secondary" variant="Secondary"
size="lg" size="lg"
@@ -147,14 +177,18 @@
</template> </template>
<script setup> <script setup>
import { Field, FieldContent, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field';
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'; import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue';
import { CircleArrowDown, Route } from 'lucide-vue-next'; import { CircleArrowDown, Route } from 'lucide-vue-next';
import { Field as VeeField, useForm } from 'vee-validate';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toTypedSchema } from '@vee-validate/zod';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { z } from 'zod';
import { useAuthStore, useGeneralSettingsStore, useVRCXUpdaterStore } from '../../stores'; import { useAuthStore, useGeneralSettingsStore, useVRCXUpdaterStore } from '../../stores';
import { openExternalLink, userImage } from '../../shared/utils'; import { openExternalLink, userImage } from '../../shared/utils';
@@ -171,8 +205,27 @@
const { t } = useI18n(); const { t } = useI18n();
const loginFormRef = ref(null);
const savedCredentials = ref({}); const savedCredentials = ref({});
const requiredMessage = 'Required';
const formSchema = toTypedSchema(
z.object({
username: z.string().min(1, requiredMessage),
password: z.string().min(1, requiredMessage),
endpoint: z.string().optional(),
websocket: z.string().optional()
})
);
const { handleSubmit, resetForm, setValues, values } = useForm({
validationSchema: formSchema,
initialValues: {
username: loginForm.value.username,
password: loginForm.value.password,
endpoint: loginForm.value.endpoint,
websocket: loginForm.value.websocket
}
});
async function clickDeleteSavedLogin(userId) { async function clickDeleteSavedLogin(userId) {
await deleteSavedLogin(userId); await deleteSavedLogin(userId);
@@ -184,15 +237,22 @@
await updateSavedCredentials(); await updateSavedCredentials();
} }
function handleLogin() { const onSubmit = handleSubmit(async (formValues) => {
if (loginFormRef.value) { loginForm.value.username = formValues.username ?? '';
loginFormRef.value.validate(async (valid) => { loginForm.value.password = formValues.password ?? '';
if (valid) { loginForm.value.endpoint = formValues.endpoint ?? '';
await login(); loginForm.value.websocket = formValues.websocket ?? '';
await updateSavedCredentials(); await login();
} await updateSavedCredentials();
}); });
}
async function handleCustomEndpointToggle() {
await toggleCustomEndpoint();
setValues({
...values,
endpoint: loginForm.value.endpoint,
websocket: loginForm.value.websocket
});
} }
async function updateSavedCredentials() { async function updateSavedCredentials() {
@@ -224,9 +284,29 @@
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (loginFormRef.value) { resetForm({
loginFormRef.value.resetFields(); values: {
} username: '',
password: '',
endpoint: '',
websocket: ''
}
});
loginForm.value.username = '';
loginForm.value.password = '';
loginForm.value.endpoint = '';
loginForm.value.websocket = '';
savedCredentials.value = {}; savedCredentials.value = {};
}); });
watch(
values,
(formValues) => {
loginForm.value.username = formValues.username ?? '';
loginForm.value.password = formValues.password ?? '';
loginForm.value.endpoint = formValues.endpoint ?? '';
loginForm.value.websocket = formValues.websocket ?? '';
},
{ deep: true }
);
</script> </script>
@@ -21,65 +21,83 @@
</Select> </Select>
</div> </div>
<br /> <br />
<el-form label-position="top" label-width="120px" size="small" style="margin-bottom: 12px"> <FieldGroup class="mb-3">
<el-form-item :label="t('dialog.translation_api.mode')"> <Field>
<Select :model-value="form.translationApiType" @update:modelValue="handleTranslationApiTypeChange"> <FieldLabel>{{ t('dialog.translation_api.mode') }}</FieldLabel>
<SelectTrigger size="sm" style="width: 100%"> <FieldContent>
<SelectValue :placeholder="t('dialog.translation_api.mode')" /> <Select :model-value="form.translationApiType" @update:modelValue="handleTranslationApiTypeChange">
</SelectTrigger> <SelectTrigger size="sm" style="width: 100%">
<SelectContent> <SelectValue :placeholder="t('dialog.translation_api.mode')" />
<SelectGroup> </SelectTrigger>
<SelectItem value="google" :text-value="t('dialog.translation_api.mode_google')"> <SelectContent>
{{ t('dialog.translation_api.mode_google') }} <SelectGroup>
</SelectItem> <SelectItem value="google" :text-value="t('dialog.translation_api.mode_google')">
<SelectItem value="openai" :text-value="t('dialog.translation_api.mode_openai')"> {{ t('dialog.translation_api.mode_google') }}
{{ t('dialog.translation_api.mode_openai') }} </SelectItem>
</SelectItem> <SelectItem value="openai" :text-value="t('dialog.translation_api.mode_openai')">
</SelectGroup> {{ t('dialog.translation_api.mode_openai') }}
</SelectContent> </SelectItem>
</Select> </SelectGroup>
</el-form-item> </SelectContent>
</el-form> </Select>
</FieldContent>
</Field>
</FieldGroup>
<template v-if="form.translationApiType === 'google'"> <template v-if="form.translationApiType === 'google'">
<el-form label-position="top" label-width="120px" size="small"> <FieldGroup>
<el-form-item :label="t('dialog.translation_api.description')"> <Field>
<InputGroupField <FieldLabel>{{ t('dialog.translation_api.description') }}</FieldLabel>
v-model="form.translationApiKey" <FieldContent>
type="password" <InputGroupField
show-password v-model="form.translationApiKey"
placeholder="AIzaSy..." type="password"
clearable /> show-password
</el-form-item> placeholder="AIzaSy..."
</el-form> clearable />
</FieldContent>
</Field>
</FieldGroup>
</template> </template>
<template v-if="form.translationApiType === 'openai'"> <template v-if="form.translationApiType === 'openai'">
<el-form label-position="top" label-width="120px" size="small"> <FieldGroup>
<el-form-item :label="t('dialog.translation_api.openai.endpoint')"> <Field>
<InputGroupField <FieldLabel>{{ t('dialog.translation_api.openai.endpoint') }}</FieldLabel>
v-model="form.translationApiEndpoint" <FieldContent>
placeholder="https://api.openai.com/v1/chat/completions" <InputGroupField
clearable /> v-model="form.translationApiEndpoint"
</el-form-item> placeholder="https://api.openai.com/v1/chat/completions"
clearable />
</FieldContent>
</Field>
<el-form-item :label="t('dialog.translation_api.openai.api_key')"> <Field>
<InputGroupField <FieldLabel>{{ t('dialog.translation_api.openai.api_key') }}</FieldLabel>
v-model="form.translationApiKey" <FieldContent>
type="password" <InputGroupField
show-password v-model="form.translationApiKey"
placeholder="sk-..." type="password"
clearable /> show-password
</el-form-item> placeholder="sk-..."
clearable />
</FieldContent>
</Field>
<el-form-item :label="t('dialog.translation_api.openai.model')"> <Field>
<InputGroupField v-model="form.translationApiModel" clearable /> <FieldLabel>{{ t('dialog.translation_api.openai.model') }}</FieldLabel>
</el-form-item> <FieldContent>
<InputGroupField v-model="form.translationApiModel" clearable />
</FieldContent>
</Field>
<el-form-item :label="t('dialog.translation_api.openai.prompt_optional')"> <Field>
<InputGroupTextareaField v-model="form.translationApiPrompt" :rows="3" clearable /> <FieldLabel>{{ t('dialog.translation_api.openai.prompt_optional') }}</FieldLabel>
</el-form-item> <FieldContent>
</el-form> <InputGroupTextareaField v-model="form.translationApiPrompt" :rows="3" clearable />
</FieldContent>
</Field>
</FieldGroup>
</template> </template>
<template #footer> <template #footer>
@@ -94,14 +112,14 @@
"> ">
{{ t('dialog.translation_api.guide') }} {{ t('dialog.translation_api.guide') }}
</Button> </Button>
<Button
variant="outline"
class="mr-2"
v-if="form.translationApiType === 'openai'"
@click="testOpenAiTranslation">
{{ t('dialog.translation_api.test') }}
</Button>
<div> <div>
<Button
variant="secondary"
class="mr-2"
v-if="form.translationApiType === 'openai'"
@click="testOpenAiTranslation">
{{ t('dialog.translation_api.test') }}
</Button>
<Button style="margin-left: auto" @click="saveTranslationApiConfig"> <Button style="margin-left: auto" @click="saveTranslationApiConfig">
{{ t('dialog.translation_api.save') }} {{ t('dialog.translation_api.save') }}
</Button> </Button>
@@ -113,9 +131,10 @@
<script setup> <script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -312,9 +312,9 @@
.x-link:hover { .x-link:hover {
text-decoration: none; text-decoration: none;
} }
.x-link:hover span { /* .x-link:hover span {
text-decoration: underline; text-decoration: underline;
} } */
.is-rotated { .is-rotated {
transform: rotate(90deg); transform: rotate(90deg);
} }