UI Refresh

This commit is contained in:
pa
2026-01-02 22:24:28 +09:00
committed by Natsumi
parent b02d287190
commit 00745b54f1
120 changed files with 3931 additions and 2015 deletions
+22 -66
View File
@@ -15,10 +15,6 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.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-kr": "^5.2.9",
"@fontsource-variable/noto-sans-sc": "^5.2.9",
"@fontsource-variable/noto-sans-tc": "^5.2.9",
"@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.1",
"@sentry/vue": "^10.32.1", "@sentry/vue": "^10.32.1",
@@ -47,12 +43,12 @@
"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.7.4",
"remixicon": "^4.7.0", "remixicon": "^4.8.0",
"sass-embedded": "^1.97.1", "sass-embedded": "^1.97.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vite": "^7.3.0", "vite": "^7.3.0",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.8",
"vue-marquee-text-component": "^2.0.1", "vue-marquee-text-component": "^2.0.1",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-showdown": "^4.2.0", "vue-showdown": "^4.2.0",
@@ -2351,46 +2347,6 @@
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
} }
}, },
"node_modules/@fontsource-variable/noto-sans-jp": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-jp/-/noto-sans-jp-5.2.9.tgz",
"integrity": "sha512-osPL5f7dvGDjuMuFwDTGPLG37030D8X5zk+3BWea6txAVDFeE/ZIrKW0DY0uSDfRn9+NiKbiFn/2QvZveKXTog==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/noto-sans-kr": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.9.tgz",
"integrity": "sha512-g1BnJdJbnAgRUP8YxyPIm8npZVUbtt6VgtLnkGR7poa/RVbVGd27i+9138DmwRvtbKhJG1fPLQ/V3BonvFykRQ==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/noto-sans-sc": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.2.9.tgz",
"integrity": "sha512-ZEEpZlxjYEIVdg85K38mqaoeBcorrN3Z6MaIkwK5w5Dqn/e9v5uVIYr0ukoLsFxaVyEXSi/c3caOeMHjbOMtfA==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/noto-sans-tc": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-tc/-/noto-sans-tc-5.2.9.tgz",
"integrity": "sha512-GhtbSE8IZTP3vZj7Fu1G/PERxguMe3jryAbHovSd22Rs7aYdbXQD8vBqkTT/tkHIUn6t2IFReTfgKUoQBPCe+w==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@gar/promisify": { "node_modules/@gar/promisify": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -2451,14 +2407,14 @@
} }
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "11.2.2", "version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==", "integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/message-compiler": "11.2.2", "@intlify/message-compiler": "11.2.8",
"@intlify/shared": "11.2.2" "@intlify/shared": "11.2.8"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -2468,13 +2424,13 @@
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "11.2.2", "version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==", "integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/shared": "11.2.2", "@intlify/shared": "11.2.8",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
@@ -2485,9 +2441,9 @@
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "11.2.2", "version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==", "integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -15566,9 +15522,9 @@
} }
}, },
"node_modules/remixicon": { "node_modules/remixicon": {
"version": "4.7.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.7.0.tgz", "resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.8.0.tgz",
"integrity": "sha512-g2pHOofQWARWpxdbrQu5+K3C8ZbqguQFzE54HIMWFCpFa63pumaAltIgZmFMRQpKKBScRWQASQfWxS9asNCcHQ==", "integrity": "sha512-8qTM/bWkmsAWitvcL9XrVPVdqHRrdmnNp4zCFBdmIHBygxfHWwoK6NzitbiMyRzjcXKBMlS/ab5M65/ZlxJTVA==",
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
@@ -18493,14 +18449,14 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "11.2.2", "version": "11.2.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==", "integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/core-base": "11.2.2", "@intlify/core-base": "11.2.8",
"@intlify/shared": "11.2.2", "@intlify/shared": "11.2.8",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {
+2 -2
View File
@@ -68,12 +68,12 @@
"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.7.4",
"remixicon": "^4.7.0", "remixicon": "^4.8.0",
"sass-embedded": "^1.97.1", "sass-embedded": "^1.97.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vite": "^7.3.0", "vite": "^7.3.0",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.8",
"vue-marquee-text-component": "^2.0.1", "vue-marquee-text-component": "^2.0.1",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-showdown": "^4.2.0", "vue-showdown": "^4.2.0",
+1 -5
View File
@@ -56,11 +56,7 @@
}); });
</script> </script>
<style lang="scss" scoped> <style scoped>
:deep(.el-splitter-bar__dragger) {
width: 4px !important;
}
/* Add title bar spacing for macOS */ /* Add title bar spacing for macOS */
.x-app.with-macos-titlebar { .x-app.with-macos-titlebar {
padding-top: 28px; padding-top: 28px;
+841 -250
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@
--offy: 14.5px; --offy: 14.5px;
} }
.flags { .flags {
background: url('../images/flags.png') no-repeat; background: url('/images/flags.png') no-repeat;
background-size: calc(var(--offx) * 6); background-size: calc(var(--offx) * 6);
width: var(--offx); width: var(--offx);
height: calc(var(--offx) / 72 * 52); height: calc(var(--offx) / 72 * 52);
+53
View File
@@ -0,0 +1,53 @@
/* Noty.js */
.noty_layout {
word-break: break-all;
}
.noty_theme__mint.noty_bar {
position: relative;
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
}
.noty_theme__mint.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
}
.noty_theme__mint.noty_bar .noty_buttons {
padding: 10px;
}
.noty_theme__mint.noty_type__alert,
.noty_theme__mint.noty_type__notification {
color: #2f2f2f;
background-color: #fff;
border-bottom: 1px solid #d1d1d1;
}
.noty_theme__mint.noty_type__warning {
color: #fff;
background-color: #ffae42;
border-bottom: 1px solid #e89f3c;
}
.noty_theme__mint.noty_type__error {
color: #fff;
background-color: #de636f;
border-bottom: 1px solid #ca5a65;
}
.noty_theme__mint.noty_type__info,
.noty_theme__mint.noty_type__information {
color: #fff;
background-color: #7f7eff;
border-bottom: 1px solid #7473e8;
}
.noty_theme__mint.noty_type__success {
color: #fff;
background-color: #afc765;
border-bottom: 1px solid #a0b55c;
}
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,8 +1,8 @@
<template> <template>
<div @click="confirm" class="avatar-info"> <div @click="confirm" class="avatar-info">
<span style="margin-right: 5px">{{ avatarName }}</span> <span v-if="avatarType" :class="color" class="mr-2"><i :class="avatarTypeIcons" /></span>
<span v-if="avatarType" :class="color" style="margin-right: 5px"><i :class="avatarTypeIcons" /></span> <span class="mr-2">{{ avatarName }}</span>
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span> <span v-if="avatarTags" style="color: var(--el-text-color-secondary); font-size: 12px">{{ avatarTags }}</span>
</div> </div>
</template> </template>
@@ -17,7 +17,7 @@
imageurl: String, imageurl: String,
userid: String, userid: String,
hintownerid: String, hintownerid: String,
hintavatarname: String, hintavatarname: [String, Object],
avatartags: Array avatartags: Array
}); });
@@ -45,7 +45,9 @@
if (!props.imageurl) { if (!props.imageurl) {
avatarName.value = ''; avatarName.value = '';
} else if (props.hintownerid) { } else if (props.hintownerid) {
avatarName.value = props.hintavatarname; if (typeof props.hintavatarname === 'string') {
avatarName.value = props.hintavatarname;
}
ownerId = props.hintownerid; ownerId = props.hintownerid;
} else { } else {
try { try {
+1 -1
View File
@@ -4,6 +4,7 @@
v-loading="loading" v-loading="loading"
:data="paginatedData" :data="paginatedData"
v-bind="mergedTableProps" v-bind="mergedTableProps"
:stripe="false"
:default-sort="resolvedDefaultSort" :default-sort="resolvedDefaultSort"
@row-click="handleRowClick"> @row-click="handleRowClick">
<slot></slot> <slot></slot>
@@ -102,7 +103,6 @@
delete rest.defaultSort; delete rest.defaultSort;
} }
return { return {
stripe: true,
...rest ...rest
}; };
}); });
+1 -1
View File
@@ -168,7 +168,7 @@
} }
</script> </script>
<style scoped lang="scss"> <style scoped>
.toolbar-icon:hover { .toolbar-icon:hover {
opacity: 1; opacity: 1;
} }
+8 -4
View File
@@ -15,9 +15,13 @@
{{ t('dialog.user.info.close_instance') }} </el-button {{ t('dialog.user.info.close_instance') }} </el-button
><br /><br /> ><br /><br />
</template> </template>
<span><span style="color: #409eff">PC: </span>{{ props.instance.platforms.standalonewindows }}</span <span
><span style="color: var(--el-color-primary)">PC: </span
>{{ props.instance.platforms.standalonewindows }}</span
><br /> ><br />
<span><span style="color: #67c23a">Android: </span>{{ props.instance.platforms.android }}</span <span
><span style="color: var(--el-color-success)">Android: </span
>{{ props.instance.platforms.android }}</span
><br /> ><br />
<span>{{ t('dialog.user.info.instance_game_version') }} {{ props.instance.gameServerVersion }}</span <span>{{ t('dialog.user.info.instance_game_version') }} {{ props.instance.gameServerVersion }}</span
><br /> ><br />
@@ -46,13 +50,13 @@
<span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span> <span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span>
<span <span
v-if="state.isValidInstance && !props.instance.hasCapacityForYou" v-if="state.isValidInstance && !props.instance.hasCapacityForYou"
style="margin-left: 5px; color: lightcoral" style="margin-left: 5px; color: var(--el-color-danger)"
>{{ t('dialog.user.info.instance_full') }}</span >{{ t('dialog.user.info.instance_full') }}</span
> >
<span v-if="props.instance.queueSize" style="margin-left: 5px" <span v-if="props.instance.queueSize" style="margin-left: 5px"
>{{ t('dialog.user.info.instance_queue') }} {{ props.instance.queueSize }}</span >{{ t('dialog.user.info.instance_queue') }} {{ props.instance.queueSize }}</span
> >
<span v-if="state.isAgeGated" style="margin-left: 5px; color: lightcoral">{{ <span v-if="state.isAgeGated" style="margin-left: 5px; color: var(--el-color-danger)">{{
t('dialog.user.info.instance_age_gated') t('dialog.user.info.instance_age_gated')
}}</span> }}</span>
</div> </div>
+53 -32
View File
@@ -1,24 +1,33 @@
<template> <template>
<div> <div>
<span v-if="!text" class="transparent">-</span> <div v-if="!text" class="transparent">-</div>
<span v-show="text"> <div v-show="text" class="flex items-center">
<span <div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
:class="{ 'x-link': link && location !== 'private' && location !== 'offline' }" <el-tooltip
@click="handleShowWorldDialog"> :content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
<el-icon :class="['is-loading', 'inline-block']" style="margin-right: 3px" v-if="isTraveling" :disabled="!instanceName"
><Loading :show-after="300"
/></el-icon> placement="top">
<span>{{ text }}</span> <div
</span> :class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
<span v-if="groupName" :class="{ 'x-link': link }" @click="handleShowGroupDialog">({{ groupName }})</span> class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
<span @click="handleShowWorldDialog">
v-if="region" <el-icon :class="['is-loading']" class="mr-1" v-if="isTraveling"><Loading /></el-icon>
:class="['flags', 'inline-block', 'ml-5', region, 'transform-[translateY(3px)]']"></span> <span class="min-w-0 truncate">{{ text }}</span>
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')"> <span
v-if="groupName"
class="ml-0.5 whitespace-nowrap"
:class="{ 'x-link': link }"
@click.stop="handleShowGroupDialog">
({{ groupName }})
</span>
</div>
</el-tooltip>
<el-tooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
<el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon> <el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon>
</NativeTooltip> </el-tooltip>
<el-icon v-if="strict" :class="['inline-block', 'ml-5']"><Lock /></el-icon> <el-icon v-if="strict" :class="['inline-block', 'ml-5']"><Lock /></el-icon>
</span> </div>
</div> </div>
</template> </template>
@@ -30,6 +39,7 @@
import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores'; import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
import { getGroupName, getWorldName, parseLocation } from '../shared/utils'; import { getGroupName, getWorldName, parseLocation } from '../shared/utils';
import { accessTypeLocaleKeyMap } from '../shared/constants';
const { t } = useI18n(); const { t } = useI18n();
@@ -67,6 +77,7 @@
const isTraveling = ref(false); const isTraveling = ref(false);
const groupName = ref(''); const groupName = ref('');
const isClosed = ref(false); const isClosed = ref(false);
const instanceName = ref('');
let isDisposed = false; let isDisposed = false;
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -108,7 +119,8 @@
isTraveling.value = true; isTraveling.value = true;
} }
const L = parseLocation(instanceId); const L = parseLocation(instanceId);
setText(L, L.instanceName); setText(L);
instanceName.value = L.instanceName;
if (!L.isRealInstance) { if (!L.isRealInstance) {
return; return;
} }
@@ -116,7 +128,8 @@
const instanceRef = cachedInstances.get(L.tag); const instanceRef = cachedInstances.get(L.tag);
if (typeof instanceRef !== 'undefined') { if (typeof instanceRef !== 'undefined') {
if (instanceRef.displayName) { if (instanceRef.displayName) {
setText(L, instanceRef.displayName); setText(L);
instanceName.value = instanceRef.displayName;
} }
if (instanceRef.closedAt) { if (instanceRef.closedAt) {
isClosed.value = true; isClosed.value = true;
@@ -147,7 +160,9 @@
strict.value = L.strict; strict.value = L.strict;
} }
function setText(L, instanceName) { function setText(L) {
const accessTypeLabel = translateAccessType(L.accessTypeName);
if (L.isOffline) { if (L.isOffline) {
text.value = 'Offline'; text.value = 'Offline';
} else if (L.isPrivate) { } else if (L.isPrivate) {
@@ -156,13 +171,13 @@
text.value = 'Traveling'; text.value = 'Traveling';
} else if (typeof props.hint === 'string' && props.hint !== '') { } else if (typeof props.hint === 'string' && props.hint !== '') {
if (L.instanceId) { if (L.instanceId) {
text.value = `${props.hint} #${instanceName} ${L.accessTypeName}`; text.value = `${props.hint} · ${accessTypeLabel}`;
} else { } else {
text.value = props.hint; text.value = props.hint;
} }
} else if (L.worldId) { } else if (L.worldId) {
if (L.instanceId) { if (L.instanceId) {
text.value = `${L.worldId} #${instanceName} ${L.accessTypeName}`; text.value = `${L.worldId} · ${accessTypeLabel}`;
} else { } else {
text.value = L.worldId; text.value = L.worldId;
} }
@@ -172,7 +187,7 @@
.then((name) => { .then((name) => {
if (!isDisposed && name && currentInstanceId() === L.tag) { if (!isDisposed && name && currentInstanceId() === L.tag) {
if (L.instanceId) { if (L.instanceId) {
text.value = `${name} #${instanceName} ${L.accessTypeName}`; text.value = `${name} · ${translateAccessType(L.accessTypeName)}`;
} else { } else {
text.value = name; text.value = name;
} }
@@ -182,13 +197,21 @@
console.error(e); console.error(e);
}); });
} else if (L.instanceId) { } else if (L.instanceId) {
text.value = `${ref.name} #${instanceName} ${L.accessTypeName}`; text.value = `${ref.name} · ${accessTypeLabel}`;
} else { } else {
text.value = ref.name; text.value = ref.name;
} }
} }
} }
function translateAccessType(accessTypeName) {
const key = accessTypeLocaleKeyMap[accessTypeName];
if (!key) {
return accessTypeName;
}
return t(key);
}
function handleShowWorldDialog() { function handleShowWorldDialog() {
if (props.link) { if (props.link) {
let instanceId = currentInstanceId(); let instanceId = currentInstanceId();
@@ -218,15 +241,13 @@
</script> </script>
<style scoped> <style scoped>
.inline-block {
display: inline-block;
}
.ml-5 {
margin-left: 5px;
}
.transparent { .transparent {
color: transparent; color: transparent;
} }
:global(html.dark .x-location),
:global(:root.dark .x-location),
:global(:root[data-theme='dark'] .x-location) {
color: var(--color-zinc-300);
}
</style> </style>
+20 -13
View File
@@ -1,14 +1,14 @@
<template> <template>
<span> <span class="x-location-world">
<span v-if="region" :class="['flags', 'inline-block', 'mr-1.25', region]"></span>
<span @click="showLaunchDialog" class="x-link"> <span @click="showLaunchDialog" class="x-link">
<el-icon v-if="isUnlocked" :class="['inline-block', 'mr-5']"><Unlock /></el-icon> <el-icon v-if="isUnlocked" :class="['inline-block', 'mr-1.25']"><Unlock /></el-icon>
<span>#{{ instanceName }} {{ accessTypeName }}</span> <span> {{ accessTypeName }} #{{ instanceName }}</span>
</span> </span>
<span v-if="groupName" @click="showGroupDialog" class="x-link">({{ groupName }})</span> <span v-if="groupName" @click="showGroupDialog" class="x-link">({{ groupName }})</span>
<span v-if="region" :class="['flags', 'inline-block', 'ml-5', region, 'transform-[translateY(3px)]']"></span> <el-tooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
<el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon> <el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon>
</NativeTooltip> </el-tooltip>
<el-icon v-if="strict" style="display: inline-block; margin-left: 5px"><Lock /></el-icon> <el-icon v-if="strict" style="display: inline-block; margin-left: 5px"><Lock /></el-icon>
</span> </span>
</template> </template>
@@ -21,6 +21,7 @@
import { useGroupStore, useInstanceStore, useLaunchStore } from '../stores'; import { useGroupStore, useInstanceStore, useLaunchStore } from '../stores';
import { getGroupName, parseLocation } from '../shared/utils'; import { getGroupName, parseLocation } from '../shared/utils';
import { accessTypeLocaleKeyMap } from '../shared/constants';
const { t } = useI18n(); const { t } = useI18n();
const { cachedInstances } = useInstanceStore(); const { cachedInstances } = useInstanceStore();
@@ -52,7 +53,7 @@
function parse() { function parse() {
const locObj = props.locationobject; const locObj = props.locationobject;
location.value = locObj.tag; location.value = locObj.tag;
accessTypeName.value = locObj.accessTypeName; accessTypeName.value = translateAccessType(locObj.accessTypeName);
strict.value = locObj.strict; strict.value = locObj.strict;
shortName.value = locObj.shortName; shortName.value = locObj.shortName;
@@ -97,6 +98,14 @@
} }
} }
function translateAccessType(accessTypeNameRaw) {
const key = accessTypeLocaleKeyMap[accessTypeNameRaw];
if (!key) {
return accessTypeNameRaw;
}
return t(key);
}
watch(() => props.locationobject, parse, { immediate: true }); watch(() => props.locationobject, parse, { immediate: true });
watch( watch(
@@ -126,11 +135,9 @@
display: inline-block; display: inline-block;
} }
.ml-5 { :global(html.dark .x-location-world),
margin-left: 5px; :global(:root.dark .x-location-world),
} :global(:root[data-theme='dark'] .x-location-world) {
color: var(--color-zinc-100);
.mr-5 {
margin-right: 5px;
} }
</style> </style>
-537
View File
@@ -1,537 +0,0 @@
<template>
<span
ref="triggerEl"
class="vrcx-native-tooltip__trigger"
:style="triggerStyle"
@mouseenter="onEnter"
@mouseleave="onLeave"
@focusin="onEnter"
@focusout="onLeave"
@keydown.esc="close">
<slot />
</span>
<div
ref="tooltipEl"
class="vrcx-native-tooltip__content"
:class="[
placementClass,
{
'has-arrow': props.showArrow,
'is-open': isOpen,
'is-closing': isClosing
}
]"
:style="contentStyle"
popover="manual"
role="tooltip"
@mouseenter="cancelClose"
@mouseleave="onLeave">
<slot name="content">
<span class="vrcx-native-tooltip__text" v-text="content" />
</slot>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue';
const props = defineProps({
content: {
type: String,
default: ''
},
showAfter: {
type: Number,
default: 0
},
placement: {
type: String,
default: 'top'
},
enterMs: {
type: Number,
default: 120
},
exitMs: {
type: Number,
default: 100
},
offset: {
type: Number,
default: 8
},
maxWidth: {
type: String,
default: '260px'
},
disabled: {
type: Boolean,
default: false
},
showArrow: {
type: Boolean,
default: true
}
});
const ARROW_SIZE_PX = 10;
const triggerEl = ref(null);
const tooltipEl = ref(null);
const isOpen = ref(false);
const isClosing = ref(false);
const anchorName = `--vrcx-tt-${Math.random().toString(36).slice(2, 10)}`;
const triggerStyle = computed(() => {
return {
'anchor-name': anchorName
};
});
const contentStyle = computed(() => {
const effectiveOffsetPx = props.offset + (props.showArrow ? ARROW_SIZE_PX / 2 : 0);
return {
'position-anchor': anchorName,
'--vrcx-tt-enter': `${props.enterMs}ms`,
'--vrcx-tt-exit': `${props.exitMs}ms`,
'--vrcx-tt-offset': `${effectiveOffsetPx}px`,
'--vrcx-tt-max-width': props.maxWidth,
'--vrcx-tt-shift-x': `${shiftX.value}px`,
'--vrcx-tt-shift-y': `${shiftY.value}px`,
'--vrcx-tt-arrow-x': `${arrowX.value}px`,
'--vrcx-tt-arrow-y': `${arrowY.value}px`
};
});
const placementClass = computed(() => {
const normalized = String(props.placement || 'top').toLowerCase();
return `is-${normalized}`;
});
const shiftX = ref(0);
const shiftY = ref(0);
const arrowX = ref(0);
const arrowY = ref(0);
const timers = {
open: 0,
close: 0,
hide: 0
};
function clearTimer(key) {
const id = timers[key];
if (id) {
window.clearTimeout(id);
timers[key] = 0;
}
}
function clearAllTimers() {
clearTimer('open');
clearTimer('close');
clearTimer('hide');
}
function resetOffsets() {
shiftX.value = 0;
shiftY.value = 0;
arrowX.value = 0;
arrowY.value = 0;
}
function isPopoverOpen(el) {
return Boolean(el?.matches?.(':popover-open'));
}
function updateViewportShift() {
const el = tooltipEl.value;
if (!el) {
return;
}
shiftX.value = 0;
shiftY.value = 0;
const rect = el.getBoundingClientRect();
const margin = 8;
const vw = window.innerWidth;
const vh = window.innerHeight;
let dx = 0;
let dy = 0;
if (rect.left < margin) {
dx += margin - rect.left;
}
if (rect.right > vw - margin) {
dx -= rect.right - (vw - margin);
}
if (rect.top < margin) {
dy += margin - rect.top;
}
if (rect.bottom > vh - margin) {
dy -= rect.bottom - (vh - margin);
}
shiftX.value = Math.round(dx);
shiftY.value = Math.round(dy);
}
function updateArrowPosition() {
if (!props.showArrow) {
return;
}
const trigger = triggerEl.value;
const tooltip = tooltipEl.value;
if (!trigger || !tooltip) {
return;
}
const placement = String(props.placement || 'top').toLowerCase();
const tr = trigger.getBoundingClientRect();
const tt = tooltip.getBoundingClientRect();
const cs = window.getComputedStyle(tooltip);
const padLeft = Number.parseFloat(cs.paddingLeft) || 0;
const padRight = Number.parseFloat(cs.paddingRight) || 0;
const padTop = Number.parseFloat(cs.paddingTop) || 0;
const padBottom = Number.parseFloat(cs.paddingBottom) || 0;
const padding = 12;
const half = ARROW_SIZE_PX / 2;
if (placement.startsWith('top') || placement.startsWith('bottom')) {
const desired = tr.left + tr.width / 2 - tt.left;
const edgeLeft = Math.max(padding, padLeft) + half;
const edgeRight = Math.max(padding, padRight) + half;
const min = edgeLeft;
const max = tt.width - edgeRight;
const clamped = min > max ? tt.width / 2 : Math.min(Math.max(desired, min), max);
arrowX.value = Math.round(clamped);
arrowY.value = 0;
return;
}
if (placement.startsWith('left') || placement.startsWith('right')) {
const desired = tr.top + tr.height / 2 - tt.top;
const edgeTop = Math.max(padding, padTop) + half;
const edgeBottom = Math.max(padding, padBottom) + half;
const min = edgeTop;
const max = tt.height - edgeBottom;
const clamped = min > max ? tt.height / 2 : Math.min(Math.max(desired, min), max);
arrowY.value = Math.round(clamped);
arrowX.value = 0;
}
}
function open() {
if (props.disabled) {
return;
}
const el = tooltipEl.value;
if (!el) {
return;
}
clearAllTimers();
const doOpen = () => {
timers.open = 0;
const tooltip = tooltipEl.value;
if (!tooltip) {
return;
}
const alreadyOpen = isPopoverOpen(tooltip);
isClosing.value = false;
if (!alreadyOpen) {
isOpen.value = false;
tooltip.showPopover();
}
window.requestAnimationFrame(() => {
updateViewportShift();
window.requestAnimationFrame(() => {
updateArrowPosition();
isOpen.value = true;
});
});
};
if (props.showAfter > 0) {
timers.open = window.setTimeout(doOpen, props.showAfter);
return;
}
doOpen();
}
function close(immediate = false) {
const el = tooltipEl.value;
if (!el) {
return;
}
clearAllTimers();
if (immediate) {
isOpen.value = false;
isClosing.value = false;
resetOffsets();
if (isPopoverOpen(el)) {
el.hidePopover();
}
return;
}
isOpen.value = false;
isClosing.value = true;
timers.hide = window.setTimeout(() => {
timers.hide = 0;
isClosing.value = false;
resetOffsets();
if (isPopoverOpen(el)) {
el.hidePopover();
}
}, props.exitMs);
}
function onEnter() {
open();
}
function onLeave() {
clearTimer('open');
clearTimer('close');
timers.close = window.setTimeout(() => {
timers.close = 0;
close();
}, 80);
}
function cancelClose() {
clearTimer('close');
clearTimer('hide');
if (isPopoverOpen(tooltipEl.value)) {
isClosing.value = false;
isOpen.value = true;
}
}
onBeforeUnmount(() => {
close(true);
clearAllTimers();
});
</script>
<style scoped>
.vrcx-native-tooltip__trigger {
display: inline-flex;
align-items: center;
justify-content: center;
}
.vrcx-native-tooltip__content {
position: fixed;
inset: auto;
overflow: visible;
clip-path: none;
inline-size: max-content;
max-inline-size: min(var(--vrcx-tt-max-width), calc(100vw - 16px));
min-inline-size: 0;
padding: 6px 10px;
border-radius: var(--el-border-radius-base);
border: 0;
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 88%, transparent));
color: var(--el-tooltip-text-color, var(--el-color-white));
box-shadow: none;
font-size: 12px;
line-height: 1.35;
white-space: pre-line;
word-break: break-word;
overflow-wrap: anywhere;
opacity: 0;
transition-property: opacity;
transition-duration: var(--vrcx-tt-exit);
transition-timing-function: linear;
transition-behavior: allow-discrete;
pointer-events: auto;
}
:global(html.dark) .vrcx-native-tooltip__content {
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 96%, transparent));
}
.vrcx-native-tooltip__content.has-arrow::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 88%, transparent));
transform: rotate(45deg);
}
:global(html.dark) .vrcx-native-tooltip__content.has-arrow::before {
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 96%, transparent));
}
.vrcx-native-tooltip__content.has-arrow.is-top::before,
.vrcx-native-tooltip__content.has-arrow.is-top-start::before,
.vrcx-native-tooltip__content.has-arrow.is-top-end::before {
left: var(--vrcx-tt-arrow-x, 50%);
bottom: -5px;
translate: -50% 0;
}
.vrcx-native-tooltip__content.has-arrow.is-bottom::before,
.vrcx-native-tooltip__content.has-arrow.is-bottom-start::before,
.vrcx-native-tooltip__content.has-arrow.is-bottom-end::before {
left: var(--vrcx-tt-arrow-x, 50%);
top: -5px;
translate: -50% 0;
}
.vrcx-native-tooltip__content.has-arrow.is-left::before,
.vrcx-native-tooltip__content.has-arrow.is-left-start::before,
.vrcx-native-tooltip__content.has-arrow.is-left-end::before {
top: var(--vrcx-tt-arrow-y, 50%);
right: -5px;
translate: 0 -50%;
}
.vrcx-native-tooltip__content.has-arrow.is-right::before,
.vrcx-native-tooltip__content.has-arrow.is-right-start::before,
.vrcx-native-tooltip__content.has-arrow.is-right-end::before {
top: var(--vrcx-tt-arrow-y, 50%);
left: -5px;
translate: 0 -50%;
}
.vrcx-native-tooltip__content:popover-open.is-open {
opacity: 1;
transition-duration: var(--vrcx-tt-enter);
}
.vrcx-native-tooltip__content:popover-open.is-closing {
opacity: 0;
transition-duration: var(--vrcx-tt-exit);
}
.vrcx-native-tooltip__content.is-top {
left: anchor(center);
bottom: anchor(top);
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
transform-origin: bottom center;
}
.vrcx-native-tooltip__content.is-top-start {
left: anchor(left);
bottom: anchor(top);
translate: var(--vrcx-tt-shift-x) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
transform-origin: bottom left;
}
.vrcx-native-tooltip__content.is-top-end {
right: anchor(right);
bottom: anchor(top);
translate: var(--vrcx-tt-shift-x) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
transform-origin: bottom right;
}
.vrcx-native-tooltip__content.is-bottom {
left: anchor(center);
top: anchor(bottom);
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
transform-origin: top center;
}
.vrcx-native-tooltip__content.is-bottom-start {
left: anchor(left);
top: anchor(bottom);
translate: var(--vrcx-tt-shift-x) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
transform-origin: top left;
}
.vrcx-native-tooltip__content.is-bottom-end {
right: anchor(right);
top: anchor(bottom);
translate: var(--vrcx-tt-shift-x) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
transform-origin: top right;
}
.vrcx-native-tooltip__content.is-left {
right: anchor(left);
top: anchor(center);
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) calc(-50% + var(--vrcx-tt-shift-y));
transform-origin: center right;
}
.vrcx-native-tooltip__content.is-left-start {
right: anchor(left);
top: anchor(top);
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
transform-origin: top right;
}
.vrcx-native-tooltip__content.is-left-end {
right: anchor(left);
bottom: anchor(bottom);
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
transform-origin: bottom right;
}
.vrcx-native-tooltip__content.is-right {
left: anchor(right);
top: anchor(center);
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) calc(-50% + var(--vrcx-tt-shift-y));
transform-origin: center left;
}
.vrcx-native-tooltip__content.is-right-start {
left: anchor(right);
top: anchor(top);
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
transform-origin: top left;
}
.vrcx-native-tooltip__content.is-right-end {
left: anchor(right);
bottom: anchor(bottom);
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
transform-origin: bottom left;
}
.vrcx-native-tooltip__content:not([class*='is-']) {
left: anchor(center);
bottom: anchor(top);
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
}
.vrcx-native-tooltip__text {
display: block;
white-space: pre-line;
}
</style>
+282 -345
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="x-menu-container nav-menu-container"> <div class="x-menu-container nav-menu-container" :class="{ 'is-collapsed': isCollapsed }">
<template v-if="navLayoutReady"> <template v-if="navLayoutReady">
<div> <div class="nav-menu-body mt-5">
<div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog"> <div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog">
<el-progress <el-progress
type="circle" type="circle"
@@ -16,80 +16,71 @@
type="success" type="success"
plain plain
style="font-size: 19px; height: 36px; width: 44px; margin: 10px" style="font-size: 19px; height: 36px; width: 44px; margin: 10px"
@click="showVRCXUpdateDialog" @click="showVRCXUpdateDialog">
><i class="ri-download-line"></i <i class="ri-download-line"></i>
></el-button> </el-button>
</div> </div>
<el-menu collapse :default-active="activeMenuIndex" :collapse-transition="false" ref="navMenuRef"> <el-menu ref="navMenuRef" class="nav-menu" :collapse="isCollapsed" :collapse-transition="false">
<el-popover <template v-for="item in menuItems" :key="item.index">
v-for="item in navMenuItems" <el-menu-item
:disabled="!item.entries?.length" v-if="!item.children?.length"
:key="item.index" :index="item.index"
:ref="(el) => setNavPopoverRef(el, item.index)" :class="{ notify: isNavItemNotified(item) }"
placement="right-start" @click="handleMenuItemClick(item)">
trigger="hover" <i :class="item.icon"></i>
:hide-after="isSteamVRRunning ? 400 : 150" <template #title>
:show-arrow="false"
:offset="0"
:width="navPopoverWidth"
transition="nav-menu-slide"
@before-enter="handleSubMenuBeforeEnter()"
:popper-style="navPopoverStyle"
popper-class="nav-menu-popover-popper">
<div class="nav-menu-popover">
<div class="nav-menu-popover__header">
<i :class="item.icon"></i>
<span>{{ item.titleIsCustom ? item.title : t(item.title || '') }}</span> <span>{{ item.titleIsCustom ? item.title : t(item.title || '') }}</span>
</div> </template>
</el-menu-item>
<div class="nav-menu-popover__menu"> <el-sub-menu v-else :index="item.index">
<button <template #title>
v-for="entry in item.entries" <div :class="{ notify: isNavItemNotified(item) }">
:key="entry.label" <i :class="item.icon"></i>
type="button" <span v-show="!isCollapsed">{{
:class="['nav-menu-popover__menu-item', { notify: isEntryNotified(entry) }]" item.titleIsCustom ? item.title : t(item.title || '')
@click="handleSubmenuClick(entry, item.index)"> }}</span>
<i v-if="entry.icon" :class="entry.icon" class="nav-menu-popover__menu-icon"></i> </div>
<span class="nav-menu-popover__menu-label">{{ t(entry.label) }}</span> </template>
</button>
</div>
</div>
<template #reference>
<el-menu-item <el-menu-item
:index="item.index" v-for="entry in item.children"
:class="{ notify: isNavItemNotified(item) }" :key="entry.index"
@click="handleMenuItemClick(item)"> :index="entry.index"
<i :class="item.icon"></i> class="pl-8!"
<template #title v-if="item.tooltip"> :class="{ notify: isEntryNotified(entry) }"
<span>{{ item.tooltipIsCustom ? item.tooltip : t(item.tooltip) }}</span> @click="handleSubmenuClick(entry, item.index)">
<i v-show="entry.icon" :class="entry.icon"></i>
<template #title>
<span>{{ t(entry.label) }}</span>
</template> </template>
</el-menu-item> </el-menu-item>
</template> </el-sub-menu>
</el-popover> </template>
</el-menu> </el-menu>
<el-divider style="width: calc(100% - 18px); margin-left: 9px"></el-divider>
<NativeTooltip :content="t('prompt.direct_access_omni.header')" placement="right">
<div class="bottom-button" @click="directAccessPaste"><i class="ri-compass-3-line"></i></div>
</NativeTooltip>
</div> </div>
<div class="nav-menu-container-bottom"> <div class="nav-menu-container-bottom mb-4">
<NativeTooltip v-if="branch === 'Nightly'" :show-after="150" :content="'Feedback'" placement="right"> <el-tooltip
v-if="branch === 'Nightly'"
:show-after="150"
:content="'Feedback'"
:disabled="!isCollapsed"
placement="right">
<div <div
class="bottom-button" class="bottom-button"
id="feedback" id="feedback"
@click="!sentryErrorReporting && setSentryErrorReporting()"> @click="!sentryErrorReporting && setSentryErrorReporting()">
<i class="ri-feedback-line"></i> <i class="ri-feedback-line"></i>
<span v-show="!isCollapsed" class="bottom-button__label">Feedback</span>
</div> </div>
</NativeTooltip> </el-tooltip>
<el-popover <el-popover
v-model:visible="supportMenuVisible" v-model:visible="supportMenuVisible"
placement="right" placement="right"
trigger="click" trigger="click"
popper-style="padding:4px;border-radius:8px;" popper-style="padding:4px;border-radius:8px;"
:offset="4" :offset="-10"
:show-arrow="false" :show-arrow="false"
:width="200" :width="200"
:hide-after="0"> :hide-after="0">
@@ -119,11 +110,18 @@
</div> </div>
<template #reference> <template #reference>
<div> <div>
<NativeTooltip :show-after="150" :content="t('nav_tooltip.help_support')" placement="right"> <el-tooltip
:show-after="150"
:content="t('nav_tooltip.help_support')"
placement="right"
:disabled="!isCollapsed">
<div class="bottom-button"> <div class="bottom-button">
<i class="ri-question-line"></i> <i class="ri-question-line"></i>
<span v-show="!isCollapsed" class="bottom-button__label">{{
t('nav_tooltip.help_support')
}}</span>
</div> </div>
</NativeTooltip> </el-tooltip>
</div> </div>
</template> </template>
</el-popover> </el-popover>
@@ -133,7 +131,7 @@
placement="right" placement="right"
trigger="click" trigger="click"
popper-style="padding:4px;border-radius:8px;" popper-style="padding:4px;border-radius:8px;"
:offset="4" :offset="-10"
:show-arrow="false" :show-arrow="false"
:width="200" :width="200"
:hide-after="0"> :hide-after="0">
@@ -143,7 +141,7 @@
<div class="nav-menu-settings__meta"> <div class="nav-menu-settings__meta">
<span class="nav-menu-settings__title" @click="openGithub" <span class="nav-menu-settings__title" @click="openGithub"
>VRCX >VRCX
<i class="ri-heart-3-fill" style="color: #64cd8a; font-size: 14px"></i> <i class="ri-heart-3-fill nav-menu-settings__heart"></i>
</span> </span>
<span class="nav-menu-settings__version">{{ version }}</span> <span class="nav-menu-settings__version">{{ version }}</span>
</div> </div>
@@ -170,7 +168,7 @@
:class="{ 'is-active': themeMode === theme }" :class="{ 'is-active': themeMode === theme }"
@click="handleThemeSelect(theme)"> @click="handleThemeSelect(theme)">
<span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span> <span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span>
<span v-if="themeMode === theme" class="nav-menu-theme__check"></span> <span v-if="themeMode === theme" class="nav-menu-theme__check"></span>
</button> </button>
</div> </div>
<template #reference> <template #reference>
@@ -190,9 +188,24 @@
<template #reference> <template #reference>
<div class="bottom-button"> <div class="bottom-button">
<i class="ri-settings-3-line"></i> <i class="ri-settings-3-line"></i>
<span v-show="!isCollapsed" class="bottom-button__label">{{
t('nav_tooltip.manage')
}}</span>
</div> </div>
</template> </template>
</el-popover> </el-popover>
<el-tooltip
:show-after="150"
:content="t('nav_tooltip.expand_menu')"
:disabled="!isCollapsed"
placement="right">
<div class="bottom-button" @click="toggleNavCollapse">
<i class="ri-side-bar-line"></i>
<span v-show="!isCollapsed" class="bottom-button__label">{{
t('nav_tooltip.collapse_menu')
}}</span>
</div>
</el-tooltip>
</div> </div>
</template> </template>
</div> </div>
@@ -215,9 +228,9 @@
useAdvancedSettingsStore, useAdvancedSettingsStore,
useAppearanceSettingsStore, useAppearanceSettingsStore,
useAuthStore, useAuthStore,
useGameStore,
useSearchStore, useSearchStore,
useUiStore, useUiStore,
useUserStore,
useVRCXUpdaterStore useVRCXUpdaterStore
} from '../stores'; } from '../stores';
import { THEME_CONFIG, links, navDefinitions } from '../shared/constants'; import { THEME_CONFIG, links, navDefinitions } from '../shared/constants';
@@ -226,8 +239,6 @@
import configRepository from '../service/config'; import configRepository from '../service/config';
import 'remixicon/fonts/remixicon.css';
const CustomNavDialog = defineAsyncComponent(() => import('./dialogs/CustomNavDialog.vue')); const CustomNavDialog = defineAsyncComponent(() => import('./dialogs/CustomNavDialog.vue'));
const { t, locale } = useI18n(); const { t, locale } = useI18n();
@@ -257,26 +268,13 @@
}, },
{ type: 'item', key: 'notification' }, { type: 'item', key: 'notification' },
{ type: 'item', key: 'charts' }, { type: 'item', key: 'charts' },
{ type: 'item', key: 'tools' } { type: 'item', key: 'tools' },
{ type: 'item', key: 'direct-access' }
]; ];
const navDefinitionMap = new Map(navDefinitions.map((item) => [item.key, item])); const navDefinitionMap = new Map(navDefinitions.map((item) => [item.key, item]));
const DEFAULT_FOLDER_ICON = 'ri-menu-fold-line'; const DEFAULT_FOLDER_ICON = 'ri-menu-fold-line';
const navPopoverWidth = 250;
const navPopoverStyle = {
zIndex: 500,
borderRadius: '0',
border: '1px solid var(--el-border-color)',
borderLeft: 'none',
borderBottom: 'none',
borderTop: 'none',
boxShadow: '0 8px 20px rgba(0,0,0,0.05)',
padding: '0',
background: 'var(--el-bg-color)',
height: '100vh'
};
const VRCXUpdaterStore = useVRCXUpdaterStore(); const VRCXUpdaterStore = useVRCXUpdaterStore();
const { pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress, branch, appVersion } = const { pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress, branch, appVersion } =
storeToRefs(VRCXUpdaterStore); storeToRefs(VRCXUpdaterStore);
@@ -288,18 +286,19 @@
const { setSentryErrorReporting } = useAdvancedSettingsStore(); const { setSentryErrorReporting } = useAdvancedSettingsStore();
const { logout } = useAuthStore(); const { logout } = useAuthStore();
const appearanceSettingsStore = useAppearanceSettingsStore(); const appearanceSettingsStore = useAppearanceSettingsStore();
const { themeMode } = storeToRefs(appearanceSettingsStore); const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
const { isSteamVRRunning } = storeToRefs(useGameStore()); const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
const { showUserDialog } = userStore;
const settingsMenuVisible = ref(false); const settingsMenuVisible = ref(false);
const themeMenuVisible = ref(false); const themeMenuVisible = ref(false);
const supportMenuVisible = ref(false); const supportMenuVisible = ref(false);
const navMenuRef = ref(null); const navMenuRef = ref(null);
const navPopoverRefs = new Map();
const navLayout = ref([]); const navLayout = ref([]);
const navLayoutReady = ref(false); const navLayoutReady = ref(false);
const navMenuItems = computed(() => { const menuItems = computed(() => {
const items = []; const items = [];
navLayout.value.forEach((entry) => { navLayout.value.forEach((entry) => {
if (entry.type === 'item') { if (entry.type === 'item') {
@@ -310,7 +309,7 @@
items.push({ items.push({
...definition, ...definition,
index: definition.key, index: definition.key,
tooltipIsCustom: false, title: definition.tooltip || definition.labelKey,
titleIsCustom: false titleIsCustom: false
}); });
return; return;
@@ -324,7 +323,6 @@
items.push({ items.push({
...definition, ...definition,
index: definition.key, index: definition.key,
tooltipIsCustom: false,
titleIsCustom: false titleIsCustom: false
}); });
}); });
@@ -334,83 +332,41 @@
const folderEntries = folderDefinitions.map((definition) => ({ const folderEntries = folderDefinitions.map((definition) => ({
label: definition.labelKey, label: definition.labelKey,
routeName: definition.routeName, routeName: definition.routeName,
key: definition.key, index: definition.key,
icon: definition.icon icon: definition.icon,
action: definition.action
})); }));
items.push({ items.push({
index: entry.id, index: entry.id,
icon: entry.icon || DEFAULT_FOLDER_ICON, icon: entry.icon || DEFAULT_FOLDER_ICON,
tooltip: entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'),
tooltipIsCustom: true,
title: entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'), title: entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'),
titleIsCustom: true, titleIsCustom: true,
entries: folderEntries children: folderEntries
}); });
} }
}); });
return items; return items;
}); });
const folderCyclePointers = new Map();
const navigateToFolderEntry = (folderIndex, entry) => {
if (!entry) {
return;
}
if (entry.routeName) {
handleRouteChange(entry.routeName, folderIndex);
return;
}
if (entry.path) {
router.push(entry.path);
if (folderIndex) {
navMenuRef.value?.updateActiveIndex(folderIndex);
}
}
};
const handleFolderCycleNavigation = (item) => {
if (!item?.entries?.length) {
return;
}
const entries = item.entries.filter((entry) => Boolean(entry?.routeName || entry?.path));
if (!entries.length) {
return;
}
let pointer = folderCyclePointers.get(item.index) ?? 0;
if (pointer >= entries.length || pointer < 0) {
pointer = 0;
}
const entry = entries[pointer];
folderCyclePointers.set(item.index, (pointer + 1) % entries.length);
navigateToFolderEntry(item.index, entry);
};
const activeMenuIndex = computed(() => { const activeMenuIndex = computed(() => {
const currentRouteName = router.currentRoute.value?.name; const currentRoute = router.currentRoute.value;
if (!currentRouteName) { const currentRouteName = currentRoute?.name;
const firstEntry = navLayout.value[0]; const navKey = currentRoute?.meta?.navKey || currentRouteName;
if (!firstEntry) { if (!navKey) {
return 'feed'; return getFirstNavRoute(navLayout.value) || 'feed';
}
return firstEntry.type === 'folder' ? firstEntry.id : firstEntry.key;
} }
for (const entry of navLayout.value) { for (const entry of navLayout.value) {
if (entry.type === 'item' && entry.key === currentRouteName) { if (entry.type === 'item' && entry.key === navKey) {
return entry.key; return entry.key;
} }
if (entry.type === 'folder' && entry.items?.includes(currentRouteName)) { if (entry.type === 'folder' && entry.items?.includes(navKey)) {
return entry.id; return navKey;
} }
} }
const fallback = navLayout.value[0]; return getFirstNavRoute(navLayout.value) || 'feed';
if (!fallback) {
return 'feed';
}
return fallback.type === 'folder' ? fallback.id : fallback.key;
}); });
const version = computed(() => appVersion.value?.split('VRCX ')?.[1] || '-'); const version = computed(() => appVersion.value?.split('VRCX ')?.[1] || '-');
@@ -448,6 +404,10 @@
const generateFolderId = () => `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`; const generateFolderId = () => `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`;
const showCurrentUserDialog = () => {
showUserDialog(currentUser.value?.id);
};
const sanitizeLayout = (layout) => { const sanitizeLayout = (layout) => {
const usedKeys = new Set(); const usedKeys = new Set();
const normalized = []; const normalized = [];
@@ -627,52 +587,47 @@
if (notifiedMenus.value.includes(item.index)) { if (notifiedMenus.value.includes(item.index)) {
return true; return true;
} }
if (item.entries?.length) { if (item.children?.length) {
return item.entries.some((entry) => isEntryNotified(entry)); return item.children.some((entry) => isEntryNotified(entry));
} }
return false; return false;
}; };
const setNavPopoverRef = (el, index) => { const closeNavFlyouts = () => {
if (!index) {
return;
}
if (el) {
navPopoverRefs.set(index, el);
} else {
navPopoverRefs.delete(index);
}
};
const closeNavPopover = (index) => {
navPopoverRefs.get(index)?.hide?.();
};
const handleSubmenuClick = (entry, index) => {
if (!entry) {
return;
}
const entries = navMenuItems.value.find((item) => item.index === index)?.entries || [];
const indexOfEntry = entries.findIndex((e) => e.label === entry.label);
folderCyclePointers.set(index, (indexOfEntry + 1) % entries.length);
if (entry.routeName) {
handleRouteChange(entry.routeName, index || entry.routeName);
} else if (entry.path) {
router.push(entry.path);
if (index) {
navMenuRef.value?.updateActiveIndex(index);
}
}
closeNavPopover(index);
};
const handleSubMenuBeforeEnter = () => {
settingsMenuVisible.value = false; settingsMenuVisible.value = false;
supportMenuVisible.value = false; supportMenuVisible.value = false;
themeMenuVisible.value = false; themeMenuVisible.value = false;
}; };
const triggerNavAction = (entry, navIndex = entry?.index) => {
if (!entry) {
return;
}
if (entry.action === 'direct-access') {
closeNavFlyouts();
directAccessPaste();
if (navIndex) {
navMenuRef.value?.updateActiveIndex(navIndex);
}
return;
}
if (entry.routeName) {
handleRouteChange(entry.routeName, navIndex);
closeNavFlyouts();
return;
}
if (entry.path) {
router.push(entry.path);
if (navIndex) {
navMenuRef.value?.updateActiveIndex(navIndex);
}
closeNavFlyouts();
}
};
const handleRouteChange = (routeName, navIndex = routeName) => { const handleRouteChange = (routeName, navIndex = routeName) => {
if (!routeName) { if (!routeName) {
return; return;
@@ -697,17 +652,23 @@
} }
}); });
const getFirstNavRoute = (layout) => { function getFirstNavRoute(layout) {
for (const entry of layout) { for (const entry of layout) {
if (entry.type === 'item') { if (entry.type === 'item') {
return entry.key; const definition = navDefinitionMap.get(entry.key);
if (definition?.routeName) {
return definition.routeName;
}
} }
if (entry.type === 'folder' && entry.items?.length) { if (entry.type === 'folder' && entry.items?.length) {
return entry.items[0]; const definition = entry.items.map((key) => navDefinitionMap.get(key)).find((def) => def?.routeName);
if (definition?.routeName) {
return definition.routeName;
}
} }
} }
return null; return null;
}; }
let hasNavigatedToInitialRoute = false; let hasNavigatedToInitialRoute = false;
const navigateToFirstNavEntry = () => { const navigateToFirstNavEntry = () => {
@@ -724,15 +685,17 @@
} }
}; };
const handleSubmenuClick = (entry, index) => {
const navIndex = index || entry?.index;
triggerNavAction(entry, navIndex);
};
const handleMenuItemClick = (item) => { const handleMenuItemClick = (item) => {
if (!item) { triggerNavAction(item, item?.index);
return; };
}
if (item.entries?.length) { const toggleNavCollapse = () => {
handleFolderCycleNavigation(item); appearanceSettingsStore.toggleNavCollapsed();
return;
}
handleRouteChange(item.routeName, item.index);
}; };
onMounted(async () => { onMounted(async () => {
@@ -755,146 +718,147 @@
:deep(.el-divider) { :deep(.el-divider) {
margin: 0; margin: 0;
} }
.nav-menu-container { .nav-menu-container {
position: relative; position: relative;
width: 240px;
height: 100%; height: 100%;
display: flex; display: flex;
flex: 0 0 240px;
flex-direction: column; flex-direction: column;
align-items: center; align-items: stretch;
justify-content: space-between; justify-content: flex-start;
z-index: 600; z-index: 600;
background-color: var(--el-bg-color); background-color: var(--el-bg-color-page);
border-right: 1px solid var(--el-border-color);
box-shadow: none; box-shadow: none;
.el-menu { backdrop-filter: blur(14px) saturate(130%);
background: 0; }
border: 0;
} .nav-menu-body {
.el-menu-item i[class*='ri-'] { display: flex;
font-size: 19px; flex-direction: column;
width: 24px; flex: 1;
height: 24px; overflow: hidden auto;
display: inline-flex; align-items: center;
align-items: center; }
justify-content: center;
text-align: center; .nav-menu {
vertical-align: middle; background: transparent;
} border: 0;
.bottom-button { width: 100%;
font-size: 19px; }
width: 64px;
height: 56px; .nav-menu :deep(.el-menu-item),
display: inline-flex; .nav-menu :deep(.el-sub-menu__title) {
align-items: center; height: 46px;
justify-content: center; line-height: 46px;
text-align: center; display: flex;
vertical-align: middle; align-items: center;
cursor: pointer; column-gap: 10px;
} font-size: 13px;
.bottom-button:hover { padding: 0 20px !important;
background-color: var(--el-menu-hover-bg-color); }
transition:
border-color var(--el-transition-duration), .nav-menu :deep(.el-menu-item i[class*='ri-']),
background-color var(--el-transition-duration), .nav-menu :deep(.el-sub-menu__title i[class*='ri-']) {
color var(--el-transition-duration); font-size: 19px;
} width: 24px;
.nav-menu-container-bottom { height: 24px;
display: flex; display: inline-flex;
flex-direction: column; align-items: center;
justify-content: center;
text-align: center;
vertical-align: middle;
line-height: 1;
flex-shrink: 0;
}
.nav-menu :deep(.el-sub-menu__title > div) {
display: inline-flex;
align-items: center;
gap: 10px;
}
.nav-menu :deep(.el-sub-menu__icon-arrow) {
right: 8px;
}
.bottom-button {
font-size: 19px;
width: 100%;
height: 46px;
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 0 20px;
text-align: left;
vertical-align: middle;
cursor: pointer;
box-sizing: border-box;
& > span {
font-size: 13px;
} }
} }
.nav-menu-popover { .bottom-button i {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.bottom-button__label {
font-size: 13px;
color: var(--el-text-color-regular);
white-space: nowrap;
}
.bottom-button:hover {
background-color: var(--el-menu-hover-bg-color);
transition:
border-color var(--el-transition-duration),
background-color var(--el-transition-duration),
color var(--el-transition-duration);
}
.nav-menu-container-bottom {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; }
min-width: 240px;
background-color: var(--el-bg-color);
border-left: 1px solid var(--el-border-color);
overflow: hidden;
.nav-menu-popover__header { .nav-menu-container.is-collapsed .nav-menu :deep(.el-menu-item),
display: inline-flex; .nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title) {
align-items: center; column-gap: 0;
gap: 10px; justify-content: center;
min-height: 52px; padding: 0;
padding: 0 20px; }
border-bottom: 1px solid var(--el-border-color-light, var(--el-border-color));
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.nav-menu-popover__header i { .nav-menu-container.is-collapsed {
font-size: 18px; width: 64px;
color: var(--el-color-primary); flex-basis: 64px;
} }
.nav-menu-popover__menu { .nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title > div) {
display: flex; gap: 0;
flex-direction: column; }
flex: 1;
gap: 6px;
padding: 12px 12px 16px;
overflow-y: auto;
scrollbar-width: thin;
}
.nav-menu-popover__menu::-webkit-scrollbar { .nav-menu-container.is-collapsed .bottom-button {
width: 6px; width: 100%;
} justify-content: center;
gap: 0;
padding: 0;
text-align: center;
}
.nav-menu-popover__menu::-webkit-scrollbar-thumb { :deep(.el-menu-item .el-menu-tooltip__trigger) {
background-color: rgba(0, 0, 0, 0.18); justify-content: center;
border-radius: 3px; }
}
.nav-menu-popover__menu::-webkit-scrollbar-track { :deep(.el-button.is-text:not(.is-disabled):hover) {
background: transparent; background-color: var(--el-menu-hover-bg-color);
}
.nav-menu-popover__menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 10px 12px;
border: none;
background: transparent;
text-align: left;
color: var(--el-text-color-primary);
font-size: 13px;
border-radius: 6px;
cursor: pointer;
transition: background-color var(--el-transition-duration);
}
.nav-menu-popover__menu-item.notify::after {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--el-text-color-primary);
margin-left: auto;
}
.nav-menu-popover__menu-item:hover {
background-color: var(--el-menu-hover-bg-color);
}
.nav-menu-popover__menu-item:focus-visible {
outline: 2px solid var(--el-color-primary);
outline-offset: 2px;
}
.nav-menu-popover__menu-icon {
font-size: 16px;
color: var(--el-text-color-secondary);
}
.nav-menu-popover__menu-label {
font-weight: 600;
}
} }
.nav-menu-settings { .nav-menu-settings {
@@ -930,6 +894,11 @@
cursor: pointer; cursor: pointer;
} }
.nav-menu-settings__heart {
font-size: 14px;
color: var(--el-color-success);
}
.nav-menu-settings__version { .nav-menu-settings__version {
font-size: 11px; font-size: 11px;
} }
@@ -942,8 +911,8 @@
width: 100%; width: 100%;
border: none; border: none;
background: transparent; background: transparent;
color: var(--el-text-color-primary); color: var(--el-text-color-regular);
font-size: 14px; font-size: 13px;
border-radius: 4px; border-radius: 4px;
transition: background-color var(--el-transition-duration); transition: background-color var(--el-transition-duration);
cursor: pointer; cursor: pointer;
@@ -962,7 +931,7 @@
} }
.nav-menu-settings__item--danger:hover { .nav-menu-settings__item--danger:hover {
background-color: rgba(245, 108, 108, 0.18); background-color: color-mix(in oklch, var(--el-color-danger) 18%, transparent);
} }
} }
@@ -971,23 +940,6 @@
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
.nav-menu-support__search {
padding: 10px 12px;
border-radius: 8px;
background: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
font-size: 12px;
font-weight: 500;
line-height: 1.2;
}
.nav-menu-support__heading {
padding: 4px 12px 0;
font-size: 13px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.nav-menu-support__section { .nav-menu-support__section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1015,7 +967,6 @@
padding: 6px 10px; padding: 6px 10px;
border: none; border: none;
background: transparent; background: transparent;
color: var(--el-text-color-primary);
font-size: 13px; font-size: 13px;
border-radius: 6px; border-radius: 6px;
transition: background-color var(--el-transition-duration); transition: background-color var(--el-transition-duration);
@@ -1031,18 +982,4 @@
background-color: var(--el-menu-hover-bg-color); background-color: var(--el-menu-hover-bg-color);
} }
} }
:global(.nav-menu-slide-enter-active),
:global(.nav-menu-slide-leave-active) {
transition:
opacity 0.1s ease,
transform 0.1s ease;
transform-origin: left center;
}
:global(.nav-menu-slide-enter-from),
:global(.nav-menu-slide-leave-to) {
opacity: 0;
transform: translateX(-12px);
}
</style> </style>
@@ -4,7 +4,8 @@
class="x-dialog x-avatar-dialog" class="x-dialog x-avatar-dialog"
v-model="avatarDialog.visible" v-model="avatarDialog.visible"
:show-close="false" :show-close="false"
width="700px"> top="10vh"
width="930px">
<div v-loading="avatarDialog.loading"> <div v-loading="avatarDialog.loading">
<div style="display: flex"> <div style="display: flex">
<img <img
@@ -246,11 +247,7 @@
style="margin-left: 5px" style="margin-left: 5px"
@click="selectAvatarWithoutConfirmation(avatarDialog.id)"></el-button> @click="selectAvatarWithoutConfirmation(avatarDialog.id)"></el-button>
</el-tooltip> </el-tooltip>
<el-dropdown <el-dropdown trigger="click" style="margin-left: 5px" @command="avatarDialogCommand">
trigger="click"
size="small"
style="margin-left: 5px"
@command="avatarDialogCommand">
<el-button <el-button
:type="avatarDialog.isBlocked ? 'danger' : 'default'" :type="avatarDialog.isBlocked ? 'danger' : 'default'"
:icon="MoreFilled" :icon="MoreFilled"
@@ -321,7 +321,7 @@
} }
</script> </script>
<style lang="scss" scoped> <style scoped>
.img-size { .img-size {
width: 500px; width: 500px;
height: 375px; height: 375px;
@@ -68,12 +68,12 @@
<span <span
v-if="avatar.releaseStatus === 'public'" v-if="avatar.releaseStatus === 'public'"
class="extra" class="extra"
style="color: #67c23a" style="color: var(--el-color-success)"
v-text="avatar.releaseStatus"></span> v-text="avatar.releaseStatus"></span>
<span <span
v-else-if="avatar.releaseStatus === 'private'" v-else-if="avatar.releaseStatus === 'private'"
class="extra" class="extra"
style="color: #f56c6c" style="color: var(--el-color-danger)"
v-text="avatar.releaseStatus"></span> v-text="avatar.releaseStatus"></span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span> <span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatarTagStrings.get(avatar.id)"></span> <span class="extra" v-text="avatarTagStrings.get(avatar.id)"></span>
+3 -2
View File
@@ -329,8 +329,9 @@
); );
const openFolderEditor = (index) => { const openFolderEditor = (index) => {
folderEditor.isEditing = !!index; const isEditing = index !== undefined && index !== null;
folderEditor.index = folderEditor.isEditing ? index : -1; folderEditor.isEditing = isEditing;
folderEditor.index = isEditing ? index : -1;
if (folderEditor.isEditing) { if (folderEditor.isEditing) {
const entry = localLayout.value[index]; const entry = localLayout.value[index];
folderEditor.data = { folderEditor.data = {
@@ -3,7 +3,8 @@
:z-index="groupDialogIndex" :z-index="groupDialogIndex"
v-model="groupDialog.visible" v-model="groupDialog.visible"
:show-close="false" :show-close="false"
width="770px" top="10vh"
width="930px"
class="x-dialog x-group-dialog"> class="x-dialog x-group-dialog">
<div v-loading="groupDialog.loading" class="group-body"> <div v-loading="groupDialog.loading" class="group-body">
<div style="display: flex"> <div style="display: flex">
@@ -258,11 +259,7 @@
@click="joinGroup(groupDialog.id)"></el-button> @click="joinGroup(groupDialog.id)"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
<el-dropdown <el-dropdown trigger="click" style="margin-left: 5px" @command="groupDialogCommand">
trigger="click"
size="small"
style="margin-left: 5px"
@command="groupDialogCommand">
<el-button <el-button
:type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'" :type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'"
:icon="MoreFilled" :icon="MoreFilled"
@@ -616,7 +613,8 @@
<span class="name">{{ t('dialog.group.info.links') }}</span> <span class="name">{{ t('dialog.group.info.links') }}</span>
<div <div
v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0" v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0"
style="margin-top: 5px"> style="margin-top: 5px"
class="flex">
<template v-for="(link, index) in groupDialog.ref.links" :key="index"> <template v-for="(link, index) in groupDialog.ref.links" :key="index">
<el-tooltip v-if="link"> <el-tooltip v-if="link">
<template #content> <template #content>
@@ -1086,7 +1084,6 @@
<el-tabs <el-tabs
v-model="groupDialogGalleryCurrentName" v-model="groupDialogGalleryCurrentName"
v-loading="isGroupGalleryLoading" v-loading="isGroupGalleryLoading"
type="card"
style="margin-top: 10px"> style="margin-top: 10px">
<template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index"> <template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index">
<el-tab-pane> <el-tab-pane>
@@ -1839,7 +1836,7 @@
} }
} }
</script> </script>
<style lang="scss" scoped> <style scoped>
.time-group-container { .time-group-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -7,7 +7,7 @@
width="90vw"> width="90vw">
<div> <div>
<h3>{{ groupMemberModeration.groupRef.name }}</h3> <h3>{{ groupMemberModeration.groupRef.name }}</h3>
<el-tabs type="card" style="height: 100%"> <el-tabs style="height: 100%">
<el-tab-pane :label="t('dialog.group_member_moderation.members')"> <el-tab-pane :label="t('dialog.group_member_moderation.members')">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<el-button <el-button
+1 -1
View File
@@ -5,7 +5,7 @@
:title="t('dialog.new_instance.header')" :title="t('dialog.new_instance.header')"
width="650px" width="650px"
append-to-body> append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick"> <el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick">
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')"> <el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px"> <el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')"> <el-form-item :label="t('dialog.new_instance.access_type')">
+2 -10
View File
@@ -63,14 +63,7 @@
</div> </div>
<template #footer> <template #footer>
<el-button <el-button size="small" @click="showGalleryPage">{{ t('dialog.boop_dialog.emoji_manager') }}</el-button>
size="small"
@click="
redirectToToolsTab();
showGalleryDialog();
"
>{{ t('dialog.boop_dialog.emoji_manager') }}</el-button
>
<el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button> <el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>
<el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{ <el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
t('dialog.boop_dialog.send') t('dialog.boop_dialog.send')
@@ -87,7 +80,6 @@
import { notificationRequest, userRequest } from '../../api'; import { notificationRequest, userRequest } from '../../api';
import { miscRequest } from '../../api'; import { miscRequest } from '../../api';
import { photonEmojis } from '../../shared/constants/photon.js'; import { photonEmojis } from '../../shared/constants/photon.js';
import { redirectToToolsTab } from '../../shared/utils/base/ui';
import { useGalleryStore } from '../../stores'; import { useGalleryStore } from '../../stores';
import { useNotificationStore } from '../../stores'; import { useNotificationStore } from '../../stores';
import { useUserStore } from '../../stores/user.js'; import { useUserStore } from '../../stores/user.js';
@@ -98,7 +90,7 @@
const { sendBoopDialog } = storeToRefs(useUserStore()); const { sendBoopDialog } = storeToRefs(useUserStore());
const { notificationTable } = storeToRefs(useNotificationStore()); const { notificationTable } = storeToRefs(useNotificationStore());
const { showGalleryDialog, refreshEmojiTable } = useGalleryStore(); const { showGalleryPage, refreshEmojiTable } = useGalleryStore();
const { emojiTable } = storeToRefs(useGalleryStore()); const { emojiTable } = storeToRefs(useGalleryStore());
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
@@ -21,7 +21,7 @@
@click="userDialogCommand('Add Favorite')"></el-button> @click="userDialogCommand('Add Favorite')"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
<el-dropdown trigger="click" size="small" @command="onCommand"> <el-dropdown trigger="click" @command="onCommand">
<el-button <el-button
:type=" :type="
userDialog.incomingRequest || userDialog.outgoingRequest userDialog.incomingRequest || userDialog.outgoingRequest
@@ -132,7 +132,7 @@
:icon="CircleCheck" :icon="CircleCheck"
command="Moderation Unblock" command="Moderation Unblock"
divided divided
style="color: #f56c6c"> style="color: var(--el-color-danger)">
{{ t('dialog.user.actions.moderation_unblock') }} {{ t('dialog.user.actions.moderation_unblock') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
@@ -147,7 +147,7 @@
v-if="userDialog.isMute" v-if="userDialog.isMute"
:icon="Microphone" :icon="Microphone"
command="Moderation Unmute" command="Moderation Unmute"
style="color: #f56c6c"> style="color: var(--el-color-danger)">
{{ t('dialog.user.actions.moderation_unmute') }} {{ t('dialog.user.actions.moderation_unmute') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
@@ -161,7 +161,7 @@
v-if="userDialog.isMuteChat" v-if="userDialog.isMuteChat"
:icon="ChatLineRound" :icon="ChatLineRound"
command="Moderation Enable Chatbox" command="Moderation Enable Chatbox"
style="color: #f56c6c"> style="color: var(--el-color-danger)">
{{ t('dialog.user.actions.moderation_enable_chatbox') }} {{ t('dialog.user.actions.moderation_enable_chatbox') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item v-else :icon="ChatDotRound" command="Moderation Disable Chatbox"> <el-dropdown-item v-else :icon="ChatDotRound" command="Moderation Disable Chatbox">
@@ -179,7 +179,7 @@
v-if="userDialog.isInteractOff" v-if="userDialog.isInteractOff"
:icon="Pointer" :icon="Pointer"
command="Moderation Enable Avatar Interaction" command="Moderation Enable Avatar Interaction"
style="color: #f56c6c"> style="color: var(--el-color-danger)">
{{ t('dialog.user.actions.moderation_enable_avatar_interaction') }} {{ t('dialog.user.actions.moderation_enable_avatar_interaction') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item v-else :icon="CircleClose" command="Moderation Disable Avatar Interaction"> <el-dropdown-item v-else :icon="CircleClose" command="Moderation Disable Avatar Interaction">
@@ -189,7 +189,11 @@
{{ t('dialog.user.actions.report_hacking') }} {{ t('dialog.user.actions.report_hacking') }}
</el-dropdown-item> </el-dropdown-item>
<template v-if="userDialog.isFriend"> <template v-if="userDialog.isFriend">
<el-dropdown-item :icon="Delete" command="Unfriend" divided style="color: #f56c6c"> <el-dropdown-item
:icon="Delete"
command="Unfriend"
divided
style="color: var(--el-color-danger)">
{{ t('dialog.user.actions.unfriend') }} {{ t('dialog.user.actions.unfriend') }}
</el-dropdown-item> </el-dropdown-item>
</template> </template>
@@ -5,7 +5,7 @@
v-model="userDialog.visible" v-model="userDialog.visible"
:show-close="false" :show-close="false"
top="10vh" top="10vh"
width="940px"> width="930px">
<div v-loading="userDialog.loading"> <div v-loading="userDialog.loading">
<UserSummaryHeader <UserSummaryHeader
:get-user-state-text="getUserStateText" :get-user-state-text="getUserStateText"
@@ -252,7 +252,7 @@
style="margin-left: 5px; padding: 0" style="margin-left: 5px; padding: 0"
@click="showBioDialog"></el-button> @click="showBioDialog"></el-button>
</div> </div>
<div style="margin-top: 5px" class="flex"> <div style="margin-top: 5px" class="flex items-center">
<el-tooltip v-for="(link, index) in userDialog.ref.bioLinks" :key="index"> <el-tooltip v-for="(link, index) in userDialog.ref.bioLinks" :key="index">
<template #content> <template #content>
<span v-text="link"></span> <span v-text="link"></span>
@@ -426,10 +426,13 @@
<div class="x-friend-item" @click="toggleAvatarCopying"> <div class="x-friend-item" @click="toggleAvatarCopying">
<div class="detail"> <div class="detail">
<span class="name">{{ t('dialog.user.info.avatar_cloning') }}</span> <span class="name">{{ t('dialog.user.info.avatar_cloning') }}</span>
<span v-if="currentUser.allowAvatarCopying" class="extra" style="color: #67c23a">{{ <span
t('dialog.user.info.avatar_cloning_allow') v-if="currentUser.allowAvatarCopying"
}}</span> class="extra"
<span v-else class="extra" style="color: #f56c6c">{{ style="color: var(--el-color-success)"
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
>
<span v-else class="extra" style="color: var(--el-color-danger)">{{
t('dialog.user.info.avatar_cloning_deny') t('dialog.user.info.avatar_cloning_deny')
}}</span> }}</span>
</div> </div>
@@ -437,10 +440,13 @@
<div class="x-friend-item" @click="toggleAllowBooping"> <div class="x-friend-item" @click="toggleAllowBooping">
<div class="detail"> <div class="detail">
<span class="name">{{ t('dialog.user.info.booping') }}</span> <span class="name">{{ t('dialog.user.info.booping') }}</span>
<span v-if="currentUser.isBoopingEnabled" class="extra" style="color: #67c23a">{{ <span
t('dialog.user.info.avatar_cloning_allow') v-if="currentUser.isBoopingEnabled"
}}</span> class="extra"
<span v-else class="extra" style="color: #f56c6c">{{ style="color: var(--el-color-success)"
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
>
<span v-else class="extra" style="color: var(--el-color-danger)">{{
t('dialog.user.info.avatar_cloning_deny') t('dialog.user.info.avatar_cloning_deny')
}}</span> }}</span>
</div> </div>
@@ -451,10 +457,10 @@
<span <span
v-if="!currentUser.hasSharedConnectionsOptOut" v-if="!currentUser.hasSharedConnectionsOptOut"
class="extra" class="extra"
style="color: #67c23a" style="color: var(--el-color-success)"
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span >{{ t('dialog.user.info.avatar_cloning_allow') }}</span
> >
<span v-else class="extra" style="color: #f56c6c">{{ <span v-else class="extra" style="color: var(--el-color-danger)">{{
t('dialog.user.info.avatar_cloning_deny') t('dialog.user.info.avatar_cloning_deny')
}}</span> }}</span>
</div> </div>
@@ -467,10 +473,10 @@
<span <span
v-if="userDialog.ref.allowAvatarCopying" v-if="userDialog.ref.allowAvatarCopying"
class="extra" class="extra"
style="color: #67c23a" style="color: var(--el-color-success)"
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span >{{ t('dialog.user.info.avatar_cloning_allow') }}</span
> >
<span v-else class="extra" style="color: #f56c6c">{{ <span v-else class="extra" style="color: var(--el-color-danger)">{{
t('dialog.user.info.avatar_cloning_deny') t('dialog.user.info.avatar_cloning_deny')
}}</span> }}</span>
</div> </div>
@@ -634,9 +640,10 @@
t('dialog.user.groups.total_count', { count: userGroups.groups.length }) t('dialog.user.groups.total_count', { count: userGroups.groups.length })
}}</span> }}</span>
<template v-if="userDialogGroupEditMode"> <template v-if="userDialogGroupEditMode">
<span style="margin-left: 10px; color: #909399; font-size: 10px">{{ <span
t('dialog.user.groups.hold_shift') style="margin-left: 10px; color: var(--el-text-color-secondary); font-size: 10px"
}}</span> >{{ t('dialog.user.groups.hold_shift') }}</span
>
</template> </template>
</div> </div>
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
@@ -872,7 +879,7 @@
size="small" size="small"
:icon="Close" :icon="Close"
circle circle
style="color: #f56c6c; margin-left: 5px" style="color: var(--el-color-danger); margin-left: 5px"
@click.stop="leaveGroup(group.id)"> @click.stop="leaveGroup(group.id)">
</el-button> </el-button>
<el-button <el-button
@@ -892,7 +899,7 @@
<span style="font-weight: bold; font-size: 16px">{{ <span style="font-weight: bold; font-size: 16px">{{
t('dialog.user.groups.own_groups') t('dialog.user.groups.own_groups')
}}</span> }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px" <span style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px"
>{{ userGroups.ownGroups.length }}/{{ >{{ userGroups.ownGroups.length }}/{{
cachedConfig?.constants?.GROUPS?.MAX_OWNED cachedConfig?.constants?.GROUPS?.MAX_OWNED
}}</span }}</span
@@ -936,9 +943,10 @@
<span style="font-weight: bold; font-size: 16px">{{ <span style="font-weight: bold; font-size: 16px">{{
t('dialog.user.groups.mutual_groups') t('dialog.user.groups.mutual_groups')
}}</span> }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ <span
userGroups.mutualGroups.length style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px"
}}</span> >{{ userGroups.mutualGroups.length }}</span
>
<div <div
class="x-friend-list" class="x-friend-list"
style="margin-top: 10px; margin-bottom: 15px; min-height: 60px"> style="margin-top: 10px; margin-bottom: 15px; min-height: 60px">
@@ -978,7 +986,7 @@
<span style="font-weight: bold; font-size: 16px">{{ <span style="font-weight: bold; font-size: 16px">{{
t('dialog.user.groups.groups') t('dialog.user.groups.groups')
}}</span> }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px"> <span style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px">
{{ userGroups.remainingGroups.length }} {{ userGroups.remainingGroups.length }}
<template v-if="currentUser.id === userDialog.id"> <template v-if="currentUser.id === userDialog.id">
/ /
@@ -1143,7 +1151,12 @@
:class="userFavoriteWorldsStatus(list[1])"> :class="userFavoriteWorldsStatus(list[1])">
</i> </i>
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span> <span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
<span style="color: #909399; font-size: 10px; margin-left: 5px" <span
style="
color: var(--el-text-color-secondary);
font-size: 10px;
margin-left: 5px;
"
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span >{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
> >
</span> </span>
@@ -1272,13 +1285,13 @@
<span <span
v-if="avatar.releaseStatus === 'public'" v-if="avatar.releaseStatus === 'public'"
class="extra" class="extra"
style="color: #67c23a" style="color: var(--el-color-success)"
v-text="avatar.releaseStatus"> v-text="avatar.releaseStatus">
</span> </span>
<span <span
v-else-if="avatar.releaseStatus === 'private'" v-else-if="avatar.releaseStatus === 'private'"
class="extra" class="extra"
style="color: #f56c6c" style="color: var(--el-color-danger)"
v-text="avatar.releaseStatus"> v-text="avatar.releaseStatus">
</span> </span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span> <span v-else class="extra" v-text="avatar.releaseStatus"></span>
@@ -1400,11 +1413,11 @@
userRequest, userRequest,
worldRequest worldRequest
} from '../../../api'; } from '../../../api';
import { getNextDialogIndex, redirectToToolsTab } from '../../../shared/utils/base/ui';
import { processBulk, request } from '../../../service/request'; import { processBulk, request } from '../../../service/request';
import { userDialogGroupSortingOptions, userDialogMutualFriendSortingOptions } from '../../../shared/constants'; import { userDialogGroupSortingOptions, userDialogMutualFriendSortingOptions } from '../../../shared/constants';
import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/'; import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/';
import { database } from '../../../service/database'; import { database } from '../../../service/database';
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue'; import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue';
import UserSummaryHeader from './UserSummaryHeader.vue'; import UserSummaryHeader from './UserSummaryHeader.vue';
@@ -1455,7 +1468,7 @@
const { refreshInviteMessageTableData } = useInviteStore(); const { refreshInviteMessageTableData } = useInviteStore();
const { friendLogTable } = storeToRefs(useFriendStore()); const { friendLogTable } = storeToRefs(useFriendStore());
const { getFriendRequest, handleFriendDelete } = useFriendStore(); const { getFriendRequest, handleFriendDelete } = useFriendStore();
const { clearInviteImageUpload, showFullscreenImageDialog } = useGalleryStore(); const { clearInviteImageUpload, showFullscreenImageDialog, showGalleryPage } = useGalleryStore();
const { logout } = useAuthStore(); const { logout } = useAuthStore();
const { cachedConfig } = storeToRefs(useAuthStore()); const { cachedConfig } = storeToRefs(useAuthStore());
@@ -1896,6 +1909,9 @@
} }
} else if (command === 'Previous Instances') { } else if (command === 'Previous Instances') {
showPreviousInstancesUserDialog(D.ref); showPreviousInstancesUserDialog(D.ref);
} else if (command === 'Manage Gallery') {
userDialog.value.visible = false;
showGalleryPage();
} else if (command === 'Invite To Group') { } else if (command === 'Invite To Group') {
showInviteGroupDialog('', D.id); showInviteGroupDialog('', D.id);
} else if (command === 'Send Boop') { } else if (command === 'Send Boop') {
@@ -316,7 +316,7 @@
} }
</script> </script>
<style lang="scss" scoped> <style scoped>
.img-size { .img-size {
width: 500px; width: 500px;
height: 375px; height: 375px;
@@ -3,8 +3,9 @@
:z-index="worldDialogIndex" :z-index="worldDialogIndex"
class="x-dialog x-world-dialog" class="x-dialog x-world-dialog"
v-model="isDialogVisible" v-model="isDialogVisible"
top="10vh"
:show-close="false" :show-close="false"
width="770px"> width="930px">
<div v-loading="worldDialog.loading"> <div v-loading="worldDialog.loading">
<div style="display: flex"> <div style="display: flex">
<img <img
@@ -204,11 +205,7 @@
style="margin-left: 5px" style="margin-left: 5px"
@click="worldDialogCommand('Add Favorite')" /> @click="worldDialogCommand('Add Favorite')" />
</el-tooltip> </el-tooltip>
<el-dropdown <el-dropdown trigger="click" style="margin-left: 5px" @command="worldDialogCommand">
trigger="click"
size="small"
style="margin-left: 5px"
@command="worldDialogCommand">
<el-button type="default" :icon="MoreFilled" size="large" circle /> <el-button type="default" :icon="MoreFilled" size="large" circle />
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@@ -302,7 +299,10 @@
command="Delete Persistent Data"> command="Delete Persistent Data">
{{ t('dialog.world.actions.delete_persistent_data') }} {{ t('dialog.world.actions.delete_persistent_data') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :icon="Delete" command="Delete" style="color: #f56c6c"> <el-dropdown-item
:icon="Delete"
command="Delete"
style="color: var(--el-color-danger)">
{{ t('dialog.world.actions.delete') }} {{ t('dialog.world.actions.delete') }}
</el-dropdown-item> </el-dropdown-item>
</template> </template>
+141
View File
@@ -0,0 +1,141 @@
import { ref } from 'vue';
import colors from 'tailwindcss/colors';
import configRepository from '../service/config';
// Tailwind indigo-500 in OKLCH
const DEFAULT_PRIMARY = 'oklch(58.5% 0.233 277.117)';
const DARK_WEIGHT = 0.2;
const CONFIG_KEY = 'VRCX_elPrimaryColor';
const STYLE_ID = 'el-dynamic-theme';
let elementThemeInstance = null;
/**
* Keep okLCH as-is; otherwise normalize hex; fallback to default.
* @param {string} color
* @param {string} fallback
*/
function toPrimaryColor(color, fallback = DEFAULT_PRIMARY) {
if (typeof color === 'string' && color.trim()) {
if (color.trim().startsWith('oklch(')) {
return color.trim();
}
}
return fallback;
}
/**
* Update Element Plus CSS variables based on a primary color.
* Light colors use Tailwind palette directly; only dark-2 is calculated.
* Dark mode overrides light-9 with a softer tint for better contrast.
* @param {string} primary
* @param {object|null} palette
*/
function setElementPlusColors(primary, palette = null) {
let styleEl = document.getElementById(STYLE_ID);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = STYLE_ID;
document.head.appendChild(styleEl);
}
// Derive Element Plus light steps either from a palette or by mixing with white.
const safePalette = palette || null;
const lightValues = safePalette
? ['400', '300', '200', '100', '50', '50', '50', '50', '50'].map(
(key) => safePalette[key] || primary
)
: Array.from({ length: 9 }, (_, idx) => {
const whitePercent = (idx + 1) * 10;
const primaryPercent = 100 - whitePercent;
return `color-mix(in oklch, ${primary} ${primaryPercent}%, white ${whitePercent}%)`;
});
const lights = lightValues
.map(
(value, index) =>
` --el-color-primary-light-${index + 1}: ${value};`
)
.join('\n');
const darkPercent = DARK_WEIGHT * 100;
const primaryPercent = 100 - darkPercent;
const darkValue = `color-mix(in oklch, ${primary} ${primaryPercent}%, black ${darkPercent}%)`;
const darkLight9 = `color-mix(in oklch, ${primary} 18%, transparent)`;
const baseSelector =
":root, html.dark, :root.dark, :root[data-theme='dark']";
const darkSelector = "html.dark, :root.dark, :root[data-theme='dark']";
styleEl.textContent =
`${baseSelector} {\n --el-color-primary: ${primary};\n${lights}\n --el-color-primary-dark-2: ${darkValue};\n}\n` +
`${darkSelector} {\n --el-color-primary-light-9: ${darkLight9};\n}`;
}
function findTailwindPalette(primary) {
const entries = Object.values(colors);
for (const palette of entries) {
if (
palette &&
typeof palette === 'object' &&
palette['500'] === primary
) {
return palette;
}
}
return null;
}
/**
* Shared Element Plus theme controller.
* @param {string} defaultColor
*/
export function useElementTheme(defaultColor = DEFAULT_PRIMARY) {
if (elementThemeInstance) {
return elementThemeInstance;
}
const currentPrimary = ref(defaultColor);
const isApplying = ref(false);
let initialized = false;
const applyPrimaryColor = async (color, palette = null) => {
const nextColor = toPrimaryColor(color, currentPrimary.value);
const effectivePalette = palette || findTailwindPalette(nextColor);
isApplying.value = true;
setElementPlusColors(nextColor, effectivePalette);
currentPrimary.value = nextColor;
try {
await configRepository.setString(CONFIG_KEY, nextColor);
} catch (error) {
console.warn('Failed to persist theme color', error);
} finally {
isApplying.value = false;
}
};
const initPrimaryColor = async (fallbackColor = currentPrimary.value) => {
if (initialized) {
return;
}
initialized = true;
const storedColor =
(await configRepository.getString(CONFIG_KEY)) ||
fallbackColor ||
DEFAULT_PRIMARY;
await applyPrimaryColor(storedColor);
};
elementThemeInstance = {
currentPrimary,
isApplying,
applyPrimaryColor,
initPrimaryColor
};
return elementThemeInstance;
}
export { toPrimaryColor };
+44
View File
@@ -0,0 +1,44 @@
import { onMounted, onUnmounted, ref } from 'vue';
export function useTableHeight(tableRef, options = {}) {
const containerRef = ref(null);
const offset = options.offset ?? 127;
const immediate = options.immediate ?? true;
let resizeObserver;
const setTableHeight = () => {
if (!tableRef?.value || !containerRef.value) {
return;
}
tableRef.value.tableProps = {
...(tableRef.value.tableProps || {}),
// @ts-ignore default is null
height: containerRef.value.clientHeight - offset
};
};
onMounted(() => {
if (immediate) {
setTableHeight();
}
resizeObserver = new ResizeObserver(() => {
setTableHeight();
});
if (containerRef.value) {
resizeObserver.observe(containerRef.value);
}
});
onUnmounted(() => {
resizeObserver?.disconnect();
});
return {
containerRef,
setTableHeight
};
}
-2
View File
@@ -11,8 +11,6 @@
<title>VRCX</title> <title>VRCX</title>
<!-- <link rel="stylesheet" href="app.css" /> -->
<link rel="preconnect" href="https://api.vrchat.cloud" /> <link rel="preconnect" href="https://api.vrchat.cloud" />
<link rel="preconnect" href="https://files.vrchat.cloud" /> <link rel="preconnect" href="https://files.vrchat.cloud" />
<link rel="preconnect" href="https://d348imysud55la.cloudfront.net" /> <link rel="preconnect" href="https://d348imysud55la.cloudfront.net" />
+2
View File
@@ -1782,6 +1782,8 @@
"notification": { "notification": {
"date": "Date", "date": "Date",
"type": "Type", "type": "Type",
"user": "User",
"group": "Group",
"user_group": "User/Group", "user_group": "User/Group",
"photo": "Photo", "photo": "Photo",
"message": "Message", "message": "Message",
+16 -3
View File
@@ -21,7 +21,10 @@
"about": "About", "about": "About",
"profile": "Profile", "profile": "Profile",
"settings": "Settings", "settings": "Settings",
"help_support": "Help & Support" "manage": "Manage",
"help_support": "Help & Support",
"expand_menu": "Expand Menu",
"collapse_menu": "Collapse Menu"
}, },
"nav_menu": { "nav_menu": {
"resources": "RESOURCES", "resources": "RESOURCES",
@@ -292,6 +295,10 @@
"bulk_unfriend_selection": "Bulk Unfriend Selection", "bulk_unfriend_selection": "Bulk Unfriend Selection",
"load": "Load missing entries", "load": "Load missing entries",
"load_tooltip": "Load", "load_tooltip": "Load",
"load_dialog_title": "Load missing entries",
"load_dialog_message": "Retrieving missing profile fields for your friends.",
"load_cancel": "Cancel",
"load_complete": "Missing entries loaded",
"favorites_only_tooltip": "Filter favorites only", "favorites_only_tooltip": "Filter favorites only",
"search_placeholder": "Search", "search_placeholder": "Search",
"filter_placeholder": "Filter", "filter_placeholder": "Filter",
@@ -560,7 +567,8 @@
"table_max_size": "Table Max Size", "table_max_size": "Table Max Size",
"table_page_sizes": "Table Page Sizes", "table_page_sizes": "Table Page Sizes",
"table_page_sizes_error": "Page size must be a number between 1 and 1000", "table_page_sizes_error": "Page size must be a number between 1 and 1000",
"show_notification_icon_dot": "Show Tray Notification Dot" "show_notification_icon_dot": "Show Tray Notification Dot",
"compact_table_mode": "Compact Table Mode"
}, },
"timedate": { "timedate": {
"header": "Time/Date", "header": "Time/Date",
@@ -569,6 +577,9 @@
"time_format_12": "12 Hour", "time_format_12": "12 Hour",
"force_iso_date_format": "Force ISO Date Format" "force_iso_date_format": "Force ISO Date Format"
}, },
"theme_color": {
"header": "Theme Color"
},
"side_panel": { "side_panel": {
"header": "Side Panel", "header": "Side Panel",
"sorting": { "sorting": {
@@ -873,7 +884,7 @@
} }
}, },
"side_panel": { "side_panel": {
"search_placeholder": "Search", "search_placeholder": "Search Friend",
"search_result_active": "Offline", "search_result_active": "Offline",
"search_result_offline": "Active", "search_result_offline": "Active",
"search_result_more": "Search More:", "search_result_more": "Search More:",
@@ -2254,6 +2265,8 @@
"notification": { "notification": {
"date": "Date", "date": "Date",
"type": "Type", "type": "Type",
"user": "User",
"group": "Group",
"user_group": "User/Group", "user_group": "User/Group",
"photo": "Photo", "photo": "Photo",
"message": "Message", "message": "Message",
+2
View File
@@ -1899,6 +1899,8 @@
"notification": { "notification": {
"date": "Fecha", "date": "Fecha",
"type": "Tipo", "type": "Tipo",
"user": "Usuario",
"group": "Grupo",
"user_group": "Usuario/Grupo", "user_group": "Usuario/Grupo",
"photo": "Foto", "photo": "Foto",
"message": "Mensaje", "message": "Mensaje",
+2
View File
@@ -1788,6 +1788,8 @@
"notification": { "notification": {
"date": "Date", "date": "Date",
"type": "Type", "type": "Type",
"user": "Utilisateur",
"group": "Groupe",
"user_group": "Utilisateur/Groupe", "user_group": "Utilisateur/Groupe",
"photo": "Photo", "photo": "Photo",
"message": "Message", "message": "Message",
+2
View File
@@ -1660,6 +1660,8 @@
"notification": { "notification": {
"date": "Date", "date": "Date",
"type": "Type", "type": "Type",
"user": "Felhasználó",
"group": "Csoport",
"user_group": "User/Group", "user_group": "User/Group",
"photo": "Photo", "photo": "Photo",
"message": "Message", "message": "Message",
+2
View File
@@ -2082,6 +2082,8 @@
"notification": { "notification": {
"date": "日付", "date": "日付",
"type": "種類", "type": "種類",
"user": "ユーザー",
"group": "グループ",
"user_group": "ユーザーまたはグループ", "user_group": "ユーザーまたはグループ",
"photo": "画像", "photo": "画像",
"message": "メッセージ", "message": "メッセージ",
+2
View File
@@ -1674,6 +1674,8 @@
"notification": { "notification": {
"date": "날짜", "date": "날짜",
"type": "유형", "type": "유형",
"user": "유저",
"group": "그룹",
"user_group": "User/Group", "user_group": "User/Group",
"photo": "사진", "photo": "사진",
"message": "메시지", "message": "메시지",
+2
View File
@@ -2225,6 +2225,8 @@
"notification": { "notification": {
"date": "Data", "date": "Data",
"type": "Typ", "type": "Typ",
"user": "Użytkownik",
"group": "Grupa",
"user_group": "Użytkownik/Grupa", "user_group": "Użytkownik/Grupa",
"photo": "Obrazek", "photo": "Obrazek",
"message": "Wiadomość", "message": "Wiadomość",
+2
View File
@@ -1660,6 +1660,8 @@
"notification": { "notification": {
"date": "Data", "date": "Data",
"type": "Tipo", "type": "Tipo",
"user": "Usuário",
"group": "Grupo",
"user_group": "Usuário/Grupo", "user_group": "Usuário/Grupo",
"photo": "Foto", "photo": "Foto",
"message": "Mensagem", "message": "Mensagem",
+2
View File
@@ -2069,6 +2069,8 @@
"notification": { "notification": {
"date": "Дата", "date": "Дата",
"type": "Тип", "type": "Тип",
"user": "Пользователь",
"group": "Группа",
"user_group": "Пользователь/Группа", "user_group": "Пользователь/Группа",
"photo": "Фото", "photo": "Фото",
"message": "Сообщение", "message": "Сообщение",
+2
View File
@@ -1964,6 +1964,8 @@
"notification": { "notification": {
"date": "วันที่", "date": "วันที่",
"type": "ประเภท", "type": "ประเภท",
"user": "ผู้ใช้",
"group": "กลุ่ม",
"user_group": "ผู้ใช้/กลุ่ม", "user_group": "ผู้ใช้/กลุ่ม",
"photo": "รูปภาพ", "photo": "รูปภาพ",
"message": "ข้อความ", "message": "ข้อความ",
+2
View File
@@ -1660,6 +1660,8 @@
"notification": { "notification": {
"date": "Date", "date": "Date",
"type": "Type", "type": "Type",
"user": "Người chơi",
"group": "Nhóm",
"user_group": "User/Group", "user_group": "User/Group",
"photo": "Photo", "photo": "Photo",
"message": "Message", "message": "Message",
+2
View File
@@ -2207,6 +2207,8 @@
"notification": { "notification": {
"date": "时间", "date": "时间",
"type": "类型", "type": "类型",
"user": "玩家",
"group": "群组",
"user_group": "玩家/群组", "user_group": "玩家/群组",
"photo": "封面", "photo": "封面",
"message": "消息", "message": "消息",
+2
View File
@@ -2192,6 +2192,8 @@
"notification": { "notification": {
"date": "時間", "date": "時間",
"type": "類型", "type": "類型",
"user": "用戶",
"group": "群組",
"user_group": "用戶/群組", "user_group": "用戶/群組",
"photo": "照片", "photo": "照片",
"message": "訊息", "message": "訊息",
-2
View File
@@ -8,12 +8,10 @@ import LastJoin from '../components/LastJoin.vue';
import Launch from '../components/Launch.vue'; import Launch from '../components/Launch.vue';
import Location from '../components/Location.vue'; import Location from '../components/Location.vue';
import LocationWorld from '../components/LocationWorld.vue'; import LocationWorld from '../components/LocationWorld.vue';
import NativeTooltip from '../components/NativeTooltip.vue';
import Timer from '../components/Timer.vue'; import Timer from '../components/Timer.vue';
export function initComponents(app) { export function initComponents(app) {
app.component('Location', Location); app.component('Location', Location);
app.component('NativeTooltip', NativeTooltip);
app.component('Timer', Timer); app.component('Timer', Timer);
app.component('InstanceInfo', InstanceInfo); app.component('InstanceInfo', InstanceInfo);
app.component('LastJoin', LastJoin); app.component('LastJoin', LastJoin);
+1 -1
View File
@@ -19,7 +19,7 @@ export function initNoty(isVrOverlay = false) {
}, },
layout: 'bottomLeft', layout: 'bottomLeft',
theme: 'mint', theme: 'mint',
timeout: 6000 timeout: 2000
}); });
} }
} }
+7
View File
@@ -19,6 +19,7 @@ import PlayerList from './../views/PlayerList/PlayerList.vue';
import Search from './../views/Search/Search.vue'; import Search from './../views/Search/Search.vue';
import Settings from './../views/Settings/Settings.vue'; import Settings from './../views/Settings/Settings.vue';
import Tools from './../views/Tools/Tools.vue'; import Tools from './../views/Tools/Tools.vue';
import Gallery from './../views/Tools/Gallery.vue';
const routes = [ const routes = [
{ {
@@ -83,6 +84,12 @@ const routes = [
component: Charts component: Charts
}, },
{ path: 'tools', name: 'tools', component: Tools }, { path: 'tools', name: 'tools', component: Tools },
{
path: 'tools/gallery',
name: 'gallery',
component: Gallery,
meta: { navKey: 'tools' }
},
{ path: 'settings', name: 'settings', component: Settings } { path: 'settings', name: 'settings', component: Settings }
] ]
} }

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 645 B

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

Before

Width:  |  Height:  |  Size: 843 B

After

Width:  |  Height:  |  Size: 843 B

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 737 B

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 591 B

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 557 B

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 823 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+13
View File
@@ -0,0 +1,13 @@
const accessTypeLocaleKeyMap = {
public: 'dialog.new_instance.access_type_public',
group: 'dialog.new_instance.access_type_group',
'friends+': 'dialog.new_instance.access_type_friend_plus',
friends: 'dialog.new_instance.access_type_friend',
'invite+': 'dialog.new_instance.access_type_invite_plus',
invite: 'dialog.new_instance.access_type_invite',
groupPublic: 'dialog.new_instance.group_access_type_public',
groupPlus: 'dialog.new_instance.group_access_type_plus',
groupMembers: 'dialog.new_instance.group_access_type_members'
};
export { accessTypeLocaleKeyMap };
+1
View File
@@ -10,3 +10,4 @@ export * from './moderation';
export * from './themes'; export * from './themes';
export * from './link'; export * from './link';
export * from './ui'; export * from './ui';
export * from './accessType';
+46 -29
View File
@@ -1,52 +1,69 @@
import amoled from '../../assets/scss/themes/theme.amoled.scss?url'; import appCss from '../../app.css?url';
import dark from '../../assets/scss/themes/theme.dark.scss?url'; // import appLegacy from '../../assets/scss/themes/app_legacy.scss?url';
import darkblue from '../../assets/scss/themes/theme.darkblue.scss?url'; // import material3 from '../../assets/scss/themes/theme.material3.scss?url';
import material3 from '../../assets/scss/themes/theme.material3.scss?url';
export const THEME_CONFIG = { export const THEME_CONFIG = {
system: { system: {
cssFile: '', cssFiles: [appCss],
isDark: 'system', isDark: 'system',
name: 'System' name: 'System'
}, },
light: { light: {
cssFile: '', cssFiles: [appCss],
isDark: false, isDark: false,
useDarkClass: false,
name: 'Light' name: 'Light'
}, },
dark: { cssFile: dark, isDark: true, name: 'Dark' }, dark: {
darkblue: { cssFiles: [appCss],
cssFile: darkblue,
isDark: true, isDark: true,
name: 'Dark Blue' useDarkClass: true,
}, name: 'Dark'
amoled: { }
cssFile: amoled, // darkold: {
isDark: true, // cssFiles: [appLegacy, dark],
name: 'Amoled'
},
// darkvanillaold: {
// cssFile: darkvanillaold,
// isDark: true, // isDark: true,
// useDarkClass: false,
// name: 'Dark (Old)'
// },
// darkblue: {
// cssFiles: [appLegacy, darkblue],
// isDark: true,
// useDarkClass: false,
// name: 'Dark Blue'
// },
// amoled: {
// cssFiles: [appLegacy, amoled],
// isDark: true,
// useDarkClass: false,
// name: 'Amoled'
// },
// darkvanillaold: {
// cssFiles: [appLegacy, darkvanillaold],
// isDark: true,
// useDarkClass: false,
// name: 'Dark Vanilla Old' // name: 'Dark Vanilla Old'
// }, // },
// darkvanilla: { // darkvanilla: {
// cssFile: darkvanilla, // cssFiles: [appLegacy, darkvanilla],
// isDark: true, // isDark: true,
// useDarkClass: false,
// name: 'Dark Vanilla' // name: 'Dark Vanilla'
// }, // },
// pink: { // pink: {
// cssFile: pink, // cssFiles: [appLegacy, pink],
// isDark: true, // isDark: true,
// useDarkClass: false,
// name: 'Pink' // name: 'Pink'
// }, // },
material3: { // material3: {
cssFile: material3, // cssFiles: [appLegacy, material3],
isDark: true, // isDark: true,
name: 'Material 3', // useDarkClass: false,
fontLinks: [ // name: 'Material 3',
'https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600&family=Noto+Sans+TC:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&family=Noto+Sans+JP:wght@300;400;500&family=Roboto&display=swap', // fontLinks: [
'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200' // 'https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600&family=Noto+Sans+TC:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&family=Noto+Sans+JP:wght@300;400;500&family=Roboto&display=swap',
] // 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200'
} // ]
// }
}; };
+7
View File
@@ -96,6 +96,13 @@ const navDefinitions = [
tooltip: 'nav_tooltip.tools', tooltip: 'nav_tooltip.tools',
labelKey: 'nav_tooltip.tools', labelKey: 'nav_tooltip.tools',
routeName: 'tools' routeName: 'tools'
},
{
key: 'direct-access',
icon: 'ri-compass-3-line',
tooltip: 'prompt.direct_access_omni.header',
labelKey: 'prompt.direct_access_omni.header',
action: 'direct-access'
} }
]; ];
+48 -21
View File
@@ -48,6 +48,25 @@ function applyThemeFonts(themeKey, fontLinks = []) {
}); });
} }
function ensureStylesheetLink(id) {
const linkEl = /** @type {HTMLLinkElement | null} */ (
document.getElementById(id)
);
if (!linkEl) {
const created = document.createElement('link');
created.setAttribute('id', id);
created.rel = 'stylesheet';
document.head.appendChild(created);
return created;
}
return linkEl;
}
function removeStylesheetLink(id) {
const linkEl = document.getElementById(id);
linkEl?.remove();
}
function changeAppThemeStyle(themeMode) { function changeAppThemeStyle(themeMode) {
if (themeMode === 'system') { if (themeMode === 'system') {
themeMode = systemIsDarkMode() ? 'dark' : 'light'; themeMode = systemIsDarkMode() ? 'dark' : 'light';
@@ -61,27 +80,35 @@ function changeAppThemeStyle(themeMode) {
themeConfig = THEME_CONFIG[themeMode]; themeConfig = THEME_CONFIG[themeMode];
} }
let filePathPrefix = 'file://vrcx/'; const cssFiles = Array.isArray(themeConfig.cssFiles)
if (LINUX) { ? themeConfig.cssFiles.filter(Boolean)
filePathPrefix = './'; : themeConfig.cssFile
} ? [themeConfig.cssFile]
if (process.env.NODE_ENV === 'development') { : [];
filePathPrefix = 'http://localhost:9000/';
console.log('Using development file path prefix:', filePathPrefix); if (cssFiles.length > 0) {
const $appThemeStyle = ensureStylesheetLink('app-theme-style');
$appThemeStyle.href = cssFiles[0];
} else {
removeStylesheetLink('app-theme-style');
} }
let $appThemeStyle = document.getElementById('app-theme-style'); if (cssFiles.length > 1) {
if (!$appThemeStyle) { const $appThemeOverlayStyle = ensureStylesheetLink(
$appThemeStyle = document.createElement('link'); 'app-theme-overlay-style'
$appThemeStyle.setAttribute('id', 'app-theme-style'); );
$appThemeStyle.rel = 'stylesheet'; $appThemeOverlayStyle.href = cssFiles[1];
document.head.appendChild($appThemeStyle); } else {
removeStylesheetLink('app-theme-overlay-style');
} }
$appThemeStyle.href = themeConfig.cssFile ? themeConfig.cssFile : '';
applyThemeFonts(themeMode, themeConfig.fontLinks); applyThemeFonts(themeMode, themeConfig.fontLinks);
if (themeConfig.isDark) { const shouldUseDarkClass =
typeof themeConfig.useDarkClass === 'boolean'
? themeConfig.useDarkClass
: Boolean(themeConfig.isDark);
if (shouldUseDarkClass) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
@@ -258,19 +285,19 @@ function setLoginContainerStyle(isDarkMode) {
loginContainerStyle.id = 'login-container-style'; loginContainerStyle.id = 'login-container-style';
loginContainerStyle.type = 'text/css'; loginContainerStyle.type = 'text/css';
const backgroundColor = isDarkMode ? '#101010' : '#ffffff'; const backgroundFallback = isDarkMode ? '#101010' : '#ffffff';
const inputBackgroundColor = isDarkMode ? '#333333' : '#ffffff'; const inputBackgroundFallback = isDarkMode ? '#1f1f1f' : '#ffffff';
const inputBorder = isDarkMode ? '1px solid #3b3b3b' : '1px solid #DCDFE6'; const borderFallback = isDarkMode ? '#3b3b3b' : '#DCDFE6';
loginContainerStyle.innerHTML = ` loginContainerStyle.innerHTML = `
.x-login-container { .x-login-container {
background-color: ${backgroundColor} !important; background-color: var(--el-bg-color-page, ${backgroundFallback}) !important;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }
.x-login-container .el-input__wrapper { .x-login-container .el-input__wrapper {
background-color: ${inputBackgroundColor} !important; background-color: var(--el-bg-color, ${inputBackgroundFallback}) !important;
border: ${inputBorder} !important; border: 1px solid var(--el-border-color, ${borderFallback}) !important;
transition: background-color 0.3s ease, border-color 0.3s ease; transition: background-color 0.3s ease, border-color 0.3s ease;
} }
`; `;
+12 -2
View File
@@ -21,6 +21,7 @@ import { AppDebug } from '../service/appConfig';
import { handleImageUploadInput } from '../shared/utils/imageUpload'; import { handleImageUploadInput } from '../shared/utils/imageUpload';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
import { router } from '../plugin/router';
import miscReq from '../api/misc'; import miscReq from '../api/misc';
@@ -122,8 +123,16 @@ export const useGalleryStore = defineStore('Gallery', () => {
} }
} }
function showGalleryDialog() { function showGalleryPage() {
galleryDialogVisible.value = true; galleryDialogVisible.value = true;
if (router.currentRoute.value?.name === 'gallery') {
loadGalleryData();
return;
}
router.push({ name: 'gallery' });
}
function loadGalleryData() {
refreshGalleryTable(); refreshGalleryTable();
refreshVRCPlusIconsTable(); refreshVRCPlusIconsTable();
refreshEmojiTable(); refreshEmojiTable();
@@ -572,7 +581,8 @@ export const useGalleryStore = defineStore('Gallery', () => {
fullscreenImageDialog, fullscreenImageDialog,
cachedEmoji, cachedEmoji,
showGalleryDialog, showGalleryPage,
loadGalleryData,
refreshGalleryTable, refreshGalleryTable,
refreshVRCPlusIconsTable, refreshVRCPlusIconsTable,
inviteImageUpload, inviteImageUpload,
+90 -31
View File
@@ -12,9 +12,12 @@ import {
systemIsDarkMode, systemIsDarkMode,
updateTrustColorClasses updateTrustColorClasses
} from '../../shared/utils/base/ui'; } from '../../shared/utils/base/ui';
import { THEME_CONFIG } from '../../shared/constants';
import { database } from '../../service/database'; import { database } from '../../service/database';
import { getNameColour } from '../../shared/utils'; import { getNameColour } from '../../shared/utils';
import { languageCodes } from '../../localization';
import { loadLocalizedStrings } from '../../plugin'; import { loadLocalizedStrings } from '../../plugin';
import { useElementTheme } from '../../composables/useElementTheme';
import { useFeedStore } from '../feed'; import { useFeedStore } from '../feed';
import { useGameLogStore } from '../gameLog'; import { useGameLogStore } from '../gameLog';
import { useUiStore } from '../ui'; import { useUiStore } from '../ui';
@@ -24,7 +27,6 @@ import { useVrcxStore } from '../vrcx';
import { watchState } from '../../service/watchState'; import { watchState } from '../../service/watchState';
import configRepository from '../../service/config'; import configRepository from '../../service/config';
import { languageCodes } from '../../localization';
export const useAppearanceSettingsStore = defineStore( export const useAppearanceSettingsStore = defineStore(
'AppearanceSettings', 'AppearanceSettings',
@@ -42,6 +44,7 @@ export const useAppearanceSettingsStore = defineStore(
const MAX_TABLE_PAGE_SIZE = 1000; const MAX_TABLE_PAGE_SIZE = 1000;
const DEFAULT_TABLE_PAGE_SIZES = [10, 15, 20, 25, 50, 100]; const DEFAULT_TABLE_PAGE_SIZES = [10, 15, 20, 25, 50, 100];
const { initPrimaryColor } = useElementTheme();
const appLanguage = ref('en'); const appLanguage = ref('en');
const themeMode = ref(''); const themeMode = ref('');
@@ -71,7 +74,8 @@ export const useAppearanceSettingsStore = defineStore(
const hideUserMemos = ref(false); const hideUserMemos = ref(false);
const hideUnfriends = ref(false); const hideUnfriends = ref(false);
const randomUserColours = ref(false); const randomUserColours = ref(false);
const trustColor = ref({ const compactTableMode = ref(false);
const TRUST_COLOR_DEFAULTS = Object.freeze({
untrusted: '#CCCCCC', untrusted: '#CCCCCC',
basic: '#1778FF', basic: '#1778FF',
known: '#2BCF5C', known: '#2BCF5C',
@@ -80,8 +84,10 @@ export const useAppearanceSettingsStore = defineStore(
vip: '#FF2626', vip: '#FF2626',
troll: '#782F2F' troll: '#782F2F'
}); });
const trustColor = ref({ ...TRUST_COLOR_DEFAULTS });
const currentCulture = ref(''); const currentCulture = ref('');
const notificationIconDot = ref(false); const notificationIconDot = ref(false);
const isNavCollapsed = ref(true);
const isSideBarTabShow = computed(() => { const isSideBarTabShow = computed(() => {
const currentRouteName = router.currentRoute.value?.name; const currentRouteName = router.currentRoute.value?.name;
return !( return !(
@@ -118,8 +124,10 @@ export const useAppearanceSettingsStore = defineStore(
hideUserMemosConfig, hideUserMemosConfig,
hideUnfriendsConfig, hideUnfriendsConfig,
randomUserColoursConfig, randomUserColoursConfig,
compactTableModeConfig,
trustColorConfig, trustColorConfig,
notificationIconDotConfig notificationIconDotConfig,
navIsCollapsedConfig
] = await Promise.all([ ] = await Promise.all([
configRepository.getString('VRCX_appLanguage'), configRepository.getString('VRCX_appLanguage'),
configRepository.getString('VRCX_ThemeMode', 'system'), configRepository.getString('VRCX_ThemeMode', 'system'),
@@ -163,19 +171,13 @@ export const useAppearanceSettingsStore = defineStore(
configRepository.getBool('VRCX_hideUserMemos', false), configRepository.getBool('VRCX_hideUserMemos', false),
configRepository.getBool('VRCX_hideUnfriends', false), configRepository.getBool('VRCX_hideUnfriends', false),
configRepository.getBool('VRCX_randomUserColours', false), configRepository.getBool('VRCX_randomUserColours', false),
configRepository.getBool('VRCX_compactTableMode', false),
configRepository.getString( configRepository.getString(
'VRCX_trustColor', 'VRCX_trustColor',
JSON.stringify({ JSON.stringify(TRUST_COLOR_DEFAULTS)
untrusted: '#CCCCCC',
basic: '#1778FF',
known: '#2BCF5C',
trusted: '#FF7B42',
veteran: '#B18FFF',
vip: '#FF2626',
troll: '#782F2F'
})
), ),
configRepository.getBool('VRCX_notificationIconDot', true) configRepository.getBool('VRCX_notificationIconDot', true),
configRepository.getBool('VRCX_navIsCollapsed', true)
]); ]);
if (!appLanguageConfig) { if (!appLanguageConfig) {
@@ -193,8 +195,18 @@ export const useAppearanceSettingsStore = defineStore(
await changeAppLanguage(appLanguageConfig); await changeAppLanguage(appLanguageConfig);
} }
themeMode.value = themeModeConfig; const normalizedThemeMode = normalizeThemeMode(themeModeConfig);
if (normalizedThemeMode !== themeModeConfig) {
configRepository.setString(
'VRCX_ThemeMode',
normalizedThemeMode
);
}
themeMode.value = normalizedThemeMode;
applyThemeMode(); applyThemeMode();
await changeAppThemeStyle(themeMode.value);
await initPrimaryColor();
displayVRCPlusIconsAsAvatar.value = displayVRCPlusIconsAsAvatar.value =
displayVRCPlusIconsAsAvatarConfig; displayVRCPlusIconsAsAvatarConfig;
@@ -222,7 +234,13 @@ export const useAppearanceSettingsStore = defineStore(
sidebarSortMethod3.value = sidebarSortMethods.value[2]; sidebarSortMethod3.value = sidebarSortMethods.value[2];
} }
trustColor.value = JSON.parse(trustColorConfig); if (trustColorConfig !== JSON.stringify(TRUST_COLOR_DEFAULTS)) {
await configRepository.setString(
'VRCX_trustColor',
JSON.stringify(TRUST_COLOR_DEFAULTS)
);
}
trustColor.value = { ...TRUST_COLOR_DEFAULTS };
asideWidth.value = asideWidthConfig; asideWidth.value = asideWidthConfig;
isSidebarGroupByInstance.value = isSidebarGroupByInstanceConfig; isSidebarGroupByInstance.value = isSidebarGroupByInstanceConfig;
isHideFriendsInSameInstance.value = isHideFriendsInSameInstance.value =
@@ -234,6 +252,11 @@ export const useAppearanceSettingsStore = defineStore(
hideUnfriends.value = hideUnfriendsConfig; hideUnfriends.value = hideUnfriendsConfig;
randomUserColours.value = randomUserColoursConfig; randomUserColours.value = randomUserColoursConfig;
notificationIconDot.value = notificationIconDotConfig; notificationIconDot.value = notificationIconDotConfig;
compactTableMode.value = compactTableModeConfig;
applyCompactTableMode(compactTableMode.value);
isNavCollapsed.value = navIsCollapsedConfig;
await configRepository.remove('VRCX_navWidth');
// Migrate old settings // Migrate old settings
// Assume all exist if one does // Assume all exist if one does
@@ -256,6 +279,12 @@ export const useAppearanceSettingsStore = defineStore(
{ flush: 'sync' } { flush: 'sync' }
); );
function normalizeThemeMode(mode) {
return Object.prototype.hasOwnProperty.call(THEME_CONFIG, mode)
? mode
: 'light';
}
/** /**
* *
* @param {string} language * @param {string} language
@@ -421,8 +450,9 @@ export const useAppearanceSettingsStore = defineStore(
* @param {string} mode * @param {string} mode
*/ */
function setThemeMode(mode) { function setThemeMode(mode) {
themeMode.value = mode; const normalizedThemeMode = normalizeThemeMode(mode);
configRepository.setString('VRCX_ThemeMode', mode); themeMode.value = normalizedThemeMode;
configRepository.setString('VRCX_ThemeMode', normalizedThemeMode);
applyThemeMode(); applyThemeMode();
} }
function applyThemeMode() { function applyThemeMode() {
@@ -557,18 +587,24 @@ export const useAppearanceSettingsStore = defineStore(
JSON.stringify(methods) JSON.stringify(methods)
); );
} }
/** function setNavCollapsed(collapsed) {
* @param {number} panelNumber isNavCollapsed.value = collapsed;
* @param {Array<number>} widthArray configRepository.setBool('VRCX_navIsCollapsed', collapsed);
*/ }
function setAsideWidth(panelNumber, widthArray) { function toggleNavCollapsed() {
if (Array.isArray(widthArray) && widthArray[1]) { setNavCollapsed(!isNavCollapsed.value);
}
function setAsideWidth(widthOrArray) {
let width = null;
if (Array.isArray(widthOrArray) && widthOrArray.length) {
width = widthOrArray[widthOrArray.length - 1];
} else if (typeof widthOrArray === 'number') {
width = widthOrArray;
}
if (width) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
asideWidth.value = widthArray[1]; asideWidth.value = width;
configRepository.setInt( configRepository.setInt('VRCX_sidePanelWidth', width);
'VRCX_sidePanelWidth',
widthArray[1]
);
}); });
} }
} }
@@ -614,14 +650,22 @@ export const useAppearanceSettingsStore = defineStore(
randomUserColours.value randomUserColours.value
); );
} }
function setCompactTableMode() {
compactTableMode.value = !compactTableMode.value;
applyCompactTableMode(compactTableMode.value);
configRepository.setBool(
'VRCX_compactTableMode',
compactTableMode.value
);
}
/** /**
* @param {object} color * @param {object} color
*/ */
function setTrustColor(color) { function setTrustColor(color) {
trustColor.value = color; trustColor.value = { ...TRUST_COLOR_DEFAULTS };
configRepository.setString( configRepository.setString(
'VRCX_trustColor', 'VRCX_trustColor',
JSON.stringify(color) JSON.stringify(trustColor.value)
); );
} }
@@ -741,6 +785,15 @@ export const useAppearanceSettingsStore = defineStore(
await userColourInit(); await userColourInit();
} }
function applyCompactTableMode(isCompact) {
const className = 'is-compact-table';
if (isCompact) {
document.documentElement.classList.add(className);
} else {
document.documentElement.classList.remove(className);
}
}
return { return {
appLanguage, appLanguage,
themeMode, themeMode,
@@ -766,10 +819,12 @@ export const useAppearanceSettingsStore = defineStore(
hideUserMemos, hideUserMemos,
hideUnfriends, hideUnfriends,
randomUserColours, randomUserColours,
compactTableMode,
trustColor, trustColor,
currentCulture, currentCulture,
isSideBarTabShow, isSideBarTabShow,
notificationIconDot, notificationIconDot,
isNavCollapsed,
setAppLanguage, setAppLanguage,
setDisplayVRCPlusIconsAsAvatar, setDisplayVRCPlusIconsAsAvatar,
@@ -793,6 +848,7 @@ export const useAppearanceSettingsStore = defineStore(
setHideUserMemos, setHideUserMemos,
setHideUnfriends, setHideUnfriends,
setRandomUserColours, setRandomUserColours,
setCompactTableMode,
setTrustColor, setTrustColor,
saveThemeMode, saveThemeMode,
tryInitUserColours, tryInitUserColours,
@@ -802,7 +858,10 @@ export const useAppearanceSettingsStore = defineStore(
applyUserTrustLevel, applyUserTrustLevel,
changeAppLanguage, changeAppLanguage,
promptMaxTableSizeDialog, promptMaxTableSizeDialog,
setNotificationIconDot setNotificationIconDot,
applyCompactTableMode,
setNavCollapsed,
toggleNavCollapsed
}; };
} }
); );
+2 -5
View File
@@ -1,8 +1,5 @@
<template> <template>
<div id="chart" class="x-container"> <div id="chart" class="x-container">
<div class="options-container" style="margin-top: 0">
<span class="header">{{ t('view.charts.header') }}</span>
</div>
<el-tabs v-model="activeTab" class="charts-tabs"> <el-tabs v-model="activeTab" class="charts-tabs">
<el-tab-pane :label="t('view.charts.instance_activity.header')" name="instance"></el-tab-pane> <el-tab-pane :label="t('view.charts.instance_activity.header')" name="instance"></el-tab-pane>
<el-tab-pane :label="t('view.charts.mutual_friend.tab_label')" name="mutual"></el-tab-pane> <el-tab-pane :label="t('view.charts.mutual_friend.tab_label')" name="mutual"></el-tab-pane>
@@ -33,7 +30,7 @@
</script> </script>
<style scoped> <style scoped>
.charts-tabs { :deep(.el-tabs__header) {
margin-bottom: 12px; margin: 0;
} }
</style> </style>
@@ -1,5 +1,5 @@
<template> <template>
<div> <div ref="instanceActivityRef" class="pt-12">
<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>
@@ -151,6 +151,33 @@
const { currentUser } = storeToRefs(useUserStore()); const { currentUser } = storeToRefs(useUserStore());
const { t } = useI18n(); const { t } = useI18n();
const instanceActivityRef = ref(null);
const instanceActivityResizeObserver = new ResizeObserver(() => {
setInstanceActivityHeight();
});
function setInstanceActivityHeight() {
if (instanceActivityRef.value) {
const availableHeight = window.innerHeight - 100;
instanceActivityRef.value.style.height = `${availableHeight}px`;
instanceActivityRef.value.style.overflowY = 'auto';
}
}
onMounted(() => {
if (instanceActivityRef.value) {
instanceActivityResizeObserver.observe(instanceActivityRef.value);
}
setInstanceActivityHeight();
});
onBeforeUnmount(() => {
if (instanceActivityRef.value) {
instanceActivityResizeObserver.unobserve(instanceActivityRef.value);
}
});
const { const {
barWidth, barWidth,
isDetailVisible, isDetailVisible,
@@ -623,7 +650,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 100px; margin-top: 100px;
color: #5c5c5c; color: var(--el-text-color-secondary);
} }
.divider { .divider {
padding: 0 400px; padding: 0 400px;
+22 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="mutual-graph"> <div class="mutual-graph pt-12" ref="mutualGraphRef">
<div class="options-container mutual-graph__toolbar"> <div class="options-container mutual-graph__toolbar">
<div class="mutual-graph__actions"> <div class="mutual-graph__actions">
<el-tooltip :content="t('view.charts.mutual_friend.force_dialog.open_label')" placement="top"> <el-tooltip :content="t('view.charts.mutual_friend.force_dialog.open_label')" placement="top">
@@ -207,6 +207,20 @@
return parsed.invalid ? null : parsed.value; return parsed.invalid ? null : parsed.value;
}; };
const mutualGraphRef = ref(null);
const mutualGraphResizeObserver = new ResizeObserver(() => {
setMutualGraphHeight();
});
function setMutualGraphHeight() {
if (mutualGraphRef.value) {
const availableHeight = window.innerHeight - 100;
mutualGraphRef.value.style.height = `${availableHeight}px`;
mutualGraphRef.value.style.overflowY = 'auto';
}
}
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
if (!chartRef.value) { if (!chartRef.value) {
@@ -215,6 +229,8 @@
createChartInstance(); createChartInstance();
resizeObserver = new ResizeObserver(() => chartInstance?.resize()); resizeObserver = new ResizeObserver(() => chartInstance?.resize());
resizeObserver.observe(chartRef.value); resizeObserver.observe(chartRef.value);
mutualGraphResizeObserver.observe(mutualGraphRef.value);
setMutualGraphHeight();
}); });
}); });
@@ -227,6 +243,9 @@
chartInstance.dispose(); chartInstance.dispose();
chartInstance = null; chartInstance = null;
} }
if (mutualGraphResizeObserver) {
mutualGraphResizeObserver.disconnect();
}
}); });
watch( watch(
@@ -676,8 +695,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
margin-top: 0; margin-top: 8px;
margin-bottom: 8px; margin-bottom: 0;
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
+1
View File
@@ -1580,6 +1580,7 @@
justify-content: space-between; justify-content: space-between;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
margin-bottom: 9px;
} }
.group-section__list { .group-section__list {
+1
View File
@@ -796,6 +796,7 @@
justify-content: space-between; justify-content: space-between;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
margin-bottom: 9px;
} }
.group-section__list { .group-section__list {
+1
View File
@@ -1276,6 +1276,7 @@
justify-content: space-between; justify-content: space-between;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
margin-bottom: 9px;
} }
.group-section__list { .group-section__list {
+32 -32
View File
@@ -1,14 +1,13 @@
<template> <template>
<div class="x-container feed"> <div class="x-container feed" ref="feedRef">
<div style="margin: 0 0 10px; display: flex; align-items: center"> <div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center"> <div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<NativeTooltip <el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
placement="bottom" <el-switch
:content="t('view.feed.favorites_only_tooltip')" v-model="feedTable.vip"
:enter-ms="140" active-color="var(--el-color-success)"
:exit-ms="120"> @change="feedTableLookup"></el-switch>
<el-switch v-model="feedTable.vip" active-color="#13ce66" @change="feedTableLookup"></el-switch> </el-tooltip>
</NativeTooltip>
</div> </div>
<el-select <el-select
v-model="feedTable.filter" v-model="feedTable.filter"
@@ -33,9 +32,9 @@
</div> </div>
<DataTable v-bind="feedTable" :data="feedDisplayData"> <DataTable v-bind="feedTable" :data="feedDisplayData">
<el-table-column type="expand" width="20"> <el-table-column type="expand" width="30">
<template #default="scope"> <template #default="scope">
<div style="position: relative; font-size: 14px"> <div style="position: relative; font-size: 14px" class="pl-5">
<template v-if="scope.row.type === 'GPS'"> <template v-if="scope.row.type === 'GPS'">
<Location <Location
v-if="scope.row.previousLocation" v-if="scope.row.previousLocation"
@@ -45,9 +44,7 @@
timeToText(scope.row.time) timeToText(scope.row.time)
}}</el-tag> }}</el-tag>
<br /> <br />
<span style="margin-right: 5px"> <span style="margin-right: 5px"> </span>
<el-icon><Right /></el-icon>
</span>
<Location <Location
v-if="scope.row.location" v-if="scope.row.location"
:location="scope.row.location" :location="scope.row.location"
@@ -91,7 +88,7 @@
</template> </template>
</div> </div>
<span style="position: relative; margin: 0 10px"> <span style="position: relative; margin: 0 10px">
<el-icon><Right /></el-icon> {{ ' → ' }}
</span> </span>
<div style="display: inline-block; vertical-align: top; width: 160px"> <div style="display: inline-block; vertical-align: top; width: 160px">
@@ -116,9 +113,7 @@
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i> <i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span> <span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
<br /> <br />
<span> <span> </span>
<el-icon><Right /></el-icon>
</span>
<i class="x-user-status" :class="statusClass(scope.row.status)" style="margin: 0 5px"></i> <i class="x-user-status" :class="statusClass(scope.row.status)" style="margin: 0 5px"></i>
<span v-text="scope.row.statusDescription"></span> <span v-text="scope.row.statusDescription"></span>
@@ -132,27 +127,29 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.feed.date')" prop="created_at" width="130"> <el-table-column :label="t('table.feed.date')" prop="created_at" width="140">
<template #default="scope"> <template #default="scope">
<NativeTooltip placement="right"> <el-tooltip placement="right">
<template #content> <template #content>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span> <span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template> </template>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span> <span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</NativeTooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.feed.type')" prop="type" width="80"> <el-table-column :label="t('table.feed.type')" prop="type" width="130">
<template #default="scope"> <template #default="scope">
<span v-text="t('view.feed.filters.' + scope.row.type)"></span> <el-tag type="info" effect="plain" size="small">{{
t('view.feed.filters.' + scope.row.type)
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.feed.user')" prop="displayName" width="180"> <el-table-column :label="t('table.feed.user')" prop="displayName" width="190">
<template #default="scope"> <template #default="scope">
<span <span
class="x-link" class="x-link table-user"
style="padding-right: 10px" style="padding-right: 10px"
@click="showUserDialog(scope.row.userId)" @click="showUserDialog(scope.row.userId)"
v-text="scope.row.displayName"></span> v-text="scope.row.displayName"></span>
@@ -178,17 +175,12 @@
<template v-else-if="scope.row.type === 'Status'"> <template v-else-if="scope.row.type === 'Status'">
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription"> <template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i> <i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
<span style="margin: 0 5px"> <span class="mx-2"> </span>
<el-icon><Right /></el-icon>
</span>
<i class="x-user-status" :class="statusClass(scope.row.status)"></i> <i class="x-user-status" :class="statusClass(scope.row.status)"></i>
</template> </template>
<template v-else> <template v-else>
<i <i class="x-user-status mr-2" :class="statusClass(scope.row.status)"></i>
class="x-user-status"
:class="statusClass(scope.row.status)"
style="margin-right: 3px"></i>
<span v-text="scope.row.statusDescription"></span> <span v-text="scope.row.statusDescription"></span>
</template> </template>
</template> </template>
@@ -210,13 +202,13 @@
</template> </template>
<script setup> <script setup>
import { Right } from '@element-plus/icons-vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { formatDateFilter, statusClass, timeToText } from '../../shared/utils'; import { formatDateFilter, statusClass, timeToText } from '../../shared/utils';
import { useFeedStore, useUserStore } from '../../stores'; import { useFeedStore, useUserStore } from '../../stores';
import { useTableHeight } from '../../composables/useTableHeight';
const { showUserDialog } = useUserStore(); const { showUserDialog } = useUserStore();
const { feedTable } = storeToRefs(useFeedStore()); const { feedTable } = storeToRefs(useFeedStore());
@@ -226,6 +218,8 @@
const { t } = useI18n(); const { t } = useI18n();
const { containerRef: feedRef } = useTableHeight(feedTable);
/** /**
* Function that format the differences between two strings with HTML tags * Function that format the differences between two strings with HTML tags
* markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined. * markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined.
@@ -341,3 +335,9 @@
.replace(/<br> /g, '<br>'); .replace(/<br> /g, '<br>');
} }
</script> </script>
<style scoped>
.table-user {
color: var(--x-table-user-text-color) !important;
}
</style>
+160 -129
View File
@@ -1,92 +1,59 @@
<template> <template>
<div class="x-container"> <div class="x-container" ref="friendsListRef">
<div style="padding: 0 10px 0 10px"> <div>
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<span class="header">{{ t('view.friend_list.header') }}</span>
<div style="font-size: 13px; display: flex; align-items: center">
<el-button size="small" @click="openChartsTab" style="margin-right: 10px">
{{ t('view.friend_list.load_mutual_friends') }}
</el-button>
<div v-if="friendsListBulkUnfriendMode" style="display: inline-block; margin-right: 10px">
<el-button size="small" @click="showBulkUnfriendSelectionConfirm">
{{ t('view.friend_list.bulk_unfriend_selection') }}
</el-button>
<!-- el-button(size="small" @click="showBulkUnfriendAllConfirm" style="margin-right:5px") Bulk Unfriend All-->
</div>
<div style="display: flex; align-items: center; margin-right: 10px">
<span class="name">{{ t('view.friend_list.bulk_unfriend') }}</span>
<el-switch
v-model="friendsListBulkUnfriendMode"
style="margin-left: 5px"
@change="toggleFriendsListBulkUnfriendMode"></el-switch>
</div>
<span>{{ t('view.friend_list.load') }}</span>
<template v-if="friendsListLoading">
<span style="margin-left: 5px" v-text="friendsListLoadingProgress"></span>
<el-tooltip placement="top" :content="t('view.friend_list.cancel_tooltip')">
<el-button
size="small"
:icon="Loading"
circle
style="margin-left: 5px"
@click="friendsListLoading = false"></el-button>
</el-tooltip>
</template>
<template v-else>
<el-tooltip placement="top" :content="t('view.friend_list.load_tooltip')">
<el-button
size="small"
:icon="RefreshLeft"
circle
style="margin-left: 5px"
@click="friendsListLoadUsers"></el-button>
</el-tooltip>
</template>
</div>
</div>
<div style="margin: 10px 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center"> <div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip placement="bottom" :content="t('view.friend_list.favorites_only_tooltip')"> <el-tooltip placement="bottom" :content="t('view.friend_list.favorites_only_tooltip')">
<el-switch <el-switch
v-model="friendsListSearchFilterVIP" v-model="friendsListSearchFilterVIP"
active-color="#13ce66" active-color="var(--el-color-success)"
@change="friendsListSearchChange"></el-switch> @change="friendsListSearchChange"></el-switch>
</el-tooltip> </el-tooltip>
<el-select
v-model="friendsListSearchFilters"
multiple
clearable
collapse-tags
style="margin: 0 10px; width: 150px"
:placeholder="t('view.friend_list.filter_placeholder')"
@change="friendsListSearchChange">
<el-option
v-for="type in ['Display Name', 'User Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo']"
:key="type"
:label="type"
:value="type"></el-option>
</el-select>
<el-input
v-model="friendsListSearch"
:placeholder="t('view.friend_list.search_placeholder')"
clearable
style="width: 250px"
@change="friendsListSearchChange"></el-input>
</div>
<div class="flex items-center">
<div v-if="friendsListBulkUnfriendMode" class="inline-block mr-10">
<el-button @click="showBulkUnfriendSelectionConfirm">
{{ t('view.friend_list.bulk_unfriend_selection') }}
</el-button>
<!-- el-button(size="small" @click="showBulkUnfriendAllConfirm" style="margin-right:5px") Bulk Unfriend All-->
</div>
<div class="flex items-center mr-3">
<span class="name mr-2 text-xs">{{ t('view.friend_list.bulk_unfriend') }}</span>
<el-switch
v-model="friendsListBulkUnfriendMode"
@change="toggleFriendsListBulkUnfriendMode"></el-switch>
</div>
<div class="flex items-center">
<el-button @click="openChartsTab">
{{ t('view.friend_list.load_mutual_friends') }}
</el-button>
<el-button @click="friendsListLoadUsers">{{ t('view.friend_list.load') }}</el-button>
</div>
</div> </div>
<el-input
v-model="friendsListSearch"
:placeholder="t('view.friend_list.search_placeholder')"
clearable
style="flex: 1"
@change="friendsListSearchChange"></el-input>
<el-select
v-model="friendsListSearchFilters"
multiple
clearable
collapse-tags
style="flex: 0.3; margin: 0 10px"
:placeholder="t('view.friend_list.filter_placeholder')"
@change="friendsListSearchChange">
<el-option
v-for="type in ['Display Name', 'User Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo']"
:key="type"
:label="type"
:value="type"></el-option>
</el-select>
<el-tooltip placement="top" :content="t('view.friend_list.refresh_tooltip')">
<el-button
type="default"
:icon="Refresh"
circle
style="flex: none"
@click="friendsListSearchChange"></el-button>
</el-tooltip>
</div> </div>
<DataTable <DataTable
v-loading="friendsListLoading"
v-bind="friendsListTable" v-bind="friendsListTable"
:table-props="{ height: 'calc(100vh - 170px)', size: 'small' }"
style="margin-top: 10px; cursor: pointer" style="margin-top: 10px; cursor: pointer"
@row-click="selectFriendsListRow"> @row-click="selectFriendsListRow">
<el-table-column v-if="friendsListBulkUnfriendMode" width="55"> <el-table-column v-if="friendsListBulkUnfriendMode" width="55">
@@ -98,39 +65,38 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.no')" width="70" prop="$friendNumber" :sortable="true"> <el-table-column width="20"></el-table-column>
<el-table-column
:label="t('table.friendList.no')"
width="70"
prop="$friendNumber"
:sortable="true"
fixed="left">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ row.$friendNumber ? row.$friendNumber : '' }}</span> <span>{{ row.$friendNumber ? row.$friendNumber : '' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.avatar')" width="70" prop="photo"> <el-table-column :label="t('table.friendList.avatar')" width="90" prop="photo">
<template #default="{ row }"> <template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover"> <div class="flex items-center">
<template #reference> <img :src="userImage(row, true)" class="friends-list-avatar" loading="lazy" />
<img :src="userImage(row, true)" class="friends-list-avatar" loading="lazy" /> </div>
</template>
<img
:src="userImageFull(row)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(row))"
loading="lazy" />
</el-popover>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.displayName')" :label="t('table.friendList.displayName')"
min-width="140" min-width="200"
prop="displayName" prop="displayName"
sortable sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')"> :sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')"
fixed="left">
<template #default="{ row }"> <template #default="{ row }">
<span :style="{ color: randomUserColours ? row.$userColour : undefined }" class="name">{{ <span :style="{ color: randomUserColours ? row.$userColour : undefined }" class="name">{{
row.displayName row.displayName
}}</span> }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.rank')" width="110" prop="$trustSortNum" :sortable="true"> <el-table-column :label="t('table.friendList.rank')" width="140" prop="$trustSortNum" :sortable="true">
<template #default="{ row }"> <template #default="{ row }">
<span <span
v-if="randomUserColours" v-if="randomUserColours"
@@ -142,7 +108,7 @@
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.status')" :label="t('table.friendList.status')"
min-width="180" min-width="200"
prop="status" prop="status"
sortable sortable
:sort-method="(a, b) => sortStatus(a.status, b.status)"> :sort-method="(a, b) => sortStatus(a.status, b.status)">
@@ -157,7 +123,7 @@
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.language')" :label="t('table.friendList.language')"
width="110" width="130"
prop="$languages" prop="$languages"
sortable sortable
:sort-method="(a, b) => sortLanguages(a, b)"> :sort-method="(a, b) => sortLanguages(a, b)">
@@ -173,7 +139,7 @@
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.bioLink')" width="100" prop="bioLinks"> <el-table-column :label="t('table.friendList.bioLink')" width="130" prop="bioLinks">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index"> <el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index">
<template #content> <template #content>
@@ -197,8 +163,14 @@
:label="t('table.friendList.joinCount')" :label="t('table.friendList.joinCount')"
width="120" width="120"
prop="$joinCount" prop="$joinCount"
sortable></el-table-column> sortable
<el-table-column :label="t('table.friendList.timeTogether')" width="140" prop="$timeSpent" sortable> align="right"></el-table-column>
<el-table-column
:label="t('table.friendList.timeTogether')"
width="140"
prop="$timeSpent"
sortable
align="right">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.$timeSpent">{{ timeToText(row.$timeSpent) }}</span> <span v-if="row.$timeSpent">{{ timeToText(row.$timeSpent) }}</span>
</template> </template>
@@ -210,17 +182,26 @@
sortable sortable
:sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')"> :sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ formatDateFilter(row.$lastSeen, 'long') }}</span> <span>{{
formatDateFilter(row.$lastSeen, 'long') === '-'
? ''
: formatDateFilter(row.$lastSeen, 'long')
}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendList.mutualFriends')" width="120" prop="$mutualCount" sortable> <el-table-column
:label="t('table.friendList.mutualFriends')"
width="120"
prop="$mutualCount"
sortable
align="right">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.$mutualCount">{{ row.$mutualCount }}</span> <span v-if="row.$mutualCount">{{ row.$mutualCount }}</span>
<span v-else></span> </template <span v-else></span> </template
></el-table-column> ></el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.lastActivity')" :label="t('table.friendList.lastActivity')"
width="170" width="200"
prop="last_activity" prop="last_activity"
sortable sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_activity')"> :sort-method="(a, b) => sortAlphabetically(a, b, 'last_activity')">
@@ -230,7 +211,7 @@
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="t('table.friendList.lastLogin')" :label="t('table.friendList.lastLogin')"
width="170" width="200"
prop="last_login" prop="last_login"
sortable sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_login')"> :sort-method="(a, b) => sortAlphabetically(a, b, 'last_login')">
@@ -246,23 +227,42 @@
:sort-method="(a, b) => sortAlphabetically(a, b, 'date_joined')"></el-table-column> :sort-method="(a, b) => sortAlphabetically(a, b, 'date_joined')"></el-table-column>
<el-table-column :label="t('table.friendList.unfriend')" width="100" align="center"> <el-table-column :label="t('table.friendList.unfriend')" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button <i
text class="ri-user-unfollow-line"
:icon="Close"
style="color: #f56c6c" style="color: #f56c6c"
size="small" @click.stop="confirmDeleteFriend(row.id)"></i>
@click.stop="confirmDeleteFriend(row.id)"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</DataTable> </DataTable>
<el-dialog
v-model="friendsListLoadDialogVisible"
:title="t('view.friend_list.load_dialog_title')"
width="420px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
align-center>
<div style="margin-bottom: 10px" v-text="t('view.friend_list.load_dialog_message')"></div>
<el-progress
:percentage="friendsListLoadingPercent"
:text-inside="true"
:stroke-width="16"></el-progress>
<div style="margin-top: 10px; text-align: right">
<span>{{ friendsListLoadingCurrent }} / {{ friendsListLoadingTotal }}</span>
</div>
<template #footer>
<el-button @click="cancelFriendsListLoad">
{{ t('view.friend_list.load_cancel') }}
</el-button>
</template>
</el-dialog>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Close, Loading, Refresh, RefreshLeft } from '@element-plus/icons-vue'; import { computed, nextTick, reactive, ref, watch } from 'vue';
import { nextTick, reactive, ref, watch } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@@ -276,19 +276,13 @@
sortStatus, sortStatus,
statusClass, statusClass,
timeToText, timeToText,
userImage, userImage
userImageFull
} from '../../shared/utils'; } from '../../shared/utils';
import { import { useAppearanceSettingsStore, useFriendStore, useSearchStore, useUserStore } from '../../stores';
useAppearanceSettingsStore,
useFriendStore,
useGalleryStore,
useSearchStore,
useUserStore
} from '../../stores';
import { friendRequest, userRequest } from '../../api'; import { friendRequest, userRequest } from '../../api';
import removeConfusables, { removeWhitespace } from '../../service/confusables'; import removeConfusables, { removeWhitespace } from '../../service/confusables';
import { router } from '../../plugin/router'; import { router } from '../../plugin/router';
import { useTableHeight } from '../../composables/useTableHeight';
const { t } = useI18n(); const { t } = useI18n();
@@ -299,21 +293,34 @@
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore()); const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore(); const { showUserDialog } = useUserStore();
const { stringComparer, friendsListSearch } = storeToRefs(useSearchStore()); const { stringComparer, friendsListSearch } = storeToRefs(useSearchStore());
const { showFullscreenImageDialog } = useGalleryStore();
const friendsListSearchFilters = ref([]); const friendsListSearchFilters = ref([]);
const friendsListTable = reactive({ const friendsListTable = reactive({
data: [], data: [],
tableProps: { stripe: true, size: 'small', defaultSort: { prop: '$friendNumber', order: 'descending' } }, tableProps: {
stripe: true,
size: 'small',
defaultSort: { prop: '$friendNumber', order: 'descending' },
scrollbarAlwaysOn: true
},
pageSize: 100, pageSize: 100,
paginationProps: { layout: 'sizes,prev,pager,next,total', pageSizes: [50, 100, 250, 500] } paginationProps: { layout: 'sizes,prev,pager,next,total', pageSizes: [50, 100, 250, 500] }
}); });
const friendsListBulkUnfriendMode = ref(false); const friendsListBulkUnfriendMode = ref(false);
const friendsListLoading = ref(false); const friendsListLoading = ref(false);
const friendsListLoadingProgress = ref(''); const friendsListLoadingCurrent = ref(0);
const friendsListLoadingTotal = ref(0);
const friendsListLoadDialogVisible = ref(false);
const friendsListSearchFilterVIP = ref(false); const friendsListSearchFilterVIP = ref(false);
const selectedFriends = ref(new Set()); const selectedFriends = ref(new Set());
const friendsListLoadingPercent = computed(() => {
if (!friendsListLoadingTotal.value) return 0;
return Math.min(100, Math.round((friendsListLoadingCurrent.value / friendsListLoadingTotal.value) * 100));
});
const { containerRef: friendsListRef } = useTableHeight(ref(friendsListTable));
const route = useRoute(); const route = useRoute();
watch( watch(
@@ -432,27 +439,43 @@
} }
async function friendsListLoadUsers() { async function friendsListLoadUsers() {
friendsListLoading.value = true;
let i = 0;
const toFetch = Array.from(friends.value.values()) const toFetch = Array.from(friends.value.values())
.filter((ctx) => ctx.ref && !ctx.ref.date_joined) .filter((ctx) => ctx.ref && !ctx.ref.date_joined)
.map((ctx) => ctx.id); .map((ctx) => ctx.id);
const total = toFetch.length; const total = toFetch.length;
friendsListLoadingTotal.value = total;
friendsListLoadingCurrent.value = 0;
if (!total) {
ElMessage.success(t('view.friend_list.load_complete'));
return;
}
friendsListLoading.value = true;
friendsListLoadDialogVisible.value = true;
let cancelled = false;
for (const userId of toFetch) { for (const userId of toFetch) {
if (!friendsListLoading.value) { if (!friendsListLoading.value) {
friendsListLoadingProgress.value = ''; cancelled = true;
return; break;
} }
i++; friendsListLoadingCurrent.value += 1;
friendsListLoadingProgress.value = `${i}/${total}`;
try { try {
await userRequest.getUser({ userId }); await userRequest.getUser({ userId });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} }
friendsListLoadingProgress.value = '';
friendsListLoading.value = false; friendsListLoading.value = false;
friendsListLoadDialogVisible.value = false;
friendsListLoadingCurrent.value = 0;
friendsListLoadingTotal.value = 0;
if (!cancelled) {
ElMessage.success(t('view.friend_list.load_complete'));
}
}
function cancelFriendsListLoad() {
friendsListLoading.value = false;
friendsListLoadDialogVisible.value = false;
} }
function selectFriendsListRow(val) { function selectFriendsListRow(val) {
@@ -476,3 +499,11 @@
router.push({ name: 'charts' }); router.push({ name: 'charts' });
} }
</script> </script>
<style scoped>
.friends-list-avatar {
object-fit: cover;
height: 22px;
width: 22px;
}
</style>
+23 -21
View File
@@ -1,6 +1,5 @@
<template> <template>
<div class="x-container"> <div class="x-container" ref="friendLogRef">
<!-- 工具栏 -->
<div style="margin: 0 0 10px; display: flex; align-items: center"> <div style="margin: 0 0 10px; display: flex; align-items: center">
<el-select <el-select
v-model="friendLogTable.filters[0].value" v-model="friendLogTable.filters[0].value"
@@ -40,27 +39,25 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendLog.type')" prop="type" width="150"> <el-table-column :label="t('table.friendLog.type')" prop="type" width="200">
<template #default="scope"> <template #default="scope">
<span v-text="t('view.friend_log.filters.' + scope.row.type)"></span> <el-tag type="info" effect="plain" size="small"
><span v-text="t('view.friend_log.filters.' + scope.row.type)"></span
></el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.friendLog.user')" prop="displayName"> <el-table-column :label="t('table.friendLog.user')" prop="displayName">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.type === 'DisplayName'"> <span v-if="scope.row.type === 'DisplayName'">{{ scope.row.previousDisplayName }} </span>
{{ scope.row.previousDisplayName }} <el-icon><Right /></el-icon>&nbsp;
</span>
<span <span
class="x-link" class="x-link table-user"
style="padding-right: 10px" style="padding-right: 10px"
@click="showUserDialog(scope.row.userId)" @click="showUserDialog(scope.row.userId)"
v-text="scope.row.displayName || scope.row.userId"></span> >{{ scope.row.displayName || scope.row.userId }}
</span>
<template v-if="scope.row.type === 'TrustLevel'"> <template v-if="scope.row.type === 'TrustLevel'">
<span> <span>({{ scope.row.previousTrustLevel }} {{ scope.row.trustLevel }})</span>
({{ scope.row.previousTrustLevel }} <el-icon><Right /></el-icon>
{{ scope.row.trustLevel }})</span
>
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
@@ -69,28 +66,27 @@
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="shiftHeld" v-if="shiftHeld"
style="color: #f56c6c" style="color: var(--el-color-danger)"
text text
:icon="Close" :icon="Close"
size="small" size="small"
class="button-pd-0" class="button-pd-0"
@click="deleteFriendLog(scope.row)"></el-button> @click="deleteFriendLog(scope.row)"></el-button>
<el-button <i
v-else v-else
text class="ri-delete-bin-line"
:icon="Delete" style="opacity: 0.85"
size="small" @click="deleteFriendLogPrompt(scope.row)"></i>
class="button-pd-0"
@click="deleteFriendLogPrompt(scope.row)"></el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="5"></el-table-column>
</DataTable> </DataTable>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Close, Delete, Right } from '@element-plus/icons-vue';
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import { Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -100,6 +96,7 @@
import { useAppearanceSettingsStore, useFriendStore, useUiStore, useUserStore } from '../../stores'; import { useAppearanceSettingsStore, useFriendStore, useUiStore, useUserStore } from '../../stores';
import { formatDateFilter, removeFromArray } from '../../shared/utils'; import { formatDateFilter, removeFromArray } from '../../shared/utils';
import { database } from '../../service/database'; import { database } from '../../service/database';
import { useTableHeight } from '../../composables/useTableHeight';
import configRepository from '../../service/config'; import configRepository from '../../service/config';
@@ -108,6 +105,8 @@
const { friendLogTable } = storeToRefs(useFriendStore()); const { friendLogTable } = storeToRefs(useFriendStore());
const { shiftHeld } = storeToRefs(useUiStore()); const { shiftHeld } = storeToRefs(useUiStore());
const { containerRef: friendLogRef } = useTableHeight(friendLogTable);
const friendLogDisplayData = computed(() => { const friendLogDisplayData = computed(() => {
const data = friendLogTable.value.data; const data = friendLogTable.value.data;
return data.slice().sort((a, b) => { return data.slice().sort((a, b) => {
@@ -160,4 +159,7 @@
.button-pd-0 { .button-pd-0 {
padding: 0 !important; padding: 0 !important;
} }
.table-user {
color: var(--x-table-user-text-color);
}
</style> </style>
+17 -17
View File
@@ -32,7 +32,7 @@
<el-slider <el-slider
v-model="cardScale" v-model="cardScale"
class="friend-view__slider" class="friend-view__slider"
:min="0.6" :min="0.5"
:max="1.0" :max="1.0"
:step="0.01" :step="0.01"
:show-tooltip="false" /> :show-tooltip="false" />
@@ -45,8 +45,8 @@
<el-slider <el-slider
v-model="cardSpacing" v-model="cardSpacing"
class="friend-view__slider" class="friend-view__slider"
:min="0.5" :min="0.25"
:max="1.5" :max="1.0"
:step="0.05" :step="0.05"
:show-tooltip="false" /> :show-tooltip="false" />
</div> </div>
@@ -688,7 +688,7 @@
}); });
</script> </script>
<style scoped lang="scss"> <style scoped>
.friend-view { .friend-view {
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
@@ -699,12 +699,12 @@
display: flex; display: flex;
gap: 20px; gap: 20px;
align-items: center; align-items: center;
padding: 6px 10px 0 2px; padding: 6px 2px 0 2px;
} }
.friend-view__toolbar--loading { .friend-view__toolbar--loading {
justify-content: flex-end; justify-content: flex-end;
color: rgba(15, 23, 42, 0.55); color: var(--el-text-color-secondary);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
} }
@@ -720,7 +720,7 @@
flex: 1; flex: 1;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
color: rgba(15, 23, 42, 0.65); color: var(--el-text-color-regular);
} }
.friend-view__settings-label { .friend-view__settings-label {
@@ -746,7 +746,7 @@
.friend-view__scale-value { .friend-view__scale-value {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: rgba(15, 23, 42, 0.55); color: var(--el-text-color-secondary);
min-width: 42px; min-width: 42px;
text-align: right; text-align: right;
} }
@@ -762,14 +762,14 @@
} }
.friend-view__scroll { .friend-view__scroll {
padding: 2px 10px 2px 2px; padding: 2px;
} }
.friend-view__initial-loading { .friend-view__initial-loading {
display: grid; display: grid;
place-items: center; place-items: center;
min-height: 240px; min-height: 240px;
color: rgba(15, 23, 42, 0.45); color: var(--el-text-color-secondary);
} }
.friend-view__grid { .friend-view__grid {
@@ -780,7 +780,7 @@
); );
gap: var(--friend-card-gap, 18px); gap: var(--friend-card-gap, 18px);
justify-content: start; justify-content: start;
padding-right: 2px; padding: 2px;
} }
.friend-view__instances { .friend-view__instances {
@@ -802,7 +802,7 @@
margin: 5px 10px; margin: 5px 10px;
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
color: rgba(15, 23, 42, 0.75); color: var(--el-text-color-primary);
} }
.friend-view__divider { .friend-view__divider {
@@ -810,7 +810,7 @@
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin: 16px 4px; margin: 16px 4px;
color: rgba(15, 23, 42, 0.6); color: var(--el-text-color-regular);
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
} }
@@ -820,7 +820,7 @@
content: ''; content: '';
flex: 1; flex: 1;
height: 1px; height: 1px;
background: rgba(148, 163, 184, 0.35); background: var(--el-border-color);
} }
.friend-view__divider-text { .friend-view__divider-text {
@@ -829,14 +829,14 @@
.friend-view__instance-count { .friend-view__instance-count {
font-size: 12px; font-size: 12px;
color: rgba(15, 23, 42, 0.45); color: var(--el-text-color-secondary);
} }
.friend-view__empty { .friend-view__empty {
display: grid; display: grid;
place-items: center; place-items: center;
min-height: 240px; min-height: 240px;
color: rgba(0, 0, 0, 0.45); color: var(--el-text-color-secondary);
font-size: 15px; font-size: 15px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
@@ -847,7 +847,7 @@
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
padding: 18px 0 12px; padding: 18px 0 12px;
color: rgba(0, 0, 0, 0.55); color: var(--el-text-color-secondary);
font-size: 14px; font-size: 14px;
} }
@@ -12,11 +12,11 @@
</el-avatar> </el-avatar>
</div> </div>
<span class="friend-card__status-dot" :class="statusDotClass"></span> <span class="friend-card__status-dot" :class="statusDotClass"></span>
<div class="friend-card__name" :title="friend.name">{{ friend.name }}</div> <div class="friend-card__name ml-0.5" :title="friend.name">{{ friend.name }}</div>
</div> </div>
<div class="friend-card__body"> <div class="friend-card__body">
<div class="friend-card__signature" :title="friend.ref?.statusDescription"> <div class="friend-card__signature" :title="friend.ref?.statusDescription">
<i v-if="friend.ref?.statusDescription" class="ri-pencil-line" style="opacity: 0.7"></i> <i v-if="friend.ref?.statusDescription" class="ri-pencil-line mr-0.5" style="opacity: 0.7"></i>
{{ friend.ref?.statusDescription || '&nbsp;' }} {{ friend.ref?.statusDescription || '&nbsp;' }}
</div> </div>
<div v-if="displayInstanceInfo" @click.stop class="friend-card__world" :title="friend.worldName"> <div v-if="displayInstanceInfo" @click.stop class="friend-card__world" :title="friend.worldName">
@@ -87,17 +87,17 @@
}); });
</script> </script>
<style scoped lang="scss"> <style scoped>
.friend-card { .friend-card {
--card-scale: 1; --card-scale: 1;
--card-spacing: 1; --card-spacing: 1;
position: relative; position: relative;
display: grid; display: grid;
gap: calc(14px * var(--card-scale) * var(--card-spacing)); gap: calc(14px * var(--card-scale) * var(--card-spacing));
border-radius: calc(8px * var(--card-scale)); border-radius: 8px;
background: #fff; background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
box-shadow: 0 calc(6px * var(--card-scale)) calc(16px * var(--card-scale)) rgba(15, 23, 42, 0.04); box-shadow: var(--el-box-shadow-lighter);
transition: transition:
box-shadow 0.2s ease, box-shadow 0.2s ease,
transform 0.2s ease; transform 0.2s ease;
@@ -105,7 +105,7 @@
min-width: var(--friend-card-min-width, 220px); min-width: var(--friend-card-min-width, 220px);
&:hover { &:hover {
box-shadow: 0 calc(10px * var(--card-scale)) calc(24px * var(--card-scale)) rgba(15, 23, 42, 0.07); box-shadow: var(--el-box-shadow-light);
transform: translateY(calc(-2px * var(--card-scale))); transform: translateY(calc(-2px * var(--card-scale)));
} }
} }
@@ -123,8 +123,8 @@
} }
.friend-card__avatar { .friend-card__avatar {
border: 1px solid rgba(255, 255, 255, 0.85); border: 1px solid var(--el-border-color);
box-shadow: 0 calc(5px * var(--card-scale)) calc(10px * var(--card-scale)) rgba(15, 23, 42, 0.14); box-shadow: var(--el-box-shadow-lighter);
} }
.friend-card__status-dot { .friend-card__status-dot {
@@ -134,8 +134,8 @@
inline-size: calc(12px * var(--card-scale)); inline-size: calc(12px * var(--card-scale));
block-size: calc(12px * var(--card-scale)); block-size: calc(12px * var(--card-scale));
border-radius: 999px; border-radius: 999px;
border: calc(2px * var(--card-scale)) solid #fff; border: calc(2px * var(--card-scale)) solid var(--el-bg-color-overlay);
box-shadow: 0 0 calc(4px * var(--card-scale)) rgba(15, 23, 42, 0.12); box-shadow: var(--el-box-shadow-lighter);
pointer-events: none; pointer-events: none;
} }
@@ -144,23 +144,23 @@
} }
.friend-card__status-dot--online { .friend-card__status-dot--online {
background: linear-gradient(145deg, #67c23a, #4aa12d); background: #67c23a;
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(103, 194, 58, 0.4); box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #67c23a 40%, transparent);
} }
.friend-card__status-dot--join { .friend-card__status-dot--join {
background: linear-gradient(145deg, #409eff, #2f7ed9); background: #409eff;
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(64, 158, 255, 0.4); box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #409eff 40%, transparent);
} }
.friend-card__status-dot--busy { .friend-card__status-dot--busy {
background: linear-gradient(145deg, #ff2c2c, #d81f1f); background: #ff2c2c;
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(255, 44, 44, 0.4); box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #ff2c2c 40%, transparent);
} }
.friend-card__status-dot--ask { .friend-card__status-dot--ask {
background: linear-gradient(145deg, #ff9500, #d97800); background: #ff9500;
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(255, 149, 0, 0.4); box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #ff9500 40%, transparent);
} }
.friend-card__body { .friend-card__body {
@@ -171,7 +171,7 @@
.friend-card__name { .friend-card__name {
font-size: calc(17px * var(--card-scale)); font-size: calc(17px * var(--card-scale));
font-weight: 600; font-weight: 600;
color: #1f2937; color: var(--el-text-color-primary);
line-height: 1.2; line-height: 1.2;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -181,7 +181,7 @@
.friend-card__signature { .friend-card__signature {
margin-top: calc(6px * var(--card-spacing)); margin-top: calc(6px * var(--card-spacing));
font-size: calc(13px * var(--card-scale)); font-size: calc(13px * var(--card-scale));
color: rgba(31, 41, 55, 0.7); color: var(--el-text-color-secondary);
line-height: 1.4; line-height: 1.4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -194,12 +194,21 @@
justify-content: center; justify-content: center;
min-height: calc(40px * var(--card-scale)); min-height: calc(40px * var(--card-scale));
padding: calc(6px * var(--card-scale)) calc(10px * var(--card-scale)); padding: calc(6px * var(--card-scale)) calc(10px * var(--card-scale));
border-radius: calc(12px * var(--card-scale)); border-radius: calc(10px * var(--card-scale));
background: rgba(148, 163, 184, 0.18); background: var(--el-fill-color);
color: rgba(71, 85, 105, 0.95); color: var(--el-text-color-regular);
font-size: calc(12px * var(--card-scale)); font-size: calc(12px * var(--card-scale));
line-height: 1.3; line-height: 1.3;
box-sizing: border-box; box-sizing: border-box;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
:global(html.dark) .friend-card__world,
:global(:root.dark) .friend-card__world,
:global(:root[data-theme='dark']) .friend-card__world {
color: var(--color-zinc-300);
} }
.friend-card__location { .friend-card__location {
+55 -40
View File
@@ -1,13 +1,13 @@
<template> <template>
<div class="x-container"> <div class="x-container" ref="gameLogRef">
<div style="margin: 0 0 10px; display: flex; align-items: center"> <div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center"> <div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<NativeTooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')"> <el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
<el-switch <el-switch
v-model="gameLogTable.vip" v-model="gameLogTable.vip"
active-color="#13ce66" active-color="var(--el-color-success)"
@change="gameLogTableLookup"></el-switch> @change="gameLogTableLookup"></el-switch>
</NativeTooltip> </el-tooltip>
</div> </div>
<el-select <el-select
v-model="gameLogTable.filter" v-model="gameLogTable.filter"
@@ -41,46 +41,47 @@
</div> </div>
<DataTable v-bind="gameLogTable" :data="gameLogDisplayData"> <DataTable v-bind="gameLogTable" :data="gameLogDisplayData">
<el-table-column :label="t('table.gameLog.date')" prop="created_at" width="130"> <el-table-column :label="t('table.gameLog.date')" prop="created_at" width="140">
<template #default="scope"> <template #default="scope">
<NativeTooltip placement="right"> <el-tooltip placement="right">
<template #content> <template #content>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span> <span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template> </template>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span> <span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</NativeTooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('table.gameLog.type')" prop="type" width="120"> <el-table-column :label="t('table.gameLog.type')" prop="type" width="150">
<template #default="scope">
<el-tag
v-if="scope.row.location && scope.row.type !== 'Location'"
type="info"
effect="plain"
size="small">
<span
class="x-link"
@click="showWorldDialog(scope.row.location)"
v-text="t('view.game_log.filters.' + scope.row.type)"></span>
</el-tag>
<el-tag v-else type="info" effect="plain" size="small">
<span v-text="t('view.game_log.filters.' + scope.row.type)"></span>
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('table.gameLog.user')" prop="displayName" width="200">
<template #default="scope"> <template #default="scope">
<span <span
v-if="scope.row.location && scope.row.type !== 'Location'" v-if="scope.row.displayName"
class="x-link" class="x-link table-user"
@click="showWorldDialog(scope.row.location)" style="padding-right: 10px"
v-text="t('view.game_log.filters.' + scope.row.type)"></span> @click="lookupUser(scope.row)"
<span v-else v-text="t('view.game_log.filters.' + scope.row.type)"></span> v-text="scope.row.displayName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
<template #default="scope">
<template v-if="gameLogIsFriend(scope.row)"> <template v-if="gameLogIsFriend(scope.row)">
<span v-if="gameLogIsFavorite(scope.row)"></span> <span v-if="gameLogIsFavorite(scope.row)"></span>
<span v-else>💚</span> <span v-else>💚</span>
</template> </template>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="t('table.gameLog.user')" prop="displayName" width="180">
<template #default="scope">
<span
v-if="scope.row.displayName"
class="x-link"
style="padding-right: 10px"
@click="lookupUser(scope.row)"
v-text="scope.row.displayName"></span>
</template> </template>
</el-table-column> </el-table-column>
@@ -158,31 +159,38 @@
size="small" size="small"
class="small-button" class="small-button"
@click="deleteGameLogEntry(scope.row)"></el-button> @click="deleteGameLogEntry(scope.row)"></el-button>
<el-button <i
class="ri-delete-bin-line small-button"
style="opacity: 0.85"
v-else v-else
text @click="deleteGameLogEntryPrompt(scope.row)"></i>
:icon="Delete"
size="small"
class="small-button"
@click="deleteGameLogEntryPrompt(scope.row)"></el-button>
</template> </template>
<NativeTooltip placement="top" :content="t('dialog.previous_instances.info')"> <el-tooltip
v-if="scope.row.type === 'Location'"
placement="top"
:content="t('dialog.previous_instances.info')">
<el-button <el-button
v-if="scope.row.type === 'Location'" v-if="shiftHeld"
text text
:icon="DataLine" :icon="DataLine"
size="small" size="small"
class="small-button" class="small-button"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button> @click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
</NativeTooltip> <i
v-else
style="opacity: 0.85"
class="ri-file-list-2-line small-button"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></i>
</el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="5"></el-table-column>
</DataTable> </DataTable>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Close, DataLine, Delete } from '@element-plus/icons-vue'; import { Close, DataLine } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { computed } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -194,6 +202,7 @@
import { formatDateFilter, openExternalLink, removeFromArray } from '../../shared/utils'; import { formatDateFilter, openExternalLink, removeFromArray } from '../../shared/utils';
import { database } from '../../service/database'; import { database } from '../../service/database';
import { useSharedFeedStore } from '../../stores'; import { useSharedFeedStore } from '../../stores';
import { useTableHeight } from '../../composables/useTableHeight';
const { showWorldDialog } = useWorldStore(); const { showWorldDialog } = useWorldStore();
const { lookupUser } = useUserStore(); const { lookupUser } = useUserStore();
@@ -252,6 +261,8 @@
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['updateGameLogSessionTable']); const emit = defineEmits(['updateGameLogSessionTable']);
const { containerRef: gameLogRef } = useTableHeight(gameLogTable);
function deleteGameLogEntry(row) { function deleteGameLogEntry(row) {
removeFromArray(gameLogTable.value.data, row); removeFromArray(gameLogTable.value.data, row);
database.deleteGameLogEntry(row); database.deleteGameLogEntry(row);
@@ -281,5 +292,9 @@
.small-button { .small-button {
padding: 0; padding: 0;
height: 18px; height: 18px;
cursor: pointer;
}
.table-user {
color: var(--x-table-user-text-color) !important;
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More