mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 14:56:06 +02:00
replace element plus components
This commit is contained in:
Generated
+127
-127
@@ -14,19 +14,19 @@
|
|||||||
"@electron/rebuild": "^4.0.2",
|
"@electron/rebuild": "^4.0.2",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@fontsource-variable/noto-sans-jp": "^5.2.9",
|
"@fontsource-variable/noto-sans-jp": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
"@fontsource-variable/noto-sans-kr": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
"@fontsource-variable/noto-sans-sc": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
"@fontsource-variable/noto-sans-tc": "^5.2.10",
|
||||||
"@internationalized/date": "^3.10.1",
|
"@internationalized/date": "^3.10.1",
|
||||||
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
||||||
"@sentry/vite-plugin": "^4.6.1",
|
"@sentry/vite-plugin": "^4.6.2",
|
||||||
"@sentry/vue": "^10.33.0",
|
"@sentry/vue": "^10.34.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@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.7",
|
"@types/node": "^25.0.8",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@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",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"esbuild-jest": "^0.5.0",
|
"esbuild-jest": "^0.5.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"noty": "^3.2.0-beta-deprecated",
|
"noty": "^3.2.0-beta-deprecated",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.0",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.7.0",
|
||||||
"remixicon": "^4.8.0",
|
"remixicon": "^4.8.0",
|
||||||
"sass-embedded": "^1.97.2",
|
"sass-embedded": "^1.97.2",
|
||||||
@@ -2097,9 +2097,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/noto-sans-jp": {
|
"node_modules/@fontsource-variable/noto-sans-jp": {
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-jp/-/noto-sans-jp-5.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-jp/-/noto-sans-jp-5.2.10.tgz",
|
||||||
"integrity": "sha512-osPL5f7dvGDjuMuFwDTGPLG37030D8X5zk+3BWea6txAVDFeE/ZIrKW0DY0uSDfRn9+NiKbiFn/2QvZveKXTog==",
|
"integrity": "sha512-m0XfZ38rZtCaGAuAQL0cBPQ6fc/RguYEOqw66zvuLOV9vNRUBf9MFqyoazTu2JVCRIcnkrxbwF29UUaHHgTKdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2107,9 +2107,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/noto-sans-kr": {
|
"node_modules/@fontsource-variable/noto-sans-kr": {
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.10.tgz",
|
||||||
"integrity": "sha512-g1BnJdJbnAgRUP8YxyPIm8npZVUbtt6VgtLnkGR7poa/RVbVGd27i+9138DmwRvtbKhJG1fPLQ/V3BonvFykRQ==",
|
"integrity": "sha512-UZOO7HF44Rt5+7SCeFMHYVgbKu36Jet6IxrAd7jjEkQMVDmeefwd0H8V5pSZydvBOOxClk3V5cQsujJqGHhQMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2117,9 +2117,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/noto-sans-sc": {
|
"node_modules/@fontsource-variable/noto-sans-sc": {
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.2.10.tgz",
|
||||||
"integrity": "sha512-ZEEpZlxjYEIVdg85K38mqaoeBcorrN3Z6MaIkwK5w5Dqn/e9v5uVIYr0ukoLsFxaVyEXSi/c3caOeMHjbOMtfA==",
|
"integrity": "sha512-zdk10i5HrDQTXI7ldD61zToX1fsgig8vDTsu7zB48SXOitWfuX0e5viZAwnkHuhwh096PU6X6i1AyAsbBCISpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2127,9 +2127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/noto-sans-tc": {
|
"node_modules/@fontsource-variable/noto-sans-tc": {
|
||||||
"version": "5.2.9",
|
"version": "5.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-tc/-/noto-sans-tc-5.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-tc/-/noto-sans-tc-5.2.10.tgz",
|
||||||
"integrity": "sha512-GhtbSE8IZTP3vZj7Fu1G/PERxguMe3jryAbHovSd22Rs7aYdbXQD8vBqkTT/tkHIUn6t2IFReTfgKUoQBPCe+w==",
|
"integrity": "sha512-COVssWiIp9VVfdGuHpbrcH8u9H5JK4Lm6U7oee2BD6aLwKzPshj49KGmmQe/r3Fcp4e4SSuCaqyN/BtDKxHOGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -4372,63 +4372,63 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/browser-utils": {
|
"node_modules/@sentry-internal/browser-utils": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.34.0.tgz",
|
||||||
"integrity": "sha512-nDJFHAfiFifBfJB0OF6DV6BIsIV5uah4lDsV4UBAgPBf+YAHclO10y1gi2U/JMh58c+s4lXi9p+PI1TFXZ0c6w==",
|
"integrity": "sha512-0YNr60rGHyedmwkO0lbDBjNx2KAmT3kWamjaqu7Aw+jsESoPLgt+fzaTVvUBvkftBDui2PeTSzXm/nqzssctYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/feedback": {
|
"node_modules/@sentry-internal/feedback": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.34.0.tgz",
|
||||||
"integrity": "sha512-sN/VLWtEf0BeV6w6wldIpTxUQxNVc9o9tjLRQa8je1ZV2FCgXA124Iff/zsowsz82dLqtg7qp6GA5zYXVq+JMA==",
|
"integrity": "sha512-wgGnq+iNxsFSOe9WX/FOvtoItSTjgLJJ4dQkVYtcVM6WGBVIg4wgNYfECCnRNztUTPzpZHLjC9r+4Pym451DDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/replay": {
|
"node_modules/@sentry-internal/replay": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.34.0.tgz",
|
||||||
"integrity": "sha512-UOU9PYxuXnPop3HoQ3l4Q7SZUXJC3Vmfm0Adgad8U03UcrThWIHYc5CxECSrVzfDFNOT7w9o7HQgRAgWxBPMXg==",
|
"integrity": "sha512-Vmea0GcOg57z/S1bVSj3saFcRvDqdLzdy4wd9fQMpMgy5OCbTlo7lxVUndKzbcZnanma6zF6VxwnWER1WuN9RA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/browser-utils": "10.33.0",
|
"@sentry-internal/browser-utils": "10.34.0",
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/replay-canvas": {
|
"node_modules/@sentry-internal/replay-canvas": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.34.0.tgz",
|
||||||
"integrity": "sha512-MTmP6uoAVzw4CCPeqCgCLsRSiOfGLxgyMFjGTCW3E7t62MJ9S0H5sLsQ34sHxXUa1gFU9UNAjEvRRpZ0JvWrPw==",
|
"integrity": "sha512-XWH/9njtgMD+LLWjc4KKgBpb+dTCkoUEIFDxcvzG/87d+jirmzf0+r8EfpLwKG+GrqNiiGRV39zIqu0SfPl+cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/replay": "10.33.0",
|
"@sentry-internal/replay": "10.34.0",
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/babel-plugin-component-annotate": {
|
"node_modules/@sentry/babel-plugin-component-annotate": {
|
||||||
"version": "4.6.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.2.tgz",
|
||||||
"integrity": "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==",
|
"integrity": "sha512-6VTjLJXtIHKwxMmThtZKwi1+hdklLNzlbYH98NhbH22/Vzb/c6BlSD2b5A0NGN9vFB807rD4x4tuP+Su7BxQXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4436,31 +4436,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/browser": {
|
"node_modules/@sentry/browser": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.34.0.tgz",
|
||||||
"integrity": "sha512-iWiPjik9zetM84jKfk01UveW1J0+X7w8XmJ8+IrhTyNDBVUWCRJWD8FrksiN1dRSg5mFWgfMRzKMz27hAScRwg==",
|
"integrity": "sha512-8WCsAXli5Z+eIN8dMY8KGQjrS3XgUp1np/pjdeWNrVPVR8q8XpS34qc+f+y/LFrYQC9bs2Of5aIBwRtDCIvRsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/browser-utils": "10.33.0",
|
"@sentry-internal/browser-utils": "10.34.0",
|
||||||
"@sentry-internal/feedback": "10.33.0",
|
"@sentry-internal/feedback": "10.34.0",
|
||||||
"@sentry-internal/replay": "10.33.0",
|
"@sentry-internal/replay": "10.34.0",
|
||||||
"@sentry-internal/replay-canvas": "10.33.0",
|
"@sentry-internal/replay-canvas": "10.34.0",
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/bundler-plugin-core": {
|
"node_modules/@sentry/bundler-plugin-core": {
|
||||||
"version": "4.6.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.2.tgz",
|
||||||
"integrity": "sha512-WPeRbnMXm927m4Kr69NTArPfI+p5/34FHftdCRI3LFPMyhZDzz6J3wLy4hzaVUgmMf10eLzmq2HGEMvpQmdynA==",
|
"integrity": "sha512-JkOc3JkVzi/fbXsFp8R9uxNKmBrPRaU4Yu4y1i3ihWfugqymsIYaN0ixLENZbGk2j4xGHIk20PAJzBJqBMTHew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.18.5",
|
"@babel/core": "^7.18.5",
|
||||||
"@sentry/babel-plugin-component-annotate": "4.6.1",
|
"@sentry/babel-plugin-component-annotate": "4.6.2",
|
||||||
"@sentry/cli": "^2.57.0",
|
"@sentry/cli": "^2.57.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"find-up": "^5.0.0",
|
"find-up": "^5.0.0",
|
||||||
@@ -4473,12 +4473,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli": {
|
"node_modules/@sentry/cli": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz",
|
||||||
"integrity": "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==",
|
"integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"https-proxy-agent": "^5.0.0",
|
"https-proxy-agent": "^5.0.0",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
@@ -4493,22 +4493,22 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@sentry/cli-darwin": "2.58.2",
|
"@sentry/cli-darwin": "2.58.4",
|
||||||
"@sentry/cli-linux-arm": "2.58.2",
|
"@sentry/cli-linux-arm": "2.58.4",
|
||||||
"@sentry/cli-linux-arm64": "2.58.2",
|
"@sentry/cli-linux-arm64": "2.58.4",
|
||||||
"@sentry/cli-linux-i686": "2.58.2",
|
"@sentry/cli-linux-i686": "2.58.4",
|
||||||
"@sentry/cli-linux-x64": "2.58.2",
|
"@sentry/cli-linux-x64": "2.58.4",
|
||||||
"@sentry/cli-win32-arm64": "2.58.2",
|
"@sentry/cli-win32-arm64": "2.58.4",
|
||||||
"@sentry/cli-win32-i686": "2.58.2",
|
"@sentry/cli-win32-i686": "2.58.4",
|
||||||
"@sentry/cli-win32-x64": "2.58.2"
|
"@sentry/cli-win32-x64": "2.58.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-darwin": {
|
"node_modules/@sentry/cli-darwin": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz",
|
||||||
"integrity": "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==",
|
"integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -4518,14 +4518,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-arm": {
|
"node_modules/@sentry/cli-linux-arm": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz",
|
||||||
"integrity": "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==",
|
"integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux",
|
"linux",
|
||||||
@@ -4537,14 +4537,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-arm64": {
|
"node_modules/@sentry/cli-linux-arm64": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz",
|
||||||
"integrity": "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==",
|
"integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux",
|
"linux",
|
||||||
@@ -4556,15 +4556,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-i686": {
|
"node_modules/@sentry/cli-linux-i686": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz",
|
||||||
"integrity": "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==",
|
"integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x86",
|
"x86",
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux",
|
"linux",
|
||||||
@@ -4576,14 +4576,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-x64": {
|
"node_modules/@sentry/cli-linux-x64": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz",
|
||||||
"integrity": "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==",
|
"integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux",
|
"linux",
|
||||||
@@ -4595,14 +4595,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-win32-arm64": {
|
"node_modules/@sentry/cli-win32-arm64": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz",
|
||||||
"integrity": "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==",
|
"integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -4612,15 +4612,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-win32-i686": {
|
"node_modules/@sentry/cli-win32-i686": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz",
|
||||||
"integrity": "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==",
|
"integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x86",
|
"x86",
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -4630,14 +4630,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-win32-x64": {
|
"node_modules/@sentry/cli-win32-x64": {
|
||||||
"version": "2.58.2",
|
"version": "2.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz",
|
||||||
"integrity": "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==",
|
"integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "FSL-1.1-MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -4647,9 +4647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/core": {
|
"node_modules/@sentry/core": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.34.0.tgz",
|
||||||
"integrity": "sha512-ehH1VSUclIHZKEZVdv+klofsFIh8FFzqA6AAV23RtLepptzA8wqQzUGraEuSN25sYcNmYJ0jti5U0Ys+WZv5Dw==",
|
"integrity": "sha512-4FFpYBMf0VFdPcsr4grDYDOR87mRu6oCfb51oQjU/Pndmty7UgYo0Bst3LEC/8v0SpytBtzXq+Wx/fkwulBesg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4657,13 +4657,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/vite-plugin": {
|
"node_modules/@sentry/vite-plugin": {
|
||||||
"version": "4.6.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.2.tgz",
|
||||||
"integrity": "sha512-Qvys1y3o8/bfL3ikrHnJS9zxdjt0z3POshdBl3967UcflrTqBmnGNkcVk53SlmtJWIfh85fgmrLvGYwZ2YiqNg==",
|
"integrity": "sha512-hK9N50LlTaPlb2P1r87CFupU7MJjvtrp+Js96a2KDdiP8ViWnw4Gsa/OvA0pkj2wAFXFeBQMLS6g/SktTKG54w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/bundler-plugin-core": "4.6.1",
|
"@sentry/bundler-plugin-core": "4.6.2",
|
||||||
"unplugin": "1.0.1"
|
"unplugin": "1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4671,14 +4671,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/vue": {
|
"node_modules/@sentry/vue": {
|
||||||
"version": "10.33.0",
|
"version": "10.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.34.0.tgz",
|
||||||
"integrity": "sha512-CUtoBl62DG8mkoYfgpkw2WdB187XA2CfPj7OJdIzt3lavhpSAPmsY4jUarK2RUJvcowr5zYbEfv50Y0tsQxuGA==",
|
"integrity": "sha512-2c+s8pQKY/MTunwIgTsiMtq4c7cPYyhB1LFOZ/VJSQH8MLD7qzxG7ed8SOJ3NTnGn8a2TpW/vPtc3uh90zLH3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "10.33.0",
|
"@sentry/browser": "10.34.0",
|
||||||
"@sentry/core": "10.33.0"
|
"@sentry/core": "10.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -5313,9 +5313,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.0.7",
|
"version": "25.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz",
|
||||||
"integrity": "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==",
|
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9004,14 +9004,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "5.5.4",
|
"version": "5.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
||||||
"integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
|
"integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier-linter-helpers": "^1.0.0",
|
"prettier-linter-helpers": "^1.0.1",
|
||||||
"synckit": "^0.11.7"
|
"synckit": "^0.11.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
@@ -14900,9 +14900,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.7.4",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -14917,9 +14917,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-linter-helpers": {
|
"node_modules/prettier-linter-helpers": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
|
||||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17080,9 +17080,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/synckit": {
|
"node_modules/synckit": {
|
||||||
"version": "0.11.11",
|
"version": "0.11.12",
|
||||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
|
||||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
"integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+9
-9
@@ -35,19 +35,19 @@
|
|||||||
"@electron/rebuild": "^4.0.2",
|
"@electron/rebuild": "^4.0.2",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@fontsource-variable/noto-sans-jp": "^5.2.9",
|
"@fontsource-variable/noto-sans-jp": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
"@fontsource-variable/noto-sans-kr": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
"@fontsource-variable/noto-sans-sc": "^5.2.10",
|
||||||
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
"@fontsource-variable/noto-sans-tc": "^5.2.10",
|
||||||
"@internationalized/date": "^3.10.1",
|
"@internationalized/date": "^3.10.1",
|
||||||
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
||||||
"@sentry/vite-plugin": "^4.6.1",
|
"@sentry/vite-plugin": "^4.6.2",
|
||||||
"@sentry/vue": "^10.33.0",
|
"@sentry/vue": "^10.34.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@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.7",
|
"@types/node": "^25.0.8",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@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",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"esbuild-jest": "^0.5.0",
|
"esbuild-jest": "^0.5.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"noty": "^3.2.0-beta-deprecated",
|
"noty": "^3.2.0-beta-deprecated",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.0",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.7.0",
|
||||||
"remixicon": "^4.8.0",
|
"remixicon": "^4.8.0",
|
||||||
"sass-embedded": "^1.97.2",
|
"sass-embedded": "^1.97.2",
|
||||||
|
|||||||
+15
-21
@@ -1,32 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<el-config-provider
|
<MacOSTitleBar></MacOSTitleBar>
|
||||||
:locale="/** @type {import('element-plus/es/locale').Language} */ (messages[locale].elementPlus)">
|
|
||||||
<MacOSTitleBar></MacOSTitleBar>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="x-app"
|
id="x-app"
|
||||||
class="x-app"
|
class="x-app"
|
||||||
:class="{ 'with-macos-titlebar': isMacOS }"
|
:class="{ 'with-macos-titlebar': isMacOS }"
|
||||||
ondragenter="event.preventDefault()"
|
ondragenter="event.preventDefault()"
|
||||||
ondragover="event.preventDefault()"
|
ondragover="event.preventDefault()"
|
||||||
ondrop="event.preventDefault()">
|
ondrop="event.preventDefault()">
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
<Toaster position="top-center"></Toaster>
|
<Toaster position="top-center"></Toaster>
|
||||||
|
|
||||||
<AlertDialogModal></AlertDialogModal>
|
<AlertDialogModal></AlertDialogModal>
|
||||||
<PromptDialogModal></PromptDialogModal>
|
<PromptDialogModal></PromptDialogModal>
|
||||||
|
|
||||||
<VRCXUpdateDialog></VRCXUpdateDialog>
|
<VRCXUpdateDialog></VRCXUpdateDialog>
|
||||||
</div>
|
</div>
|
||||||
<div id="x-dialog-portal" class="x-dialog-portal"></div>
|
<div id="x-dialog-portal" class="x-dialog-portal"></div>
|
||||||
</el-config-provider>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeMount, onMounted } from 'vue';
|
import { computed, onBeforeMount, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { Toaster } from './components/ui/sonner';
|
import { Toaster } from './components/ui/sonner';
|
||||||
import { TooltipProvider } from './components/ui/tooltip';
|
import { TooltipProvider } from './components/ui/tooltip';
|
||||||
@@ -45,8 +41,6 @@
|
|||||||
|
|
||||||
const isMacOS = computed(() => navigator.platform.includes('Mac'));
|
const isMacOS = computed(() => navigator.platform.includes('Mac'));
|
||||||
|
|
||||||
const { locale, messages } = useI18n();
|
|
||||||
|
|
||||||
initNoty();
|
initNoty();
|
||||||
|
|
||||||
const store = createGlobalStores();
|
const store = createGlobalStores();
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { ArrowUp } from 'lucide-vue-next';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
target: { type: [String, Object], default: null },
|
||||||
|
|
||||||
|
bottom: { type: Number, default: 20 },
|
||||||
|
right: { type: Number, default: 20 },
|
||||||
|
|
||||||
|
visibilityHeight: { type: Number, default: 200 },
|
||||||
|
|
||||||
|
behavior: {
|
||||||
|
type: String,
|
||||||
|
default: 'smooth',
|
||||||
|
validator: (value) => value === 'auto' || value === 'smooth'
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: { type: Boolean, default: true },
|
||||||
|
tooltipText: { type: String, default: 'Back to top' },
|
||||||
|
|
||||||
|
teleport: { type: Boolean, default: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
let containerEl = null;
|
||||||
|
|
||||||
|
function resolveTarget() {
|
||||||
|
if (!props.target) return null;
|
||||||
|
if (typeof props.target === 'string') {
|
||||||
|
return document.querySelector(props.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.target === 'object') {
|
||||||
|
if ('value' in props.target) {
|
||||||
|
return props.target.value;
|
||||||
|
}
|
||||||
|
if ('$el' in props.target) {
|
||||||
|
return props.target.$el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollTop() {
|
||||||
|
if (!containerEl || typeof containerEl.scrollTop !== 'number') {
|
||||||
|
return window.scrollY || document.documentElement.scrollTop || 0;
|
||||||
|
}
|
||||||
|
return containerEl.scrollTop || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
visible.value = getScrollTop() >= props.visibilityHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
const behavior = props.behavior === 'auto' ? 'auto' : 'smooth';
|
||||||
|
if (!containerEl || typeof containerEl.scrollTo !== 'function') {
|
||||||
|
window.scrollTo({ top: 0, behavior });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
containerEl.scrollTo({ top: 0, behavior });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind() {
|
||||||
|
containerEl = resolveTarget();
|
||||||
|
|
||||||
|
const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window;
|
||||||
|
target.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
handleScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbind() {
|
||||||
|
const target = containerEl || window;
|
||||||
|
target.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bind();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.target,
|
||||||
|
() => {
|
||||||
|
unbind();
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unbind();
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapperStyle = computed(
|
||||||
|
() => `position:fixed; right:${props.right}px; bottom:${props.bottom}px; z-index:50;`
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="teleport" to="body">
|
||||||
|
<Transition name="back-to-top">
|
||||||
|
<div v-if="visible" :style="wrapperStyle">
|
||||||
|
<TooltipProvider v-if="tooltip">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
class="rounded-full shadow"
|
||||||
|
aria-label="Back to top"
|
||||||
|
@click="scrollToTop">
|
||||||
|
<ArrowUp class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">
|
||||||
|
{{ tooltipText }}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
class="rounded-full shadow"
|
||||||
|
aria-label="Back to top"
|
||||||
|
@click="scrollToTop">
|
||||||
|
<ArrowUp class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<Transition v-else name="back-to-top">
|
||||||
|
<div v-if="visible" :style="wrapperStyle">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
class="rounded-full shadow"
|
||||||
|
aria-label="Back to top"
|
||||||
|
@click="scrollToTop">
|
||||||
|
<ArrowUp class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.back-to-top-enter-active,
|
||||||
|
.back-to-top-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top-enter-from,
|
||||||
|
.back-to-top-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top-enter-to,
|
||||||
|
.back-to-top-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.back-to-top-enter-active,
|
||||||
|
.back-to-top-leave-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,154 +1,332 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<Dialog v-model:open="open">
|
||||||
v-if="fullscreenImageDialog.visible"
|
<DialogPortal :to="portalTo">
|
||||||
class="fullscreen-image-overlay"
|
<RekaDialogOverlay class="fixed inset-0 bg-background/80 backdrop-blur-sm" @click="closeDialog" />
|
||||||
:style="{ zIndex: overlayZIndex }"
|
|
||||||
@click.self="closeDialog">
|
<RekaDialogContent
|
||||||
<el-image
|
class="fixed inset-0 p-6 sm:p-10 border-0 bg-transparent shadow-none outline-none"
|
||||||
v-if="fullscreenImageDialog.imageUrl"
|
@open-auto-focus.prevent
|
||||||
ref="imageRef"
|
@close-auto-focus.prevent>
|
||||||
class="fullscreen-image"
|
<div ref="viewerEl" class="relative h-full w-full overflow-hidden select-none">
|
||||||
:src="fullscreenImageDialog.imageUrl"
|
<!-- toolbar -->
|
||||||
:preview-src-list="[fullscreenImageDialog.imageUrl]"
|
<div
|
||||||
:z-index="100000"
|
class="absolute right-3 top-3 z-10 flex items-center gap-2 rounded-md bg-background/70 backdrop-blur px-2 py-1 border">
|
||||||
fit="contain"
|
<Button
|
||||||
preview-teleported
|
variant="ghost"
|
||||||
hide-on-click-modal
|
size="icon"
|
||||||
:initial-index="0"
|
class="h-8 w-8"
|
||||||
@load="handleImageLoad"
|
:disabled="!imageUrl"
|
||||||
@close="closeDialog">
|
@click="copyImageToClipboard(imageUrl)"
|
||||||
<template #toolbar="{ actions }">
|
aria-label="Copy">
|
||||||
<Copy @click="copyImageToClipboard(fullscreenImageDialog.imageUrl)" class="toolbar-icon" />
|
<Copy class="h-4 w-4" />
|
||||||
<Download
|
</Button>
|
||||||
@click="downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)"
|
|
||||||
class="toolbar-icon" />
|
<Button
|
||||||
<ZoomOut @click="actions('zoomOut')" class="toolbar-icon" />
|
variant="ghost"
|
||||||
<ZoomIn @click="actions('zoomIn')" class="toolbar-icon" />
|
size="icon"
|
||||||
<RotateCw @click="actions('clockwise')" class="toolbar-icon" />
|
class="h-8 w-8"
|
||||||
<RotateCcw @click="actions('anticlockwise')" class="toolbar-icon" />
|
:disabled="!imageUrl"
|
||||||
</template>
|
@click="downloadAndSaveImage(imageUrl, fullscreenImageDialog.fileName)"
|
||||||
</el-image>
|
aria-label="Download">
|
||||||
</div>
|
<Download class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="mx-1 h-5 w-px bg-border" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="zoomOutCenter"
|
||||||
|
aria-label="Zoom out">
|
||||||
|
<ZoomOut class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click="zoomInCenter" aria-label="Zoom in">
|
||||||
|
<ZoomIn class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="rotateCW"
|
||||||
|
aria-label="Rotate clockwise">
|
||||||
|
<RotateCw class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="rotateCCW"
|
||||||
|
aria-label="Rotate counterclockwise">
|
||||||
|
<RotateCcw class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click="resetTransform" aria-label="Reset">
|
||||||
|
<RefreshCcw class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="mx-1 h-5 w-px bg-border" />
|
||||||
|
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeDialog" aria-label="Close">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="h-full w-full flex items-center justify-center"
|
||||||
|
@wheel="onWheel"
|
||||||
|
@pointerdown="onPointerDown"
|
||||||
|
@pointermove="onPointerMove"
|
||||||
|
@pointerup="onPointerUp"
|
||||||
|
@pointercancel="onPointerUp">
|
||||||
|
<img
|
||||||
|
v-if="imageUrl"
|
||||||
|
:src="imageUrl"
|
||||||
|
class="max-h-full max-w-full x-viewer-img"
|
||||||
|
:style="transformStyle"
|
||||||
|
draggable="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RekaDialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Copy, Download, RotateCcw, RotateCw, ZoomIn, ZoomOut } from 'lucide-vue-next';
|
import { Copy, Download, RefreshCcw, RotateCcw, RotateCw, X, ZoomIn, ZoomOut } from 'lucide-vue-next';
|
||||||
import { nextTick, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { DialogContent as RekaDialogContent, DialogOverlay as RekaDialogOverlay, DialogPortal } from 'reka-ui';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Dialog } from '@/components/ui/dialog';
|
||||||
|
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
import Noty from 'noty';
|
import Noty from 'noty';
|
||||||
|
|
||||||
import { escapeTag, extractFileId } from '../shared/utils';
|
import { escapeTag, extractFileId } from '../shared/utils';
|
||||||
import { getNextDialogIndex } from '../shared/utils/base/ui';
|
|
||||||
import { useGalleryStore } from '../stores';
|
import { useGalleryStore } from '../stores';
|
||||||
|
|
||||||
const galleryStore = useGalleryStore();
|
const galleryStore = useGalleryStore();
|
||||||
const { fullscreenImageDialog } = storeToRefs(galleryStore);
|
const { fullscreenImageDialog } = storeToRefs(galleryStore);
|
||||||
|
|
||||||
const imageRef = ref();
|
const viewerEl = ref(null);
|
||||||
const overlayZIndex = ref(4000);
|
const portalLayer = acquireModalPortalLayer();
|
||||||
|
const portalTo = portalLayer.element;
|
||||||
|
|
||||||
function showPreview() {
|
const scale = ref(1);
|
||||||
nextTick(() => {
|
const rotate = ref(0); // deg
|
||||||
imageRef.value?.showPreview?.();
|
const tx = ref(0);
|
||||||
});
|
const ty = ref(0);
|
||||||
|
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStartX = ref(0);
|
||||||
|
const dragStartY = ref(0);
|
||||||
|
const startTx = ref(0);
|
||||||
|
const startTy = ref(0);
|
||||||
|
|
||||||
|
const imageUrl = computed(() => fullscreenImageDialog.value.imageUrl || '');
|
||||||
|
|
||||||
|
const open = computed({
|
||||||
|
get: () => fullscreenImageDialog.value.visible,
|
||||||
|
set: (v) => {
|
||||||
|
fullscreenImageDialog.value.visible = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clamp(n, min, max) {
|
||||||
|
return Math.min(max, Math.max(min, n));
|
||||||
|
}
|
||||||
|
function degToRad(deg) {
|
||||||
|
return (deg * Math.PI) / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImageLoad() {
|
function resetTransform() {
|
||||||
showPreview();
|
scale.value = 1;
|
||||||
|
rotate.value = 0;
|
||||||
|
tx.value = 0;
|
||||||
|
ty.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => fullscreenImageDialog.value.visible,
|
|
||||||
(visible) => {
|
|
||||||
if (!visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
overlayZIndex.value = Math.max(getNextDialogIndex(), 4000);
|
|
||||||
showPreview();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => fullscreenImageDialog.value.imageUrl,
|
|
||||||
(url) => {
|
|
||||||
if (!url || !fullscreenImageDialog.value.visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showPreview();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
fullscreenImageDialog.value.visible = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageToClipboard(url) {
|
function zoomAtCenter(factor) {
|
||||||
if (!url) {
|
const el = viewerEl.value;
|
||||||
|
if (!el) {
|
||||||
|
scale.value = clamp(scale.value * factor, 0.1, 10);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scale.value = clamp(scale.value * factor, 0.1, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomInCenter() {
|
||||||
|
zoomAtCenter(1.2);
|
||||||
|
}
|
||||||
|
function zoomOutCenter() {
|
||||||
|
zoomAtCenter(1 / 1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateCW() {
|
||||||
|
rotate.value = (rotate.value + 90) % 360;
|
||||||
|
}
|
||||||
|
function rotateCCW() {
|
||||||
|
rotate.value = (rotate.value - 90 + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomAtPointer(e, factor) {
|
||||||
|
const el = viewerEl.value;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
// mouse in container space
|
||||||
|
const mx = e.clientX - rect.left;
|
||||||
|
const my = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// container center
|
||||||
|
const cx = rect.width / 2;
|
||||||
|
const cy = rect.height / 2;
|
||||||
|
|
||||||
|
const oldScale = scale.value;
|
||||||
|
const newScale = clamp(oldScale * factor, 0.1, 10);
|
||||||
|
|
||||||
|
const r = degToRad(rotate.value);
|
||||||
|
const cos = Math.cos(r);
|
||||||
|
const sin = Math.sin(r);
|
||||||
|
|
||||||
|
// vector from transformed center (includes current translation)
|
||||||
|
const vx = mx - cx - tx.value;
|
||||||
|
const vy = my - cy - ty.value;
|
||||||
|
|
||||||
|
// inverse rotate + unscale => local point
|
||||||
|
const ux = (vx * cos + vy * sin) / oldScale;
|
||||||
|
const uy = (-vx * sin + vy * cos) / oldScale;
|
||||||
|
|
||||||
|
// forward rotate + scale => new vector
|
||||||
|
const v2x = (ux * cos - uy * sin) * newScale;
|
||||||
|
const v2y = (ux * sin + uy * cos) * newScale;
|
||||||
|
|
||||||
|
// keep pointer anchored
|
||||||
|
tx.value = mx - cx - v2x;
|
||||||
|
ty.value = my - cy - v2y;
|
||||||
|
scale.value = newScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWheel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||||
|
zoomAtPointer(e, factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e) {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
isDragging.value = true;
|
||||||
|
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||||
|
dragStartX.value = e.clientX;
|
||||||
|
dragStartY.value = e.clientY;
|
||||||
|
startTx.value = tx.value;
|
||||||
|
startTy.value = ty.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e) {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
const dx = e.clientX - dragStartX.value;
|
||||||
|
const dy = e.clientY - dragStartY.value;
|
||||||
|
tx.value = startTx.value + dx;
|
||||||
|
ty.value = startTy.value + dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(e) {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
isDragging.value = false;
|
||||||
|
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformStyle = computed(() => ({
|
||||||
|
transform: `translate(${tx.value}px, ${ty.value}px) scale(${scale.value}) rotate(${rotate.value}deg)`,
|
||||||
|
transformOrigin: 'center center'
|
||||||
|
}));
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => open.value,
|
||||||
|
(v) => {
|
||||||
|
if (v) {
|
||||||
|
portalLayer.bringToFront();
|
||||||
|
resetTransform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
portalLayer.release();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => imageUrl.value,
|
||||||
|
(url) => {
|
||||||
|
if (!url || !open.value) return;
|
||||||
|
resetTransform();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (!open.value) return;
|
||||||
|
if (e.key === '+' || e.key === '=') zoomInCenter();
|
||||||
|
else if (e.key === '-' || e.key === '_') zoomOutCenter();
|
||||||
|
else if (e.key.toLowerCase() === 'r') rotateCW();
|
||||||
|
else if (e.key === '0') resetTransform();
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener('keydown', onKeydown));
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown));
|
||||||
|
|
||||||
|
async function copyImageToClipboard(url) {
|
||||||
|
if (!url) return;
|
||||||
const msg = toast.info('Downloading image...');
|
const msg = toast.info('Downloading image...');
|
||||||
try {
|
try {
|
||||||
const response = await webApiService.execute({
|
const response = await webApiService.execute({ url, method: 'GET' });
|
||||||
url,
|
if (response.status !== 200 || !String(response.data).startsWith('data:image/png')) {
|
||||||
method: 'GET'
|
|
||||||
});
|
|
||||||
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
|
|
||||||
throw new Error(`Error: ${response.data}`);
|
throw new Error(`Error: ${response.data}`);
|
||||||
}
|
}
|
||||||
await navigator.clipboard.write([
|
const blob = await (await fetch(response.data)).blob();
|
||||||
new ClipboardItem({
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
||||||
'image/png': await (await fetch(response.data)).blob()
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
toast.success('Image copied to clipboard');
|
toast.success('Image copied to clipboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading image:', error);
|
console.error('Error downloading image:', error);
|
||||||
new Noty({
|
new Noty({ type: 'error', text: escapeTag(`Failed to download image. ${url}`) }).show();
|
||||||
type: 'error',
|
|
||||||
text: escapeTag(`Failed to download image. ${url}`)
|
|
||||||
}).show();
|
|
||||||
} finally {
|
} finally {
|
||||||
toast.dismiss(msg);
|
toast.dismiss(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAndSaveImage(url, fileName) {
|
async function downloadAndSaveImage(url, fileName) {
|
||||||
if (!url) {
|
if (!url) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = toast.info('Downloading image...');
|
const msg = toast.info('Downloading image...');
|
||||||
try {
|
try {
|
||||||
const response = await webApiService.execute({
|
const response = await webApiService.execute({ url, method: 'GET' });
|
||||||
url,
|
if (response.status !== 200 || !String(response.data).startsWith('data:image/png')) {
|
||||||
method: 'GET'
|
|
||||||
});
|
|
||||||
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
|
|
||||||
throw new Error(`Error: ${response.data}`);
|
throw new Error(`Error: ${response.data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = response.data;
|
link.href = response.data;
|
||||||
|
|
||||||
const fileId = extractFileId(url);
|
const fileId = extractFileId(url);
|
||||||
if (!fileName && fileId) {
|
let name = fileName;
|
||||||
fileName = `${fileId}.png`;
|
if (!name && fileId) name = `${fileId}.png`;
|
||||||
}
|
if (!name) name = `${url.split('/').pop()}.png`;
|
||||||
if (!fileName) {
|
if (!name) name = 'image.png';
|
||||||
fileName = `${url.split('/').pop()}.png`;
|
|
||||||
}
|
link.setAttribute('download', name);
|
||||||
if (!fileName) {
|
|
||||||
fileName = 'image.png';
|
|
||||||
}
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading image:', error);
|
console.error('Error downloading image:', error);
|
||||||
new Noty({
|
new Noty({ type: 'error', text: escapeTag(`Failed to download image. ${url}`) }).show();
|
||||||
type: 'error',
|
|
||||||
text: escapeTag(`Failed to download image. ${url}`)
|
|
||||||
}).show();
|
|
||||||
} finally {
|
} finally {
|
||||||
toast.dismiss(msg);
|
toast.dismiss(msg);
|
||||||
}
|
}
|
||||||
@@ -156,27 +334,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.toolbar-icon:hover {
|
.x-viewer-img {
|
||||||
opacity: 1;
|
will-change: transform;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.toolbar-icon {
|
.x-viewer-img:active {
|
||||||
cursor: pointer;
|
cursor: grabbing;
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.fullscreen-image-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.fullscreen-image {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
:deep(.el-image__preview) {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Popover v-model:open="visible">
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
{{ t('nav_menu.icon_picker.pick_icon') }}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent side="bottom" align="start" class="w-155">
|
|
||||||
<div class="icon-picker">
|
|
||||||
<InputGroupSearch
|
|
||||||
v-model="search"
|
|
||||||
class="icon-picker__search"
|
|
||||||
:placeholder="t('nav_menu.icon_picker.search_placeholder')" />
|
|
||||||
<el-scrollbar v-if="filteredCategories.length" height="600px" class="icon-picker__scroll">
|
|
||||||
<div v-for="category in filteredCategories" :key="category.name" class="icon-picker__category">
|
|
||||||
<div class="icon-picker__category-title">
|
|
||||||
{{ category.name }}
|
|
||||||
</div>
|
|
||||||
<div class="icon-picker__grid">
|
|
||||||
<div v-for="group in category.groups" :key="group.id" class="icon-picker__group">
|
|
||||||
<div class="icon-picker__group-label">
|
|
||||||
{{ group.label }}
|
|
||||||
</div>
|
|
||||||
<div class="icon-picker__variants">
|
|
||||||
<button
|
|
||||||
v-for="variant in group.variants"
|
|
||||||
:key="variant.className"
|
|
||||||
type="button"
|
|
||||||
class="icon-picker__variant"
|
|
||||||
:class="{ 'is-active': variant.className === modelValue }"
|
|
||||||
:title="group.tooltip"
|
|
||||||
@click="handleSelect(variant.className)">
|
|
||||||
<i :class="[variant.className, 'ri-2x']"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
<div v-else class="icon-picker__empty">{{ t('nav_menu.icon_picker.no_icon_found') }}</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
|
||||||
|
|
||||||
import remixIconTags from '../shared/constants/remixIconTags.json';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const search = ref('');
|
|
||||||
|
|
||||||
const parseTags = (tagsText) =>
|
|
||||||
typeof tagsText === 'string'
|
|
||||||
? tagsText
|
|
||||||
.split(',')
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const formatLabel = (baseName) =>
|
|
||||||
baseName
|
|
||||||
.split('-')
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const createGroup = (categoryName, baseName, tagsText) => {
|
|
||||||
const normalizedTags = parseTags(tagsText);
|
|
||||||
const label = formatLabel(baseName);
|
|
||||||
const variants = ['line', 'fill'].map((variant) => ({
|
|
||||||
className: `ri-${baseName}-${variant}`,
|
|
||||||
variant
|
|
||||||
}));
|
|
||||||
const searchText = [
|
|
||||||
baseName,
|
|
||||||
label,
|
|
||||||
...baseName.split('-'),
|
|
||||||
...normalizedTags,
|
|
||||||
...variants.map((variant) => variant.className),
|
|
||||||
'line',
|
|
||||||
'fill'
|
|
||||||
]
|
|
||||||
.join(' ')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${categoryName}-${baseName}`,
|
|
||||||
label,
|
|
||||||
tooltip: normalizedTags.length ? `${label} • ${normalizedTags.join(', ')}` : label,
|
|
||||||
variants,
|
|
||||||
searchable: searchText
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const categories = computed(() =>
|
|
||||||
Object.entries(remixIconTags)
|
|
||||||
.filter(([key]) => key !== '_comment')
|
|
||||||
.map(([name, icons]) => ({
|
|
||||||
name,
|
|
||||||
groups: Object.entries(icons || {}).map(([baseName, tags]) => createGroup(name, baseName, tags))
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredCategories = computed(() => {
|
|
||||||
const query = search.value.trim().toLowerCase();
|
|
||||||
if (!query) {
|
|
||||||
return categories.value;
|
|
||||||
}
|
|
||||||
return categories.value
|
|
||||||
.map((category) => ({
|
|
||||||
name: category.name,
|
|
||||||
groups: category.groups.filter((group) => group.searchable.includes(query))
|
|
||||||
}))
|
|
||||||
.filter((category) => category.groups.length > 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelect = (className) => {
|
|
||||||
emit('update:modelValue', className);
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(visible, (nextVisible) => {
|
|
||||||
if (!nextVisible) {
|
|
||||||
search.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.icon-picker__trigger i {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
height: 600px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__search {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__scroll {
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__category {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__category-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 4px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__group-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__variants {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__variant {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 84px;
|
|
||||||
height: 84px;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
transition:
|
|
||||||
color 0.2s ease,
|
|
||||||
background 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__variant i {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__variant:hover {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__variant.is-active {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
background: var(--el-color-primary-light-9);
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-picker__empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 200px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
<div v-if="isVisible" class="inline-block">
|
<div v-if="isVisible" class="inline-block">
|
||||||
<TooltipWrapper side="top" :content="t('dialog.user.info.launch_invite_tooltip')"
|
<TooltipWrapper side="top" :content="t('dialog.user.info.launch_invite_tooltip')"
|
||||||
><Button
|
><Button
|
||||||
class="rounded-full w-6 h-6 text-muted-foreground hover:text-foreground"
|
class="rounded-full w-6 h-6 text-xs text-muted-foreground hover:text-foreground"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
><Star />
|
><LogIn />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Star } from 'lucide-vue-next';
|
import { LogIn } from 'lucide-vue-next';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
<div v-if="!text" class="transparent">-</div>
|
<div v-if="!text" class="transparent">-</div>
|
||||||
<div v-show="text" class="flex items-center">
|
<div v-show="text" class="flex items-center">
|
||||||
<div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
|
<div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
|
||||||
<TooltipWrapper
|
<template v-if="disableTooltip">
|
||||||
:content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
|
|
||||||
:disabled="!instanceName || showInstanceIdInLocation"
|
|
||||||
:delay-duration="300"
|
|
||||||
side="top">
|
|
||||||
<div
|
<div
|
||||||
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
|
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
|
||||||
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
|
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
|
||||||
@@ -21,10 +17,37 @@
|
|||||||
({{ groupName }})
|
({{ groupName }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
<AlertTriangle v-if="isClosed" :class="['inline-block', 'ml-5']" style="color: lightcoral" />
|
||||||
<AlertTriangle :class="['inline-block', 'ml-5']" style="color: lightcoral" />
|
</template>
|
||||||
</TooltipWrapper>
|
|
||||||
|
<template v-else>
|
||||||
|
<TooltipWrapper
|
||||||
|
:content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
|
||||||
|
:disabled="!instanceName || showInstanceIdInLocation"
|
||||||
|
:delay-duration="300"
|
||||||
|
side="top">
|
||||||
|
<div
|
||||||
|
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
|
||||||
|
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
|
||||||
|
@click="handleShowWorldDialog">
|
||||||
|
<Loader2 :class="['is-loading']" class="mr-1" v-if="isTraveling" />
|
||||||
|
<span class="min-w-0 truncate">{{ text }}</span>
|
||||||
|
<span v-if="showInstanceIdInLocation && instanceName" class="ml-1 whitespace-nowrap">{{
|
||||||
|
` · #${instanceName}`
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="groupName"
|
||||||
|
class="ml-0.5 whitespace-nowrap x-link"
|
||||||
|
@click.stop="handleShowGroupDialog">
|
||||||
|
({{ groupName }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipWrapper>
|
||||||
|
<TooltipWrapper v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||||
|
<AlertTriangle :class="['inline-block', 'ml-5']" style="color: lightcoral" />
|
||||||
|
</TooltipWrapper>
|
||||||
|
</template>
|
||||||
<Lock v-if="strict" :class="['inline-block', 'ml-5']" />
|
<Lock v-if="strict" :class="['inline-block', 'ml-5']" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +94,10 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
disableTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isOpenPreviousInstanceInfoDialog: {
|
isOpenPreviousInstanceInfoDialog: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Lock, Unlock, AlertTriangle } from 'lucide-vue-next';
|
import { AlertTriangle, Lock, Unlock } from 'lucide-vue-next';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|||||||
@@ -239,13 +239,13 @@
|
|||||||
<CustomNavDialog
|
<CustomNavDialog
|
||||||
v-model:visible="customNavDialogVisible"
|
v-model:visible="customNavDialogVisible"
|
||||||
:layout="navLayout"
|
:layout="navLayout"
|
||||||
:default-folder-icon="DEFAULT_FOLDER_ICON"
|
|
||||||
@save="handleCustomNavSave"
|
@save="handleCustomNavSave"
|
||||||
@reset="handleCustomNavReset" />
|
@reset="handleCustomNavReset" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||||
|
import { ElMenu, ElMenuItem, ElPopover, ElSubMenu } from 'element-plus';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { dayjs } from 'element-plus';
|
import { dayjs } from 'element-plus';
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
|
||||||
|
presets: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
|
||||||
|
clearable: { type: Boolean, default: false },
|
||||||
|
emptyValue: { type: String, default: '' },
|
||||||
|
|
||||||
|
cols: { type: Number, default: 6 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
||||||
|
function normalizeHex(v) {
|
||||||
|
const s = String(v || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (/^#[0-9a-f]{6}$/.test(s)) return s;
|
||||||
|
return '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeValue = computed(() => normalizeHex(props.modelValue));
|
||||||
|
const displayText = computed(() => (props.modelValue ? String(props.modelValue) : props.emptyValue));
|
||||||
|
|
||||||
|
function setColor(color) {
|
||||||
|
if (props.disabled) return;
|
||||||
|
emit('update:modelValue', color);
|
||||||
|
emit('change', color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(e) {
|
||||||
|
if (props.disabled) return;
|
||||||
|
const v = e?.target?.value;
|
||||||
|
setColor(String(v || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
if (props.disabled || !props.clearable) return;
|
||||||
|
emit('update:modelValue', props.emptyValue);
|
||||||
|
emit('change', props.emptyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: `repeat(${Math.max(1, props.cols)}, minmax(0, 1fr))`
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm" class="flex items-center gap-2 px-2" :disabled="disabled">
|
||||||
|
<span class="h-4 w-4 rounded border" :style="{ backgroundColor: safeValue }" />
|
||||||
|
<span class="font-mono text-xs opacity-80">
|
||||||
|
{{ displayText }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="clearable && modelValue" class="ml-1 opacity-60">✕</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent class="w-56 p-3">
|
||||||
|
<div class="mb-3 grid gap-2" :style="gridStyle">
|
||||||
|
<button
|
||||||
|
v-for="color in presets"
|
||||||
|
:key="color"
|
||||||
|
type="button"
|
||||||
|
class="h-6 w-6 rounded border"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-disabled="disabled ? 'true' : 'false'"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
safeValue === String(color).toLowerCase() ? 'ring-2 ring-offset-2' : ''
|
||||||
|
]"
|
||||||
|
@click="setColor(color)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
class="h-8 w-full cursor-pointer border-none bg-transparent p-0"
|
||||||
|
:value="safeValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="onInput" />
|
||||||
|
|
||||||
|
<div v-if="clearable" class="mt-3 flex justify-end">
|
||||||
|
<Button variant="ghost" size="sm" :disabled="disabled" @click="clear"> Clear </Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
@@ -5,81 +5,83 @@
|
|||||||
<DialogTitle>{{ t('dialog.favorite.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.favorite.header') }}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div v-loading="loading">
|
<div v-loading="loading">
|
||||||
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
|
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
|
||||||
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
|
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
style="width: 100%; white-space: initial"
|
style="width: 100%; white-space: initial"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
|
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
|
||||||
<Check />{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} /
|
<Check />{{ favoriteDialog.currentGroup.displayName }} ({{
|
||||||
{{ favoriteDialog.currentGroup.capacity }})
|
favoriteDialog.currentGroup.count
|
||||||
</Button>
|
}}
|
||||||
</template>
|
/ {{ favoriteDialog.currentGroup.capacity }})
|
||||||
<template v-else>
|
</Button>
|
||||||
<Button
|
</template>
|
||||||
variant="outline"
|
<template v-else>
|
||||||
v-for="group in groups"
|
<Button
|
||||||
:key="group.key"
|
variant="outline"
|
||||||
style="width: 100%; white-space: initial"
|
v-for="group in groups"
|
||||||
class="my-1"
|
:key="group.key"
|
||||||
@click="addFavorite(group)">
|
style="width: 100%; white-space: initial"
|
||||||
{{ group.displayName }} ({{ group.count }} / {{ group.capacity }})
|
class="my-1"
|
||||||
</Button>
|
@click="addFavorite(group)">
|
||||||
</template>
|
{{ group.displayName }} ({{ group.count }} / {{ group.capacity }})
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
|
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
|
||||||
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
|
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
|
||||||
<template v-for="group in localWorldFavoriteGroups" :key="group">
|
<template v-for="group in localWorldFavoriteGroups" :key="group">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
|
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
|
||||||
style="width: 100%; white-space: initial"
|
style="width: 100%; white-space: initial"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
|
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
|
||||||
<Check />{{ group }} ({{ localWorldFavGroupLength(group) }})
|
<Check />{{ group }} ({{ localWorldFavGroupLength(group) }})
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-else
|
v-else
|
||||||
style="width: 100%; white-space: initial"
|
style="width: 100%; white-space: initial"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
|
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
|
||||||
{{ group }} ({{ localWorldFavGroupLength(group) }})
|
{{ group }} ({{ localWorldFavGroupLength(group) }})
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
|
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
|
||||||
<span style="text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
|
<span style="text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
|
||||||
<template v-for="group in localAvatarFavoriteGroups" :key="group">
|
<template v-for="group in localAvatarFavoriteGroups" :key="group">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
|
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
|
||||||
style="width: 100%; white-space: initial"
|
style="width: 100%; white-space: initial"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
||||||
<Check />{{ group }} ({{ localAvatarFavGroupLength(group) }})
|
<Check />{{ group }} ({{ localAvatarFavGroupLength(group) }})
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-else
|
v-else
|
||||||
style="width: 100%; white-space: initial"
|
style="width: 100%; white-space: initial"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
:disabled="!isLocalUserVrcPlusSupporter"
|
:disabled="!isLocalUserVrcPlusSupporter"
|
||||||
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
||||||
{{ group }} ({{ localAvatarFavGroupLength(group) }})
|
{{ group }} ({{ localAvatarFavGroupLength(group) }})
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Check } from 'lucide-vue-next';
|
import { Check } from 'lucide-vue-next';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
|||||||
@@ -4,51 +4,20 @@
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
|
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="custom-nav-dialog__list" v-if="localLayout.length">
|
<div class="custom-nav-dialog__list" v-if="localLayout.length">
|
||||||
<div
|
<div
|
||||||
v-for="(entry, index) in localLayout"
|
v-for="(entry, index) in localLayout"
|
||||||
:key="entry.key || entry.id"
|
:key="entry.key || entry.id"
|
||||||
:class="['custom-nav-entry', `custom-nav-entry--${entry.type}`]">
|
:class="['custom-nav-entry', `custom-nav-entry--${entry.type}`]">
|
||||||
<template v-if="entry.type === 'item'">
|
<template v-if="entry.type === 'item'">
|
||||||
<div class="custom-nav-entry__info">
|
|
||||||
<i :class="definitionsMap.get(entry.key)?.icon"></i>
|
|
||||||
<span>{{ t(definitionsMap.get(entry.key)?.labelKey || entry.key) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="custom-nav-entry__controls">
|
|
||||||
<div class="custom-nav-entry__move">
|
|
||||||
<Button
|
|
||||||
class="rounded-full w-6 h-6 text-xs"
|
|
||||||
size="icon-sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="index === 0"
|
|
||||||
@click="handleMoveEntry(index, -1)">
|
|
||||||
<i class="ri-arrow-up-line"></i>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
class="rounded-full w-6 h-6 text-xs"
|
|
||||||
size="icon-sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="index === localLayout.length - 1"
|
|
||||||
@click="handleMoveEntry(index, 1)">
|
|
||||||
<i class="ri-arrow-down-line"></i>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="custom-nav-entry__folder-header">
|
|
||||||
<div class="custom-nav-entry__info">
|
<div class="custom-nav-entry__info">
|
||||||
<i :class="entry.icon || defaultFolderIcon"></i>
|
<i :class="definitionsMap.get(entry.key)?.icon"></i>
|
||||||
<span>{{ entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder') }}</span>
|
<span>{{ t(definitionsMap.get(entry.key)?.labelKey || entry.key) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-nav-entry__actions">
|
<div class="custom-nav-entry__controls">
|
||||||
<Button size="icon-sm w-6 h-6 text-xs" variant="outline" @click="openFolderEditor(index)">
|
|
||||||
<i class="ri-edit-box-line"></i>
|
|
||||||
{{ t('nav_menu.custom_nav.edit_folder') }}
|
|
||||||
</Button>
|
|
||||||
<div class="custom-nav-entry__move">
|
<div class="custom-nav-entry__move">
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full text-xs w-6 h-6"
|
class="rounded-full w-6 h-6 text-xs"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:disabled="index === 0"
|
:disabled="index === 0"
|
||||||
@@ -56,7 +25,7 @@
|
|||||||
<i class="ri-arrow-up-line"></i>
|
<i class="ri-arrow-up-line"></i>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full text-xs w-6 h-6"
|
class="rounded-full w-6 h-6 text-xs"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:disabled="index === localLayout.length - 1"
|
:disabled="index === localLayout.length - 1"
|
||||||
@@ -65,54 +34,90 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="custom-nav-entry__folder-items">
|
<template v-else>
|
||||||
<template v-if="entry.items?.length">
|
<div class="custom-nav-entry__folder-header">
|
||||||
<Badge
|
<div class="custom-nav-entry__info">
|
||||||
v-for="key in entry.items"
|
<i :class="entry.icon || defaultFolderIcon"></i>
|
||||||
:key="`${entry.id}-${key}`"
|
<span>{{
|
||||||
variant="outline"
|
entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder')
|
||||||
class="custom-nav-entry__folder-tag">
|
}}</span>
|
||||||
{{ t(definitionsMap.get(key)?.labelKey || key) }}
|
</div>
|
||||||
</Badge>
|
<div class="custom-nav-entry__actions">
|
||||||
</template>
|
<Button
|
||||||
<span v-else class="custom-nav-entry__folder-empty">
|
size="icon-sm w-6 h-6 text-xs"
|
||||||
{{ t('nav_menu.custom_nav.folder_empty') }}
|
variant="outline"
|
||||||
</span>
|
@click="openFolderEditor(index)">
|
||||||
</div>
|
<i class="ri-edit-box-line"></i>
|
||||||
</template>
|
{{ t('nav_menu.custom_nav.edit_folder') }}
|
||||||
|
</Button>
|
||||||
|
<div class="custom-nav-entry__move">
|
||||||
|
<Button
|
||||||
|
class="rounded-full text-xs w-6 h-6"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="index === 0"
|
||||||
|
@click="handleMoveEntry(index, -1)">
|
||||||
|
<i class="ri-arrow-up-line"></i>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="rounded-full text-xs w-6 h-6"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="index === localLayout.length - 1"
|
||||||
|
@click="handleMoveEntry(index, 1)">
|
||||||
|
<i class="ri-arrow-down-line"></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-nav-entry__folder-items">
|
||||||
|
<template v-if="entry.items?.length">
|
||||||
|
<Badge
|
||||||
|
v-for="key in entry.items"
|
||||||
|
:key="`${entry.id}-${key}`"
|
||||||
|
variant="outline"
|
||||||
|
class="custom-nav-entry__folder-tag">
|
||||||
|
{{ t(definitionsMap.get(key)?.labelKey || key) }}
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
|
<span v-else class="custom-nav-entry__folder-empty">
|
||||||
|
{{ t('nav_menu.custom_nav.folder_empty') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- <el-alert
|
||||||
<el-alert
|
v-if="invalidFolders.length"
|
||||||
v-if="invalidFolders.length"
|
type="warning"
|
||||||
type="warning"
|
:closable="false"
|
||||||
:closable="false"
|
:title="t('nav_menu.custom_nav.invalid_folder')" /> -->
|
||||||
:title="t('nav_menu.custom_nav.invalid_folder')" />
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div class="custom-nav-dialog__footer">
|
<div class="custom-nav-dialog__footer">
|
||||||
<div class="custom-nav-dialog__footer-left">
|
<div class="custom-nav-dialog__footer-left">
|
||||||
<Button variant="outline" @click="openFolderEditor()">
|
<Button variant="outline" @click="openFolderEditor()">
|
||||||
{{ t('nav_menu.custom_nav.add_folder') }}
|
{{ t('nav_menu.custom_nav.add_folder') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" @click="handleReset">
|
<Button variant="outline" @click="handleReset">
|
||||||
{{ t('nav_menu.custom_nav.restore_default') }}
|
{{ t('nav_menu.custom_nav.restore_default') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-nav-dialog__footer-right">
|
||||||
|
<Button variant="secondary" @click="handleClose">
|
||||||
|
{{ t('nav_menu.custom_nav.cancel') }}
|
||||||
|
</Button>
|
||||||
|
<Button :disabled="isSaveDisabled" @click="handleSave">
|
||||||
|
{{ t('nav_menu.custom_nav.save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-nav-dialog__footer-right">
|
|
||||||
<Button variant="secondary" @click="handleClose">
|
|
||||||
{{ t('nav_menu.custom_nav.cancel') }}
|
|
||||||
</Button>
|
|
||||||
<Button :disabled="isSaveDisabled" @click="handleSave">
|
|
||||||
{{ t('nav_menu.custom_nav.save') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog v-model:open="folderEditor.visible">
|
<Dialog v-model:open="folderEditor.visible">
|
||||||
<DialogContent class="folder-editor-dialog">
|
<DialogContent class="folder-editor-dialog sm:max-w-[50vw]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{{
|
{{
|
||||||
@@ -122,113 +127,151 @@
|
|||||||
}}
|
}}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="folder-editor">
|
<div class="folder-editor">
|
||||||
<div class="folder-editor__form">
|
<div class="folder-editor__form">
|
||||||
<InputGroupField
|
<InputGroupField
|
||||||
v-model="folderEditor.data.name"
|
class="col-span-2"
|
||||||
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
|
v-model="folderEditor.data.name"
|
||||||
<IconPicker v-model="folderEditor.data.icon" class="folder-editor__icon-picker" />
|
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
|
||||||
</div>
|
<InputGroupField
|
||||||
<div class="folder-editor__lists">
|
class="col-span-2"
|
||||||
<div class="folder-editor__column">
|
v-model="folderEditor.data.icon"
|
||||||
<div class="folder-editor__column-title">
|
:placeholder="t('nav_menu.custom_nav.folder_icon_placeholder')">
|
||||||
{{ t('nav_menu.custom_nav.folder_available') }}
|
<template #trailing>
|
||||||
</div>
|
<HoverCard>
|
||||||
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
|
<HoverCardTrigger as-child>
|
||||||
{{ t('nav_menu.custom_nav.folder_empty') }}
|
<InputGroupButton
|
||||||
</div>
|
size="icon-xs"
|
||||||
<el-scrollbar v-else always class="folder-editor__scroll">
|
:aria-label="t('nav_menu.custom_nav.folder_icon_placeholder')">
|
||||||
<div v-for="item in folderEditorAvailableItems" :key="item.key" class="folder-editor__option">
|
<LinkIcon class="size-3.5" />
|
||||||
<label class="folder-editor__option-label">
|
</InputGroupButton>
|
||||||
<Checkbox
|
</HoverCardTrigger>
|
||||||
:model-value="folderEditor.data.items.includes(item.key)"
|
<HoverCardContent side="bottom" align="end" class="w-80">
|
||||||
@update:modelValue="(val) => toggleFolderItem(item.key, val)" />
|
<div class="text-sm leading-snug">
|
||||||
<span>
|
<div>
|
||||||
<i :class="item.icon"></i>
|
Find the icon you want on this site and paste its class name here, e.g.
|
||||||
{{ t(item.labelKey) }}
|
<span class="font-mono">ri-arrow-left-up-line</span>
|
||||||
</span>
|
</div>
|
||||||
</label>
|
<div class="mt-2">
|
||||||
</div>
|
<a
|
||||||
</el-scrollbar>
|
class="x-link"
|
||||||
|
@click.prevent="openExternalLink('https://remixicon.com/')">
|
||||||
|
https://remixicon.com/
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
</template>
|
||||||
|
</InputGroupField>
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-editor__column folder-editor__column--selected">
|
<div class="folder-editor__lists">
|
||||||
<div class="folder-editor__column-title">
|
<div class="folder-editor__column">
|
||||||
{{ t('nav_menu.custom_nav.folder_selected') }}
|
<div class="folder-editor__column-title">
|
||||||
</div>
|
{{ t('nav_menu.custom_nav.folder_available') }}
|
||||||
<div v-if="!folderEditor.data.items.length" class="folder-editor__empty">
|
|
||||||
{{ t('nav_menu.custom_nav.folder_selected_empty') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(key, index) in folderEditor.data.items"
|
|
||||||
:key="`selected-${key}`"
|
|
||||||
class="folder-editor__selected-item">
|
|
||||||
<div class="folder-editor__selected-label">
|
|
||||||
<i :class="definitionsMap.get(key)?.icon"></i>
|
|
||||||
<span>{{ t(definitionsMap.get(key)?.labelKey || key) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-editor__selected-actions">
|
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
|
||||||
<div class="custom-nav-entry__move">
|
{{ t('nav_menu.custom_nav.folder_empty') }}
|
||||||
<Button
|
</div>
|
||||||
class="rounded-full text-xs w-6 h-6"
|
<ScrollArea v-else type="always" class="folder-editor__scroll">
|
||||||
size="icon-sm"
|
<div
|
||||||
variant="outline"
|
v-for="item in folderEditorAvailableItems"
|
||||||
:disabled="index === 0"
|
:key="item.key"
|
||||||
@click="handleFolderItemMove(index, -1)">
|
class="folder-editor__option">
|
||||||
<i class="ri-arrow-up-line"></i>
|
<label class="folder-editor__option-label">
|
||||||
</Button>
|
<Checkbox
|
||||||
<Button
|
:model-value="folderEditor.data.items.includes(item.key)"
|
||||||
class="rounded-full text-xs w-6 h-6"
|
@update:modelValue="(val) => toggleFolderItem(item.key, val)" />
|
||||||
size="icon-sm"
|
<span>
|
||||||
variant="outline"
|
<i :class="item.icon"></i>
|
||||||
:disabled="index === folderEditor.data.items.length - 1"
|
{{ t(item.labelKey) }}
|
||||||
@click="handleFolderItemMove(index, 1)">
|
</span>
|
||||||
<i class="ri-arrow-down-line"></i>
|
</label>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
<div class="folder-editor__column folder-editor__column--selected">
|
||||||
|
<div class="folder-editor__column-title">
|
||||||
|
{{ t('nav_menu.custom_nav.folder_selected') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="!folderEditor.data.items.length" class="folder-editor__empty">
|
||||||
|
{{ t('nav_menu.custom_nav.folder_selected_empty') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(key, index) in folderEditor.data.items"
|
||||||
|
:key="`selected-${key}`"
|
||||||
|
class="folder-editor__selected-item">
|
||||||
|
<div class="folder-editor__selected-label">
|
||||||
|
<i :class="definitionsMap.get(key)?.icon"></i>
|
||||||
|
<span>{{ t(definitionsMap.get(key)?.labelKey || key) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-editor__selected-actions">
|
||||||
|
<div class="custom-nav-entry__move">
|
||||||
|
<Button
|
||||||
|
class="rounded-full text-xs w-6 h-6"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="index === 0"
|
||||||
|
@click="handleFolderItemMove(index, -1)">
|
||||||
|
<i class="ri-arrow-up-line"></i>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="rounded-full text-xs w-6 h-6"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="index === folderEditor.data.items.length - 1"
|
||||||
|
@click="handleFolderItemMove(index, 1)">
|
||||||
|
<i class="ri-arrow-down-line"></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" @click="toggleFolderItem(key, false)">
|
||||||
|
{{ t('nav_menu.custom_nav.remove_from_folder') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="outline" @click="toggleFolderItem(key, false)">
|
|
||||||
{{ t('nav_menu.custom_nav.remove_from_folder') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div class="folder-editor__footer">
|
<div class="folder-editor__footer">
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
v-if="folderEditor.isEditing"
|
v-if="folderEditor.isEditing"
|
||||||
:disabled="!canDeleteFolder"
|
:disabled="!canDeleteFolder"
|
||||||
@click="handleFolderEditorDelete">
|
@click="handleFolderEditorDelete">
|
||||||
{{ t('nav_menu.custom_nav.delete_folder') }}
|
{{ t('nav_menu.custom_nav.delete_folder') }}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="folder-editor__footer-spacer"></div>
|
<div class="folder-editor__footer-spacer"></div>
|
||||||
<Button variant="secondary" class="mr-2" @click="closeFolderEditor">
|
<Button variant="secondary" class="mr-2" @click="closeFolderEditor">
|
||||||
{{ t('nav_menu.custom_nav.cancel') }}
|
{{ t('nav_menu.custom_nav.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="folderEditorSaveDisabled" @click="handleFolderEditorSave">
|
<Button :disabled="folderEditorSaveDisabled" @click="handleFolderEditorSave">
|
||||||
{{ t('nav_menu.custom_nav.save') }}
|
{{ t('nav_menu.custom_nav.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Link as LinkIcon } from 'lucide-vue-next';
|
||||||
|
import { openExternalLink } from '@/shared/utils/common';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { InputGroupButton, InputGroupField } from '../ui/input-group';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Checkbox } from '../ui/checkbox';
|
import { Checkbox } from '../ui/checkbox';
|
||||||
import { InputGroupField } from '../ui/input-group';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import { navDefinitions } from '../../shared/constants/ui.js';
|
import { navDefinitions } from '../../shared/constants/ui.js';
|
||||||
|
|
||||||
import IconPicker from '../IconPicker.vue';
|
// import IconPicker from '../IconPicker.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -240,8 +283,7 @@
|
|||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
defaultFolderIcon: {
|
defaultFolderIcon: {
|
||||||
type: String,
|
type: String
|
||||||
default: 'ri-menu-fold-line'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,7 +300,7 @@
|
|||||||
type: 'folder',
|
type: 'folder',
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
icon: entry.icon || props.defaultFolderIcon,
|
icon: entry.icon,
|
||||||
items: Array.isArray(entry.items) ? [...entry.items] : []
|
items: Array.isArray(entry.items) ? [...entry.items] : []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -358,14 +400,14 @@
|
|||||||
folderEditor.data = {
|
folderEditor.data = {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
icon: entry.icon || props.defaultFolderIcon,
|
icon: entry.icon,
|
||||||
items: Array.isArray(entry.items) ? [...entry.items] : []
|
items: Array.isArray(entry.items) ? [...entry.items] : []
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
folderEditor.data = {
|
folderEditor.data = {
|
||||||
id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`,
|
id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`,
|
||||||
name: '',
|
name: '',
|
||||||
icon: props.defaultFolderIcon,
|
icon: '',
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -399,6 +441,7 @@
|
|||||||
|
|
||||||
const applyFolderChanges = () => {
|
const applyFolderChanges = () => {
|
||||||
const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key));
|
const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key));
|
||||||
|
const sanitizedIcon = folderEditor.data.icon?.trim() || '';
|
||||||
const entries = [...localLayout.value];
|
const entries = [...localLayout.value];
|
||||||
|
|
||||||
if (folderEditor.isEditing) {
|
if (folderEditor.isEditing) {
|
||||||
@@ -421,7 +464,7 @@
|
|||||||
type: 'folder',
|
type: 'folder',
|
||||||
id: folderEditor.data.id,
|
id: folderEditor.data.id,
|
||||||
name: folderEditor.data.name.trim(),
|
name: folderEditor.data.name.trim(),
|
||||||
icon: folderEditor.data.icon || props.defaultFolderIcon,
|
icon: sanitizedIcon,
|
||||||
items: sanitizedItems
|
items: sanitizedItems
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -442,7 +485,7 @@
|
|||||||
type: 'folder',
|
type: 'folder',
|
||||||
id: folderEditor.data.id,
|
id: folderEditor.data.id,
|
||||||
name: folderEditor.data.name.trim(),
|
name: folderEditor.data.name.trim(),
|
||||||
icon: folderEditor.data.icon || props.defaultFolderIcon,
|
icon: sanitizedIcon,
|
||||||
items: sanitizedItems
|
items: sanitizedItems
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,64 +6,64 @@
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
|
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
|
||||||
<span>{{ t('dialog.invite_to_group.description') }}</span>
|
<span>{{ t('dialog.invite_to_group.description') }}</span>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div style="margin-top: 15px; width: 100%">
|
<div style="margin-top: 15px; width: 100%">
|
||||||
<VirtualCombobox
|
<VirtualCombobox
|
||||||
v-model="inviteGroupDialog.groupId"
|
v-model="inviteGroupDialog.groupId"
|
||||||
:groups="groupPickerGroups"
|
:groups="groupPickerGroups"
|
||||||
:disabled="inviteGroupDialog.loading"
|
:disabled="inviteGroupDialog.loading"
|
||||||
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
|
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
|
||||||
:search-placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
|
:search-placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:deselect-on-reselect="true">
|
:deselect-on-reselect="true">
|
||||||
<template #item="{ item, selected }">
|
<template #item="{ item, selected }">
|
||||||
<div class="x-friend-item flex w-full items-center">
|
<div class="x-friend-item flex w-full items-center">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<img :src="item.iconUrl" loading="lazy" />
|
<img :src="item.iconUrl" loading="lazy" />
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name" v-text="item.label"></span>
|
|
||||||
</div>
|
|
||||||
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VirtualCombobox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="width: 100%; margin-top: 15px">
|
|
||||||
<VirtualCombobox
|
|
||||||
v-model="inviteGroupDialog.userIds"
|
|
||||||
:groups="friendPickerGroups"
|
|
||||||
multiple
|
|
||||||
:disabled="inviteGroupDialog.loading"
|
|
||||||
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
|
|
||||||
:search-placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
|
|
||||||
:clearable="true">
|
|
||||||
<template #item="{ item, selected }">
|
|
||||||
<div class="x-friend-item flex w-full items-center">
|
|
||||||
<template v-if="item.user">
|
|
||||||
<div class="avatar" :class="userStatusClass(item.user)">
|
|
||||||
<img :src="userImage(item.user)" 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 :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
||||||
<template v-else>
|
</div>
|
||||||
<span v-text="item.label"></span>
|
</template>
|
||||||
</template>
|
</VirtualCombobox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
<div style="width: 100%; margin-top: 15px">
|
||||||
</div>
|
<VirtualCombobox
|
||||||
</template>
|
v-model="inviteGroupDialog.userIds"
|
||||||
</VirtualCombobox>
|
:groups="friendPickerGroups"
|
||||||
</div>
|
multiple
|
||||||
|
:disabled="inviteGroupDialog.loading"
|
||||||
|
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
|
||||||
|
:search-placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
|
||||||
|
:clearable="true">
|
||||||
|
<template #item="{ item, selected }">
|
||||||
|
<div class="x-friend-item flex w-full items-center">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -80,9 +80,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { Check as CheckIcon } from 'lucide-vue-next';
|
import { Check as CheckIcon } from 'lucide-vue-next';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
|||||||
@@ -5,69 +5,69 @@
|
|||||||
<DialogTitle>{{ t('dialog.launch.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.launch.header') }}</DialogTitle>
|
||||||
<DialogDescription class="sr-only">{{ t('dialog.launch.header') }}</DialogDescription>
|
<DialogDescription class="sr-only">{{ t('dialog.launch.header') }}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup class="gap-4">
|
<FieldGroup class="gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
|
<FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
|
||||||
<FieldContent class="flex-row items-center gap-2">
|
<FieldContent class="flex-row items-center gap-2">
|
||||||
<InputGroupField
|
<InputGroupField
|
||||||
v-model="launchDialog.url"
|
v-model="launchDialog.url"
|
||||||
size="sm"
|
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"
|
class="rounded-full"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="copyInstanceMessage(launchDialog.url)"
|
@click="copyInstanceMessage(launchDialog.url)"
|
||||||
><Copy
|
><Copy
|
||||||
/></Button>
|
/></Button>
|
||||||
</TooltipWrapper>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field v-if="launchDialog.shortUrl">
|
|
||||||
<FieldLabel>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span>{{ t('dialog.launch.short_url') }}</span>
|
|
||||||
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
|
|
||||||
<AlertTriangle />
|
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
</span>
|
</FieldContent>
|
||||||
</FieldLabel>
|
</Field>
|
||||||
<FieldContent class="flex-row items-center gap-2">
|
<Field v-if="launchDialog.shortUrl">
|
||||||
<InputGroupField
|
<FieldLabel>
|
||||||
v-model="launchDialog.shortUrl"
|
<span class="flex items-center gap-1">
|
||||||
size="sm"
|
<span>{{ t('dialog.launch.short_url') }}</span>
|
||||||
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
|
||||||
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
|
<AlertTriangle />
|
||||||
<Button
|
</TooltipWrapper>
|
||||||
class="rounded-full"
|
</span>
|
||||||
size="icon-sm"
|
</FieldLabel>
|
||||||
variant="ghost"
|
<FieldContent class="flex-row items-center gap-2">
|
||||||
@click="copyInstanceMessage(launchDialog.shortUrl)"
|
<InputGroupField
|
||||||
><Copy
|
v-model="launchDialog.shortUrl"
|
||||||
/></Button>
|
size="sm"
|
||||||
</TooltipWrapper>
|
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||||
</FieldContent>
|
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
|
||||||
</Field>
|
<Button
|
||||||
<Field>
|
class="rounded-full"
|
||||||
<FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
|
size="icon-sm"
|
||||||
<FieldContent class="flex-row items-center gap-2">
|
variant="ghost"
|
||||||
<InputGroupField
|
@click="copyInstanceMessage(launchDialog.shortUrl)"
|
||||||
v-model="launchDialog.location"
|
><Copy
|
||||||
size="sm"
|
/></Button>
|
||||||
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
</TooltipWrapper>
|
||||||
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
|
</FieldContent>
|
||||||
<Button
|
</Field>
|
||||||
class="rounded-full"
|
<Field>
|
||||||
size="icon-sm"
|
<FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
|
||||||
variant="ghost"
|
<FieldContent class="flex-row items-center gap-2">
|
||||||
@click="copyInstanceMessage(launchDialog.location)"
|
<InputGroupField
|
||||||
><Copy
|
v-model="launchDialog.location"
|
||||||
/></Button>
|
size="sm"
|
||||||
</TooltipWrapper>
|
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||||
</FieldContent>
|
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
|
||||||
</Field>
|
<Button
|
||||||
</FieldGroup>
|
class="rounded-full"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="copyInstanceMessage(launchDialog.location)"
|
||||||
|
><Copy
|
||||||
|
/></Button>
|
||||||
|
</TooltipWrapper>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
class="mr-1.5"
|
class="mr-1.5"
|
||||||
@@ -129,8 +129,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import {
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -138,6 +144,7 @@
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { AlertTriangle, Copy, MoreHorizontal } from 'lucide-vue-next';
|
import { AlertTriangle, Copy, MoreHorizontal } from 'lucide-vue-next';
|
||||||
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';
|
||||||
|
|||||||
@@ -6,37 +6,37 @@
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div v-if="moderateGroupDialog.visible">
|
<div v-if="moderateGroupDialog.visible">
|
||||||
<div class="x-friend-item" style="cursor: default">
|
<div class="x-friend-item" style="cursor: default">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
|
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="detail">
|
||||||
|
<span
|
||||||
|
v-if="moderateGroupDialog.userObject.id"
|
||||||
|
class="name"
|
||||||
|
:style="{ color: moderateGroupDialog.userObject.$userColour }"
|
||||||
|
v-text="moderateGroupDialog.userObject.displayName"></span>
|
||||||
|
<span v-else v-text="moderateGroupDialog.userId"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail">
|
|
||||||
<span
|
|
||||||
v-if="moderateGroupDialog.userObject.id"
|
|
||||||
class="name"
|
|
||||||
:style="{ color: moderateGroupDialog.userObject.$userColour }"
|
|
||||||
v-text="moderateGroupDialog.userObject.displayName"></span>
|
|
||||||
<span v-else v-text="moderateGroupDialog.userId"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 15px; width: 100%">
|
<div style="margin-top: 15px; width: 100%">
|
||||||
<VirtualCombobox
|
<VirtualCombobox
|
||||||
:model-value="moderateGroupDialog.groupId"
|
:model-value="moderateGroupDialog.groupId"
|
||||||
@update:modelValue="setGroupId"
|
@update:modelValue="setGroupId"
|
||||||
:groups="groupPickerGroups"
|
:groups="groupPickerGroups"
|
||||||
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
|
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
|
||||||
:search-placeholder="t('dialog.moderate_group.choose_group_placeholder')"
|
:search-placeholder="t('dialog.moderate_group.choose_group_placeholder')"
|
||||||
:close-on-select="true">
|
:close-on-select="true">
|
||||||
<template #item="{ item, selected }">
|
<template #item="{ item, selected }">
|
||||||
<div class="flex w-full items-center gap-2">
|
<div class="flex w-full items-center gap-2">
|
||||||
<img :src="item.iconUrl" loading="lazy" class="size-5 rounded-sm" />
|
<img :src="item.iconUrl" loading="lazy" class="size-5 rounded-sm" />
|
||||||
<span class="truncate text-sm" v-text="item.label"></span>
|
<span class="truncate text-sm" v-text="item.label"></span>
|
||||||
<span v-if="selected" class="ml-auto opacity-70">✓</span>
|
<span v-if="selected" class="ml-auto opacity-70">✓</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VirtualCombobox>
|
</VirtualCombobox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -54,9 +54,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
|||||||
@@ -5,208 +5,429 @@
|
|||||||
<DialogTitle>{{ t('dialog.new_instance.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.new_instance.header') }}</DialogTitle>
|
||||||
<DialogDescription class="sr-only">{{ t('dialog.new_instance.header') }}</DialogDescription>
|
<DialogDescription class="sr-only">{{ t('dialog.new_instance.header') }}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<TabsUnderline
|
<TabsUnderline
|
||||||
v-model="newInstanceDialog.selectedTab"
|
v-model="newInstanceDialog.selectedTab"
|
||||||
:items="newInstanceTabs"
|
:items="newInstanceTabs"
|
||||||
:unmount-on-hide="false"
|
:unmount-on-hide="false"
|
||||||
@update:modelValue="newInstanceTabClick">
|
@update:modelValue="newInstanceTabClick">
|
||||||
<template #Normal>
|
<template #Normal>
|
||||||
<FieldGroup class="gap-4">
|
<FieldGroup class="gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
required
|
required
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="newInstanceDialog.accessType"
|
:model-value="newInstanceDialog.accessType"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(value) => {
|
(value) => {
|
||||||
newInstanceDialog.accessType = value;
|
newInstanceDialog.accessType = value;
|
||||||
buildInstance();
|
buildInstance();
|
||||||
}
|
}
|
||||||
">
|
">
|
||||||
<ToggleGroupItem value="public">{{
|
<ToggleGroupItem value="public">{{
|
||||||
t('dialog.new_instance.access_type_public')
|
t('dialog.new_instance.access_type_public')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="group">{{
|
<ToggleGroupItem value="group">{{
|
||||||
t('dialog.new_instance.access_type_group')
|
t('dialog.new_instance.access_type_group')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="friends+">{{
|
<ToggleGroupItem value="friends+">{{
|
||||||
t('dialog.new_instance.access_type_friend_plus')
|
t('dialog.new_instance.access_type_friend_plus')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="friends">{{
|
<ToggleGroupItem value="friends">{{
|
||||||
t('dialog.new_instance.access_type_friend')
|
t('dialog.new_instance.access_type_friend')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="invite+">{{
|
<ToggleGroupItem value="invite+">{{
|
||||||
t('dialog.new_instance.access_type_invite_plus')
|
t('dialog.new_instance.access_type_invite_plus')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="invite">{{
|
<ToggleGroupItem value="invite">{{
|
||||||
t('dialog.new_instance.access_type_invite')
|
t('dialog.new_instance.access_type_invite')
|
||||||
}}</ToggleGroupItem>
|
}}</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
<Field v-if="newInstanceDialog.accessType === 'group'">
|
<Field v-if="newInstanceDialog.accessType === 'group'">
|
||||||
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
|
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
required
|
required
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="newInstanceDialog.groupAccessType"
|
:model-value="newInstanceDialog.groupAccessType"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(value) => {
|
(value) => {
|
||||||
newInstanceDialog.groupAccessType = value;
|
newInstanceDialog.groupAccessType = value;
|
||||||
buildInstance();
|
buildInstance();
|
||||||
}
|
}
|
||||||
">
|
">
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="members"
|
value="members"
|
||||||
:disabled="
|
:disabled="
|
||||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
|
!hasGroupPermission(
|
||||||
"
|
newInstanceDialog.groupRef,
|
||||||
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
|
'group-instance-open-create'
|
||||||
>
|
)
|
||||||
<ToggleGroupItem
|
"
|
||||||
value="plus"
|
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
|
||||||
:disabled="
|
>
|
||||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
|
<ToggleGroupItem
|
||||||
"
|
value="plus"
|
||||||
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
|
:disabled="
|
||||||
>
|
!hasGroupPermission(
|
||||||
<ToggleGroupItem
|
newInstanceDialog.groupRef,
|
||||||
value="public"
|
'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(
|
!hasGroupPermission(
|
||||||
newInstanceDialog.groupRef,
|
newInstanceDialog.groupRef,
|
||||||
'group-instance-public-create'
|
'group-instance-age-gated-create'
|
||||||
) || newInstanceDialog.groupRef.privacy === 'private'
|
)
|
||||||
"
|
"
|
||||||
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
|
@update:modelValue="buildInstance" />
|
||||||
>
|
</FieldContent>
|
||||||
</ToggleGroup>
|
</Field>
|
||||||
</FieldContent>
|
<Field>
|
||||||
</Field>
|
<FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
|
||||||
<Field>
|
<FieldContent>
|
||||||
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
|
<InputGroupField
|
||||||
<FieldContent>
|
:disabled="!isLocalUserVrcPlusSupporter"
|
||||||
<ToggleGroup
|
v-model="newInstanceDialog.displayName"
|
||||||
type="single"
|
size="sm"
|
||||||
required
|
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
|
||||||
variant="outline"
|
@change="buildInstance" />
|
||||||
size="sm"
|
</FieldContent>
|
||||||
:model-value="newInstanceDialog.region"
|
</Field>
|
||||||
@update:model-value="
|
<Field v-if="newInstanceDialog.accessType === 'group'">
|
||||||
(value) => {
|
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
|
||||||
newInstanceDialog.region = value;
|
<FieldContent>
|
||||||
buildInstance();
|
<VirtualCombobox
|
||||||
}
|
v-model="newInstanceDialog.groupId"
|
||||||
">
|
:groups="normalGroupPickerGroups"
|
||||||
<ToggleGroupItem value="US West">{{
|
:placeholder="t('dialog.new_instance.group_placeholder')"
|
||||||
t('dialog.new_instance.region_usw')
|
:search-placeholder="t('dialog.new_instance.group_placeholder')"
|
||||||
}}</ToggleGroupItem>
|
:clearable="true"
|
||||||
<ToggleGroupItem value="US East">{{
|
:close-on-select="true"
|
||||||
t('dialog.new_instance.region_use')
|
:deselect-on-reselect="true"
|
||||||
}}</ToggleGroupItem>
|
@change="buildInstance">
|
||||||
<ToggleGroupItem value="Europe">{{
|
<template #item="{ item, selected }">
|
||||||
t('dialog.new_instance.region_eu')
|
<div class="x-friend-item flex w-full items-center">
|
||||||
}}</ToggleGroupItem>
|
<div class="avatar">
|
||||||
<ToggleGroupItem value="Japan">{{
|
<img :src="item.iconUrl" loading="lazy" />
|
||||||
t('dialog.new_instance.region_jp')
|
</div>
|
||||||
}}</ToggleGroupItem>
|
<div class="detail">
|
||||||
</ToggleGroup>
|
<span class="name" v-text="item.label"></span>
|
||||||
</FieldContent>
|
</div>
|
||||||
</Field>
|
<CheckIcon
|
||||||
<Field v-if="newInstanceDialog.accessType === 'group'">
|
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
||||||
<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="
|
|
||||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
|
|
||||||
"
|
|
||||||
@update:modelValue="buildInstance" />
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<InputGroupField
|
|
||||||
:disabled="!isLocalUserVrcPlusSupporter"
|
|
||||||
v-model="newInstanceDialog.displayName"
|
|
||||||
size="sm"
|
|
||||||
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
|
|
||||||
@change="buildInstance" />
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field v-if="newInstanceDialog.accessType === 'group'">
|
|
||||||
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<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>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
v-if="
|
||||||
|
newInstanceDialog.accessType === 'group' &&
|
||||||
|
newInstanceDialog.groupAccessType === 'members'
|
||||||
|
"
|
||||||
|
class="items-start">
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
:model-value="
|
||||||
|
Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []
|
||||||
|
"
|
||||||
|
@update:modelValue="handleRoleIdsChange">
|
||||||
|
<SelectTrigger size="sm" class="w-full">
|
||||||
|
<SelectValue>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
|
||||||
|
</span>
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem
|
||||||
|
v-for="role in newInstanceDialog.selectedGroupRoles"
|
||||||
|
:key="role.id"
|
||||||
|
:value="role.id">
|
||||||
|
{{ role.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<template v-if="newInstanceDialog.instanceCreated">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<InputGroupField
|
||||||
|
v-model="newInstanceDialog.location"
|
||||||
|
size="sm"
|
||||||
|
readonly
|
||||||
|
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</FieldGroup>
|
||||||
|
</template>
|
||||||
|
<template #Legacy>
|
||||||
|
<FieldGroup class="gap-4">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
required
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:model-value="newInstanceDialog.accessType"
|
||||||
|
@update:model-value="
|
||||||
|
(value) => {
|
||||||
|
newInstanceDialog.accessType = value;
|
||||||
|
buildLegacyInstance();
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<ToggleGroupItem value="public">{{
|
||||||
|
t('dialog.new_instance.access_type_public')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="group">{{
|
||||||
|
t('dialog.new_instance.access_type_group')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="friends+">{{
|
||||||
|
t('dialog.new_instance.access_type_friend_plus')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="friends">{{
|
||||||
|
t('dialog.new_instance.access_type_friend')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="invite+">{{
|
||||||
|
t('dialog.new_instance.access_type_invite_plus')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="invite">{{
|
||||||
|
t('dialog.new_instance.access_type_invite')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<Field v-if="newInstanceDialog.accessType === 'group'">
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
required
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:model-value="newInstanceDialog.groupAccessType"
|
||||||
|
@update:model-value="
|
||||||
|
(value) => {
|
||||||
|
newInstanceDialog.groupAccessType = value;
|
||||||
|
buildLegacyInstance();
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<ToggleGroupItem value="members">{{
|
||||||
|
t('dialog.new_instance.group_access_type_members')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="plus">{{
|
||||||
|
t('dialog.new_instance.group_access_type_plus')
|
||||||
|
}}</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="public">{{
|
||||||
|
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;
|
||||||
|
buildLegacyInstance();
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<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.ageGate') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
|
||||||
|
</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="
|
||||||
|
newInstanceDialog.selectedTab === 'Legacy' &&
|
||||||
|
newInstanceDialog.accessType !== 'public' &&
|
||||||
|
newInstanceDialog.accessType !== 'group'
|
||||||
|
"
|
||||||
|
class="items-start">
|
||||||
|
<FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<VirtualCombobox
|
||||||
|
v-model="newInstanceDialog.userId"
|
||||||
|
:groups="creatorPickerGroups"
|
||||||
|
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
|
||||||
|
:search-placeholder="t('dialog.new_instance.instance_creator_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">
|
||||||
|
<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>
|
</div>
|
||||||
<CheckIcon
|
</template>
|
||||||
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
</VirtualCombobox>
|
||||||
</div>
|
</FieldContent>
|
||||||
</template>
|
</Field>
|
||||||
</VirtualCombobox>
|
<Field v-if="newInstanceDialog.accessType === 'group'">
|
||||||
</FieldContent>
|
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
|
||||||
</Field>
|
<FieldContent>
|
||||||
<Field
|
<VirtualCombobox
|
||||||
v-if="
|
v-model="newInstanceDialog.groupId"
|
||||||
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'
|
:groups="legacyGroupPickerGroups"
|
||||||
"
|
:placeholder="t('dialog.new_instance.group_placeholder')"
|
||||||
class="items-start">
|
:search-placeholder="t('dialog.new_instance.group_placeholder')"
|
||||||
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
|
:clearable="true"
|
||||||
<FieldContent>
|
:close-on-select="true"
|
||||||
<Select
|
:deselect-on-reselect="true"
|
||||||
multiple
|
@change="buildLegacyInstance">
|
||||||
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []"
|
<template #item="{ item, selected }">
|
||||||
@update:modelValue="handleRoleIdsChange">
|
<div class="x-friend-item flex w-full items-center">
|
||||||
<SelectTrigger size="sm" class="w-full">
|
<div class="avatar">
|
||||||
<SelectValue>
|
<img :src="item.iconUrl" loading="lazy" />
|
||||||
<span class="truncate">
|
</div>
|
||||||
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
|
<div class="detail">
|
||||||
</span>
|
<span class="name" v-text="item.label"></span>
|
||||||
</SelectValue>
|
</div>
|
||||||
</SelectTrigger>
|
<CheckIcon
|
||||||
<SelectContent>
|
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
||||||
<SelectGroup>
|
</div>
|
||||||
<SelectItem
|
</template>
|
||||||
v-for="role in newInstanceDialog.selectedGroupRoles"
|
</VirtualCombobox>
|
||||||
:key="role.id"
|
</FieldContent>
|
||||||
:value="role.id">
|
</Field>
|
||||||
{{ role.name }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<template v-if="newInstanceDialog.instanceCreated">
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
|
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
@@ -223,220 +444,49 @@
|
|||||||
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
|
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
</template>
|
</FieldGroup>
|
||||||
</FieldGroup>
|
</template>
|
||||||
</template>
|
</TabsUnderline>
|
||||||
<template #Legacy>
|
<DialogFooter v-if="newInstanceDialog.selectedTab === 'Normal'">
|
||||||
<FieldGroup class="gap-4">
|
<template v-if="newInstanceDialog.instanceCreated">
|
||||||
<Field>
|
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
||||||
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
t('dialog.new_instance.copy_url')
|
||||||
<FieldContent>
|
}}</Button>
|
||||||
<ToggleGroup
|
<Button variant="outline" class="mr-2" @click="selfInvite(newInstanceDialog.location)">{{
|
||||||
type="single"
|
t('dialog.new_instance.self_invite')
|
||||||
required
|
}}</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
:model-value="newInstanceDialog.accessType"
|
class="mr-2"
|
||||||
@update:model-value="
|
:disabled="
|
||||||
(value) => {
|
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
||||||
newInstanceDialog.accessType = value;
|
newInstanceDialog.userId !== currentUser.id
|
||||||
buildLegacyInstance();
|
|
||||||
}
|
|
||||||
">
|
|
||||||
<ToggleGroupItem value="public">{{
|
|
||||||
t('dialog.new_instance.access_type_public')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="group">{{
|
|
||||||
t('dialog.new_instance.access_type_group')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="friends+">{{
|
|
||||||
t('dialog.new_instance.access_type_friend_plus')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="friends">{{
|
|
||||||
t('dialog.new_instance.access_type_friend')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="invite+">{{
|
|
||||||
t('dialog.new_instance.access_type_invite_plus')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="invite">{{
|
|
||||||
t('dialog.new_instance.access_type_invite')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
</ToggleGroup>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field v-if="newInstanceDialog.accessType === 'group'">
|
|
||||||
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<ToggleGroup
|
|
||||||
type="single"
|
|
||||||
required
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
:model-value="newInstanceDialog.groupAccessType"
|
|
||||||
@update:model-value="
|
|
||||||
(value) => {
|
|
||||||
newInstanceDialog.groupAccessType = value;
|
|
||||||
buildLegacyInstance();
|
|
||||||
}
|
|
||||||
">
|
|
||||||
<ToggleGroupItem value="members">{{
|
|
||||||
t('dialog.new_instance.group_access_type_members')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="plus">{{
|
|
||||||
t('dialog.new_instance.group_access_type_plus')
|
|
||||||
}}</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem value="public">{{
|
|
||||||
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;
|
|
||||||
buildLegacyInstance();
|
|
||||||
}
|
|
||||||
">
|
|
||||||
<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.ageGate') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
|
|
||||||
</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="
|
|
||||||
newInstanceDialog.selectedTab === 'Legacy' &&
|
|
||||||
newInstanceDialog.accessType !== 'public' &&
|
|
||||||
newInstanceDialog.accessType !== 'group'
|
|
||||||
"
|
"
|
||||||
class="items-start">
|
@click="showInviteDialog(newInstanceDialog.location)"
|
||||||
<FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
|
>{{ t('dialog.new_instance.invite') }}</Button
|
||||||
<FieldContent>
|
>
|
||||||
<VirtualCombobox
|
<template v-if="canOpenInstanceInGame">
|
||||||
v-model="newInstanceDialog.userId"
|
<Button
|
||||||
:groups="creatorPickerGroups"
|
variant="secondary"
|
||||||
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
|
class="mr-2"
|
||||||
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
|
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
|
||||||
:clearable="true"
|
>{{ t('dialog.new_instance.launch') }}</Button
|
||||||
:close-on-select="true"
|
>
|
||||||
:deselect-on-reselect="true"
|
<Button @click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
|
||||||
@change="buildLegacyInstance">
|
{{ t('dialog.new_instance.open_ingame') }}
|
||||||
<template #item="{ item, selected }">
|
</Button>
|
||||||
<div class="x-friend-item flex w-full items-center">
|
</template>
|
||||||
<template v-if="item.user">
|
<template v-else>
|
||||||
<div class="avatar" :class="userStatusClass(item.user)">
|
<Button @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)">{{
|
||||||
<img :src="userImage(item.user)" loading="lazy" />
|
t('dialog.new_instance.launch')
|
||||||
</div>
|
}}</Button>
|
||||||
<div class="detail">
|
</template>
|
||||||
<span
|
</template>
|
||||||
class="name"
|
<template v-else>
|
||||||
:style="{ color: item.user.$userColour }"
|
<Button @click="handleCreateNewInstance">{{ t('dialog.new_instance.create_instance') }}</Button>
|
||||||
v-text="item.user.displayName"></span>
|
</template>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</template>
|
<DialogFooter v-else-if="newInstanceDialog.selectedTab === 'Legacy'">
|
||||||
<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 class="detail">
|
|
||||||
<span class="name" v-text="item.label"></span>
|
|
||||||
</div>
|
|
||||||
<CheckIcon
|
|
||||||
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VirtualCombobox>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<InputGroupField
|
|
||||||
v-model="newInstanceDialog.location"
|
|
||||||
size="sm"
|
|
||||||
readonly
|
|
||||||
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</template>
|
|
||||||
</TabsUnderline>
|
|
||||||
<DialogFooter v-if="newInstanceDialog.selectedTab === 'Normal'">
|
|
||||||
<template v-if="newInstanceDialog.instanceCreated">
|
|
||||||
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
||||||
t('dialog.new_instance.copy_url')
|
t('dialog.new_instance.copy_url')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
@@ -445,7 +495,6 @@
|
|||||||
}}</Button>
|
}}</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="mr-2"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
||||||
newInstanceDialog.userId !== currentUser.id
|
newInstanceDialog.userId !== currentUser.id
|
||||||
@@ -469,44 +518,7 @@
|
|||||||
t('dialog.new_instance.launch')
|
t('dialog.new_instance.launch')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</DialogFooter>
|
||||||
<template v-else>
|
|
||||||
<Button @click="handleCreateNewInstance">{{ t('dialog.new_instance.create_instance') }}</Button>
|
|
||||||
</template>
|
|
||||||
</DialogFooter>
|
|
||||||
<DialogFooter v-else-if="newInstanceDialog.selectedTab === 'Legacy'">
|
|
||||||
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
|
||||||
t('dialog.new_instance.copy_url')
|
|
||||||
}}</Button>
|
|
||||||
<Button variant="outline" class="mr-2" @click="selfInvite(newInstanceDialog.location)">{{
|
|
||||||
t('dialog.new_instance.self_invite')
|
|
||||||
}}</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
:disabled="
|
|
||||||
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
|
||||||
newInstanceDialog.userId !== currentUser.id
|
|
||||||
"
|
|
||||||
@click="showInviteDialog(newInstanceDialog.location)"
|
|
||||||
>{{ t('dialog.new_instance.invite') }}</Button
|
|
||||||
>
|
|
||||||
<template v-if="canOpenInstanceInGame">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
class="mr-2"
|
|
||||||
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
|
|
||||||
>{{ t('dialog.new_instance.launch') }}</Button
|
|
||||||
>
|
|
||||||
<Button @click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
|
|
||||||
{{ t('dialog.new_instance.open_ingame') }}
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<Button @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)">{{
|
|
||||||
t('dialog.new_instance.launch')
|
|
||||||
}}</Button>
|
|
||||||
</template>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
|
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
|
||||||
@@ -514,8 +526,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Check as CheckIcon } from 'lucide-vue-next';
|
import { Check as CheckIcon } from 'lucide-vue-next';
|
||||||
|
|||||||
@@ -4,79 +4,79 @@
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ t('dialog.boop_dialog.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.boop_dialog.header') }}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<span>{{ displayName }}</span>
|
<span>{{ displayName }}</span>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div v-if="sendBoopDialog.visible" style="width: 100%">
|
<div v-if="sendBoopDialog.visible" style="width: 100%">
|
||||||
<VirtualCombobox
|
<VirtualCombobox
|
||||||
v-model="emojiModel"
|
v-model="emojiModel"
|
||||||
:groups="emojiPickerGroups"
|
:groups="emojiPickerGroups"
|
||||||
:placeholder="t('dialog.boop_dialog.select_default_emoji')"
|
:placeholder="t('dialog.boop_dialog.select_default_emoji')"
|
||||||
:search-placeholder="t('dialog.boop_dialog.select_default_emoji')"
|
:search-placeholder="t('dialog.boop_dialog.select_default_emoji')"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:deselect-on-reselect="true">
|
:deselect-on-reselect="true">
|
||||||
<template #item="{ item, selected }">
|
<template #item="{ item, selected }">
|
||||||
<span v-text="item.label"></span>
|
<span v-text="item.label"></span>
|
||||||
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
|
||||||
</template>
|
</template>
|
||||||
</VirtualCombobox>
|
</VirtualCombobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isLocalUserVrcPlusSupporter"
|
|
||||||
style="
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
margin-top: 10px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
">
|
|
||||||
<div
|
<div
|
||||||
v-for="image in emojiTable"
|
v-if="isLocalUserVrcPlusSupporter"
|
||||||
:key="image.id"
|
style="
|
||||||
:class="image.id === fileId ? 'x-image-selected' : ''"
|
display: grid;
|
||||||
style="cursor: pointer; border: 1px solid transparent; border-radius: 8px"
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
@click="fileId = image.id">
|
gap: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-for="image in emojiTable"
|
||||||
image.versions &&
|
:key="image.id"
|
||||||
image.versions.length > 0 &&
|
:class="image.id === fileId ? 'x-image-selected' : ''"
|
||||||
image.versions[image.versions.length - 1].file.url
|
style="cursor: pointer; border: 1px solid transparent; border-radius: 8px"
|
||||||
"
|
@click="fileId = image.id">
|
||||||
class="x-popover-image"
|
<div
|
||||||
style="padding: 8px">
|
v-if="
|
||||||
<Emoji :imageUrl="image.versions[image.versions.length - 1].file.url" :size="100"></Emoji>
|
image.versions &&
|
||||||
|
image.versions.length > 0 &&
|
||||||
|
image.versions[image.versions.length - 1].file.url
|
||||||
|
"
|
||||||
|
class="x-popover-image"
|
||||||
|
style="padding: 8px">
|
||||||
|
<Emoji :imageUrl="image.versions[image.versions.length - 1].file.url" :size="100"></Emoji>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button size="sm" variant="outline" class="mr-2" @click="showGalleryPage">{{
|
<Button size="sm" variant="outline" class="mr-2" @click="showGalleryPage">{{
|
||||||
t('dialog.boop_dialog.emoji_manager')
|
t('dialog.boop_dialog.emoji_manager')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
<Button size="sm" variant="secondary" class="mr-2" @click="closeDialog">{{
|
<Button size="sm" variant="secondary" class="mr-2" @click="closeDialog">{{
|
||||||
t('dialog.boop_dialog.cancel')
|
t('dialog.boop_dialog.cancel')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
<Button size="sm" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
|
<Button size="sm" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
|
||||||
t('dialog.boop_dialog.send')
|
t('dialog.boop_dialog.send')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Check as CheckIcon } from 'lucide-vue-next';
|
import { Check as CheckIcon } from 'lucide-vue-next';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<Location
|
<Location
|
||||||
:location="userDialog.ref.location"
|
:location="userDialog.ref.location"
|
||||||
:traveling="userDialog.ref.travelingToLocation"
|
:traveling="userDialog.ref.travelingToLocation" />
|
||||||
style="display: block; margin-top: 5px" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="x-friend-list" style="flex: 1; margin-top: 10px; max-height: 150px">
|
<div class="x-friend-list" style="flex: 1; margin-top: 10px; max-height: 150px">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,86 +5,86 @@
|
|||||||
<DialogTitle>{{ t('dialog.vrcx_updater.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.vrcx_updater.header') }}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
|
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
|
||||||
<template v-if="updateInProgress">
|
<template v-if="updateInProgress">
|
||||||
<Progress :model-value="updateProgress" class="w-full" />
|
<Progress :model-value="updateProgress" class="w-full" />
|
||||||
<div class="mt-2 text-xs" v-text="updateProgressText()"></div>
|
<div class="mt-2 text-xs" v-text="updateProgressText()"></div>
|
||||||
<br />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="VRCXUpdateDialog.updatePending" style="margin-bottom: 15px">
|
|
||||||
<span>{{ pendingVRCXInstall }}</span>
|
|
||||||
<br />
|
<br />
|
||||||
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
|
</template>
|
||||||
</div>
|
<template v-else>
|
||||||
<Tabs :model-value="branch" class="w-full" @update:modelValue="handleBranchChange">
|
<div v-if="VRCXUpdateDialog.updatePending" style="margin-bottom: 15px">
|
||||||
<TabsList class="grid w-full grid-cols-2">
|
<span>{{ pendingVRCXInstall }}</span>
|
||||||
<TabsTrigger value="Stable">{{ t('dialog.vrcx_updater.branch_stable') }}</TabsTrigger>
|
<br />
|
||||||
<TabsTrigger value="Nightly">{{ t('dialog.vrcx_updater.branch_nightly') }}</TabsTrigger>
|
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
|
||||||
</TabsList>
|
</div>
|
||||||
<TabsContent value="Nightly">
|
<Tabs :model-value="branch" class="w-full" @update:modelValue="handleBranchChange">
|
||||||
<Alert variant="destructive">
|
<TabsList class="grid w-full grid-cols-2">
|
||||||
<AlertCircle class="text-muted-foreground" />
|
<TabsTrigger value="Stable">{{ t('dialog.vrcx_updater.branch_stable') }}</TabsTrigger>
|
||||||
<AlertTitle>{{ t('dialog.vrcx_updater.nightly_title') }}</AlertTitle>
|
<TabsTrigger value="Nightly">{{ t('dialog.vrcx_updater.branch_nightly') }}</TabsTrigger>
|
||||||
<AlertDescription>
|
</TabsList>
|
||||||
{{ t('dialog.vrcx_updater.nightly_notice') }}
|
<TabsContent value="Nightly">
|
||||||
</AlertDescription>
|
<Alert variant="destructive">
|
||||||
</Alert>
|
<AlertCircle class="text-muted-foreground" />
|
||||||
</TabsContent>
|
<AlertTitle>{{ t('dialog.vrcx_updater.nightly_title') }}</AlertTitle>
|
||||||
</Tabs>
|
<AlertDescription>
|
||||||
<FieldGroup class="mt-3">
|
{{ t('dialog.vrcx_updater.nightly_notice') }}
|
||||||
<Field>
|
</AlertDescription>
|
||||||
<FieldLabel>{{ t('dialog.vrcx_updater.release') }}</FieldLabel>
|
</Alert>
|
||||||
<FieldContent>
|
</TabsContent>
|
||||||
<Select
|
</Tabs>
|
||||||
:model-value="VRCXUpdateDialog.release"
|
<FieldGroup class="mt-3">
|
||||||
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
|
<Field>
|
||||||
<SelectTrigger class="w-full">
|
<FieldLabel>{{ t('dialog.vrcx_updater.release') }}</FieldLabel>
|
||||||
<SelectValue />
|
<FieldContent>
|
||||||
</SelectTrigger>
|
<Select
|
||||||
<SelectContent>
|
:model-value="VRCXUpdateDialog.release"
|
||||||
<SelectItem
|
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
|
||||||
v-for="item in VRCXUpdateDialog.releases"
|
<SelectTrigger class="w-full">
|
||||||
:key="item.name"
|
<SelectValue />
|
||||||
:value="item.name">
|
</SelectTrigger>
|
||||||
{{ item.tag_name }}
|
<SelectContent>
|
||||||
</SelectItem>
|
<SelectItem
|
||||||
</SelectContent>
|
v-for="item in VRCXUpdateDialog.releases"
|
||||||
</Select>
|
:key="item.name"
|
||||||
</FieldContent>
|
:value="item.name">
|
||||||
</Field>
|
{{ item.tag_name }}
|
||||||
</FieldGroup>
|
</SelectItem>
|
||||||
<div
|
</SelectContent>
|
||||||
v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion"
|
</Select>
|
||||||
class="mt-3 text-xs text-muted-foreground">
|
</FieldContent>
|
||||||
<span>{{ t('dialog.vrcx_updater.latest_version') }}</span>
|
</Field>
|
||||||
</div>
|
</FieldGroup>
|
||||||
</template>
|
<div
|
||||||
|
v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion"
|
||||||
|
class="mt-3 text-xs text-muted-foreground">
|
||||||
|
<span>{{ t('dialog.vrcx_updater.latest_version') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" class="mr-2" v-if="updateInProgress" @click="cancelUpdate">
|
<Button variant="secondary" class="mr-2" v-if="updateInProgress" @click="cancelUpdate">
|
||||||
{{ t('dialog.vrcx_updater.cancel') }}
|
{{ t('dialog.vrcx_updater.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
v-if="VRCXUpdateDialog.release !== pendingVRCXInstall"
|
v-if="VRCXUpdateDialog.release !== pendingVRCXInstall"
|
||||||
:disabled="updateInProgress"
|
:disabled="updateInProgress"
|
||||||
@click="installVRCXUpdate">
|
@click="installVRCXUpdate">
|
||||||
{{ t('dialog.vrcx_updater.download') }}
|
{{ t('dialog.vrcx_updater.download') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" v-if="!updateInProgress && pendingVRCXInstall" @click="restartVRCX(true)">
|
<Button variant="default" v-if="!updateInProgress && pendingVRCXInstall" @click="restartVRCX(true)">
|
||||||
{{ t('dialog.vrcx_updater.install') }}
|
{{ t('dialog.vrcx_updater.install') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { AlertCircle } from 'lucide-vue-next';
|
import { AlertCircle } from 'lucide-vue-next';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
|||||||
@@ -56,13 +56,13 @@
|
|||||||
<AlertDialogPortal :to="portalTo">
|
<AlertDialogPortal :to="portalTo">
|
||||||
<AlertDialogOverlay
|
<AlertDialogOverlay
|
||||||
data-slot="alert-dialog-overlay"
|
data-slot="alert-dialog-overlay"
|
||||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" />
|
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-11000 bg-black/80" />
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
data-slot="alert-dialog-content"
|
data-slot="alert-dialog-content"
|
||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-11000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-12000 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-12000 min-w-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwardedProps }"
|
v-bind="{ ...$attrs, ...forwardedProps }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-12000 w-64 rounded-md border p-4 shadow-md outline-hidden',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-12000 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
|
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui';
|
||||||
|
import { ref, useAttrs } from 'vue';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
import ScrollBar from './ScrollBar.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: { type: null, required: false },
|
||||||
|
dir: { type: null, required: false },
|
||||||
|
scrollHideDelay: { type: Number, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const viewportEl = ref(null);
|
||||||
|
|
||||||
|
function setViewportEl(el) {
|
||||||
|
viewportEl.value = el?.$el ?? el ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ viewportEl, update: () => {} });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaRoot data-slot="scroll-area" v-bind="delegatedProps" :class="cn('relative', props.class)">
|
||||||
|
<ScrollAreaViewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
:ref="setViewportEl"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1">
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaCorner />
|
||||||
|
</ScrollAreaRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
orientation: { type: String, required: false, default: "vertical" },
|
||||||
|
forceMount: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex touch-none p-px transition-colors select-none',
|
||||||
|
orientation === 'vertical' &&
|
||||||
|
'h-full w-2.5 border-l border-l-transparent',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'h-2.5 flex-col border-t border-t-transparent',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
class="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ScrollArea } from "./ScrollArea.vue";
|
||||||
|
export { default as ScrollBar } from "./ScrollBar.vue";
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-12000 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
position === 'popper' &&
|
position === 'popper' &&
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
props.class
|
props.class
|
||||||
|
|||||||
@@ -20,17 +20,20 @@
|
|||||||
ariaLabel: { type: String, default: '' },
|
ariaLabel: { type: String, default: '' },
|
||||||
|
|
||||||
variant: { type: String, default: 'fit' },
|
variant: { type: String, default: 'fit' },
|
||||||
unmountOnHide: { type: Boolean, default: false }
|
unmountOnHide: { type: Boolean, default: false },
|
||||||
|
fill: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide } = toRefs(props);
|
const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide, fill } = toRefs(props);
|
||||||
|
|
||||||
|
const itemsList = computed(() => (Array.isArray(items.value) ? items.value : []));
|
||||||
|
|
||||||
const resolvedDefault = computed(() => {
|
const resolvedDefault = computed(() => {
|
||||||
return defaultValue.value ?? items.value?.[0]?.value;
|
return defaultValue.value ?? itemsList.value?.[0]?.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isValueValid = (value) => items.value?.some((item) => item?.value === value);
|
const isValueValid = (value) => itemsList.value.some((item) => item?.value === value);
|
||||||
|
|
||||||
const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value);
|
const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value);
|
||||||
|
|
||||||
@@ -40,12 +43,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([items, defaultValue], () => {
|
watch([itemsList, defaultValue], () => {
|
||||||
if (!isValueValid(innerValue.value)) {
|
if (!isValueValid(innerValue.value)) {
|
||||||
innerValue.value = resolvedDefault.value;
|
innerValue.value = resolvedDefault.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isValueValid(modelValue.value)) {
|
|
||||||
|
if (modelValue.value !== undefined && modelValue.value !== null && !isValueValid(modelValue.value)) {
|
||||||
innerValue.value = resolvedDefault.value;
|
innerValue.value = resolvedDefault.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
<TabsRoot
|
<TabsRoot
|
||||||
:model-value="innerValue"
|
:model-value="innerValue"
|
||||||
:default-value="resolvedDefault"
|
:default-value="resolvedDefault"
|
||||||
class="w-full"
|
:class="['w-full', fill ? 'flex min-h-0 flex-col' : '']"
|
||||||
:unmount-on-hide="unmountOnHide"
|
:unmount-on-hide="unmountOnHide"
|
||||||
@update:modelValue="onValueChange">
|
@update:modelValue="onValueChange">
|
||||||
<TabsList :class="listClass" :aria-label="ariaLabel || undefined">
|
<TabsList :class="listClass" :aria-label="ariaLabel || undefined">
|
||||||
@@ -89,7 +93,7 @@
|
|||||||
</TabsIndicator>
|
</TabsIndicator>
|
||||||
|
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
v-for="it in items"
|
v-for="it in itemsList"
|
||||||
:key="it.value"
|
:key="it.value"
|
||||||
:value="it.value"
|
:value="it.value"
|
||||||
:disabled="it.disabled"
|
:disabled="it.disabled"
|
||||||
@@ -99,10 +103,13 @@
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
v-for="it in items"
|
v-for="it in itemsList"
|
||||||
:key="it.value"
|
:key="it.value"
|
||||||
:value="it.value"
|
:value="it.value"
|
||||||
class="pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background">
|
:class="[
|
||||||
|
'pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background',
|
||||||
|
fill ? 'min-h-0 flex-1' : ''
|
||||||
|
]">
|
||||||
<slot :name="it.value" />
|
<slot :name="it.value" />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</TabsRoot>
|
</TabsRoot>
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui';
|
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { reactiveOmit } from '@vueuse/core';
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false
|
inheritAttrs: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
forceMount: { type: Boolean, required: false },
|
forceMount: { type: Boolean, required: false },
|
||||||
ariaLabel: { type: String, required: false },
|
ariaLabel: { type: String, required: false },
|
||||||
asChild: { type: Boolean, required: false },
|
asChild: { type: Boolean, required: false },
|
||||||
as: { type: null, required: false },
|
as: { type: null, required: false },
|
||||||
side: { type: null, required: false },
|
side: { type: null, required: false },
|
||||||
sideOffset: { type: Number, required: false, default: 4 },
|
sideOffset: { type: Number, required: false, default: 4 },
|
||||||
align: { type: null, required: false },
|
align: { type: null, required: false },
|
||||||
alignOffset: { type: Number, required: false },
|
alignOffset: { type: Number, required: false },
|
||||||
avoidCollisions: { type: Boolean, required: false },
|
avoidCollisions: { type: Boolean, required: false },
|
||||||
collisionBoundary: { type: null, required: false },
|
collisionBoundary: { type: null, required: false },
|
||||||
collisionPadding: { type: [Number, Object], required: false },
|
collisionPadding: { type: [Number, Object], required: false },
|
||||||
arrowPadding: { type: Number, required: false },
|
arrowPadding: { type: Number, required: false },
|
||||||
sticky: { type: String, required: false },
|
sticky: { type: String, required: false },
|
||||||
hideWhenDetached: { type: Boolean, required: false },
|
hideWhenDetached: { type: Boolean, required: false },
|
||||||
positionStrategy: { type: String, required: false },
|
positionStrategy: { type: String, required: false },
|
||||||
updatePositionStrategy: { type: String, required: false },
|
updatePositionStrategy: { type: String, required: false },
|
||||||
class: { type: null, required: false }
|
class: { type: null, required: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside']);
|
const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside']);
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, 'class');
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -40,14 +40,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
v-bind="{ ...forwarded, ...$attrs }"
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
|
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-12000 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<TooltipArrow
|
<TooltipArrow
|
||||||
class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
|
class="bg-foreground fill-foreground z-12000 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const MODAL_PORTAL_ROOT_ID = 'vrcx-modal-portal-root';
|
const MODAL_PORTAL_ROOT_ID = 'vrcx-modal-portal-root';
|
||||||
const APP_PORTAL_ROOT_ID = 'x-dialog-portal';
|
const APP_PORTAL_ROOT_ID = 'x-dialog-portal';
|
||||||
|
|
||||||
const BASE_Z_INDEX = 50;
|
const BASE_Z_INDEX = 10000;
|
||||||
const Z_STEP = 10;
|
const Z_STEP = 10;
|
||||||
|
|
||||||
let nextLayerIndex = 0;
|
let nextLayerIndex = 0;
|
||||||
@@ -15,11 +15,15 @@ function ensureModalPortalRoot() {
|
|||||||
if (root) {
|
if (root) {
|
||||||
root.style.position ||= 'relative';
|
root.style.position ||= 'relative';
|
||||||
root.style.isolation ||= 'isolate';
|
root.style.isolation ||= 'isolate';
|
||||||
|
root.style.zIndex ||= String(BASE_Z_INDEX);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
root = document.getElementById(MODAL_PORTAL_ROOT_ID);
|
root = document.getElementById(MODAL_PORTAL_ROOT_ID);
|
||||||
if (root) {
|
if (root) {
|
||||||
|
root.style.position ||= 'relative';
|
||||||
|
root.style.isolation ||= 'isolate';
|
||||||
|
root.style.zIndex ||= String(BASE_Z_INDEX);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +31,7 @@ function ensureModalPortalRoot() {
|
|||||||
root.id = MODAL_PORTAL_ROOT_ID;
|
root.id = MODAL_PORTAL_ROOT_ID;
|
||||||
root.style.position = 'relative';
|
root.style.position = 'relative';
|
||||||
root.style.isolation = 'isolate';
|
root.style.isolation = 'isolate';
|
||||||
|
root.style.zIndex = String(BASE_Z_INDEX);
|
||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,3 @@
|
|||||||
const elementPlusStrings = {
|
|
||||||
// Vite does not support dynamic imports to `node_modules`.
|
|
||||||
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
|
|
||||||
cs: () => import('element-plus/es/locale/lang/cs'),
|
|
||||||
en: () => import('element-plus/es/locale/lang/en'),
|
|
||||||
es: () => import('element-plus/es/locale/lang/es'),
|
|
||||||
fr: () => import('element-plus/es/locale/lang/fr'),
|
|
||||||
hu: () => import('element-plus/es/locale/lang/hu'),
|
|
||||||
ja: () => import('element-plus/es/locale/lang/ja'),
|
|
||||||
ko: () => import('element-plus/es/locale/lang/ko'),
|
|
||||||
pl: () => import('element-plus/es/locale/lang/pl'),
|
|
||||||
pt: () => import('element-plus/es/locale/lang/pt'),
|
|
||||||
ru: () => import('element-plus/es/locale/lang/ru'),
|
|
||||||
th: () => import('element-plus/es/locale/lang/th'),
|
|
||||||
vi: () => import('element-plus/es/locale/lang/vi'),
|
|
||||||
'zh-CN': () => import('element-plus/es/locale/lang/zh-cn'),
|
|
||||||
'zh-TW': () => import('element-plus/es/locale/lang/zh-tw')
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getElementPlusStrings(code) {
|
|
||||||
const loader = elementPlusStrings[code] || elementPlusStrings.en;
|
|
||||||
return (await loader().catch(() => elementPlusStrings.en())).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localizedStringsUrls = import.meta.glob('./*.json', {
|
const localizedStringsUrls = import.meta.glob('./*.json', {
|
||||||
eager: true,
|
eager: true,
|
||||||
query: '?url',
|
query: '?url',
|
||||||
@@ -50,10 +26,7 @@ async function getLocalizedStrings(code) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return localizedStrings;
|
||||||
...localizedStrings,
|
|
||||||
elementPlus: await getElementPlusStrings(code)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageNames = import.meta.glob('./*.json', {
|
const languageNames = import.meta.glob('./*.json', {
|
||||||
|
|||||||
@@ -403,13 +403,15 @@ function openExternalLink(link) {
|
|||||||
confirmText: 'Open',
|
confirmText: 'Open',
|
||||||
cancelText: 'Copy'
|
cancelText: 'Copy'
|
||||||
})
|
})
|
||||||
// TODO: beforeClose alert dialog
|
.then(({ ok, reason }) => {
|
||||||
.then(({ ok }) => {
|
if (reason === 'cancel') {
|
||||||
if (!ok) {
|
|
||||||
copyToClipboard(link, 'Link copied to clipboard!');
|
copyToClipboard(link, 'Link copied to clipboard!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppApi.OpenLink(link);
|
if (ok) {
|
||||||
|
AppApi.OpenLink(link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<MutualFriends />
|
<MutualFriends />
|
||||||
</template>
|
</template>
|
||||||
</TabsUnderline>
|
</TabsUnderline>
|
||||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
<BackToTop target="#chart" :right="30" :bottom="30" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import BackToTop from '@/components/BackToTop.vue';
|
||||||
|
|
||||||
import { useChartsStore } from '../../stores';
|
import { useChartsStore } from '../../stores';
|
||||||
|
|
||||||
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
|
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="instanceActivityRef" class="pt-12">
|
<div ref="instanceActivityRef" class="pt-12">
|
||||||
|
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
|
||||||
<div class="options-container instance-activity" style="margin-top: 0">
|
<div class="options-container instance-activity" style="margin-top: 0">
|
||||||
<div>
|
<div>
|
||||||
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
import { toDate } from 'reka-ui/date';
|
import { toDate } from 'reka-ui/date';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import BackToTop from '@/components/BackToTop.vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover';
|
||||||
@@ -204,7 +206,7 @@
|
|||||||
|
|
||||||
function setInstanceActivityHeight() {
|
function setInstanceActivityHeight() {
|
||||||
if (instanceActivityRef.value) {
|
if (instanceActivityRef.value) {
|
||||||
const availableHeight = window.innerHeight - 100;
|
const availableHeight = window.innerHeight - 110;
|
||||||
instanceActivityRef.value.style.height = `${availableHeight}px`;
|
instanceActivityRef.value.style.height = `${availableHeight}px`;
|
||||||
instanceActivityRef.value.style.overflowY = 'auto';
|
instanceActivityRef.value.style.overflowY = 'auto';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<DropdownMenuSubContent
|
<DropdownMenuSubContent
|
||||||
side="right"
|
side="right"
|
||||||
align="start"
|
align="start"
|
||||||
class="w-[180px] p-1 rounded-lg">
|
class="w-45 p-1 rounded-lg">
|
||||||
<div class="group-visibility-menu">
|
<div class="group-visibility-menu">
|
||||||
<button
|
<button
|
||||||
v-for="visibility in avatarGroupVisibilityOptions"
|
v-for="visibility in avatarGroupVisibilityOptions"
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="activeLocalGroupName">
|
<template v-else-if="activeLocalGroupName">
|
||||||
<el-scrollbar
|
<ScrollArea
|
||||||
ref="localAvatarScrollbarRef"
|
ref="localAvatarScrollbarRef"
|
||||||
class="favorites-content__scroll"
|
class="favorites-content__scroll"
|
||||||
@scroll="handleLocalAvatarScroll">
|
@scroll="handleLocalAvatarScroll">
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">No Data</div>
|
<div v-else class="favorites-empty">No Data</div>
|
||||||
</el-scrollbar>
|
</ScrollArea>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isHistorySelected">
|
<template v-else-if="isHistorySelected">
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
@@ -529,6 +529,7 @@
|
|||||||
import { ArrowUpDown, Check, Ellipsis, Loader, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
import { ArrowUpDown, Check, Ellipsis, Loader, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
||||||
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
@@ -1101,7 +1102,7 @@
|
|||||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wrap = localAvatarScrollbarRef.value?.wrapRef;
|
const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1135,7 +1136,7 @@
|
|||||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wrap = localAvatarScrollbarRef.value?.wrapRef;
|
const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,7 +404,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">No Data</div>
|
<div v-else class="favorites-empty">No Data</div>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar
|
<ScrollArea
|
||||||
v-else-if="activeLocalGroupName && isLocalGroupSelected"
|
v-else-if="activeLocalGroupName && isLocalGroupSelected"
|
||||||
ref="localFavoritesScrollbarRef"
|
ref="localFavoritesScrollbarRef"
|
||||||
class="favorites-content__scroll"
|
class="favorites-content__scroll"
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">No Data</div>
|
<div v-else class="favorites-empty">No Data</div>
|
||||||
</el-scrollbar>
|
</ScrollArea>
|
||||||
<div v-else class="favorites-empty">No Data</div>
|
<div v-else class="favorites-empty">No Data</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,6 +440,7 @@
|
|||||||
import { ArrowUpDown, Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
import { ArrowUpDown, Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
||||||
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
@@ -1004,7 +1005,7 @@
|
|||||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wrap = localFavoritesScrollbarRef.value?.wrapRef;
|
const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1098,7 @@
|
|||||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wrap = localFavoritesScrollbarRef.value?.wrapRef;
|
const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
:rows="10"
|
:rows="10"
|
||||||
style="margin-top: 10px"
|
style="margin-top: 10px"
|
||||||
input-class="resize-none" />
|
input-class="resize-none" />
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
|
<div>
|
||||||
<div>
|
<div class="mb-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
:model-value="worldImportFavoriteGroupSelection"
|
:model-value="worldImportFavoriteGroupSelection"
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export const columns = [
|
|||||||
location={original.location}
|
location={original.location}
|
||||||
hint={original.worldName}
|
hint={original.worldName}
|
||||||
grouphint={original.groupName}
|
grouphint={original.groupName}
|
||||||
|
disableTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -318,6 +319,7 @@ export const columns = [
|
|||||||
location={original.location}
|
location={original.location}
|
||||||
hint={original.worldName}
|
hint={original.worldName}
|
||||||
grouphint={original.groupName}
|
grouphint={original.groupName}
|
||||||
|
disableTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -350,7 +352,7 @@ export const columns = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span class="block w-full min-w-0 truncate">
|
<div class="block w-full min-w-0 truncate">
|
||||||
<i
|
<i
|
||||||
class={[
|
class={[
|
||||||
'x-user-status',
|
'x-user-status',
|
||||||
@@ -359,7 +361,7 @@ export const columns = [
|
|||||||
]}
|
]}
|
||||||
></i>
|
></i>
|
||||||
<span>{original.statusDescription}</span>
|
<span>{original.statusDescription}</span>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,13 +381,9 @@ export const columns = [
|
|||||||
|
|
||||||
if (type === 'Bio') {
|
if (type === 'Bio') {
|
||||||
return (
|
return (
|
||||||
<span
|
<div class="block w-full min-w-0 truncate">
|
||||||
class="block w-full min-w-0 truncate"
|
{original.bio}
|
||||||
innerHTML={formatDifference(
|
</div>
|
||||||
original.previousBio,
|
|
||||||
original.bio
|
|
||||||
)}
|
|
||||||
></span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
||||||
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
||||||
<template v-if="isSameInstanceView">
|
<template v-if="isSameInstanceView">
|
||||||
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
|
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
|
||||||
<section
|
<section
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
<Loader2 class="friend-view__loading-icon" :size="18" />
|
<Loader2 class="friend-view__loading-icon" :size="18" />
|
||||||
<span>{{ t('view.friends_locations.loading_more') }}</span>
|
<span>{{ t('view.friends_locations.loading_more') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</ScrollArea>
|
||||||
<div v-else class="friend-view__initial-loading">
|
<div v-else class="friend-view__initial-loading">
|
||||||
<Loader2 class="friend-view__loading-icon" :size="22" />
|
<Loader2 class="friend-view__loading-icon" :size="22" />
|
||||||
</div>
|
</div>
|
||||||
@@ -164,6 +164,7 @@
|
|||||||
import { Loader2, Settings } from 'lucide-vue-next';
|
import { Loader2, Settings } from 'lucide-vue-next';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
import { InputGroupSearch } from '@/components/ui/input-group';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@
|
|||||||
let cleanupResize;
|
let cleanupResize;
|
||||||
|
|
||||||
const updateGridWidth = () => {
|
const updateGridWidth = () => {
|
||||||
const wrap = scrollbarRef.value?.wrapRef;
|
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -276,7 +277,7 @@
|
|||||||
cleanupResize = undefined;
|
cleanupResize = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrap = scrollbarRef.value?.wrapRef;
|
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -559,7 +560,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrap = scrollbarRef.value?.wrapRef;
|
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||||
|
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
@@ -590,7 +591,7 @@
|
|||||||
|
|
||||||
function maybeFillViewport() {
|
function maybeFillViewport() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const wrap = scrollbarRef.value?.wrapRef;
|
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -634,7 +635,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scrollbarRef.value?.update?.();
|
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
maybeFillViewport();
|
maybeFillViewport();
|
||||||
});
|
});
|
||||||
@@ -697,7 +697,6 @@
|
|||||||
settingsReady.value = true;
|
settingsReady.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setupResizeHandling();
|
setupResizeHandling();
|
||||||
scrollbarRef.value?.update?.();
|
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
maybeFillViewport();
|
maybeFillViewport();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -352,66 +352,52 @@
|
|||||||
@change="updateTrustColor('', '', true)"></simple-switch>
|
@change="updateTrustColor('', '', true)"></simple-switch>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.untrusted"
|
:model-value="trustColor.untrusted"
|
||||||
size="small"
|
:presets="['#CCCCCC']"
|
||||||
:predefine="['#CCCCCC']"
|
@change="updateTrustColor('untrusted', $event)" />
|
||||||
@change="updateTrustColor('untrusted', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-untrusted">Visitor</span>
|
<span class="color-picker x-tag-untrusted">Visitor</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.basic"
|
:model-value="trustColor.basic"
|
||||||
size="small"
|
:presets="['#1778ff']"
|
||||||
:predefine="['#1778ff']"
|
@change="updateTrustColor('basic', $event)" />
|
||||||
@change="updateTrustColor('basic', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-basic">New User</span>
|
<span class="color-picker x-tag-basic">New User</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.known"
|
:model-value="trustColor.known"
|
||||||
size="small"
|
:presets="['#2bcf5c']"
|
||||||
:predefine="['#2bcf5c']"
|
@change="updateTrustColor('known', $event)" />
|
||||||
@change="updateTrustColor('known', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-known">User</span>
|
<span class="color-picker x-tag-known">User</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.trusted"
|
:model-value="trustColor.trusted"
|
||||||
size="small"
|
:presets="['#ff7b42']"
|
||||||
:predefine="['#ff7b42']"
|
@change="updateTrustColor('trusted', $event)" />
|
||||||
@change="updateTrustColor('trusted', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-trusted">Known User</span>
|
<span class="color-picker x-tag-trusted">Known User</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.veteran"
|
:model-value="trustColor.veteran"
|
||||||
size="small"
|
:presets="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
|
||||||
:predefine="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
|
@change="updateTrustColor('veteran', $event)" />
|
||||||
@change="updateTrustColor('veteran', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-veteran">Trusted User</span>
|
<span class="color-picker x-tag-veteran">Trusted User</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.vip"
|
:model-value="trustColor.vip"
|
||||||
size="small"
|
:presets="['#ff2626']"
|
||||||
:predefine="['#ff2626']"
|
@change="updateTrustColor('vip', $event)" />
|
||||||
@change="updateTrustColor('vip', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-vip">VRChat Team</span>
|
<span class="color-picker x-tag-vip">VRChat Team</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-color-picker
|
<PresetColorPicker
|
||||||
:model-value="trustColor.troll"
|
:model-value="trustColor.troll"
|
||||||
size="small"
|
:presets="['#782f2f']"
|
||||||
:predefine="['#782f2f']"
|
@change="updateTrustColor('troll', $event)" />
|
||||||
@change="updateTrustColor('troll', $event)">
|
|
||||||
</el-color-picker>
|
|
||||||
<span class="color-picker x-tag-troll">Nuisance</span>
|
<span class="color-picker x-tag-troll">Nuisance</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,6 +431,8 @@
|
|||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import PresetColorPicker from '@/components/PresetColorPicker.vue';
|
||||||
|
|
||||||
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores';
|
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores';
|
||||||
import { getLanguageName, languageCodes } from '../../../../localization';
|
import { getLanguageName, languageCodes } from '../../../../localization';
|
||||||
import { THEME_CONFIG } from '../../../../shared/constants';
|
import { THEME_CONFIG } from '../../../../shared/constants';
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
<div style="flex: 1; padding: 10px; padding-left: 0">
|
<div style="flex: 1; padding: 10px; padding-left: 0">
|
||||||
<Popover v-model:open="isQuickSearchOpen">
|
<Popover v-model:open="isQuickSearchOpen">
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Input
|
<Input v-model="quickSearchQuery" :placeholder="t('side_panel.search_placeholder')" />
|
||||||
v-model="quickSearchQuery"
|
|
||||||
:placeholder="t('side_panel.search_placeholder')"
|
|
||||||
@focus="handleQuickSearchFocus" />
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
side="bottom"
|
side="bottom"
|
||||||
@@ -78,6 +75,7 @@
|
|||||||
:items="sidebarTabs"
|
:items="sidebarTabs"
|
||||||
:unmount-on-hide="false"
|
:unmount-on-hide="false"
|
||||||
variant="equal"
|
variant="equal"
|
||||||
|
fill
|
||||||
class="zero-margin-tabs"
|
class="zero-margin-tabs"
|
||||||
style="height: calc(100% - 70px); margin-top: 5px">
|
style="height: calc(100% - 70px); margin-top: 5px">
|
||||||
<template #label-friends>
|
<template #label-friends>
|
||||||
@@ -89,14 +87,19 @@
|
|||||||
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
||||||
</template>
|
</template>
|
||||||
<template #friends>
|
<template #friends>
|
||||||
<div class="el-tabs__content">
|
<div class="h-full overflow-hidden">
|
||||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
<ScrollArea ref="friendsScrollAreaRef" class="h-full">
|
||||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||||
|
</ScrollArea>
|
||||||
|
<BackToTop :target="friendsScrollTarget" :bottom="20" :right="20" :teleport="false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #groups>
|
<template #groups>
|
||||||
<div class="el-tabs__content">
|
<div class="h-full overflow-hidden">
|
||||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
<ScrollArea ref="groupsScrollAreaRef" class="h-full">
|
||||||
|
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||||
|
</ScrollArea>
|
||||||
|
<BackToTop :target="groupsScrollTarget" :bottom="20" :right="20" :teleport="false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TabsUnderline>
|
</TabsUnderline>
|
||||||
@@ -104,16 +107,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { RefreshCw } from 'lucide-vue-next';
|
import { RefreshCw } from 'lucide-vue-next';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { TabsUnderline } from '@/components/ui/tabs';
|
import { TabsUnderline } from '@/components/ui/tabs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import BackToTop from '@/components/BackToTop.vue';
|
||||||
|
|
||||||
import { useFriendStore, useGroupStore, useSearchStore } from '../../stores';
|
import { useFriendStore, useGroupStore, useSearchStore } from '../../stores';
|
||||||
import { userImage } from '../../shared/utils';
|
import { userImage } from '../../shared/utils';
|
||||||
|
|
||||||
@@ -134,6 +140,25 @@
|
|||||||
const quickSearchQuery = ref('');
|
const quickSearchQuery = ref('');
|
||||||
const isQuickSearchOpen = ref(false);
|
const isQuickSearchOpen = ref(false);
|
||||||
|
|
||||||
|
const friendsScrollAreaRef = ref(null);
|
||||||
|
const groupsScrollAreaRef = ref(null);
|
||||||
|
const friendsScrollTarget = ref(null);
|
||||||
|
const groupsScrollTarget = ref(null);
|
||||||
|
|
||||||
|
function resolveScrollViewport(scrollAreaComponentRef) {
|
||||||
|
// Our ScrollArea renders a DOM element root; the viewport is marked by data-slot.
|
||||||
|
const rootEl = scrollAreaComponentRef?.$el ?? null;
|
||||||
|
if (!rootEl || typeof rootEl.querySelector !== 'function') return null;
|
||||||
|
return rootEl.querySelector('[data-slot="scroll-area-viewport"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Ensure child components are mounted before querying their DOM.
|
||||||
|
await nextTick();
|
||||||
|
friendsScrollTarget.value = resolveScrollViewport(friendsScrollAreaRef.value);
|
||||||
|
groupsScrollTarget.value = resolveScrollViewport(groupsScrollAreaRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
quickSearchQuery,
|
quickSearchQuery,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -142,11 +167,6 @@
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleQuickSearchFocus() {
|
|
||||||
isQuickSearchOpen.value = true;
|
|
||||||
quickSearchRemoteMethod(String(quickSearchQuery.value ?? ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleQuickSearchSelect(value) {
|
function handleQuickSearchSelect(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="skeleton" aria-busy="true" aria-label="Loading">
|
<!-- <div v-else class="skeleton" aria-busy="true" aria-label="Loading">
|
||||||
<div>
|
<div>
|
||||||
<Skeleton class="h-10 w-10 rounded-full" />
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
<div>
|
<div>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<Skeleton class="mt-1.5 h-3 w-full" />
|
<Skeleton class="mt-1.5 h-3 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,7 @@
|
|||||||
|
|
||||||
const timeZone = getLocalTimeZone();
|
const timeZone = getLocalTimeZone();
|
||||||
|
|
||||||
// JSDoc casts: this project can end up with nominal-type mismatches for DateValue
|
|
||||||
// due to duplicate @internationalized/date copies in tooling.
|
|
||||||
/** @type {import('vue').Ref<any>} */
|
|
||||||
const internalValue = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
const internalValue = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
||||||
/** @type {import('vue').Ref<any>} */
|
|
||||||
const placeholder = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
const placeholder = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -147,6 +143,10 @@
|
|||||||
:class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'">
|
:class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'">
|
||||||
{{ eventCountFor(weekDate) }}
|
{{ eventCountFor(weekDate) }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
v-if="eventCountFor(weekDate) > 0"
|
||||||
|
class="calendar-event-dot"
|
||||||
|
aria-hidden="true" /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CalendarCellTrigger>
|
</CalendarCellTrigger>
|
||||||
@@ -165,7 +165,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
@@ -193,21 +192,20 @@
|
|||||||
|
|
||||||
.calendar-event-badge {
|
.calendar-event-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: -4px;
|
||||||
right: 2px;
|
right: -4px;
|
||||||
min-width: 18px;
|
min-width: 14px;
|
||||||
height: 18px;
|
height: 14px;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
color: var(--el-color-white, #fff);
|
color: var(--el-color-white, #fff);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: var(--el-box-shadow-lighter);
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
line-height: 18px;
|
line-height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-event-badge.has-following {
|
.calendar-event-badge.has-following {
|
||||||
@@ -217,4 +215,18 @@
|
|||||||
.calendar-event-badge.no-following {
|
.calendar-event-badge.no-following {
|
||||||
background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
|
background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-event-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 4px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--group-calendar-event-dot, #ef4444);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
|
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
|
||||||
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] sm:max-w-[70vw] h-[60vh] overflow-hidden">
|
<DialogContent class="x-dialog sm:max-w-[50vw] h-[60vh] overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div class="dialog-title-container">
|
<div class="dialog-title-container">
|
||||||
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
|
||||||
@@ -18,82 +18,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<transition name="el-fade-in-linear" mode="out-in">
|
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
|
||||||
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
|
<div class="timeline-container">
|
||||||
<div class="timeline-container">
|
<div v-if="groupedTimelineEvents.length" class="timeline-list">
|
||||||
<el-timeline v-if="groupedTimelineEvents.length">
|
<div v-for="(timeGroup, key) of groupedTimelineEvents" :key="key" class="timeline-group">
|
||||||
<el-timeline-item
|
<div class="timeline-timestamp">
|
||||||
v-for="(timeGroup, key) of groupedTimelineEvents"
|
{{ dayjs(timeGroup.startsAt).format('MM-DD ddd') }} {{ timeGroup.startTime }}
|
||||||
:key="key"
|
</div>
|
||||||
:timestamp="
|
<div class="time-group-container">
|
||||||
dayjs(timeGroup.startsAt).format('MM-DD ddd') + ' ' + timeGroup.startTime
|
<GroupCalendarEventCard
|
||||||
"
|
v-for="value in timeGroup.events"
|
||||||
placement="top">
|
:key="value.id"
|
||||||
<div class="time-group-container">
|
:event="value"
|
||||||
<GroupCalendarEventCard
|
mode="timeline"
|
||||||
v-for="value in timeGroup.events"
|
:is-following="isEventFollowing(value.id)"
|
||||||
:key="value.id"
|
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
|
||||||
:event="value"
|
@update-following-calendar-data="updateFollowingCalendarData"
|
||||||
mode="timeline"
|
@click-action="showGroupDialog(value.ownerId)" />
|
||||||
:is-following="isEventFollowing(value.id)"
|
|
||||||
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
|
|
||||||
@update-following-calendar-data="updateFollowingCalendarData"
|
|
||||||
@click-action="showGroupDialog(value.ownerId)" />
|
|
||||||
</div>
|
|
||||||
</el-timeline-item>
|
|
||||||
</el-timeline>
|
|
||||||
<div v-else>{{ t('dialog.group_calendar.no_events') }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="calendar-container">
|
|
||||||
<GroupCalendarMonth
|
|
||||||
v-model="selectedDay"
|
|
||||||
:is-loading="isLoading"
|
|
||||||
:events-by-date="filteredCalendar"
|
|
||||||
:following-by-date="followingCalendarDate" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else key="grid" class="grid-view">
|
|
||||||
<div class="search-container">
|
|
||||||
<InputGroupSearch
|
|
||||||
v-model="searchQuery"
|
|
||||||
size="sm"
|
|
||||||
:placeholder="t('dialog.group_calendar.search_placeholder')"
|
|
||||||
class="search-input" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="groups-grid" v-loading="isLoading">
|
|
||||||
<div v-if="filteredGroupEvents.length" class="groups-container">
|
|
||||||
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
|
|
||||||
<div class="group-header" @click="toggleGroup(group.groupId)">
|
|
||||||
<ArrowRight
|
|
||||||
class="rotation-transition"
|
|
||||||
:class="{ rotate: !groupCollapsed[group.groupId] }" />
|
|
||||||
{{ group.groupName }}
|
|
||||||
</div>
|
|
||||||
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
|
|
||||||
<GroupCalendarEventCard
|
|
||||||
v-for="event in group.events"
|
|
||||||
:key="event.id"
|
|
||||||
:event="event"
|
|
||||||
mode="grid"
|
|
||||||
:is-following="isEventFollowing(event.id)"
|
|
||||||
@update-following-calendar-data="updateFollowingCalendarData"
|
|
||||||
@click-action="showGroupDialog(event.ownerId)"
|
|
||||||
card-class="grid-card" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-events">
|
</div>
|
||||||
{{
|
<div v-else class="timeline-empty">{{ t('dialog.group_calendar.no_events') }}</div>
|
||||||
searchQuery
|
</div>
|
||||||
? t('dialog.group_calendar.search_no_matching')
|
|
||||||
: t('dialog.group_calendar.search_no_this_month')
|
<div class="calendar-container">
|
||||||
}}
|
<GroupCalendarMonth
|
||||||
|
v-model="selectedDay"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:events-by-date="filteredCalendar"
|
||||||
|
:following-by-date="followingCalendarDate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else key="grid" class="grid-view">
|
||||||
|
<div class="search-container">
|
||||||
|
<InputGroupSearch
|
||||||
|
v-model="searchQuery"
|
||||||
|
size="sm"
|
||||||
|
:placeholder="t('dialog.group_calendar.search_placeholder')"
|
||||||
|
class="search-input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="groups-grid" v-loading="isLoading">
|
||||||
|
<div v-if="filteredGroupEvents.length" class="groups-container">
|
||||||
|
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
|
||||||
|
<div class="group-header" @click="toggleGroup(group.groupId)">
|
||||||
|
<ArrowRight
|
||||||
|
class="rotation-transition"
|
||||||
|
:class="{ rotate: !groupCollapsed[group.groupId] }" />
|
||||||
|
{{ group.groupName }}
|
||||||
|
</div>
|
||||||
|
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
|
||||||
|
<GroupCalendarEventCard
|
||||||
|
v-for="event in group.events"
|
||||||
|
:key="event.id"
|
||||||
|
:event="event"
|
||||||
|
mode="grid"
|
||||||
|
:is-following="isEventFollowing(event.id)"
|
||||||
|
@update-following-calendar-data="updateFollowingCalendarData"
|
||||||
|
@click-action="showGroupDialog(event.ownerId)"
|
||||||
|
card-class="grid-card" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="no-events">
|
||||||
|
{{
|
||||||
|
searchQuery
|
||||||
|
? t('dialog.group_calendar.search_no_matching')
|
||||||
|
: t('dialog.group_calendar.search_no_this_month')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -334,7 +329,8 @@
|
|||||||
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
|
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDateKey = (date) => formatDateFilter(date, 'date');
|
// Use a stable key for calendar maps (independent of locale/appearance date formatting).
|
||||||
|
const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
function getGroupNameFromCache(groupId) {
|
function getGroupNameFromCache(groupId) {
|
||||||
if (!groupNamesCache.has(groupId)) {
|
if (!groupNamesCache.has(groupId)) {
|
||||||
@@ -462,18 +458,36 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.timeline-view {
|
.timeline-view {
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
:deep(.el-timeline) {
|
min-width: 200px;
|
||||||
width: 100%;
|
padding-left: 4px;
|
||||||
|
padding-right: 16px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.timeline-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-group {
|
||||||
|
padding: 0 20px 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-timestamp {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-empty {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 200px;
|
display: flex;
|
||||||
padding-left: 4px;
|
align-items: center;
|
||||||
padding-right: 16px;
|
justify-content: center;
|
||||||
margin-left: 10px;
|
color: var(--el-text-color-secondary);
|
||||||
margin-right: 6px;
|
|
||||||
overflow: auto;
|
|
||||||
.el-timeline-item {
|
|
||||||
padding: 0 20px 20px 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.time-group-container {
|
.time-group-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -571,7 +585,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.calendar-container {
|
.calendar-container {
|
||||||
width: 609px;
|
width: 609px;
|
||||||
|
|||||||
Reference in New Issue
Block a user