UI Refresh
88
package-lock.json
generated
@@ -15,10 +15,6 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@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",
|
||||
"@sentry/vite-plugin": "^4.6.1",
|
||||
"@sentry/vue": "^10.32.1",
|
||||
@@ -47,12 +43,12 @@
|
||||
"noty": "^3.2.0-beta-deprecated",
|
||||
"pinia": "^3.0.4",
|
||||
"prettier": "^3.7.4",
|
||||
"remixicon": "^4.7.0",
|
||||
"remixicon": "^4.8.0",
|
||||
"sass-embedded": "^1.97.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-marquee-text-component": "^2.0.1",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-showdown": "^4.2.0",
|
||||
@@ -2351,46 +2347,6 @@
|
||||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||
@@ -2451,14 +2407,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
|
||||
"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
"@intlify/message-compiler": "11.2.8",
|
||||
"@intlify/shared": "11.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -2468,13 +2424,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
|
||||
"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/shared": "11.2.8",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2485,9 +2441,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
|
||||
"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -15566,9 +15522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/remixicon": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.7.0.tgz",
|
||||
"integrity": "sha512-g2pHOofQWARWpxdbrQu5+K3C8ZbqguQFzE54HIMWFCpFa63pumaAltIgZmFMRQpKKBScRWQASQfWxS9asNCcHQ==",
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.8.0.tgz",
|
||||
"integrity": "sha512-8qTM/bWkmsAWitvcL9XrVPVdqHRrdmnNp4zCFBdmIHBygxfHWwoK6NzitbiMyRzjcXKBMlS/ab5M65/ZlxJTVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
@@ -18493,14 +18449,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
|
||||
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/core-base": "11.2.8",
|
||||
"@intlify/shared": "11.2.8",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -68,12 +68,12 @@
|
||||
"noty": "^3.2.0-beta-deprecated",
|
||||
"pinia": "^3.0.4",
|
||||
"prettier": "^3.7.4",
|
||||
"remixicon": "^4.7.0",
|
||||
"remixicon": "^4.8.0",
|
||||
"sass-embedded": "^1.97.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-marquee-text-component": "^2.0.1",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-showdown": "^4.2.0",
|
||||
|
||||
@@ -56,11 +56,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-splitter-bar__dragger) {
|
||||
width: 4px !important;
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
/* Add title bar spacing for macOS */
|
||||
.x-app.with-macos-titlebar {
|
||||
padding-top: 28px;
|
||||
|
||||
1091
src/app.css
@@ -3,7 +3,7 @@
|
||||
--offy: 14.5px;
|
||||
}
|
||||
.flags {
|
||||
background: url('../images/flags.png') no-repeat;
|
||||
background: url('/images/flags.png') no-repeat;
|
||||
background-size: calc(var(--offx) * 6);
|
||||
width: var(--offx);
|
||||
height: calc(var(--offx) / 72 * 52);
|
||||
|
||||
53
src/assets/scss/noty.css
Normal 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;
|
||||
}
|
||||
1052
src/assets/scss/themes/app_legacy.scss
Normal file
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div @click="confirm" class="avatar-info">
|
||||
<span style="margin-right: 5px">{{ avatarName }}</span>
|
||||
<span v-if="avatarType" :class="color" style="margin-right: 5px"><i :class="avatarTypeIcons" /></span>
|
||||
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span>
|
||||
<span v-if="avatarType" :class="color" class="mr-2"><i :class="avatarTypeIcons" /></span>
|
||||
<span class="mr-2">{{ avatarName }}</span>
|
||||
<span v-if="avatarTags" style="color: var(--el-text-color-secondary); font-size: 12px">{{ avatarTags }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
imageurl: String,
|
||||
userid: String,
|
||||
hintownerid: String,
|
||||
hintavatarname: String,
|
||||
hintavatarname: [String, Object],
|
||||
avatartags: Array
|
||||
});
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
if (!props.imageurl) {
|
||||
avatarName.value = '';
|
||||
} else if (props.hintownerid) {
|
||||
avatarName.value = props.hintavatarname;
|
||||
if (typeof props.hintavatarname === 'string') {
|
||||
avatarName.value = props.hintavatarname;
|
||||
}
|
||||
ownerId = props.hintownerid;
|
||||
} else {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-loading="loading"
|
||||
:data="paginatedData"
|
||||
v-bind="mergedTableProps"
|
||||
:stripe="false"
|
||||
:default-sort="resolvedDefaultSort"
|
||||
@row-click="handleRowClick">
|
||||
<slot></slot>
|
||||
@@ -102,7 +103,6 @@
|
||||
delete rest.defaultSort;
|
||||
}
|
||||
return {
|
||||
stripe: true,
|
||||
...rest
|
||||
};
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.toolbar-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
{{ t('dialog.user.info.close_instance') }} </el-button
|
||||
><br /><br />
|
||||
</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 />
|
||||
<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 />
|
||||
<span>{{ t('dialog.user.info.instance_game_version') }} {{ props.instance.gameServerVersion }}</span
|
||||
><br />
|
||||
@@ -46,13 +50,13 @@
|
||||
<span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span>
|
||||
<span
|
||||
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
|
||||
>
|
||||
<span v-if="props.instance.queueSize" style="margin-left: 5px"
|
||||
>{{ 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')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="!text" class="transparent">-</span>
|
||||
<span v-show="text">
|
||||
<span
|
||||
:class="{ 'x-link': link && location !== 'private' && location !== 'offline' }"
|
||||
@click="handleShowWorldDialog">
|
||||
<el-icon :class="['is-loading', 'inline-block']" style="margin-right: 3px" v-if="isTraveling"
|
||||
><Loading
|
||||
/></el-icon>
|
||||
<span>{{ text }}</span>
|
||||
</span>
|
||||
<span v-if="groupName" :class="{ 'x-link': link }" @click="handleShowGroupDialog">({{ groupName }})</span>
|
||||
<span
|
||||
v-if="region"
|
||||
:class="['flags', 'inline-block', 'ml-5', region, 'transform-[translateY(3px)]']"></span>
|
||||
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<div v-if="!text" class="transparent">-</div>
|
||||
<div v-show="text" class="flex items-center">
|
||||
<div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
|
||||
<el-tooltip
|
||||
:content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
|
||||
:disabled="!instanceName"
|
||||
:show-after="300"
|
||||
placement="top">
|
||||
<div
|
||||
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
|
||||
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
|
||||
@click="handleShowWorldDialog">
|
||||
<el-icon :class="['is-loading']" class="mr-1" v-if="isTraveling"><Loading /></el-icon>
|
||||
<span class="min-w-0 truncate">{{ text }}</span>
|
||||
<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>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
<el-icon v-if="strict" :class="['inline-block', 'ml-5']"><Lock /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,6 +39,7 @@
|
||||
|
||||
import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
|
||||
import { getGroupName, getWorldName, parseLocation } from '../shared/utils';
|
||||
import { accessTypeLocaleKeyMap } from '../shared/constants';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -67,6 +77,7 @@
|
||||
const isTraveling = ref(false);
|
||||
const groupName = ref('');
|
||||
const isClosed = ref(false);
|
||||
const instanceName = ref('');
|
||||
|
||||
let isDisposed = false;
|
||||
onBeforeUnmount(() => {
|
||||
@@ -108,7 +119,8 @@
|
||||
isTraveling.value = true;
|
||||
}
|
||||
const L = parseLocation(instanceId);
|
||||
setText(L, L.instanceName);
|
||||
setText(L);
|
||||
instanceName.value = L.instanceName;
|
||||
if (!L.isRealInstance) {
|
||||
return;
|
||||
}
|
||||
@@ -116,7 +128,8 @@
|
||||
const instanceRef = cachedInstances.get(L.tag);
|
||||
if (typeof instanceRef !== 'undefined') {
|
||||
if (instanceRef.displayName) {
|
||||
setText(L, instanceRef.displayName);
|
||||
setText(L);
|
||||
instanceName.value = instanceRef.displayName;
|
||||
}
|
||||
if (instanceRef.closedAt) {
|
||||
isClosed.value = true;
|
||||
@@ -147,7 +160,9 @@
|
||||
strict.value = L.strict;
|
||||
}
|
||||
|
||||
function setText(L, instanceName) {
|
||||
function setText(L) {
|
||||
const accessTypeLabel = translateAccessType(L.accessTypeName);
|
||||
|
||||
if (L.isOffline) {
|
||||
text.value = 'Offline';
|
||||
} else if (L.isPrivate) {
|
||||
@@ -156,13 +171,13 @@
|
||||
text.value = 'Traveling';
|
||||
} else if (typeof props.hint === 'string' && props.hint !== '') {
|
||||
if (L.instanceId) {
|
||||
text.value = `${props.hint} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${props.hint} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = props.hint;
|
||||
}
|
||||
} else if (L.worldId) {
|
||||
if (L.instanceId) {
|
||||
text.value = `${L.worldId} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${L.worldId} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = L.worldId;
|
||||
}
|
||||
@@ -172,7 +187,7 @@
|
||||
.then((name) => {
|
||||
if (!isDisposed && name && currentInstanceId() === L.tag) {
|
||||
if (L.instanceId) {
|
||||
text.value = `${name} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${name} · ${translateAccessType(L.accessTypeName)}`;
|
||||
} else {
|
||||
text.value = name;
|
||||
}
|
||||
@@ -182,13 +197,21 @@
|
||||
console.error(e);
|
||||
});
|
||||
} else if (L.instanceId) {
|
||||
text.value = `${ref.name} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${ref.name} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = ref.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function translateAccessType(accessTypeName) {
|
||||
const key = accessTypeLocaleKeyMap[accessTypeName];
|
||||
if (!key) {
|
||||
return accessTypeName;
|
||||
}
|
||||
return t(key);
|
||||
}
|
||||
|
||||
function handleShowWorldDialog() {
|
||||
if (props.link) {
|
||||
let instanceId = currentInstanceId();
|
||||
@@ -218,15 +241,13 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<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">
|
||||
<el-icon v-if="isUnlocked" :class="['inline-block', 'mr-5']"><Unlock /></el-icon>
|
||||
<span>#{{ instanceName }} {{ accessTypeName }}</span>
|
||||
<el-icon v-if="isUnlocked" :class="['inline-block', 'mr-1.25']"><Unlock /></el-icon>
|
||||
<span> {{ accessTypeName }} #{{ instanceName }}</span>
|
||||
</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>
|
||||
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<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>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
<el-icon v-if="strict" style="display: inline-block; margin-left: 5px"><Lock /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import { useGroupStore, useInstanceStore, useLaunchStore } from '../stores';
|
||||
import { getGroupName, parseLocation } from '../shared/utils';
|
||||
import { accessTypeLocaleKeyMap } from '../shared/constants';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { cachedInstances } = useInstanceStore();
|
||||
@@ -52,7 +53,7 @@
|
||||
function parse() {
|
||||
const locObj = props.locationobject;
|
||||
location.value = locObj.tag;
|
||||
accessTypeName.value = locObj.accessTypeName;
|
||||
accessTypeName.value = translateAccessType(locObj.accessTypeName);
|
||||
strict.value = locObj.strict;
|
||||
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(
|
||||
@@ -126,11 +135,9 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px;
|
||||
:global(html.dark .x-location-world),
|
||||
:global(:root.dark .x-location-world),
|
||||
:global(:root[data-theme='dark'] .x-location-world) {
|
||||
color: var(--color-zinc-100);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<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">
|
||||
<div>
|
||||
<div class="nav-menu-body mt-5">
|
||||
<div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog">
|
||||
<el-progress
|
||||
type="circle"
|
||||
@@ -16,80 +16,71 @@
|
||||
type="success"
|
||||
plain
|
||||
style="font-size: 19px; height: 36px; width: 44px; margin: 10px"
|
||||
@click="showVRCXUpdateDialog"
|
||||
><i class="ri-download-line"></i
|
||||
></el-button>
|
||||
@click="showVRCXUpdateDialog">
|
||||
<i class="ri-download-line"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-menu collapse :default-active="activeMenuIndex" :collapse-transition="false" ref="navMenuRef">
|
||||
<el-popover
|
||||
v-for="item in navMenuItems"
|
||||
:disabled="!item.entries?.length"
|
||||
:key="item.index"
|
||||
:ref="(el) => setNavPopoverRef(el, item.index)"
|
||||
placement="right-start"
|
||||
trigger="hover"
|
||||
:hide-after="isSteamVRRunning ? 400 : 150"
|
||||
: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>
|
||||
<el-menu ref="navMenuRef" class="nav-menu" :collapse="isCollapsed" :collapse-transition="false">
|
||||
<template v-for="item in menuItems" :key="item.index">
|
||||
<el-menu-item
|
||||
v-if="!item.children?.length"
|
||||
:index="item.index"
|
||||
:class="{ notify: isNavItemNotified(item) }"
|
||||
@click="handleMenuItemClick(item)">
|
||||
<i :class="item.icon"></i>
|
||||
<template #title>
|
||||
<span>{{ item.titleIsCustom ? item.title : t(item.title || '') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu-popover__menu">
|
||||
<button
|
||||
v-for="entry in item.entries"
|
||||
:key="entry.label"
|
||||
type="button"
|
||||
:class="['nav-menu-popover__menu-item', { notify: isEntryNotified(entry) }]"
|
||||
@click="handleSubmenuClick(entry, item.index)">
|
||||
<i v-if="entry.icon" :class="entry.icon" class="nav-menu-popover__menu-icon"></i>
|
||||
<span class="nav-menu-popover__menu-label">{{ t(entry.label) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="item.index">
|
||||
<template #title>
|
||||
<div :class="{ notify: isNavItemNotified(item) }">
|
||||
<i :class="item.icon"></i>
|
||||
<span v-show="!isCollapsed">{{
|
||||
item.titleIsCustom ? item.title : t(item.title || '')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-menu-item
|
||||
:index="item.index"
|
||||
:class="{ notify: isNavItemNotified(item) }"
|
||||
@click="handleMenuItemClick(item)">
|
||||
<i :class="item.icon"></i>
|
||||
<template #title v-if="item.tooltip">
|
||||
<span>{{ item.tooltipIsCustom ? item.tooltip : t(item.tooltip) }}</span>
|
||||
v-for="entry in item.children"
|
||||
:key="entry.index"
|
||||
:index="entry.index"
|
||||
class="pl-8!"
|
||||
:class="{ notify: isEntryNotified(entry) }"
|
||||
@click="handleSubmenuClick(entry, item.index)">
|
||||
<i v-show="entry.icon" :class="entry.icon"></i>
|
||||
<template #title>
|
||||
<span>{{ t(entry.label) }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</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 class="nav-menu-container-bottom">
|
||||
<NativeTooltip v-if="branch === 'Nightly'" :show-after="150" :content="'Feedback'" placement="right">
|
||||
<div class="nav-menu-container-bottom mb-4">
|
||||
<el-tooltip
|
||||
v-if="branch === 'Nightly'"
|
||||
:show-after="150"
|
||||
:content="'Feedback'"
|
||||
:disabled="!isCollapsed"
|
||||
placement="right">
|
||||
<div
|
||||
class="bottom-button"
|
||||
id="feedback"
|
||||
@click="!sentryErrorReporting && setSentryErrorReporting()">
|
||||
<i class="ri-feedback-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">Feedback</span>
|
||||
</div>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
|
||||
<el-popover
|
||||
v-model:visible="supportMenuVisible"
|
||||
placement="right"
|
||||
trigger="click"
|
||||
popper-style="padding:4px;border-radius:8px;"
|
||||
:offset="4"
|
||||
:offset="-10"
|
||||
:show-arrow="false"
|
||||
:width="200"
|
||||
:hide-after="0">
|
||||
@@ -119,11 +110,18 @@
|
||||
</div>
|
||||
<template #reference>
|
||||
<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">
|
||||
<i class="ri-question-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
||||
t('nav_tooltip.help_support')
|
||||
}}</span>
|
||||
</div>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
@@ -133,7 +131,7 @@
|
||||
placement="right"
|
||||
trigger="click"
|
||||
popper-style="padding:4px;border-radius:8px;"
|
||||
:offset="4"
|
||||
:offset="-10"
|
||||
:show-arrow="false"
|
||||
:width="200"
|
||||
:hide-after="0">
|
||||
@@ -143,7 +141,7 @@
|
||||
<div class="nav-menu-settings__meta">
|
||||
<span class="nav-menu-settings__title" @click="openGithub"
|
||||
>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 class="nav-menu-settings__version">{{ version }}</span>
|
||||
</div>
|
||||
@@ -170,7 +168,7 @@
|
||||
:class="{ 'is-active': themeMode === theme }"
|
||||
@click="handleThemeSelect(theme)">
|
||||
<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>
|
||||
</div>
|
||||
<template #reference>
|
||||
@@ -190,9 +188,24 @@
|
||||
<template #reference>
|
||||
<div class="bottom-button">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
||||
t('nav_tooltip.manage')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -215,9 +228,9 @@
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useAuthStore,
|
||||
useGameStore,
|
||||
useSearchStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useVRCXUpdaterStore
|
||||
} from '../stores';
|
||||
import { THEME_CONFIG, links, navDefinitions } from '../shared/constants';
|
||||
@@ -226,8 +239,6 @@
|
||||
|
||||
import configRepository from '../service/config';
|
||||
|
||||
import 'remixicon/fonts/remixicon.css';
|
||||
|
||||
const CustomNavDialog = defineAsyncComponent(() => import('./dialogs/CustomNavDialog.vue'));
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
@@ -257,26 +268,13 @@
|
||||
},
|
||||
{ type: 'item', key: 'notification' },
|
||||
{ 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 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 { pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress, branch, appVersion } =
|
||||
storeToRefs(VRCXUpdaterStore);
|
||||
@@ -288,18 +286,19 @@
|
||||
const { setSentryErrorReporting } = useAdvancedSettingsStore();
|
||||
const { logout } = useAuthStore();
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const { themeMode } = storeToRefs(appearanceSettingsStore);
|
||||
const { isSteamVRRunning } = storeToRefs(useGameStore());
|
||||
const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
|
||||
const userStore = useUserStore();
|
||||
const { currentUser } = storeToRefs(userStore);
|
||||
const { showUserDialog } = userStore;
|
||||
|
||||
const settingsMenuVisible = ref(false);
|
||||
const themeMenuVisible = ref(false);
|
||||
const supportMenuVisible = ref(false);
|
||||
const navMenuRef = ref(null);
|
||||
const navPopoverRefs = new Map();
|
||||
const navLayout = ref([]);
|
||||
const navLayoutReady = ref(false);
|
||||
|
||||
const navMenuItems = computed(() => {
|
||||
const menuItems = computed(() => {
|
||||
const items = [];
|
||||
navLayout.value.forEach((entry) => {
|
||||
if (entry.type === 'item') {
|
||||
@@ -310,7 +309,7 @@
|
||||
items.push({
|
||||
...definition,
|
||||
index: definition.key,
|
||||
tooltipIsCustom: false,
|
||||
title: definition.tooltip || definition.labelKey,
|
||||
titleIsCustom: false
|
||||
});
|
||||
return;
|
||||
@@ -324,7 +323,6 @@
|
||||
items.push({
|
||||
...definition,
|
||||
index: definition.key,
|
||||
tooltipIsCustom: false,
|
||||
titleIsCustom: false
|
||||
});
|
||||
});
|
||||
@@ -334,83 +332,41 @@
|
||||
const folderEntries = folderDefinitions.map((definition) => ({
|
||||
label: definition.labelKey,
|
||||
routeName: definition.routeName,
|
||||
key: definition.key,
|
||||
icon: definition.icon
|
||||
index: definition.key,
|
||||
icon: definition.icon,
|
||||
action: definition.action
|
||||
}));
|
||||
|
||||
items.push({
|
||||
index: entry.id,
|
||||
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'),
|
||||
titleIsCustom: true,
|
||||
entries: folderEntries
|
||||
children: folderEntries
|
||||
});
|
||||
}
|
||||
});
|
||||
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 currentRouteName = router.currentRoute.value?.name;
|
||||
if (!currentRouteName) {
|
||||
const firstEntry = navLayout.value[0];
|
||||
if (!firstEntry) {
|
||||
return 'feed';
|
||||
}
|
||||
return firstEntry.type === 'folder' ? firstEntry.id : firstEntry.key;
|
||||
const currentRoute = router.currentRoute.value;
|
||||
const currentRouteName = currentRoute?.name;
|
||||
const navKey = currentRoute?.meta?.navKey || currentRouteName;
|
||||
if (!navKey) {
|
||||
return getFirstNavRoute(navLayout.value) || 'feed';
|
||||
}
|
||||
|
||||
for (const entry of navLayout.value) {
|
||||
if (entry.type === 'item' && entry.key === currentRouteName) {
|
||||
if (entry.type === 'item' && entry.key === navKey) {
|
||||
return entry.key;
|
||||
}
|
||||
if (entry.type === 'folder' && entry.items?.includes(currentRouteName)) {
|
||||
return entry.id;
|
||||
if (entry.type === 'folder' && entry.items?.includes(navKey)) {
|
||||
return navKey;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = navLayout.value[0];
|
||||
if (!fallback) {
|
||||
return 'feed';
|
||||
}
|
||||
return fallback.type === 'folder' ? fallback.id : fallback.key;
|
||||
return getFirstNavRoute(navLayout.value) || 'feed';
|
||||
});
|
||||
|
||||
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 showCurrentUserDialog = () => {
|
||||
showUserDialog(currentUser.value?.id);
|
||||
};
|
||||
|
||||
const sanitizeLayout = (layout) => {
|
||||
const usedKeys = new Set();
|
||||
const normalized = [];
|
||||
@@ -627,52 +587,47 @@
|
||||
if (notifiedMenus.value.includes(item.index)) {
|
||||
return true;
|
||||
}
|
||||
if (item.entries?.length) {
|
||||
return item.entries.some((entry) => isEntryNotified(entry));
|
||||
if (item.children?.length) {
|
||||
return item.children.some((entry) => isEntryNotified(entry));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const setNavPopoverRef = (el, index) => {
|
||||
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 = () => {
|
||||
const closeNavFlyouts = () => {
|
||||
settingsMenuVisible.value = false;
|
||||
supportMenuVisible.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) => {
|
||||
if (!routeName) {
|
||||
return;
|
||||
@@ -697,17 +652,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
const getFirstNavRoute = (layout) => {
|
||||
function getFirstNavRoute(layout) {
|
||||
for (const entry of layout) {
|
||||
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) {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
let hasNavigatedToInitialRoute = false;
|
||||
const navigateToFirstNavEntry = () => {
|
||||
@@ -724,15 +685,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmenuClick = (entry, index) => {
|
||||
const navIndex = index || entry?.index;
|
||||
triggerNavAction(entry, navIndex);
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.entries?.length) {
|
||||
handleFolderCycleNavigation(item);
|
||||
return;
|
||||
}
|
||||
handleRouteChange(item.routeName, item.index);
|
||||
triggerNavAction(item, item?.index);
|
||||
};
|
||||
|
||||
const toggleNavCollapse = () => {
|
||||
appearanceSettingsStore.toggleNavCollapsed();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -755,146 +718,147 @@
|
||||
:deep(.el-divider) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-menu-container {
|
||||
position: relative;
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 0 0 240px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
z-index: 600;
|
||||
background-color: var(--el-bg-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color-page);
|
||||
box-shadow: none;
|
||||
.el-menu {
|
||||
background: 0;
|
||||
border: 0;
|
||||
}
|
||||
.el-menu-item i[class*='ri-'] {
|
||||
font-size: 19px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.bottom-button {
|
||||
font-size: 19px;
|
||||
width: 64px;
|
||||
height: 56px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(14px) saturate(130%);
|
||||
}
|
||||
|
||||
.nav-menu-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-menu-item),
|
||||
.nav-menu :deep(.el-sub-menu__title) {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
font-size: 13px;
|
||||
padding: 0 20px !important;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-menu-item i[class*='ri-']),
|
||||
.nav-menu :deep(.el-sub-menu__title i[class*='ri-']) {
|
||||
font-size: 19px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
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;
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
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-container.is-collapsed .nav-menu :deep(.el-menu-item),
|
||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title) {
|
||||
column-gap: 0;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-menu-popover__header i {
|
||||
font-size: 18px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.nav-menu-container.is-collapsed {
|
||||
width: 64px;
|
||||
flex-basis: 64px;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 6px;
|
||||
padding: 12px 12px 16px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title > div) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.nav-menu-container.is-collapsed .bottom-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.el-menu-item .el-menu-tooltip__trigger) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
:deep(.el-button.is-text:not(.is-disabled):hover) {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
}
|
||||
|
||||
.nav-menu-settings {
|
||||
@@ -930,6 +894,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-menu-settings__heart {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.nav-menu-settings__version {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -942,8 +911,8 @@
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: background-color var(--el-transition-duration);
|
||||
cursor: pointer;
|
||||
@@ -962,7 +931,7 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1015,7 +967,6 @@
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
transition: background-color var(--el-transition-duration);
|
||||
@@ -1031,18 +982,4 @@
|
||||
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>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
class="x-dialog x-avatar-dialog"
|
||||
v-model="avatarDialog.visible"
|
||||
:show-close="false"
|
||||
width="700px">
|
||||
top="10vh"
|
||||
width="930px">
|
||||
<div v-loading="avatarDialog.loading">
|
||||
<div style="display: flex">
|
||||
<img
|
||||
@@ -246,11 +247,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="selectAvatarWithoutConfirmation(avatarDialog.id)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="avatarDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="avatarDialogCommand">
|
||||
<el-button
|
||||
:type="avatarDialog.isBlocked ? 'danger' : 'default'"
|
||||
:icon="MoreFilled"
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.img-size {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
|
||||
@@ -68,12 +68,12 @@
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
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>
|
||||
|
||||
@@ -329,8 +329,9 @@
|
||||
);
|
||||
|
||||
const openFolderEditor = (index) => {
|
||||
folderEditor.isEditing = !!index;
|
||||
folderEditor.index = folderEditor.isEditing ? index : -1;
|
||||
const isEditing = index !== undefined && index !== null;
|
||||
folderEditor.isEditing = isEditing;
|
||||
folderEditor.index = isEditing ? index : -1;
|
||||
if (folderEditor.isEditing) {
|
||||
const entry = localLayout.value[index];
|
||||
folderEditor.data = {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
:z-index="groupDialogIndex"
|
||||
v-model="groupDialog.visible"
|
||||
:show-close="false"
|
||||
width="770px"
|
||||
top="10vh"
|
||||
width="930px"
|
||||
class="x-dialog x-group-dialog">
|
||||
<div v-loading="groupDialog.loading" class="group-body">
|
||||
<div style="display: flex">
|
||||
@@ -258,11 +259,7 @@
|
||||
@click="joinGroup(groupDialog.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="groupDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="groupDialogCommand">
|
||||
<el-button
|
||||
:type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'"
|
||||
:icon="MoreFilled"
|
||||
@@ -616,7 +613,8 @@
|
||||
<span class="name">{{ t('dialog.group.info.links') }}</span>
|
||||
<div
|
||||
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">
|
||||
<el-tooltip v-if="link">
|
||||
<template #content>
|
||||
@@ -1086,7 +1084,6 @@
|
||||
<el-tabs
|
||||
v-model="groupDialogGalleryCurrentName"
|
||||
v-loading="isGroupGalleryLoading"
|
||||
type="card"
|
||||
style="margin-top: 10px">
|
||||
<template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index">
|
||||
<el-tab-pane>
|
||||
@@ -1839,7 +1836,7 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.time-group-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
width="90vw">
|
||||
<div>
|
||||
<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')">
|
||||
<div style="margin-top: 10px">
|
||||
<el-button
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="t('dialog.new_instance.header')"
|
||||
width="650px"
|
||||
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-form :model="newInstanceDialog" label-width="150px">
|
||||
<el-form-item :label="t('dialog.new_instance.access_type')">
|
||||
|
||||
@@ -63,14 +63,7 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
redirectToToolsTab();
|
||||
showGalleryDialog();
|
||||
"
|
||||
>{{ t('dialog.boop_dialog.emoji_manager') }}</el-button
|
||||
>
|
||||
<el-button size="small" @click="showGalleryPage">{{ 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" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
|
||||
t('dialog.boop_dialog.send')
|
||||
@@ -87,7 +80,6 @@
|
||||
import { notificationRequest, userRequest } from '../../api';
|
||||
import { miscRequest } from '../../api';
|
||||
import { photonEmojis } from '../../shared/constants/photon.js';
|
||||
import { redirectToToolsTab } from '../../shared/utils/base/ui';
|
||||
import { useGalleryStore } from '../../stores';
|
||||
import { useNotificationStore } from '../../stores';
|
||||
import { useUserStore } from '../../stores/user.js';
|
||||
@@ -98,7 +90,7 @@
|
||||
|
||||
const { sendBoopDialog } = storeToRefs(useUserStore());
|
||||
const { notificationTable } = storeToRefs(useNotificationStore());
|
||||
const { showGalleryDialog, refreshEmojiTable } = useGalleryStore();
|
||||
const { showGalleryPage, refreshEmojiTable } = useGalleryStore();
|
||||
const { emojiTable } = storeToRefs(useGalleryStore());
|
||||
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
@click="userDialogCommand('Add Favorite')"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-dropdown trigger="click" size="small" @command="onCommand">
|
||||
<el-dropdown trigger="click" @command="onCommand">
|
||||
<el-button
|
||||
:type="
|
||||
userDialog.incomingRequest || userDialog.outgoingRequest
|
||||
@@ -132,7 +132,7 @@
|
||||
:icon="CircleCheck"
|
||||
command="Moderation Unblock"
|
||||
divided
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_unblock') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@@ -147,7 +147,7 @@
|
||||
v-if="userDialog.isMute"
|
||||
:icon="Microphone"
|
||||
command="Moderation Unmute"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_unmute') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@@ -161,7 +161,7 @@
|
||||
v-if="userDialog.isMuteChat"
|
||||
:icon="ChatLineRound"
|
||||
command="Moderation Enable Chatbox"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_enable_chatbox') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else :icon="ChatDotRound" command="Moderation Disable Chatbox">
|
||||
@@ -179,7 +179,7 @@
|
||||
v-if="userDialog.isInteractOff"
|
||||
:icon="Pointer"
|
||||
command="Moderation Enable Avatar Interaction"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_enable_avatar_interaction') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else :icon="CircleClose" command="Moderation Disable Avatar Interaction">
|
||||
@@ -189,7 +189,11 @@
|
||||
{{ t('dialog.user.actions.report_hacking') }}
|
||||
</el-dropdown-item>
|
||||
<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') }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-model="userDialog.visible"
|
||||
:show-close="false"
|
||||
top="10vh"
|
||||
width="940px">
|
||||
width="930px">
|
||||
<div v-loading="userDialog.loading">
|
||||
<UserSummaryHeader
|
||||
:get-user-state-text="getUserStateText"
|
||||
@@ -252,7 +252,7 @@
|
||||
style="margin-left: 5px; padding: 0"
|
||||
@click="showBioDialog"></el-button>
|
||||
</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">
|
||||
<template #content>
|
||||
<span v-text="link"></span>
|
||||
@@ -426,10 +426,13 @@
|
||||
<div class="x-friend-item" @click="toggleAvatarCopying">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.user.info.avatar_cloning') }}</span>
|
||||
<span v-if="currentUser.allowAvatarCopying" class="extra" style="color: #67c23a">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span
|
||||
v-if="currentUser.allowAvatarCopying"
|
||||
class="extra"
|
||||
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')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -437,10 +440,13 @@
|
||||
<div class="x-friend-item" @click="toggleAllowBooping">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.user.info.booping') }}</span>
|
||||
<span v-if="currentUser.isBoopingEnabled" class="extra" style="color: #67c23a">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span
|
||||
v-if="currentUser.isBoopingEnabled"
|
||||
class="extra"
|
||||
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')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -451,10 +457,10 @@
|
||||
<span
|
||||
v-if="!currentUser.hasSharedConnectionsOptOut"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ 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')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -467,10 +473,10 @@
|
||||
<span
|
||||
v-if="userDialog.ref.allowAvatarCopying"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ 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')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -634,9 +640,10 @@
|
||||
t('dialog.user.groups.total_count', { count: userGroups.groups.length })
|
||||
}}</span>
|
||||
<template v-if="userDialogGroupEditMode">
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 10px">{{
|
||||
t('dialog.user.groups.hold_shift')
|
||||
}}</span>
|
||||
<span
|
||||
style="margin-left: 10px; color: var(--el-text-color-secondary); font-size: 10px"
|
||||
>{{ t('dialog.user.groups.hold_shift') }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -872,7 +879,7 @@
|
||||
size="small"
|
||||
:icon="Close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
style="color: var(--el-color-danger); margin-left: 5px"
|
||||
@click.stop="leaveGroup(group.id)">
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -892,7 +899,7 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.own_groups')
|
||||
}}</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 }}/{{
|
||||
cachedConfig?.constants?.GROUPS?.MAX_OWNED
|
||||
}}</span
|
||||
@@ -936,9 +943,10 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.mutual_groups')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
userGroups.mutualGroups.length
|
||||
}}</span>
|
||||
<span
|
||||
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px"
|
||||
>{{ userGroups.mutualGroups.length }}</span
|
||||
>
|
||||
<div
|
||||
class="x-friend-list"
|
||||
style="margin-top: 10px; margin-bottom: 15px; min-height: 60px">
|
||||
@@ -978,7 +986,7 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.groups')
|
||||
}}</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 }}
|
||||
<template v-if="currentUser.id === userDialog.id">
|
||||
/
|
||||
@@ -1143,7 +1151,12 @@
|
||||
:class="userFavoriteWorldsStatus(list[1])">
|
||||
</i>
|
||||
<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
|
||||
>
|
||||
</span>
|
||||
@@ -1272,13 +1285,13 @@
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus">
|
||||
</span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
v-text="avatar.releaseStatus">
|
||||
</span>
|
||||
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
|
||||
@@ -1400,11 +1413,11 @@
|
||||
userRequest,
|
||||
worldRequest
|
||||
} from '../../../api';
|
||||
import { getNextDialogIndex, redirectToToolsTab } from '../../../shared/utils/base/ui';
|
||||
import { processBulk, request } from '../../../service/request';
|
||||
import { userDialogGroupSortingOptions, userDialogMutualFriendSortingOptions } from '../../../shared/constants';
|
||||
import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/';
|
||||
import { database } from '../../../service/database';
|
||||
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
|
||||
|
||||
import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue';
|
||||
import UserSummaryHeader from './UserSummaryHeader.vue';
|
||||
@@ -1455,7 +1468,7 @@
|
||||
const { refreshInviteMessageTableData } = useInviteStore();
|
||||
const { friendLogTable } = storeToRefs(useFriendStore());
|
||||
const { getFriendRequest, handleFriendDelete } = useFriendStore();
|
||||
const { clearInviteImageUpload, showFullscreenImageDialog } = useGalleryStore();
|
||||
const { clearInviteImageUpload, showFullscreenImageDialog, showGalleryPage } = useGalleryStore();
|
||||
|
||||
const { logout } = useAuthStore();
|
||||
const { cachedConfig } = storeToRefs(useAuthStore());
|
||||
@@ -1896,6 +1909,9 @@
|
||||
}
|
||||
} else if (command === 'Previous Instances') {
|
||||
showPreviousInstancesUserDialog(D.ref);
|
||||
} else if (command === 'Manage Gallery') {
|
||||
userDialog.value.visible = false;
|
||||
showGalleryPage();
|
||||
} else if (command === 'Invite To Group') {
|
||||
showInviteGroupDialog('', D.id);
|
||||
} else if (command === 'Send Boop') {
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.img-size {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
:z-index="worldDialogIndex"
|
||||
class="x-dialog x-world-dialog"
|
||||
v-model="isDialogVisible"
|
||||
top="10vh"
|
||||
:show-close="false"
|
||||
width="770px">
|
||||
width="930px">
|
||||
<div v-loading="worldDialog.loading">
|
||||
<div style="display: flex">
|
||||
<img
|
||||
@@ -204,11 +205,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="worldDialogCommand('Add Favorite')" />
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="worldDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="worldDialogCommand">
|
||||
<el-button type="default" :icon="MoreFilled" size="large" circle />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -302,7 +299,10 @@
|
||||
command="Delete Persistent Data">
|
||||
{{ t('dialog.world.actions.delete_persistent_data') }}
|
||||
</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') }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
|
||||
141
src/composables/useElementTheme.js
Normal 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
src/composables/useTableHeight.js
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
<title>VRCX</title>
|
||||
|
||||
<!-- <link rel="stylesheet" href="app.css" /> -->
|
||||
|
||||
<link rel="preconnect" href="https://api.vrchat.cloud" />
|
||||
<link rel="preconnect" href="https://files.vrchat.cloud" />
|
||||
<link rel="preconnect" href="https://d348imysud55la.cloudfront.net" />
|
||||
|
||||
@@ -1782,6 +1782,8 @@
|
||||
"notification": {
|
||||
"date": "Date",
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"group": "Group",
|
||||
"user_group": "User/Group",
|
||||
"photo": "Photo",
|
||||
"message": "Message",
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
"about": "About",
|
||||
"profile": "Profile",
|
||||
"settings": "Settings",
|
||||
"help_support": "Help & Support"
|
||||
"manage": "Manage",
|
||||
"help_support": "Help & Support",
|
||||
"expand_menu": "Expand Menu",
|
||||
"collapse_menu": "Collapse Menu"
|
||||
},
|
||||
"nav_menu": {
|
||||
"resources": "RESOURCES",
|
||||
@@ -292,6 +295,10 @@
|
||||
"bulk_unfriend_selection": "Bulk Unfriend Selection",
|
||||
"load": "Load missing entries",
|
||||
"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",
|
||||
"search_placeholder": "Search",
|
||||
"filter_placeholder": "Filter",
|
||||
@@ -560,7 +567,8 @@
|
||||
"table_max_size": "Table Max Size",
|
||||
"table_page_sizes": "Table Page Sizes",
|
||||
"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": {
|
||||
"header": "Time/Date",
|
||||
@@ -569,6 +577,9 @@
|
||||
"time_format_12": "12 Hour",
|
||||
"force_iso_date_format": "Force ISO Date Format"
|
||||
},
|
||||
"theme_color": {
|
||||
"header": "Theme Color"
|
||||
},
|
||||
"side_panel": {
|
||||
"header": "Side Panel",
|
||||
"sorting": {
|
||||
@@ -873,7 +884,7 @@
|
||||
}
|
||||
},
|
||||
"side_panel": {
|
||||
"search_placeholder": "Search",
|
||||
"search_placeholder": "Search Friend",
|
||||
"search_result_active": "Offline",
|
||||
"search_result_offline": "Active",
|
||||
"search_result_more": "Search More:",
|
||||
@@ -2254,6 +2265,8 @@
|
||||
"notification": {
|
||||
"date": "Date",
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"group": "Group",
|
||||
"user_group": "User/Group",
|
||||
"photo": "Photo",
|
||||
"message": "Message",
|
||||
|
||||
@@ -1899,6 +1899,8 @@
|
||||
"notification": {
|
||||
"date": "Fecha",
|
||||
"type": "Tipo",
|
||||
"user": "Usuario",
|
||||
"group": "Grupo",
|
||||
"user_group": "Usuario/Grupo",
|
||||
"photo": "Foto",
|
||||
"message": "Mensaje",
|
||||
|
||||
@@ -1788,6 +1788,8 @@
|
||||
"notification": {
|
||||
"date": "Date",
|
||||
"type": "Type",
|
||||
"user": "Utilisateur",
|
||||
"group": "Groupe",
|
||||
"user_group": "Utilisateur/Groupe",
|
||||
"photo": "Photo",
|
||||
"message": "Message",
|
||||
|
||||
@@ -1660,6 +1660,8 @@
|
||||
"notification": {
|
||||
"date": "Date",
|
||||
"type": "Type",
|
||||
"user": "Felhasználó",
|
||||
"group": "Csoport",
|
||||
"user_group": "User/Group",
|
||||
"photo": "Photo",
|
||||
"message": "Message",
|
||||
|
||||
@@ -2082,6 +2082,8 @@
|
||||
"notification": {
|
||||
"date": "日付",
|
||||
"type": "種類",
|
||||
"user": "ユーザー",
|
||||
"group": "グループ",
|
||||
"user_group": "ユーザーまたはグループ",
|
||||
"photo": "画像",
|
||||
"message": "メッセージ",
|
||||
|
||||
@@ -1674,6 +1674,8 @@
|
||||
"notification": {
|
||||
"date": "날짜",
|
||||
"type": "유형",
|
||||
"user": "유저",
|
||||
"group": "그룹",
|
||||
"user_group": "User/Group",
|
||||
"photo": "사진",
|
||||
"message": "메시지",
|
||||
|
||||
@@ -2225,6 +2225,8 @@
|
||||
"notification": {
|
||||
"date": "Data",
|
||||
"type": "Typ",
|
||||
"user": "Użytkownik",
|
||||
"group": "Grupa",
|
||||
"user_group": "Użytkownik/Grupa",
|
||||
"photo": "Obrazek",
|
||||
"message": "Wiadomość",
|
||||
|
||||
@@ -1660,6 +1660,8 @@
|
||||
"notification": {
|
||||
"date": "Data",
|
||||
"type": "Tipo",
|
||||
"user": "Usuário",
|
||||
"group": "Grupo",
|
||||
"user_group": "Usuário/Grupo",
|
||||
"photo": "Foto",
|
||||
"message": "Mensagem",
|
||||
|
||||
@@ -2069,6 +2069,8 @@
|
||||
"notification": {
|
||||
"date": "Дата",
|
||||
"type": "Тип",
|
||||
"user": "Пользователь",
|
||||
"group": "Группа",
|
||||
"user_group": "Пользователь/Группа",
|
||||
"photo": "Фото",
|
||||
"message": "Сообщение",
|
||||
|
||||
@@ -1964,6 +1964,8 @@
|
||||
"notification": {
|
||||
"date": "วันที่",
|
||||
"type": "ประเภท",
|
||||
"user": "ผู้ใช้",
|
||||
"group": "กลุ่ม",
|
||||
"user_group": "ผู้ใช้/กลุ่ม",
|
||||
"photo": "รูปภาพ",
|
||||
"message": "ข้อความ",
|
||||
|
||||
@@ -1660,6 +1660,8 @@
|
||||
"notification": {
|
||||
"date": "Date",
|
||||
"type": "Type",
|
||||
"user": "Người chơi",
|
||||
"group": "Nhóm",
|
||||
"user_group": "User/Group",
|
||||
"photo": "Photo",
|
||||
"message": "Message",
|
||||
|
||||
@@ -2207,6 +2207,8 @@
|
||||
"notification": {
|
||||
"date": "时间",
|
||||
"type": "类型",
|
||||
"user": "玩家",
|
||||
"group": "群组",
|
||||
"user_group": "玩家/群组",
|
||||
"photo": "封面",
|
||||
"message": "消息",
|
||||
|
||||
@@ -2192,6 +2192,8 @@
|
||||
"notification": {
|
||||
"date": "時間",
|
||||
"type": "類型",
|
||||
"user": "用戶",
|
||||
"group": "群組",
|
||||
"user_group": "用戶/群組",
|
||||
"photo": "照片",
|
||||
"message": "訊息",
|
||||
|
||||
@@ -8,12 +8,10 @@ import LastJoin from '../components/LastJoin.vue';
|
||||
import Launch from '../components/Launch.vue';
|
||||
import Location from '../components/Location.vue';
|
||||
import LocationWorld from '../components/LocationWorld.vue';
|
||||
import NativeTooltip from '../components/NativeTooltip.vue';
|
||||
import Timer from '../components/Timer.vue';
|
||||
|
||||
export function initComponents(app) {
|
||||
app.component('Location', Location);
|
||||
app.component('NativeTooltip', NativeTooltip);
|
||||
app.component('Timer', Timer);
|
||||
app.component('InstanceInfo', InstanceInfo);
|
||||
app.component('LastJoin', LastJoin);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function initNoty(isVrOverlay = false) {
|
||||
},
|
||||
layout: 'bottomLeft',
|
||||
theme: 'mint',
|
||||
timeout: 6000
|
||||
timeout: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import PlayerList from './../views/PlayerList/PlayerList.vue';
|
||||
import Search from './../views/Search/Search.vue';
|
||||
import Settings from './../views/Settings/Settings.vue';
|
||||
import Tools from './../views/Tools/Tools.vue';
|
||||
import Gallery from './../views/Tools/Gallery.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -83,6 +84,12 @@ const routes = [
|
||||
component: Charts
|
||||
},
|
||||
{ path: 'tools', name: 'tools', component: Tools },
|
||||
{
|
||||
path: 'tools/gallery',
|
||||
name: 'gallery',
|
||||
component: Gallery,
|
||||
meta: { navKey: 'tools' }
|
||||
},
|
||||
{ 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
src/shared/constants/accessType.js
Normal 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 };
|
||||
@@ -10,3 +10,4 @@ export * from './moderation';
|
||||
export * from './themes';
|
||||
export * from './link';
|
||||
export * from './ui';
|
||||
export * from './accessType';
|
||||
|
||||
@@ -1,52 +1,69 @@
|
||||
import amoled from '../../assets/scss/themes/theme.amoled.scss?url';
|
||||
import dark from '../../assets/scss/themes/theme.dark.scss?url';
|
||||
import darkblue from '../../assets/scss/themes/theme.darkblue.scss?url';
|
||||
import material3 from '../../assets/scss/themes/theme.material3.scss?url';
|
||||
import appCss from '../../app.css?url';
|
||||
// import appLegacy from '../../assets/scss/themes/app_legacy.scss?url';
|
||||
// import material3 from '../../assets/scss/themes/theme.material3.scss?url';
|
||||
|
||||
export const THEME_CONFIG = {
|
||||
system: {
|
||||
cssFile: '',
|
||||
cssFiles: [appCss],
|
||||
isDark: 'system',
|
||||
name: 'System'
|
||||
},
|
||||
light: {
|
||||
cssFile: '',
|
||||
cssFiles: [appCss],
|
||||
isDark: false,
|
||||
useDarkClass: false,
|
||||
name: 'Light'
|
||||
},
|
||||
dark: { cssFile: dark, isDark: true, name: 'Dark' },
|
||||
darkblue: {
|
||||
cssFile: darkblue,
|
||||
dark: {
|
||||
cssFiles: [appCss],
|
||||
isDark: true,
|
||||
name: 'Dark Blue'
|
||||
},
|
||||
amoled: {
|
||||
cssFile: amoled,
|
||||
isDark: true,
|
||||
name: 'Amoled'
|
||||
},
|
||||
// darkvanillaold: {
|
||||
// cssFile: darkvanillaold,
|
||||
useDarkClass: true,
|
||||
name: 'Dark'
|
||||
}
|
||||
// darkold: {
|
||||
// cssFiles: [appLegacy, dark],
|
||||
// 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'
|
||||
// },
|
||||
// darkvanilla: {
|
||||
// cssFile: darkvanilla,
|
||||
// cssFiles: [appLegacy, darkvanilla],
|
||||
// isDark: true,
|
||||
// useDarkClass: false,
|
||||
// name: 'Dark Vanilla'
|
||||
// },
|
||||
// pink: {
|
||||
// cssFile: pink,
|
||||
// cssFiles: [appLegacy, pink],
|
||||
// isDark: true,
|
||||
// useDarkClass: false,
|
||||
// name: 'Pink'
|
||||
// },
|
||||
material3: {
|
||||
cssFile: material3,
|
||||
isDark: true,
|
||||
name: 'Material 3',
|
||||
fontLinks: [
|
||||
'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'
|
||||
]
|
||||
}
|
||||
// material3: {
|
||||
// cssFiles: [appLegacy, material3],
|
||||
// isDark: true,
|
||||
// useDarkClass: false,
|
||||
// name: 'Material 3',
|
||||
// fontLinks: [
|
||||
// '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'
|
||||
// ]
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -96,6 +96,13 @@ const navDefinitions = [
|
||||
tooltip: 'nav_tooltip.tools',
|
||||
labelKey: 'nav_tooltip.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,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) {
|
||||
if (themeMode === 'system') {
|
||||
themeMode = systemIsDarkMode() ? 'dark' : 'light';
|
||||
@@ -61,27 +80,35 @@ function changeAppThemeStyle(themeMode) {
|
||||
themeConfig = THEME_CONFIG[themeMode];
|
||||
}
|
||||
|
||||
let filePathPrefix = 'file://vrcx/';
|
||||
if (LINUX) {
|
||||
filePathPrefix = './';
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
filePathPrefix = 'http://localhost:9000/';
|
||||
console.log('Using development file path prefix:', filePathPrefix);
|
||||
const cssFiles = Array.isArray(themeConfig.cssFiles)
|
||||
? themeConfig.cssFiles.filter(Boolean)
|
||||
: themeConfig.cssFile
|
||||
? [themeConfig.cssFile]
|
||||
: [];
|
||||
|
||||
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 (!$appThemeStyle) {
|
||||
$appThemeStyle = document.createElement('link');
|
||||
$appThemeStyle.setAttribute('id', 'app-theme-style');
|
||||
$appThemeStyle.rel = 'stylesheet';
|
||||
document.head.appendChild($appThemeStyle);
|
||||
if (cssFiles.length > 1) {
|
||||
const $appThemeOverlayStyle = ensureStylesheetLink(
|
||||
'app-theme-overlay-style'
|
||||
);
|
||||
$appThemeOverlayStyle.href = cssFiles[1];
|
||||
} else {
|
||||
removeStylesheetLink('app-theme-overlay-style');
|
||||
}
|
||||
$appThemeStyle.href = themeConfig.cssFile ? themeConfig.cssFile : '';
|
||||
|
||||
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');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
@@ -258,19 +285,19 @@ function setLoginContainerStyle(isDarkMode) {
|
||||
loginContainerStyle.id = 'login-container-style';
|
||||
loginContainerStyle.type = 'text/css';
|
||||
|
||||
const backgroundColor = isDarkMode ? '#101010' : '#ffffff';
|
||||
const inputBackgroundColor = isDarkMode ? '#333333' : '#ffffff';
|
||||
const inputBorder = isDarkMode ? '1px solid #3b3b3b' : '1px solid #DCDFE6';
|
||||
const backgroundFallback = isDarkMode ? '#101010' : '#ffffff';
|
||||
const inputBackgroundFallback = isDarkMode ? '#1f1f1f' : '#ffffff';
|
||||
const borderFallback = isDarkMode ? '#3b3b3b' : '#DCDFE6';
|
||||
|
||||
loginContainerStyle.innerHTML = `
|
||||
.x-login-container {
|
||||
background-color: ${backgroundColor} !important;
|
||||
background-color: var(--el-bg-color-page, ${backgroundFallback}) !important;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.x-login-container .el-input__wrapper {
|
||||
background-color: ${inputBackgroundColor} !important;
|
||||
border: ${inputBorder} !important;
|
||||
background-color: var(--el-bg-color, ${inputBackgroundFallback}) !important;
|
||||
border: 1px solid var(--el-border-color, ${borderFallback}) !important;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { AppDebug } from '../service/appConfig';
|
||||
import { handleImageUploadInput } from '../shared/utils/imageUpload';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
import { watchState } from '../service/watchState';
|
||||
import { router } from '../plugin/router';
|
||||
|
||||
import miscReq from '../api/misc';
|
||||
|
||||
@@ -122,8 +123,16 @@ export const useGalleryStore = defineStore('Gallery', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function showGalleryDialog() {
|
||||
function showGalleryPage() {
|
||||
galleryDialogVisible.value = true;
|
||||
if (router.currentRoute.value?.name === 'gallery') {
|
||||
loadGalleryData();
|
||||
return;
|
||||
}
|
||||
router.push({ name: 'gallery' });
|
||||
}
|
||||
|
||||
function loadGalleryData() {
|
||||
refreshGalleryTable();
|
||||
refreshVRCPlusIconsTable();
|
||||
refreshEmojiTable();
|
||||
@@ -572,7 +581,8 @@ export const useGalleryStore = defineStore('Gallery', () => {
|
||||
fullscreenImageDialog,
|
||||
cachedEmoji,
|
||||
|
||||
showGalleryDialog,
|
||||
showGalleryPage,
|
||||
loadGalleryData,
|
||||
refreshGalleryTable,
|
||||
refreshVRCPlusIconsTable,
|
||||
inviteImageUpload,
|
||||
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
systemIsDarkMode,
|
||||
updateTrustColorClasses
|
||||
} from '../../shared/utils/base/ui';
|
||||
import { THEME_CONFIG } from '../../shared/constants';
|
||||
import { database } from '../../service/database';
|
||||
import { getNameColour } from '../../shared/utils';
|
||||
import { languageCodes } from '../../localization';
|
||||
import { loadLocalizedStrings } from '../../plugin';
|
||||
import { useElementTheme } from '../../composables/useElementTheme';
|
||||
import { useFeedStore } from '../feed';
|
||||
import { useGameLogStore } from '../gameLog';
|
||||
import { useUiStore } from '../ui';
|
||||
@@ -24,7 +27,6 @@ import { useVrcxStore } from '../vrcx';
|
||||
import { watchState } from '../../service/watchState';
|
||||
|
||||
import configRepository from '../../service/config';
|
||||
import { languageCodes } from '../../localization';
|
||||
|
||||
export const useAppearanceSettingsStore = defineStore(
|
||||
'AppearanceSettings',
|
||||
@@ -42,6 +44,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
|
||||
const MAX_TABLE_PAGE_SIZE = 1000;
|
||||
const DEFAULT_TABLE_PAGE_SIZES = [10, 15, 20, 25, 50, 100];
|
||||
const { initPrimaryColor } = useElementTheme();
|
||||
|
||||
const appLanguage = ref('en');
|
||||
const themeMode = ref('');
|
||||
@@ -71,7 +74,8 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
const hideUserMemos = ref(false);
|
||||
const hideUnfriends = ref(false);
|
||||
const randomUserColours = ref(false);
|
||||
const trustColor = ref({
|
||||
const compactTableMode = ref(false);
|
||||
const TRUST_COLOR_DEFAULTS = Object.freeze({
|
||||
untrusted: '#CCCCCC',
|
||||
basic: '#1778FF',
|
||||
known: '#2BCF5C',
|
||||
@@ -80,8 +84,10 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
vip: '#FF2626',
|
||||
troll: '#782F2F'
|
||||
});
|
||||
const trustColor = ref({ ...TRUST_COLOR_DEFAULTS });
|
||||
const currentCulture = ref('');
|
||||
const notificationIconDot = ref(false);
|
||||
const isNavCollapsed = ref(true);
|
||||
const isSideBarTabShow = computed(() => {
|
||||
const currentRouteName = router.currentRoute.value?.name;
|
||||
return !(
|
||||
@@ -118,8 +124,10 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
hideUserMemosConfig,
|
||||
hideUnfriendsConfig,
|
||||
randomUserColoursConfig,
|
||||
compactTableModeConfig,
|
||||
trustColorConfig,
|
||||
notificationIconDotConfig
|
||||
notificationIconDotConfig,
|
||||
navIsCollapsedConfig
|
||||
] = await Promise.all([
|
||||
configRepository.getString('VRCX_appLanguage'),
|
||||
configRepository.getString('VRCX_ThemeMode', 'system'),
|
||||
@@ -163,19 +171,13 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
configRepository.getBool('VRCX_hideUserMemos', false),
|
||||
configRepository.getBool('VRCX_hideUnfriends', false),
|
||||
configRepository.getBool('VRCX_randomUserColours', false),
|
||||
configRepository.getBool('VRCX_compactTableMode', false),
|
||||
configRepository.getString(
|
||||
'VRCX_trustColor',
|
||||
JSON.stringify({
|
||||
untrusted: '#CCCCCC',
|
||||
basic: '#1778FF',
|
||||
known: '#2BCF5C',
|
||||
trusted: '#FF7B42',
|
||||
veteran: '#B18FFF',
|
||||
vip: '#FF2626',
|
||||
troll: '#782F2F'
|
||||
})
|
||||
JSON.stringify(TRUST_COLOR_DEFAULTS)
|
||||
),
|
||||
configRepository.getBool('VRCX_notificationIconDot', true)
|
||||
configRepository.getBool('VRCX_notificationIconDot', true),
|
||||
configRepository.getBool('VRCX_navIsCollapsed', true)
|
||||
]);
|
||||
|
||||
if (!appLanguageConfig) {
|
||||
@@ -193,8 +195,18 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
await changeAppLanguage(appLanguageConfig);
|
||||
}
|
||||
|
||||
themeMode.value = themeModeConfig;
|
||||
const normalizedThemeMode = normalizeThemeMode(themeModeConfig);
|
||||
if (normalizedThemeMode !== themeModeConfig) {
|
||||
configRepository.setString(
|
||||
'VRCX_ThemeMode',
|
||||
normalizedThemeMode
|
||||
);
|
||||
}
|
||||
|
||||
themeMode.value = normalizedThemeMode;
|
||||
applyThemeMode();
|
||||
await changeAppThemeStyle(themeMode.value);
|
||||
await initPrimaryColor();
|
||||
|
||||
displayVRCPlusIconsAsAvatar.value =
|
||||
displayVRCPlusIconsAsAvatarConfig;
|
||||
@@ -222,7 +234,13 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
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;
|
||||
isSidebarGroupByInstance.value = isSidebarGroupByInstanceConfig;
|
||||
isHideFriendsInSameInstance.value =
|
||||
@@ -234,6 +252,11 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
hideUnfriends.value = hideUnfriendsConfig;
|
||||
randomUserColours.value = randomUserColoursConfig;
|
||||
notificationIconDot.value = notificationIconDotConfig;
|
||||
compactTableMode.value = compactTableModeConfig;
|
||||
applyCompactTableMode(compactTableMode.value);
|
||||
isNavCollapsed.value = navIsCollapsedConfig;
|
||||
|
||||
await configRepository.remove('VRCX_navWidth');
|
||||
|
||||
// Migrate old settings
|
||||
// Assume all exist if one does
|
||||
@@ -256,6 +279,12 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
function normalizeThemeMode(mode) {
|
||||
return Object.prototype.hasOwnProperty.call(THEME_CONFIG, mode)
|
||||
? mode
|
||||
: 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
@@ -421,8 +450,9 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
* @param {string} mode
|
||||
*/
|
||||
function setThemeMode(mode) {
|
||||
themeMode.value = mode;
|
||||
configRepository.setString('VRCX_ThemeMode', mode);
|
||||
const normalizedThemeMode = normalizeThemeMode(mode);
|
||||
themeMode.value = normalizedThemeMode;
|
||||
configRepository.setString('VRCX_ThemeMode', normalizedThemeMode);
|
||||
applyThemeMode();
|
||||
}
|
||||
function applyThemeMode() {
|
||||
@@ -557,18 +587,24 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
JSON.stringify(methods)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {number} panelNumber
|
||||
* @param {Array<number>} widthArray
|
||||
*/
|
||||
function setAsideWidth(panelNumber, widthArray) {
|
||||
if (Array.isArray(widthArray) && widthArray[1]) {
|
||||
function setNavCollapsed(collapsed) {
|
||||
isNavCollapsed.value = collapsed;
|
||||
configRepository.setBool('VRCX_navIsCollapsed', collapsed);
|
||||
}
|
||||
function toggleNavCollapsed() {
|
||||
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(() => {
|
||||
asideWidth.value = widthArray[1];
|
||||
configRepository.setInt(
|
||||
'VRCX_sidePanelWidth',
|
||||
widthArray[1]
|
||||
);
|
||||
asideWidth.value = width;
|
||||
configRepository.setInt('VRCX_sidePanelWidth', width);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -614,14 +650,22 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
randomUserColours.value
|
||||
);
|
||||
}
|
||||
function setCompactTableMode() {
|
||||
compactTableMode.value = !compactTableMode.value;
|
||||
applyCompactTableMode(compactTableMode.value);
|
||||
configRepository.setBool(
|
||||
'VRCX_compactTableMode',
|
||||
compactTableMode.value
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {object} color
|
||||
*/
|
||||
function setTrustColor(color) {
|
||||
trustColor.value = color;
|
||||
trustColor.value = { ...TRUST_COLOR_DEFAULTS };
|
||||
configRepository.setString(
|
||||
'VRCX_trustColor',
|
||||
JSON.stringify(color)
|
||||
JSON.stringify(trustColor.value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -741,6 +785,15 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
await userColourInit();
|
||||
}
|
||||
|
||||
function applyCompactTableMode(isCompact) {
|
||||
const className = 'is-compact-table';
|
||||
if (isCompact) {
|
||||
document.documentElement.classList.add(className);
|
||||
} else {
|
||||
document.documentElement.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appLanguage,
|
||||
themeMode,
|
||||
@@ -766,10 +819,12 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
hideUserMemos,
|
||||
hideUnfriends,
|
||||
randomUserColours,
|
||||
compactTableMode,
|
||||
trustColor,
|
||||
currentCulture,
|
||||
isSideBarTabShow,
|
||||
notificationIconDot,
|
||||
isNavCollapsed,
|
||||
|
||||
setAppLanguage,
|
||||
setDisplayVRCPlusIconsAsAvatar,
|
||||
@@ -793,6 +848,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
setHideUserMemos,
|
||||
setHideUnfriends,
|
||||
setRandomUserColours,
|
||||
setCompactTableMode,
|
||||
setTrustColor,
|
||||
saveThemeMode,
|
||||
tryInitUserColours,
|
||||
@@ -802,7 +858,10 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
applyUserTrustLevel,
|
||||
changeAppLanguage,
|
||||
promptMaxTableSizeDialog,
|
||||
setNotificationIconDot
|
||||
setNotificationIconDot,
|
||||
applyCompactTableMode,
|
||||
setNavCollapsed,
|
||||
toggleNavCollapsed
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<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-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>
|
||||
@@ -33,7 +30,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.charts-tabs {
|
||||
margin-bottom: 12px;
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="instanceActivityRef" class="pt-12">
|
||||
<div class="options-container instance-activity" style="margin-top: 0">
|
||||
<div>
|
||||
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
||||
@@ -151,6 +151,33 @@
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
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 {
|
||||
barWidth,
|
||||
isDetailVisible,
|
||||
@@ -623,7 +650,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 100px;
|
||||
color: #5c5c5c;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.divider {
|
||||
padding: 0 400px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mutual-graph">
|
||||
<div class="mutual-graph pt-12" ref="mutualGraphRef">
|
||||
<div class="options-container mutual-graph__toolbar">
|
||||
<div class="mutual-graph__actions">
|
||||
<el-tooltip :content="t('view.charts.mutual_friend.force_dialog.open_label')" placement="top">
|
||||
@@ -207,6 +207,20 @@
|
||||
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(() => {
|
||||
nextTick(() => {
|
||||
if (!chartRef.value) {
|
||||
@@ -215,6 +229,8 @@
|
||||
createChartInstance();
|
||||
resizeObserver = new ResizeObserver(() => chartInstance?.resize());
|
||||
resizeObserver.observe(chartRef.value);
|
||||
mutualGraphResizeObserver.observe(mutualGraphRef.value);
|
||||
setMutualGraphHeight();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,6 +243,9 @@
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
if (mutualGraphResizeObserver) {
|
||||
mutualGraphResizeObserver.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -676,8 +695,8 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
@@ -1580,6 +1580,7 @@
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.group-section__list {
|
||||
|
||||
@@ -796,6 +796,7 @@
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.group-section__list {
|
||||
|
||||
@@ -1276,6 +1276,7 @@
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.group-section__list {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<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="flex: none; margin-right: 10px; display: flex; align-items: center">
|
||||
<NativeTooltip
|
||||
placement="bottom"
|
||||
:content="t('view.feed.favorites_only_tooltip')"
|
||||
:enter-ms="140"
|
||||
:exit-ms="120">
|
||||
<el-switch v-model="feedTable.vip" active-color="#13ce66" @change="feedTableLookup"></el-switch>
|
||||
</NativeTooltip>
|
||||
<el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
|
||||
<el-switch
|
||||
v-model="feedTable.vip"
|
||||
active-color="var(--el-color-success)"
|
||||
@change="feedTableLookup"></el-switch>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="feedTable.filter"
|
||||
@@ -33,9 +32,9 @@
|
||||
</div>
|
||||
|
||||
<DataTable v-bind="feedTable" :data="feedDisplayData">
|
||||
<el-table-column type="expand" width="20">
|
||||
<el-table-column type="expand" width="30">
|
||||
<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'">
|
||||
<Location
|
||||
v-if="scope.row.previousLocation"
|
||||
@@ -45,9 +44,7 @@
|
||||
timeToText(scope.row.time)
|
||||
}}</el-tag>
|
||||
<br />
|
||||
<span style="margin-right: 5px">
|
||||
<el-icon><Right /></el-icon>
|
||||
</span>
|
||||
<span style="margin-right: 5px"> ↓ </span>
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
@@ -91,7 +88,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<span style="position: relative; margin: 0 10px">
|
||||
<el-icon><Right /></el-icon>
|
||||
{{ ' → ' }}
|
||||
</span>
|
||||
|
||||
<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>
|
||||
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
|
||||
<br />
|
||||
<span>
|
||||
<el-icon><Right /></el-icon>
|
||||
</span>
|
||||
<span> → </span>
|
||||
|
||||
<i class="x-user-status" :class="statusClass(scope.row.status)" style="margin: 0 5px"></i>
|
||||
<span v-text="scope.row.statusDescription"></span>
|
||||
@@ -132,27 +127,29 @@
|
||||
</template>
|
||||
</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">
|
||||
<NativeTooltip placement="right">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</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">
|
||||
<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>
|
||||
</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">
|
||||
<span
|
||||
class="x-link"
|
||||
class="x-link table-user"
|
||||
style="padding-right: 10px"
|
||||
@click="showUserDialog(scope.row.userId)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
@@ -178,17 +175,12 @@
|
||||
<template v-else-if="scope.row.type === 'Status'">
|
||||
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
|
||||
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
|
||||
<span style="margin: 0 5px">
|
||||
<el-icon><Right /></el-icon>
|
||||
</span>
|
||||
<span class="mx-2"> → </span>
|
||||
|
||||
<i class="x-user-status" :class="statusClass(scope.row.status)"></i>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.status)"
|
||||
style="margin-right: 3px"></i>
|
||||
<i class="x-user-status mr-2" :class="statusClass(scope.row.status)"></i>
|
||||
<span v-text="scope.row.statusDescription"></span>
|
||||
</template>
|
||||
</template>
|
||||
@@ -210,13 +202,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Right } from '@element-plus/icons-vue';
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { formatDateFilter, statusClass, timeToText } from '../../shared/utils';
|
||||
import { useFeedStore, useUserStore } from '../../stores';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { feedTable } = storeToRefs(useFeedStore());
|
||||
@@ -226,6 +218,8 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { containerRef: feedRef } = useTableHeight(feedTable);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -341,3 +335,9 @@
|
||||
.replace(/<br> /g, '<br>');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-user {
|
||||
color: var(--x-table-user-text-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,92 +1,59 @@
|
||||
<template>
|
||||
<div class="x-container">
|
||||
<div style="padding: 0 10px 0 10px">
|
||||
<div class="x-container" ref="friendsListRef">
|
||||
<div>
|
||||
<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">
|
||||
<el-tooltip placement="bottom" :content="t('view.friend_list.favorites_only_tooltip')">
|
||||
<el-switch
|
||||
v-model="friendsListSearchFilterVIP"
|
||||
active-color="#13ce66"
|
||||
active-color="var(--el-color-success)"
|
||||
@change="friendsListSearchChange"></el-switch>
|
||||
</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>
|
||||
<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>
|
||||
<DataTable
|
||||
v-loading="friendsListLoading"
|
||||
v-bind="friendsListTable"
|
||||
:table-props="{ height: 'calc(100vh - 170px)', size: 'small' }"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="selectFriendsListRow">
|
||||
<el-table-column v-if="friendsListBulkUnfriendMode" width="55">
|
||||
@@ -98,39 +65,38 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</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 }">
|
||||
<span>{{ row.$friendNumber ? row.$friendNumber : '' }}</span>
|
||||
</template>
|
||||
</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 }">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img :src="userImage(row, true)" class="friends-list-avatar" loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(row)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(row))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
<div class="flex items-center">
|
||||
<img :src="userImage(row, true)" class="friends-list-avatar" loading="lazy" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.displayName')"
|
||||
min-width="140"
|
||||
min-width="200"
|
||||
prop="displayName"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')">
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')"
|
||||
fixed="left">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: randomUserColours ? row.$userColour : undefined }" class="name">{{
|
||||
row.displayName
|
||||
}}</span>
|
||||
</template>
|
||||
</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 }">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
@@ -142,7 +108,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.status')"
|
||||
min-width="180"
|
||||
min-width="200"
|
||||
prop="status"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortStatus(a.status, b.status)">
|
||||
@@ -157,7 +123,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.language')"
|
||||
width="110"
|
||||
width="130"
|
||||
prop="$languages"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortLanguages(a, b)">
|
||||
@@ -173,7 +139,7 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</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 }">
|
||||
<el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index">
|
||||
<template #content>
|
||||
@@ -197,8 +163,14 @@
|
||||
:label="t('table.friendList.joinCount')"
|
||||
width="120"
|
||||
prop="$joinCount"
|
||||
sortable></el-table-column>
|
||||
<el-table-column :label="t('table.friendList.timeTogether')" width="140" prop="$timeSpent" sortable>
|
||||
sortable
|
||||
align="right"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.timeTogether')"
|
||||
width="140"
|
||||
prop="$timeSpent"
|
||||
sortable
|
||||
align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.$timeSpent">{{ timeToText(row.$timeSpent) }}</span>
|
||||
</template>
|
||||
@@ -210,17 +182,26 @@
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDateFilter(row.$lastSeen, 'long') }}</span>
|
||||
<span>{{
|
||||
formatDateFilter(row.$lastSeen, 'long') === '-'
|
||||
? ''
|
||||
: formatDateFilter(row.$lastSeen, 'long')
|
||||
}}</span>
|
||||
</template>
|
||||
</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 }">
|
||||
<span v-if="row.$mutualCount">{{ row.$mutualCount }}</span>
|
||||
<span v-else></span> </template
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.lastActivity')"
|
||||
width="170"
|
||||
width="200"
|
||||
prop="last_activity"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_activity')">
|
||||
@@ -230,7 +211,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.friendList.lastLogin')"
|
||||
width="170"
|
||||
width="200"
|
||||
prop="last_login"
|
||||
sortable
|
||||
: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>
|
||||
<el-table-column :label="t('table.friendList.unfriend')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
text
|
||||
:icon="Close"
|
||||
<i
|
||||
class="ri-user-unfollow-line"
|
||||
style="color: #f56c6c"
|
||||
size="small"
|
||||
@click.stop="confirmDeleteFriend(row.id)"></el-button>
|
||||
@click.stop="confirmDeleteFriend(row.id)"></i>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Close, Loading, Refresh, RefreshLeft } from '@element-plus/icons-vue';
|
||||
import { nextTick, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
@@ -276,19 +276,13 @@
|
||||
sortStatus,
|
||||
statusClass,
|
||||
timeToText,
|
||||
userImage,
|
||||
userImageFull
|
||||
userImage
|
||||
} from '../../shared/utils';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useFriendStore,
|
||||
useGalleryStore,
|
||||
useSearchStore,
|
||||
useUserStore
|
||||
} from '../../stores';
|
||||
import { useAppearanceSettingsStore, useFriendStore, useSearchStore, useUserStore } from '../../stores';
|
||||
import { friendRequest, userRequest } from '../../api';
|
||||
import removeConfusables, { removeWhitespace } from '../../service/confusables';
|
||||
import { router } from '../../plugin/router';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -299,21 +293,34 @@
|
||||
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { stringComparer, friendsListSearch } = storeToRefs(useSearchStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const friendsListSearchFilters = ref([]);
|
||||
const friendsListTable = reactive({
|
||||
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,
|
||||
paginationProps: { layout: 'sizes,prev,pager,next,total', pageSizes: [50, 100, 250, 500] }
|
||||
});
|
||||
const friendsListBulkUnfriendMode = 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 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();
|
||||
|
||||
watch(
|
||||
@@ -432,27 +439,43 @@
|
||||
}
|
||||
|
||||
async function friendsListLoadUsers() {
|
||||
friendsListLoading.value = true;
|
||||
let i = 0;
|
||||
const toFetch = Array.from(friends.value.values())
|
||||
.filter((ctx) => ctx.ref && !ctx.ref.date_joined)
|
||||
.map((ctx) => ctx.id);
|
||||
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) {
|
||||
if (!friendsListLoading.value) {
|
||||
friendsListLoadingProgress.value = '';
|
||||
return;
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
friendsListLoadingProgress.value = `${i}/${total}`;
|
||||
friendsListLoadingCurrent.value += 1;
|
||||
try {
|
||||
await userRequest.getUser({ userId });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
friendsListLoadingProgress.value = '';
|
||||
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) {
|
||||
@@ -476,3 +499,11 @@
|
||||
router.push({ name: 'charts' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.friends-list-avatar {
|
||||
object-fit: cover;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="x-container">
|
||||
<!-- 工具栏 -->
|
||||
<div class="x-container" ref="friendLogRef">
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<el-select
|
||||
v-model="friendLogTable.filters[0].value"
|
||||
@@ -40,27 +39,25 @@
|
||||
</template>
|
||||
</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">
|
||||
<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>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.friendLog.user')" prop="displayName">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 'DisplayName'">
|
||||
{{ scope.row.previousDisplayName }} <el-icon><Right /></el-icon>
|
||||
</span>
|
||||
<span v-if="scope.row.type === 'DisplayName'">{{ scope.row.previousDisplayName }} → </span>
|
||||
<span
|
||||
class="x-link"
|
||||
class="x-link table-user"
|
||||
style="padding-right: 10px"
|
||||
@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'">
|
||||
<span>
|
||||
({{ scope.row.previousTrustLevel }} <el-icon><Right /></el-icon>
|
||||
{{ scope.row.trustLevel }})</span
|
||||
>
|
||||
<span>({{ scope.row.previousTrustLevel }} → {{ scope.row.trustLevel }})</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -69,28 +66,27 @@
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
text
|
||||
:icon="Close"
|
||||
size="small"
|
||||
class="button-pd-0"
|
||||
@click="deleteFriendLog(scope.row)"></el-button>
|
||||
<el-button
|
||||
<i
|
||||
v-else
|
||||
text
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
class="button-pd-0"
|
||||
@click="deleteFriendLogPrompt(scope.row)"></el-button>
|
||||
class="ri-delete-bin-line"
|
||||
style="opacity: 0.85"
|
||||
@click="deleteFriendLogPrompt(scope.row)"></i>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="5"></el-table-column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Close, Delete, Right } from '@element-plus/icons-vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -100,6 +96,7 @@
|
||||
import { useAppearanceSettingsStore, useFriendStore, useUiStore, useUserStore } from '../../stores';
|
||||
import { formatDateFilter, removeFromArray } from '../../shared/utils';
|
||||
import { database } from '../../service/database';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
import configRepository from '../../service/config';
|
||||
|
||||
@@ -108,6 +105,8 @@
|
||||
const { friendLogTable } = storeToRefs(useFriendStore());
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
|
||||
const { containerRef: friendLogRef } = useTableHeight(friendLogTable);
|
||||
|
||||
const friendLogDisplayData = computed(() => {
|
||||
const data = friendLogTable.value.data;
|
||||
return data.slice().sort((a, b) => {
|
||||
@@ -160,4 +159,7 @@
|
||||
.button-pd-0 {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.table-user {
|
||||
color: var(--x-table-user-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-slider
|
||||
v-model="cardScale"
|
||||
class="friend-view__slider"
|
||||
:min="0.6"
|
||||
:min="0.5"
|
||||
:max="1.0"
|
||||
:step="0.01"
|
||||
:show-tooltip="false" />
|
||||
@@ -45,8 +45,8 @@
|
||||
<el-slider
|
||||
v-model="cardSpacing"
|
||||
class="friend-view__slider"
|
||||
:min="0.5"
|
||||
:max="1.5"
|
||||
:min="0.25"
|
||||
:max="1.0"
|
||||
:step="0.05"
|
||||
:show-tooltip="false" />
|
||||
</div>
|
||||
@@ -688,7 +688,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.friend-view {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
@@ -699,12 +699,12 @@
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 6px 10px 0 2px;
|
||||
padding: 6px 2px 0 2px;
|
||||
}
|
||||
|
||||
.friend-view__toolbar--loading {
|
||||
justify-content: flex-end;
|
||||
color: rgba(15, 23, 42, 0.55);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -720,7 +720,7 @@
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
color: rgba(15, 23, 42, 0.65);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.friend-view__settings-label {
|
||||
@@ -746,7 +746,7 @@
|
||||
.friend-view__scale-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(15, 23, 42, 0.55);
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 42px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -762,14 +762,14 @@
|
||||
}
|
||||
|
||||
.friend-view__scroll {
|
||||
padding: 2px 10px 2px 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.friend-view__initial-loading {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 240px;
|
||||
color: rgba(15, 23, 42, 0.45);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.friend-view__grid {
|
||||
@@ -780,7 +780,7 @@
|
||||
);
|
||||
gap: var(--friend-card-gap, 18px);
|
||||
justify-content: start;
|
||||
padding-right: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.friend-view__instances {
|
||||
@@ -802,7 +802,7 @@
|
||||
margin: 5px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: rgba(15, 23, 42, 0.75);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.friend-view__divider {
|
||||
@@ -810,7 +810,7 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 4px;
|
||||
color: rgba(15, 23, 42, 0.6);
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -820,7 +820,7 @@
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: rgba(148, 163, 184, 0.35);
|
||||
background: var(--el-border-color);
|
||||
}
|
||||
|
||||
.friend-view__divider-text {
|
||||
@@ -829,14 +829,14 @@
|
||||
|
||||
.friend-view__instance-count {
|
||||
font-size: 12px;
|
||||
color: rgba(15, 23, 42, 0.45);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.friend-view__empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 240px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -847,7 +847,7 @@
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 18px 0 12px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
</el-avatar>
|
||||
</div>
|
||||
<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 class="friend-card__body">
|
||||
<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 || ' ' }}
|
||||
</div>
|
||||
<div v-if="displayInstanceInfo" @click.stop class="friend-card__world" :title="friend.worldName">
|
||||
@@ -87,17 +87,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.friend-card {
|
||||
--card-scale: 1;
|
||||
--card-spacing: 1;
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: calc(14px * var(--card-scale) * var(--card-spacing));
|
||||
border-radius: calc(8px * var(--card-scale));
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
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:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
@@ -105,7 +105,7 @@
|
||||
min-width: var(--friend-card-min-width, 220px);
|
||||
|
||||
&: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)));
|
||||
}
|
||||
}
|
||||
@@ -123,8 +123,8 @@
|
||||
}
|
||||
|
||||
.friend-card__avatar {
|
||||
border: 1px solid rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 calc(5px * var(--card-scale)) calc(10px * var(--card-scale)) rgba(15, 23, 42, 0.14);
|
||||
border: 1px solid var(--el-border-color);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
}
|
||||
|
||||
.friend-card__status-dot {
|
||||
@@ -134,8 +134,8 @@
|
||||
inline-size: calc(12px * var(--card-scale));
|
||||
block-size: calc(12px * var(--card-scale));
|
||||
border-radius: 999px;
|
||||
border: calc(2px * var(--card-scale)) solid #fff;
|
||||
box-shadow: 0 0 calc(4px * var(--card-scale)) rgba(15, 23, 42, 0.12);
|
||||
border: calc(2px * var(--card-scale)) solid var(--el-bg-color-overlay);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -144,23 +144,23 @@
|
||||
}
|
||||
|
||||
.friend-card__status-dot--online {
|
||||
background: linear-gradient(145deg, #67c23a, #4aa12d);
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(103, 194, 58, 0.4);
|
||||
background: #67c23a;
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #67c23a 40%, transparent);
|
||||
}
|
||||
|
||||
.friend-card__status-dot--join {
|
||||
background: linear-gradient(145deg, #409eff, #2f7ed9);
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(64, 158, 255, 0.4);
|
||||
background: #409eff;
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #409eff 40%, transparent);
|
||||
}
|
||||
|
||||
.friend-card__status-dot--busy {
|
||||
background: linear-gradient(145deg, #ff2c2c, #d81f1f);
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(255, 44, 44, 0.4);
|
||||
background: #ff2c2c;
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #ff2c2c 40%, transparent);
|
||||
}
|
||||
|
||||
.friend-card__status-dot--ask {
|
||||
background: linear-gradient(145deg, #ff9500, #d97800);
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) rgba(255, 149, 0, 0.4);
|
||||
background: #ff9500;
|
||||
box-shadow: 0 0 calc(8px * var(--card-scale)) color-mix(in oklch, #ff9500 40%, transparent);
|
||||
}
|
||||
|
||||
.friend-card__body {
|
||||
@@ -171,7 +171,7 @@
|
||||
.friend-card__name {
|
||||
font-size: calc(17px * var(--card-scale));
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -181,7 +181,7 @@
|
||||
.friend-card__signature {
|
||||
margin-top: calc(6px * var(--card-spacing));
|
||||
font-size: calc(13px * var(--card-scale));
|
||||
color: rgba(31, 41, 55, 0.7);
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -194,12 +194,21 @@
|
||||
justify-content: center;
|
||||
min-height: calc(40px * var(--card-scale));
|
||||
padding: calc(6px * var(--card-scale)) calc(10px * var(--card-scale));
|
||||
border-radius: calc(12px * var(--card-scale));
|
||||
background: rgba(148, 163, 184, 0.18);
|
||||
color: rgba(71, 85, 105, 0.95);
|
||||
border-radius: calc(10px * var(--card-scale));
|
||||
background: var(--el-fill-color);
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: calc(12px * var(--card-scale));
|
||||
line-height: 1.3;
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="x-container">
|
||||
<div class="x-container" ref="gameLogRef">
|
||||
<div style="margin: 0 0 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
|
||||
v-model="gameLogTable.vip"
|
||||
active-color="#13ce66"
|
||||
active-color="var(--el-color-success)"
|
||||
@change="gameLogTableLookup"></el-switch>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="gameLogTable.filter"
|
||||
@@ -41,46 +41,47 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<NativeTooltip placement="right">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</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">
|
||||
<span
|
||||
v-if="scope.row.location && scope.row.type !== 'Location'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location)"
|
||||
v-text="t('view.game_log.filters.' + scope.row.type)"></span>
|
||||
<span v-else v-text="t('view.game_log.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
|
||||
<template #default="scope">
|
||||
v-if="scope.row.displayName"
|
||||
class="x-link table-user"
|
||||
style="padding-right: 10px"
|
||||
@click="lookupUser(scope.row)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
<template v-if="gameLogIsFriend(scope.row)">
|
||||
<span v-if="gameLogIsFavorite(scope.row)">⭐</span>
|
||||
<span v-else>💚</span>
|
||||
</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>
|
||||
</el-table-column>
|
||||
|
||||
@@ -158,31 +159,38 @@
|
||||
size="small"
|
||||
class="small-button"
|
||||
@click="deleteGameLogEntry(scope.row)"></el-button>
|
||||
<el-button
|
||||
<i
|
||||
class="ri-delete-bin-line small-button"
|
||||
style="opacity: 0.85"
|
||||
v-else
|
||||
text
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
class="small-button"
|
||||
@click="deleteGameLogEntryPrompt(scope.row)"></el-button>
|
||||
@click="deleteGameLogEntryPrompt(scope.row)"></i>
|
||||
</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
|
||||
v-if="scope.row.type === 'Location'"
|
||||
v-if="shiftHeld"
|
||||
text
|
||||
:icon="DataLine"
|
||||
size="small"
|
||||
class="small-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>
|
||||
</el-table-column>
|
||||
<el-table-column width="5"></el-table-column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -194,6 +202,7 @@
|
||||
import { formatDateFilter, openExternalLink, removeFromArray } from '../../shared/utils';
|
||||
import { database } from '../../service/database';
|
||||
import { useSharedFeedStore } from '../../stores';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { lookupUser } = useUserStore();
|
||||
@@ -252,6 +261,8 @@
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['updateGameLogSessionTable']);
|
||||
|
||||
const { containerRef: gameLogRef } = useTableHeight(gameLogTable);
|
||||
|
||||
function deleteGameLogEntry(row) {
|
||||
removeFromArray(gameLogTable.value.data, row);
|
||||
database.deleteGameLogEntry(row);
|
||||
@@ -281,5 +292,9 @@
|
||||
.small-button {
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table-user {
|
||||
color: var(--x-table-user-text-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||