mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 06:46:04 +02:00
UI Refresh
This commit is contained in:
@@ -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 {
|
||||
|
||||
+32
-32
@@ -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>
|
||||
|
||||
+160
-129
@@ -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>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<template v-if="watchState.isLoggedIn">
|
||||
<NavMenu></NavMenu>
|
||||
|
||||
<el-splitter @resize-end="setAsideWidth">
|
||||
<el-splitter @resize-end="handleResizeEnd">
|
||||
<el-splitter-panel>
|
||||
<RouterView></RouterView>
|
||||
</el-splitter-panel>
|
||||
@@ -86,6 +85,20 @@
|
||||
const { setAsideWidth } = appearanceStore;
|
||||
const { asideWidth, isSideBarTabShow } = storeToRefs(appearanceStore);
|
||||
|
||||
const handleResizeEnd = (index, sizes) => {
|
||||
if (!Array.isArray(sizes) || sizes.length < 2) {
|
||||
return;
|
||||
}
|
||||
const asideSplitterIndex = sizes.length - 2;
|
||||
if (index !== asideSplitterIndex) {
|
||||
return;
|
||||
}
|
||||
const asideSize = sizes[sizes.length - 1];
|
||||
if (Number.isFinite(asideSize) && asideSize > 0) {
|
||||
setAsideWidth(asideSize);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => watchState.isLoggedIn,
|
||||
(isLoggedIn) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="x-container">
|
||||
<div class="x-container" ref="moderationRef">
|
||||
<!-- 工具栏 -->
|
||||
<div class="tool-slot">
|
||||
<el-select
|
||||
@@ -30,7 +30,8 @@
|
||||
</div>
|
||||
|
||||
<DataTable v-bind="playerModerationTable">
|
||||
<el-table-column :label="t('table.moderation.date')" prop="created" :sortable="true" width="130">
|
||||
<el-table-column width="20"></el-table-column>
|
||||
<el-table-column :label="t('table.moderation.date')" prop="created" :sortable="true" width="150">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
@@ -40,12 +41,14 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.moderation.type')" prop="type" width="100">
|
||||
<el-table-column :label="t('table.moderation.type')" prop="type" width="150">
|
||||
<template #default="scope">
|
||||
<span v-text="t('view.moderation.filters.' + scope.row.type)"></span>
|
||||
<el-tag type="info" effect="plain" size="small">
|
||||
<span v-text="t('view.moderation.filters.' + scope.row.type)"></span
|
||||
></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.moderation.source')" prop="sourceDisplayName">
|
||||
<el-table-column :label="t('table.moderation.source')" prop="sourceDisplayName" width="200">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -66,7 +69,7 @@
|
||||
<template v-if="scope.row.sourceUserId === currentUser.id">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
text
|
||||
:icon="Close"
|
||||
size="small"
|
||||
@@ -94,6 +97,7 @@
|
||||
import { formatDateFilter } from '../../shared/utils';
|
||||
import { moderationTypes } from '../../shared/constants';
|
||||
import { playerModerationRequest } from '../../api';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
import configRepository from '../../service/config.js';
|
||||
|
||||
@@ -104,6 +108,8 @@
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const { containerRef: moderationRef } = useTableHeight(playerModerationTable);
|
||||
|
||||
async function init() {
|
||||
playerModerationTable.value.filters[0].value = JSON.parse(
|
||||
await configRepository.getString('VRCX_playerModerationTableFilters', '[]')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-loading="isNotificationsLoading" class="x-container">
|
||||
<div v-loading="isNotificationsLoading" class="x-container" ref="notificationsRef">
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<el-select
|
||||
v-model="notificationTable.filters[0].value"
|
||||
@@ -54,7 +54,7 @@
|
||||
:data="notificationDisplayData"
|
||||
ref="notificationTableRef"
|
||||
class="notification-table">
|
||||
<el-table-column :label="t('table.notification.date')" prop="created_at" width="130">
|
||||
<el-table-column :label="t('table.notification.date')" prop="created_at" width="110">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
@@ -67,89 +67,121 @@
|
||||
|
||||
<el-table-column :label="t('table.notification.type')" prop="type" width="180">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="scope.row.type === 'invite'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.details.worldId)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
<el-tooltip
|
||||
v-else-if="scope.row.type === 'group.queueReady' || scope.row.type === 'instance.closed'"
|
||||
placement="top">
|
||||
<template #content>
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false" />
|
||||
</template>
|
||||
<el-tag type="info" effect="plain" size="small">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location)"
|
||||
v-if="scope.row.type === 'invite'"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else-if="scope.row.link" placement="top" :content="scope.row.linkText">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<span v-else v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
<el-tooltip
|
||||
v-else-if="scope.row.type === 'group.queueReady' || scope.row.type === 'instance.closed'"
|
||||
placement="top">
|
||||
<template #content>
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false" />
|
||||
</template>
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else-if="scope.row.link" placement="top" :content="scope.row.linkText">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<span v-else v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.user_group')" prop="senderUsername" width="150">
|
||||
<el-table-column :label="t('table.notification.user')" prop="senderUsername" width="150">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'groupChange'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showGroupDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.senderUserId">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showUserDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link && scope.row.data?.groupName">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.data?.groupName"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.linkText"></span>
|
||||
</template>
|
||||
<div class="table-user-text">
|
||||
<template v-if="scope.row.senderUserId && !isGroupId(scope.row.senderUserId)">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showUserDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link?.startsWith('user:')">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.linkText || scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.senderUsername && !isGroupId(scope.row.senderUserId)">
|
||||
<span v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.photo')" width="100" prop="photo">
|
||||
<el-table-column :label="t('table.notification.group')" prop="groupName" width="150">
|
||||
<template #default="scope">
|
||||
<div class="table-user-text">
|
||||
<template
|
||||
v-if="
|
||||
scope.row.senderUserId &&
|
||||
(scope.row.type === 'groupChange' || isGroupId(scope.row.senderUserId))
|
||||
">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showGroupDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername || scope.row.groupName"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'groupChange' && scope.row.senderUsername">
|
||||
<span v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link?.startsWith('group:')">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.data?.groupName || scope.row.linkText"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link?.startsWith('event:')">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.data?.groupName || scope.row.groupName || scope.row.linkText"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.data?.groupName">
|
||||
<span v-text="scope.row.data.groupName"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.details?.groupName">
|
||||
<span v-text="scope.row.details.groupName"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.groupName">
|
||||
<span v-text="scope.row.groupName"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.photo')" width="80" prop="photo">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'boop'">
|
||||
<Emoji
|
||||
class="x-link"
|
||||
class="x-link notification-image"
|
||||
@click="showFullscreenImageDialog(scope.row.details.imageUrl)"
|
||||
v-if="scope.row.details?.imageUrl && !scope.row.details.imageUrl.startsWith('default_')"
|
||||
:imageUrl="scope.row.details.imageUrl"
|
||||
:size="50"></Emoji>
|
||||
:size="30"></Emoji>
|
||||
</template>
|
||||
<template v-else-if="scope.row.details && scope.row.details.imageUrl">
|
||||
<img
|
||||
class="x-link"
|
||||
class="x-link notification-image"
|
||||
:src="getSmallThumbnailUrl(scope.row.details.imageUrl)"
|
||||
style="flex: none; height: 50px; border-radius: 4px"
|
||||
@click="showFullscreenImageDialog(scope.row.details.imageUrl)"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.imageUrl">
|
||||
<img
|
||||
class="x-link"
|
||||
class="x-link notification-image"
|
||||
:src="getSmallThumbnailUrl(scope.row.imageUrl)"
|
||||
style="flex: none; height: 50px; border-radius: 4px"
|
||||
@click="showFullscreenImageDialog(scope.row.imageUrl)"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
@@ -193,7 +225,7 @@
|
||||
<el-button
|
||||
text
|
||||
:icon="Check"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
size="small"
|
||||
class="button-pd-0"
|
||||
@click="acceptFriendRequestNotification(scope.row)" />
|
||||
@@ -216,7 +248,7 @@
|
||||
<el-button
|
||||
text
|
||||
:icon="Check"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
size="small"
|
||||
class="button-pd-0"
|
||||
@click="acceptRequestInvite(scope.row)" />
|
||||
@@ -321,7 +353,7 @@
|
||||
<el-tooltip placement="top" content="Decline">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
text
|
||||
:icon="Close"
|
||||
size="small"
|
||||
@@ -341,7 +373,7 @@
|
||||
<el-tooltip placement="top" content="Delete log">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
text
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
@@ -367,7 +399,7 @@
|
||||
<el-tooltip placement="top" content="Delete log">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
style="color: var(--el-color-danger); margin-left: 5px"
|
||||
text
|
||||
:icon="Close"
|
||||
size="small"
|
||||
@@ -384,6 +416,7 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="5"></el-table-column>
|
||||
</DataTable>
|
||||
<SendInviteResponseDialog
|
||||
v-model:send-invite-response-dialog="sendInviteResponseDialog"
|
||||
@@ -435,6 +468,7 @@
|
||||
} from '../../shared/utils';
|
||||
import { friendRequest, notificationRequest, worldRequest } from '../../api';
|
||||
import { database } from '../../service/database';
|
||||
import { useTableHeight } from '../../composables/useTableHeight';
|
||||
|
||||
import Emoji from '../../components/Emoji.vue';
|
||||
import SendInviteRequestResponseDialog from './dialogs/SendInviteRequestResponseDialog.vue';
|
||||
@@ -456,6 +490,8 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { containerRef: notificationsRef } = useTableHeight(notificationTable);
|
||||
|
||||
function getNotificationCreatedAt(row) {
|
||||
if (typeof row?.created_at === 'string' && row.created_at.length > 0) {
|
||||
return row.created_at;
|
||||
@@ -501,6 +537,8 @@
|
||||
|
||||
const sendInviteRequestResponseDialogVisible = ref(false);
|
||||
|
||||
const isGroupId = (id) => typeof id === 'string' && id.startsWith('grp_');
|
||||
|
||||
function saveTableFilters() {
|
||||
configRepository.setString(
|
||||
'VRCX_notificationTableFilters',
|
||||
@@ -710,11 +748,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.button-pd-0 {
|
||||
padding: 0;
|
||||
}
|
||||
.ml-5 {
|
||||
margin-left: 5px !important; // due to ".el-button + .el-button"
|
||||
margin-left: 5px !important; /* due to ".el-button + .el-button" */
|
||||
}
|
||||
.notification-image {
|
||||
flex: none;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.table-user-text {
|
||||
color: var(--x-table-user-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="x-container" style="padding-top: 5px">
|
||||
<div class="x-container" ref="playerListRef">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<div v-if="currentInstanceWorld.ref.id" style="display: flex">
|
||||
<div v-if="currentInstanceWorld.ref.id" style="display: flex; height: 120px">
|
||||
<img
|
||||
:src="currentInstanceWorld.ref.thumbnailImageUrl"
|
||||
class="x-link"
|
||||
@@ -139,27 +139,8 @@
|
||||
<div style="margin-top: 5px">
|
||||
<span
|
||||
v-show="currentInstanceWorld.ref.name !== currentInstanceWorld.ref.description"
|
||||
:style="{
|
||||
fontSize: '12px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: currentInstanceWorldDescriptionExpanded ? 'none' : '2'
|
||||
}"
|
||||
class="description"
|
||||
v-text="currentInstanceWorld.ref.description"></span>
|
||||
<div style="display: flex; justify-content: end">
|
||||
<el-button
|
||||
v-if="
|
||||
currentInstanceWorld.ref.description.length > 50 &&
|
||||
!currentInstanceWorldDescriptionExpanded
|
||||
"
|
||||
text
|
||||
size="small"
|
||||
@click="currentInstanceWorldDescriptionExpanded = true"
|
||||
>{{ !currentInstanceWorldDescriptionExpanded && 'Show more' }}</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; margin-left: 20px">
|
||||
@@ -200,31 +181,61 @@
|
||||
layout="table"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="selectCurrentInstanceRow">
|
||||
<el-table-column :label="t('table.playerList.avatar')" width="70" prop="photo">
|
||||
<el-table-column :label="t('table.playerList.avatar')" width="70" prop="photo" fixed>
|
||||
<template #default="scope">
|
||||
<template v-if="userImage(scope.row.ref)">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.ref)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.ref)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
<div v-if="userImage(scope.row.ref)" class="flex items-center pl-2">
|
||||
<img :src="userImage(scope.row.ref)" class="friends-list-avatar" loading="lazy" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.timer')" width="90" prop="timer" sortable>
|
||||
<el-table-column :label="t('table.playerList.timer')" width="90" prop="timer" sortable fixed>
|
||||
<template #default="scope">
|
||||
<Timer :epoch="scope.row.timer" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
class="table-user"
|
||||
:label="t('table.playerList.displayName')"
|
||||
width="200"
|
||||
prop="displayName"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')"
|
||||
fixed>
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.ref.$userColour }"
|
||||
v-text="scope.row.ref.displayName"></span>
|
||||
<span v-else v-text="scope.row.ref.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
:label="t('table.playerList.rank')"
|
||||
width="110"
|
||||
prop="$trustSortNum"
|
||||
:sortable="true">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="name"
|
||||
:class="scope.row.ref.$trustClass"
|
||||
v-text="scope.row.ref.$trustLevel"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.status')" min-width="200" prop="ref.status">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.ref.status">
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.ref.status)"
|
||||
style="margin-right: 3px"></i>
|
||||
<span v-text="scope.row.ref.statusDescription"></span>
|
||||
<!--//- el-table-column(label="Group" min-width="180" prop="groupOnNameplate" sortable)-->
|
||||
<!--//- template(v-once #default="scope")-->
|
||||
<!--//- span(v-text="scope.row.groupOnNameplate")-->
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="photonLoggingEnabled"
|
||||
:label="t('table.playerList.photonId')"
|
||||
@@ -277,20 +288,20 @@
|
||||
<el-icon style="color: red"><CircleClose /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.isMuted" placement="left" content="Muted">
|
||||
<el-icon style="color: orange"><Mute /></el-icon>
|
||||
<el-icon style="color: var(--el-color-warning)"><Mute /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="scope.row.isAvatarInteractionDisabled"
|
||||
placement="left"
|
||||
content="Avatar Interaction Disabled
|
||||
">
|
||||
<el-icon style="color: orange"><Pointer /></el-icon>
|
||||
<el-icon style="color: var(--el-color-warning)"><Pointer /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.isChatBoxMuted" placement="left" content="Chatbox Muted">
|
||||
<el-icon style="color: orange"><ChatLineRound /></el-icon>
|
||||
<el-icon style="color: var(--el-color-warning)"><ChatLineRound /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.timeoutTime" placement="left" content="Timeout">
|
||||
<span style="color: red">🔴{{ scope.row.timeoutTime }}s</span>
|
||||
<span style="color: var(--el-color-danger)">🔴{{ scope.row.timeoutTime }}s</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.ageVerified" placement="left" content="18+ Verified">
|
||||
<i class="ri-id-card-line"></i>
|
||||
@@ -300,13 +311,17 @@
|
||||
<el-table-column :label="t('table.playerList.platform')" prop="inVRMode" width="90">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.ref.$platform">
|
||||
<span v-if="scope.row.ref.$platform === 'standalonewindows'" style="color: #409eff"
|
||||
<span
|
||||
v-if="scope.row.ref.$platform === 'standalonewindows'"
|
||||
style="color: var(--el-color-primary)"
|
||||
><i class="ri-computer-line"></i
|
||||
></span>
|
||||
<span v-else-if="scope.row.ref.$platform === 'android'" style="color: #67c23a"
|
||||
<span
|
||||
v-else-if="scope.row.ref.$platform === 'android'"
|
||||
style="color: var(--el-color-success)"
|
||||
><i class="ri-android-line"></i
|
||||
></span>
|
||||
<span v-else-if="scope.row.ref.$platform === 'ios'" style="color: #c7c7ce"
|
||||
<span v-else-if="scope.row.ref.$platform === 'ios'" style="color: var(--el-color-info)"
|
||||
><i class="ri-apple-line"></i
|
||||
></span>
|
||||
<span v-else>{{ scope.row.ref.$platform }}</span>
|
||||
@@ -324,46 +339,6 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.displayName')"
|
||||
min-width="140"
|
||||
prop="displayName"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.ref.$userColour }"
|
||||
v-text="scope.row.ref.displayName"></span>
|
||||
<span v-else v-text="scope.row.ref.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.status')" min-width="180" prop="ref.status">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.ref.status">
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.ref.status)"
|
||||
style="margin-right: 3px"></i>
|
||||
<span v-text="scope.row.ref.statusDescription"></span>
|
||||
<!--//- el-table-column(label="Group" min-width="180" prop="groupOnNameplate" sortable)-->
|
||||
<!--//- template(v-once #default="scope")-->
|
||||
<!--//- span(v-text="scope.row.groupOnNameplate")-->
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.rank')"
|
||||
width="110"
|
||||
prop="$trustSortNum"
|
||||
:sortable="true">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="name"
|
||||
:class="scope.row.ref.$trustClass"
|
||||
v-text="scope.row.ref.$trustLevel"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.language')" width="100" prop="ref.$languages">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-for="item in scope.row.ref.$languages" :key="item.key" placement="top">
|
||||
@@ -401,7 +376,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.note')" width="150" prop="ref.note">
|
||||
<el-table-column :label="t('table.playerList.note')" width="400" prop="ref.note">
|
||||
<template #default="scope">
|
||||
<span v-text="scope.row.ref.note"></span>
|
||||
</template>
|
||||
@@ -417,7 +392,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ChatLineRound, CircleClose, HomeFilled, Microphone, Mute, Pointer } from '@element-plus/icons-vue';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -428,8 +403,7 @@
|
||||
languageClass,
|
||||
openExternalLink,
|
||||
statusClass,
|
||||
userImage,
|
||||
userImageFull
|
||||
userImage
|
||||
} from '../../shared/utils';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
@@ -437,7 +411,6 @@
|
||||
useInstanceStore,
|
||||
useLocationStore,
|
||||
usePhotonStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
} from '../../stores';
|
||||
@@ -459,6 +432,63 @@
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const playerListRef = ref(null);
|
||||
const tableHeight = ref(0);
|
||||
|
||||
onMounted(() => {
|
||||
if (playerListRef.value) {
|
||||
resizeObserver.observe(playerListRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setPlayerListTableHeight();
|
||||
});
|
||||
|
||||
function setPlayerListTableHeight() {
|
||||
if (currentInstanceWorld.value.ref.id) {
|
||||
tableHeight.value = playerListRef.value.clientHeight - 110;
|
||||
return;
|
||||
}
|
||||
if (currentInstanceUsersTable.value.data.length === 0) {
|
||||
tableHeight.value = playerListRef.value.clientHeight;
|
||||
return;
|
||||
}
|
||||
if (playerListRef.value) {
|
||||
tableHeight.value = playerListRef.value.clientHeight - 110;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentInstanceWorld.value.ref.id,
|
||||
() => {
|
||||
setPlayerListTableHeight();
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
|
||||
const compactCellStyle = () => ({
|
||||
padding: '4px 10px'
|
||||
});
|
||||
|
||||
const compactInstanceUsersTable = computed(() => {
|
||||
const baseTableConfig = currentInstanceUsersTable.value;
|
||||
const tableProps = baseTableConfig.tableProps || {};
|
||||
|
||||
return {
|
||||
...baseTableConfig,
|
||||
tableProps: {
|
||||
...tableProps,
|
||||
cellStyle: compactCellStyle,
|
||||
headerCellStyle: compactCellStyle,
|
||||
height: tableHeight.value
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const chatboxBlacklistDialog = ref({
|
||||
@@ -466,8 +496,6 @@
|
||||
loading: false
|
||||
});
|
||||
|
||||
const currentInstanceWorldDescriptionExpanded = ref(false);
|
||||
|
||||
function showChatboxBlacklistDialog() {
|
||||
const D = chatboxBlacklistDialog.value;
|
||||
D.visible = true;
|
||||
@@ -517,3 +545,26 @@
|
||||
return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.description {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#x-app .current-instance-table .el-table .el-table__cell {
|
||||
padding: 3px 10px !important;
|
||||
}
|
||||
.table-user {
|
||||
color: var(--x-table-user-text-color);
|
||||
}
|
||||
.friends-list-avatar {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,12 +25,7 @@
|
||||
t('view.player_list.photon.chatbox_blacklist')
|
||||
}}</el-button>
|
||||
<el-tooltip placement="bottom" :content="t('view.player_list.photon.status_tooltip')">
|
||||
<div
|
||||
style="
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
">
|
||||
<div style="display: inline-flex; align-items: center; font-size: 14px">
|
||||
<span v-if="ipcEnabled && !photonEventIcon">🟢</span>
|
||||
<span v-else-if="ipcEnabled">⚪</span>
|
||||
<span v-else>🔴</span>
|
||||
@@ -170,11 +165,13 @@
|
||||
</span>
|
||||
<span v-else-if="scope.row.type === 'ChatBoxMessage'" v-text="scope.row.text"></span>
|
||||
<span v-else-if="scope.row.type === 'OnPlayerJoined'">
|
||||
<span v-if="scope.row.platform === 'Desktop'" style="color: #409eff"
|
||||
<span v-if="scope.row.platform === 'Desktop'" style="color: var(--el-color-primary)"
|
||||
>Desktop </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'VR'" style="color: #409eff">VR </span>
|
||||
<span v-else-if="scope.row.platform === 'Quest'" style="color: #67c23a"
|
||||
<span v-else-if="scope.row.platform === 'VR'" style="color: var(--el-color-primary)"
|
||||
>VR </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'Quest'" style="color: var(--el-color-success)"
|
||||
>Android </span
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@click="handleClearSearch"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs ref="searchTabRef" type="card" style="margin-top: 15px" @tab-click="searchText = ''">
|
||||
<el-tabs ref="searchTabRef" style="margin-top: 15px" @tab-click="searchText = ''">
|
||||
<el-tab-pane v-loading="isSearchUserLoading" :label="t('view.search.user.header')" style="min-height: 60px">
|
||||
<el-checkbox v-model="searchUserByBio" style="margin-left: 10px">{{
|
||||
t('view.search.user.search_by_bio')
|
||||
@@ -221,12 +221,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="avatar.authorName"></span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="options-container" style="margin-top: 0; padding: 5px">
|
||||
<span class="header">{{ t('view.settings.header') }}</span>
|
||||
</div>
|
||||
<el-tabs type="card" style="height: calc(100% - 51px)">
|
||||
<el-tabs style="height: calc(100% - 51px)">
|
||||
<el-tab-pane :label="t('view.settings.category.general')">
|
||||
<GeneralTab />
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="simple-switch">
|
||||
<div class="name" :style="{ width: longLabel ? '300px' : undefined }">
|
||||
{{ label }}
|
||||
|
||||
@@ -115,12 +115,19 @@
|
||||
<el-option v-for="size in tablePageSizes" :key="size" :label="String(size)" :value="String(size)" />
|
||||
</el-select>
|
||||
</div>
|
||||
<simple-switch
|
||||
:label="t('view.settings.appearance.appearance.compact_table_mode')"
|
||||
:value="compactTableMode"
|
||||
@change="setCompactTableMode" />
|
||||
<div class="options-container-item">
|
||||
<el-button size="small" :icon="Notebook" style="margin-right: 10px" @click="promptMaxTableSizeDialog">{{
|
||||
t('view.settings.appearance.appearance.table_max_size')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<ThemePicker />
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.appearance.timedate.header') }}</span>
|
||||
<div class="options-container-item">
|
||||
@@ -387,6 +394,7 @@
|
||||
import { getLanguageName, languageCodes } from '../../../../localization';
|
||||
|
||||
import SimpleSwitch from '../SimpleSwitch.vue';
|
||||
import ThemePicker from '../ThemePicker.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -415,7 +423,8 @@
|
||||
randomUserColours,
|
||||
trustColor,
|
||||
notificationIconDot,
|
||||
tablePageSizes
|
||||
tablePageSizes,
|
||||
compactTableMode
|
||||
} = storeToRefs(appearanceSettingsStore);
|
||||
|
||||
const { saveSortFavoritesOption } = useFavoriteStore();
|
||||
@@ -441,7 +450,8 @@
|
||||
changeAppLanguage,
|
||||
promptMaxTableSizeDialog,
|
||||
setNotificationIconDot,
|
||||
setTablePageSizes
|
||||
setTablePageSizes,
|
||||
setCompactTableMode
|
||||
} = appearanceSettingsStore;
|
||||
|
||||
const zoomLevel = ref(100);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="theme-picker">
|
||||
<div class="theme-picker__header">
|
||||
<div>
|
||||
<span class="header">{{ t('view.settings.appearance.theme_color.header') }}</span>
|
||||
</div>
|
||||
<div class="theme-picker__current ml-25">
|
||||
<span class="theme-picker__chip" :style="{ backgroundColor: currentPrimary }"></span>
|
||||
<button type="button" class="theme-picker__toggle" @click="isOpen = !isOpen">
|
||||
{{ isOpen ? 'Collapse' : 'Expand' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isOpen" class="theme-picker__panel">
|
||||
<div class="theme-picker__grid">
|
||||
<button
|
||||
v-for="color in colorFamilies"
|
||||
:key="color.name"
|
||||
type="button"
|
||||
class="theme-picker__item"
|
||||
:class="{ 'is-active': color.base === currentPrimary }"
|
||||
:disabled="isApplying"
|
||||
@click="selectColor(color)">
|
||||
<span class="theme-picker__swatch" :style="{ backgroundColor: color.base }"></span>
|
||||
<span class="theme-picker__badge">{{ color.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import { useElementTheme } from '../../../composables/useElementTheme';
|
||||
|
||||
// Tailwind indigo-500
|
||||
const defaultPrimary = 'oklch(58.5% 0.233 277.117)';
|
||||
const { currentPrimary, isApplying, applyPrimaryColor, initPrimaryColor } = useElementTheme(defaultPrimary);
|
||||
const { t } = useI18n();
|
||||
|
||||
const invalidKeys = new Set([
|
||||
'inherit',
|
||||
'current',
|
||||
'transparent',
|
||||
'black',
|
||||
'white',
|
||||
'lightBlue',
|
||||
'warmGray',
|
||||
'trueGray',
|
||||
'coolGray',
|
||||
'blueGray'
|
||||
]);
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const colorFamilies = computed(() =>
|
||||
Object.entries(colors)
|
||||
.filter(([name, palette]) => {
|
||||
return !invalidKeys.has(name) && palette && typeof palette === 'object' && palette['500'];
|
||||
})
|
||||
.map(([name, palette]) => {
|
||||
const base = palette['500'];
|
||||
const light = palette['300'];
|
||||
const vivid = palette['600'];
|
||||
const dark = palette['700'];
|
||||
return {
|
||||
name,
|
||||
base,
|
||||
light,
|
||||
vivid,
|
||||
dark,
|
||||
palette
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
const selectColor = async (color) => {
|
||||
await applyPrimaryColor(color.base, color.palette);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await initPrimaryColor(defaultPrimary);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.theme-picker {
|
||||
padding: 6px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.theme-picker__header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.theme-picker__current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.theme-picker__toggle {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.theme-picker__toggle:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.theme-picker__chip {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-zinc-100);
|
||||
}
|
||||
|
||||
.theme-picker__panel {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.theme-picker__grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
max-height: 360px;
|
||||
gap: 10px 18px;
|
||||
}
|
||||
|
||||
.theme-picker__item {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
width: calc(50% - 9px);
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-bg-color);
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.theme-picker__item:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-picker__item.is-active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-picker__item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.theme-picker__swatch {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-zinc-100);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.theme-picker__badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
text-transform: capitalize;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.theme-picker__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.theme-picker__current {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
remote
|
||||
:remote-method="quickSearchRemoteMethod"
|
||||
popper-class="x-quick-search"
|
||||
style="flex: 1; padding: 10px"
|
||||
style="flex: 1; padding: 10px; padding-left: 0"
|
||||
@change="quickSearchChange">
|
||||
<el-option v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label">
|
||||
<div class="x-friend-item">
|
||||
@@ -41,7 +41,7 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div>
|
||||
<NativeTooltip placement="bottom" :content="t('side_panel.refresh_tooltip')">
|
||||
<el-tooltip placement="bottom" :content="t('side_panel.refresh_tooltip')">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="isRefreshFriendsLoading"
|
||||
@@ -50,16 +50,14 @@
|
||||
circle
|
||||
style="margin-right: 10px"
|
||||
@click="refreshFriendsList"></el-button>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 60px); margin-top: 5px">
|
||||
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 70px); margin-top: 5px">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span>{{ t('side_panel.friends') }}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">
|
||||
({{ onlineFriendCount }}/{{ friends.size }})
|
||||
</span>
|
||||
<span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span>
|
||||
</template>
|
||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
@@ -67,9 +65,7 @@
|
||||
<el-tab-pane lazy>
|
||||
<template #label>
|
||||
<span>{{ t('side_panel.groups') }}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">
|
||||
({{ groupInstances.length }})
|
||||
</span>
|
||||
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
||||
</template>
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
</el-tab-pane>
|
||||
@@ -97,11 +93,17 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-tab-count {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.group-calendar-button {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
border: none;
|
||||
z-index: 5;
|
||||
width: 40px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:class="isFriendActiveOrOffline ? undefined : userStatusClass(friend.ref, friend.pendingOffline)">
|
||||
<img :src="userImage(friend.ref, true)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div class="detail h-9 flex flex-col justify-between">
|
||||
<span v-if="!hideNicknames && friend.$nickName" class="name" :style="{ color: friend.ref.$userColour }">
|
||||
{{ friend.ref.displayName }} ({{ friend.$nickName }})
|
||||
</span>
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
<span v-if="isFriendActiveOrOffline" class="extra">{{ friend.ref.statusDescription }}</span>
|
||||
<template v-else>
|
||||
<span v-if="friend.pendingOffline" class="extra">
|
||||
<div v-if="friend.pendingOffline" class="extra">
|
||||
<el-icon><WarningFilled /></el-icon> {{ t('side_panel.pending_offline') }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-else-if="isGroupByInstance">
|
||||
<el-icon v-if="isFriendTraveling" class="is-loading" style="margin-right: 3px"
|
||||
><Loading
|
||||
@@ -87,7 +87,7 @@
|
||||
const travelingProp = computed(() => props.friend.ref?.travelingToLocation || '');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.skeleton {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
@@ -104,11 +104,11 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
:deep(.el-skeleton__circle) {
|
||||
.el-skeleton__circle {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
:deep(.el-skeleton__text) {
|
||||
.el-skeleton__text {
|
||||
&:first-child {
|
||||
height: 14px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="x-dialog"
|
||||
:model-value="galleryDialogVisible"
|
||||
:title="t('dialog.gallery_icons.header')"
|
||||
width="97vw"
|
||||
append-to-body
|
||||
@close="closeGalleryDialog">
|
||||
<div class="gallery-page x-container">
|
||||
<div class="gallery-page__header">
|
||||
<el-button text size="small" :icon="ArrowLeft" class="gallery-page__back" @click="goBack">
|
||||
{{ t('nav_tooltip.tools') }}
|
||||
</el-button>
|
||||
<span class="header">{{ t('dialog.gallery_icons.header') }}</span>
|
||||
</div>
|
||||
<el-progress
|
||||
v-if="isUploading"
|
||||
:show-text="false"
|
||||
@@ -13,14 +13,12 @@
|
||||
:percentage="100"
|
||||
:stroke-width="3"
|
||||
style="margin-bottom: 12px" />
|
||||
<el-tabs type="card" ref="galleryTabs">
|
||||
<el-tabs>
|
||||
<el-tab-pane v-loading="galleryDialogGalleryLoading">
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.gallery') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
{{ galleryTable.length }}/64
|
||||
</span>
|
||||
<span class="gallery-tab-count"> {{ galleryTable.length }}/64 </span>
|
||||
</span>
|
||||
</template>
|
||||
<input
|
||||
@@ -92,9 +90,7 @@
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.icons') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
{{ VRCPlusIconsTable.length }}/64
|
||||
</span>
|
||||
<span class="gallery-tab-count"> {{ VRCPlusIconsTable.length }}/64 </span>
|
||||
</span>
|
||||
</template>
|
||||
<input
|
||||
@@ -166,7 +162,7 @@
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.emojis') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
<span class="gallery-tab-count">
|
||||
{{ emojiTable.length }}/{{ cachedConfig?.maxUserEmoji }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -308,7 +304,7 @@
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.stickers') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
<span class="gallery-tab-count">
|
||||
{{ stickerTable.length }}/{{ cachedConfig?.maxUserStickers }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -374,9 +370,7 @@
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.prints') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
{{ printTable.length }}/64
|
||||
</span>
|
||||
<span class="gallery-tab-count"> {{ printTable.length }}/64 </span>
|
||||
</span>
|
||||
</template>
|
||||
<input
|
||||
@@ -438,16 +432,12 @@
|
||||
style="display: block" />
|
||||
<span v-else style="display: block"> </span>
|
||||
<DisplayName
|
||||
class="x-ellipsis"
|
||||
class="x-ellipsis gallery-meta"
|
||||
v-if="image.authorId"
|
||||
:userid="image.authorId"
|
||||
:hint="image.authorName"
|
||||
style="color: #909399; font-family: monospace; display: block" />
|
||||
<span v-else style="font-family: monospace; display: block"> </span>
|
||||
<span
|
||||
class="x-ellipsis"
|
||||
v-if="image.createdAt"
|
||||
style="color: #909399; font-family: monospace; font-size: 11px; display: block">
|
||||
:hint="image.authorName" />
|
||||
<span v-else class="gallery-meta"> </span>
|
||||
<span v-if="image.createdAt" class="x-ellipsis gallery-meta gallery-meta--small">
|
||||
{{ formatDateFilter(image.createdAt, 'long') }}
|
||||
</span>
|
||||
<span v-else style="display: block"> </span>
|
||||
@@ -474,7 +464,7 @@
|
||||
<template #label>
|
||||
<span>
|
||||
{{ t('dialog.gallery_icons.inventory') }}
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
<span class="gallery-tab-count">
|
||||
{{ inventoryTable.length }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -508,9 +498,7 @@
|
||||
v-text="item.description"
|
||||
style="display: block"></span>
|
||||
<span v-else style="display: block"> </span>
|
||||
<span
|
||||
class="x-ellipsis"
|
||||
style="color: #909399; font-family: monospace; font-size: 11px; display: block">
|
||||
<span class="x-ellipsis gallery-meta gallery-meta--small">
|
||||
{{ formatDateFilter(item.created_at, 'long') }}
|
||||
</span>
|
||||
<span v-if="item.itemType === 'prop'">{{ t('dialog.gallery_icons.item') }}</span>
|
||||
@@ -533,15 +521,16 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Close, Delete, Link, Picture, Plus, Present, Refresh, Upload } from '@element-plus/icons-vue';
|
||||
import { ArrowLeft, Close, Delete, Link, Picture, Plus, Present, Refresh, Upload } from '@element-plus/icons-vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
extractFileId,
|
||||
@@ -549,16 +538,17 @@
|
||||
getEmojiFileName,
|
||||
getPrintFileName,
|
||||
openExternalLink
|
||||
} from '../../../shared/utils';
|
||||
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../../api';
|
||||
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useUserStore } from '../../../stores';
|
||||
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../../shared/constants';
|
||||
import { AppDebug } from '../../../service/appConfig';
|
||||
import { handleImageUploadInput } from '../../../shared/utils/imageUpload';
|
||||
} from '../../shared/utils';
|
||||
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
|
||||
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useUserStore } from '../../stores';
|
||||
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
|
||||
import { AppDebug } from '../../service/appConfig';
|
||||
import { handleImageUploadInput } from '../../shared/utils/imageUpload';
|
||||
|
||||
import Emoji from '../../../components/Emoji.vue';
|
||||
import Emoji from '../../components/Emoji.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
galleryTable,
|
||||
@@ -578,6 +568,7 @@
|
||||
inventoryTable
|
||||
} = storeToRefs(useGalleryStore());
|
||||
const {
|
||||
loadGalleryData,
|
||||
refreshGalleryTable,
|
||||
refreshVRCPlusIconsTable,
|
||||
refreshStickerTable,
|
||||
@@ -602,6 +593,15 @@
|
||||
const pendingUploads = ref(0);
|
||||
const isUploading = computed(() => pendingUploads.value > 0);
|
||||
|
||||
onMounted(() => {
|
||||
galleryDialogVisible.value = true;
|
||||
loadGalleryData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
galleryDialogVisible.value = false;
|
||||
});
|
||||
|
||||
function startUpload() {
|
||||
pendingUploads.value += 1;
|
||||
}
|
||||
@@ -610,8 +610,9 @@
|
||||
pendingUploads.value = Math.max(0, pendingUploads.value - 1);
|
||||
}
|
||||
|
||||
function closeGalleryDialog() {
|
||||
function goBack() {
|
||||
galleryDialogVisible.value = false;
|
||||
router.push({ name: 'tools' });
|
||||
}
|
||||
|
||||
function onFileChangeGallery(e) {
|
||||
@@ -1116,3 +1117,32 @@
|
||||
.catch(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gallery-page__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.gallery-tab-count {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.gallery-meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-family: monospace;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gallery-meta--small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.gallery-page__back {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card :body-style="{ padding: '0px' }" class="tool-card">
|
||||
<div class="tool-content" @click="showGalleryDialog">
|
||||
<div class="tool-content" @click="showGalleryPage">
|
||||
<div class="tool-icon">
|
||||
<i class="ri-multi-image-line"></i>
|
||||
</div>
|
||||
@@ -191,7 +191,6 @@
|
||||
<NoteExportDialog
|
||||
:isNoteExportDialogVisible="isNoteExportDialogVisible"
|
||||
@close="isNoteExportDialogVisible = false" />
|
||||
<GalleryDialog />
|
||||
<ExportDiscordNamesDialog
|
||||
v-model:discordNamesDialogVisible="isExportDiscordNamesDialogVisible"
|
||||
:friends="friends" />
|
||||
@@ -207,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -219,22 +218,21 @@
|
||||
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
|
||||
const ScreenshotMetadataDialog = defineAsyncComponent(() => import('./dialogs/ScreenshotMetadataDialog.vue'));
|
||||
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
|
||||
const GalleryDialog = defineAsyncComponent(() => import('./dialogs/GalleryDialog.vue'));
|
||||
const EditInviteMessageDialog = defineAsyncComponent(() => import('./dialogs/EditInviteMessagesDialog.vue'));
|
||||
|
||||
const ExportDiscordNamesDialog = defineAsyncComponent(() => import('./dialogs/ExportDiscordNamesDialog.vue'));
|
||||
const ExportFriendsListDialog = defineAsyncComponent(() => import('./dialogs/ExportFriendsListDialog.vue'));
|
||||
const ExportAvatarsListDialog = defineAsyncComponent(() => import('./dialogs/ExportAvatarsListDialog.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { showGalleryDialog } = useGalleryStore();
|
||||
const { showGalleryPage } = useGalleryStore();
|
||||
const { friends } = storeToRefs(useFriendStore());
|
||||
|
||||
const categoryCollapsed = ref({
|
||||
group: false,
|
||||
image: false,
|
||||
user: false
|
||||
user: false,
|
||||
other: false
|
||||
});
|
||||
|
||||
const isGroupCalendarDialogVisible = ref(false);
|
||||
@@ -313,7 +311,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.tool-categories {
|
||||
margin-top: 20px;
|
||||
padding: 0 20px;
|
||||
@@ -329,7 +327,6 @@
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
@@ -347,7 +344,6 @@
|
||||
margin-left: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,7 +364,7 @@
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
@@ -389,7 +385,7 @@
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 12px;
|
||||
margin-right: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.event-card {
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
@@ -196,7 +196,7 @@
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
&.grouped-card {
|
||||
margin-bottom: 0;
|
||||
@@ -232,17 +232,17 @@
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-text-color-regular);
|
||||
color: #ffffff;
|
||||
color: var(--el-bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
.is-following {
|
||||
background-color: var(--group-calendar-badge-following, #67c23a);
|
||||
background-color: var(--group-calendar-badge-following, var(--el-color-success));
|
||||
}
|
||||
.event-content {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px">
|
||||
<el-tabs type="card">
|
||||
<el-tabs>
|
||||
<el-tab-pane :label="t('dialog.export_friends_list.csv')">
|
||||
<el-input
|
||||
v-model="exportFriendsListCsv"
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.x-dialog {
|
||||
:deep(.el-dialog) {
|
||||
max-height: 750px;
|
||||
@@ -519,7 +519,10 @@
|
||||
position: relative;
|
||||
|
||||
&.has-events {
|
||||
background-color: var(--group-calendar-event-bg, rgba(25, 102, 154, 0.05));
|
||||
background-color: var(
|
||||
--group-calendar-event-bg,
|
||||
color-mix(in oklch, var(--el-color-primary) 10%, transparent)
|
||||
);
|
||||
}
|
||||
.calendar-event-badge {
|
||||
position: absolute;
|
||||
@@ -528,21 +531,21 @@
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
color: var(--el-color-white, #fff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
z-index: 10;
|
||||
padding: 0 4px;
|
||||
line-height: 16px;
|
||||
&.has-following {
|
||||
background-color: var(--group-calendar-badge-following, #67c23a);
|
||||
background-color: var(--group-calendar-badge-following, var(--el-color-success));
|
||||
}
|
||||
&.no-following {
|
||||
background-color: var(--group-calendar-badge-normal, #409eff);
|
||||
background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,7 +609,7 @@
|
||||
flex-direction: column;
|
||||
.search-container {
|
||||
padding: 2px 20px 12px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.search-input {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@dragover.prevent
|
||||
@dragenter.prevent
|
||||
@drop="handleDrop">
|
||||
<span style="margin-left: 5px; color: #909399; font-family: monospace">{{
|
||||
<span style="margin-left: 5px; color: var(--el-text-color-secondary); font-family: monospace">{{
|
||||
t('dialog.screenshot_metadata.drag')
|
||||
}}</span>
|
||||
<br />
|
||||
@@ -105,7 +105,7 @@
|
||||
v-if="screenshotMetadataDialog.metadata.author"
|
||||
:userid="screenshotMetadataDialog.metadata.author.id"
|
||||
:hint="screenshotMetadataDialog.metadata.author.displayName"
|
||||
style="color: #909399; font-family: monospace" />
|
||||
style="color: var(--el-text-color-secondary); font-family: monospace" />
|
||||
<br />
|
||||
<el-carousel
|
||||
ref="screenshotMetadataCarouselRef"
|
||||
@@ -147,7 +147,7 @@
|
||||
<span class="x-link" @click="lookupUser(user)" v-text="user.displayName"></span>
|
||||
<span
|
||||
v-if="user.pos"
|
||||
style="margin-left: 5px; color: #909399; font-family: monospace"
|
||||
style="margin-left: 5px; color: var(--el-text-color-secondary); font-family: monospace"
|
||||
v-text="'(' + user.pos.x + ', ' + user.pos.y + ', ' + user.pos.z + ')'"></span>
|
||||
<br />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user