UI Refresh

This commit is contained in:
pa
2026-01-02 22:24:28 +09:00
committed by Natsumi
parent b02d287190
commit 00745b54f1
120 changed files with 3931 additions and 2015 deletions
+2 -5
View File
@@ -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;
+22 -3
View File
@@ -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;
+1
View File
@@ -1580,6 +1580,7 @@
justify-content: space-between;
font-weight: 600;
font-size: 14px;
margin-bottom: 9px;
}
.group-section__list {
+1
View File
@@ -796,6 +796,7 @@
justify-content: space-between;
font-weight: 600;
font-size: 14px;
margin-bottom: 9px;
}
.group-section__list {
+1
View File
@@ -1276,6 +1276,7 @@
justify-content: space-between;
font-weight: 600;
font-size: 14px;
margin-bottom: 9px;
}
.group-section__list {
+32 -32
View File
@@ -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
View File
@@ -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>
+23 -21
View File
@@ -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>&nbsp;
</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>
+17 -17
View File
@@ -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 || '&nbsp;' }}
</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 {
+55 -40
View File
@@ -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>
+15 -2
View File
@@ -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) => {
+12 -6
View File
@@ -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', '[]')
+115 -67
View File
@@ -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>
+145 -94
View File
@@ -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&nbsp;</span
>
<span v-else-if="scope.row.platform === 'VR'" style="color: #409eff">VR&nbsp;</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&nbsp;</span
>
<span v-else-if="scope.row.platform === 'Quest'" style="color: var(--el-color-success)"
>Android&nbsp;</span
>
<span
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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>
+13 -11
View File
@@ -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 -6
View File
@@ -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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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>
+8 -12
View File
@@ -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>