mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
replace el-splitter
This commit is contained in:
@@ -111,11 +111,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-online">
|
<div class="status-online">
|
||||||
<el-statistic
|
<div class="text-center">
|
||||||
:title="t('view.charts.instance_activity.online_time')"
|
<div class="text-sm text-muted-foreground">
|
||||||
:formatter="(val) => timeToText(val, true)"
|
{{ t('view.charts.instance_activity.online_time') }}
|
||||||
:value="totalOnlineTime">
|
</div>
|
||||||
</el-statistic>
|
<div class="text-2xl font-semibold">
|
||||||
|
{{ timeToText(totalOnlineTime, true) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="activityChartRef" style="width: 100%"></div>
|
<div ref="activityChartRef" style="width: 100%"></div>
|
||||||
@@ -713,9 +716,6 @@
|
|||||||
.status-online {
|
.status-online {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
:deep(.el-statistic__head) {
|
text-align: center;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -72,8 +72,19 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-splitter class="favorites-splitter" @resize-end="handleAvatarSplitterResize">
|
<ResizablePanelGroup
|
||||||
<el-splitter-panel :size="avatarSplitterSize" :min="0" :max="360" collapsible>
|
ref="avatarSplitterGroupRef"
|
||||||
|
direction="horizontal"
|
||||||
|
class="favorites-splitter"
|
||||||
|
@layout="handleAvatarSplitterLayout">
|
||||||
|
<ResizablePanel
|
||||||
|
ref="avatarSplitterPanelRef"
|
||||||
|
:default-size="avatarSplitterDefaultSize"
|
||||||
|
:min-size="avatarSplitterMinSize"
|
||||||
|
:max-size="avatarSplitterMaxSize"
|
||||||
|
:collapsed-size="0"
|
||||||
|
collapsible
|
||||||
|
:order="1">
|
||||||
<div class="favorites-groups-panel">
|
<div class="favorites-groups-panel">
|
||||||
<div class="group-section">
|
<div class="group-section">
|
||||||
<div class="group-section__header">
|
<div class="group-section__header">
|
||||||
@@ -329,8 +340,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
<el-splitter-panel>
|
<ResizableHandle with-handle @dragging="setAvatarSplitterDragging" />
|
||||||
|
<ResizablePanel :order="2">
|
||||||
<div class="favorites-content">
|
<div class="favorites-content">
|
||||||
<div class="favorites-content__header">
|
<div class="favorites-content__header">
|
||||||
<div class="favorites-content__title">
|
<div class="favorites-content__title">
|
||||||
@@ -502,8 +514,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
</el-splitter>
|
</ResizablePanelGroup>
|
||||||
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
|
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -513,8 +525,8 @@
|
|||||||
import { ElMessageBox, ElNotification, ElProgress } from 'element-plus';
|
import { ElMessageBox, ElNotification, ElProgress } from 'element-plus';
|
||||||
import { MoreFilled, Plus, Refresh } from '@element-plus/icons-vue';
|
import { MoreFilled, Plus, Refresh } from '@element-plus/icons-vue';
|
||||||
import { Ellipsis, RefreshCcw } from 'lucide-vue-next';
|
import { Ellipsis, RefreshCcw } from 'lucide-vue-next';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader } from 'lucide-vue-next';
|
import { Loader } from 'lucide-vue-next';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -538,6 +550,7 @@
|
|||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../stores';
|
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../stores';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
||||||
import { avatarRequest, favoriteRequest } from '../../api';
|
import { avatarRequest, favoriteRequest } from '../../api';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Slider } from '../../components/ui/slider';
|
import { Slider } from '../../components/ui/slider';
|
||||||
@@ -563,6 +576,12 @@
|
|||||||
const avatarGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
const avatarGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
||||||
const historyGroupKey = 'local-history';
|
const historyGroupKey = 'local-history';
|
||||||
const avatarSplitterSize = ref(260);
|
const avatarSplitterSize = ref(260);
|
||||||
|
const avatarSplitterFallbackWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1200;
|
||||||
|
const avatarSplitterGroupRef = ref(null);
|
||||||
|
const avatarSplitterPanelRef = ref(null);
|
||||||
|
const avatarSplitterWidth = ref(avatarSplitterFallbackWidth);
|
||||||
|
const avatarSplitterDraggingCount = ref(0);
|
||||||
|
let avatarSplitterObserver = null;
|
||||||
|
|
||||||
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
||||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
const { setSortFavorites } = useAppearanceSettingsStore();
|
||||||
@@ -698,22 +717,108 @@
|
|||||||
|
|
||||||
async function loadAvatarSplitterPreferences() {
|
async function loadAvatarSplitterPreferences() {
|
||||||
const storedSize = await configRepository.getString('VRCX_FavoritesAvatarSplitter', '260');
|
const storedSize = await configRepository.getString('VRCX_FavoritesAvatarSplitter', '260');
|
||||||
if (typeof storedSize === 'string' && !Number.isNaN(Number(storedSize)) && Number(storedSize) > 0) {
|
const parsedSize = Number(storedSize);
|
||||||
avatarSplitterSize.value = Number(storedSize);
|
if (Number.isFinite(parsedSize) && parsedSize >= 0) {
|
||||||
|
avatarSplitterSize.value = parsedSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAvatarSplitterResize(panelIndex, sizes) {
|
const getAvatarSplitterWidthRaw = () => {
|
||||||
|
const element = avatarSplitterGroupRef.value?.$el ?? avatarSplitterGroupRef.value;
|
||||||
|
const width = element?.getBoundingClientRect?.().width;
|
||||||
|
return Number.isFinite(width) ? width : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvatarSplitterWidth = () => {
|
||||||
|
const width = getAvatarSplitterWidthRaw();
|
||||||
|
return Number.isFinite(width) && width > 0 ? width : avatarSplitterFallbackWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveDraggingPayload = (payload) => {
|
||||||
|
if (typeof payload === 'boolean') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
if (typeof payload.detail === 'boolean') {
|
||||||
|
return payload.detail;
|
||||||
|
}
|
||||||
|
if (typeof payload.dragging === 'boolean') {
|
||||||
|
return payload.dragging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Boolean(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAvatarSplitterDragging = (payload) => {
|
||||||
|
const isDragging = resolveDraggingPayload(payload);
|
||||||
|
const next = avatarSplitterDraggingCount.value + (isDragging ? 1 : -1);
|
||||||
|
avatarSplitterDraggingCount.value = Math.max(0, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pxToPercent = (px, groupWidth, min = 0) => {
|
||||||
|
const width = groupWidth ?? getAvatarSplitterWidth();
|
||||||
|
return Math.min(100, Math.max(min, (px / width) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
||||||
|
|
||||||
|
const avatarSplitterDefaultSize = computed(() =>
|
||||||
|
pxToPercent(avatarSplitterSize.value, avatarSplitterWidth.value, 0)
|
||||||
|
);
|
||||||
|
const avatarSplitterMinSize = computed(() => pxToPercent(0, avatarSplitterWidth.value, 0));
|
||||||
|
const avatarSplitterMaxSize = computed(() => pxToPercent(360, avatarSplitterWidth.value, 0));
|
||||||
|
|
||||||
|
const handleAvatarSplitterLayout = (sizes) => {
|
||||||
if (!Array.isArray(sizes) || !sizes.length) {
|
if (!Array.isArray(sizes) || !sizes.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextSize = sizes[0];
|
|
||||||
if (nextSize <= 0) {
|
if (avatarSplitterDraggingCount.value === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
avatarSplitterSize.value = nextSize;
|
|
||||||
configRepository.setString('VRCX_FavoritesAvatarSplitter', nextSize.toString());
|
const rawWidth = getAvatarSplitterWidthRaw();
|
||||||
}
|
if (!Number.isFinite(rawWidth) || rawWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSize = sizes[0];
|
||||||
|
if (!Number.isFinite(nextSize)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPx = Math.round(percentToPx(nextSize, rawWidth));
|
||||||
|
const clampedPx = Math.min(360, Math.max(0, nextPx));
|
||||||
|
avatarSplitterSize.value = clampedPx;
|
||||||
|
configRepository.setString('VRCX_FavoritesAvatarSplitter', clampedPx.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAvatarSplitterWidth = () => {
|
||||||
|
const width = getAvatarSplitterWidth();
|
||||||
|
avatarSplitterWidth.value = width;
|
||||||
|
const targetSize = pxToPercent(avatarSplitterSize.value, width, 0);
|
||||||
|
avatarSplitterPanelRef.value?.resize?.(targetSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
updateAvatarSplitterWidth();
|
||||||
|
const element = avatarSplitterGroupRef.value?.$el ?? avatarSplitterGroupRef.value;
|
||||||
|
if (element && typeof ResizeObserver !== 'undefined') {
|
||||||
|
avatarSplitterObserver = new ResizeObserver(updateAvatarSplitterWidth);
|
||||||
|
avatarSplitterObserver.observe(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(avatarSplitterSize, (value, previous) => {
|
||||||
|
if (value === previous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (avatarSplitterDraggingCount.value > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateAvatarSplitterWidth();
|
||||||
|
});
|
||||||
|
|
||||||
const groupedAvatarFavorites = computed(() => {
|
const groupedAvatarFavorites = computed(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
@@ -1536,6 +1641,10 @@
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.removeEventListener('resize', maybeFillLocalAvatarViewport);
|
window.removeEventListener('resize', maybeFillLocalAvatarViewport);
|
||||||
}
|
}
|
||||||
|
if (avatarSplitterObserver) {
|
||||||
|
avatarSplitterObserver.disconnect();
|
||||||
|
avatarSplitterObserver = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatVisibility(value) {
|
function formatVisibility(value) {
|
||||||
|
|||||||
@@ -72,8 +72,19 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-splitter class="favorites-splitter" @resize-end="handleFriendSplitterResize">
|
<ResizablePanelGroup
|
||||||
<el-splitter-panel :size="friendSplitterSize" :min="0" :max="360" collapsible>
|
ref="friendSplitterGroupRef"
|
||||||
|
direction="horizontal"
|
||||||
|
class="favorites-splitter"
|
||||||
|
@layout="handleFriendSplitterLayout">
|
||||||
|
<ResizablePanel
|
||||||
|
ref="friendSplitterPanelRef"
|
||||||
|
:default-size="friendSplitterDefaultSize"
|
||||||
|
:min-size="friendSplitterMinSize"
|
||||||
|
:max-size="friendSplitterMaxSize"
|
||||||
|
:collapsed-size="0"
|
||||||
|
collapsible
|
||||||
|
:order="1">
|
||||||
<div class="favorites-groups-panel">
|
<div class="favorites-groups-panel">
|
||||||
<div class="group-section">
|
<div class="group-section">
|
||||||
<div class="group-section__header">
|
<div class="group-section__header">
|
||||||
@@ -174,8 +185,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
<el-splitter-panel>
|
<ResizableHandle with-handle @dragging="setFriendSplitterDragging" />
|
||||||
|
<ResizablePanel :order="2">
|
||||||
<div class="favorites-content">
|
<div class="favorites-content">
|
||||||
<div class="favorites-content__header">
|
<div class="favorites-content__header">
|
||||||
<div class="favorites-content__title">
|
<div class="favorites-content__title">
|
||||||
@@ -286,19 +298,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
</el-splitter>
|
</ResizablePanelGroup>
|
||||||
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
|
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import { MoreFilled, Refresh } from '@element-plus/icons-vue';
|
import { MoreFilled, Refresh } from '@element-plus/icons-vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
import { Ellipsis } from 'lucide-vue-next';
|
import { Ellipsis } from 'lucide-vue-next';
|
||||||
|
import { InputGroupSearch } from '@/components/ui/input-group';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
@@ -320,6 +332,7 @@
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
||||||
import { useAppearanceSettingsStore, useFavoriteStore, useUserStore } from '../../stores';
|
import { useAppearanceSettingsStore, useFavoriteStore, useUserStore } from '../../stores';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Slider } from '../../components/ui/slider';
|
import { Slider } from '../../components/ui/slider';
|
||||||
@@ -335,6 +348,12 @@
|
|||||||
const friendGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
const friendGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
||||||
|
|
||||||
const friendSplitterSize = ref(260);
|
const friendSplitterSize = ref(260);
|
||||||
|
const friendSplitterFallbackWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1200;
|
||||||
|
const friendSplitterGroupRef = ref(null);
|
||||||
|
const friendSplitterPanelRef = ref(null);
|
||||||
|
const friendSplitterWidth = ref(friendSplitterFallbackWidth);
|
||||||
|
const friendSplitterDraggingCount = ref(0);
|
||||||
|
let friendSplitterObserver = null;
|
||||||
|
|
||||||
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
||||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
const { setSortFavorites } = useAppearanceSettingsStore();
|
||||||
@@ -437,22 +456,115 @@
|
|||||||
|
|
||||||
async function loadFriendSplitterPreferences() {
|
async function loadFriendSplitterPreferences() {
|
||||||
const storedSize = await configRepository.getString('VRCX_FavoritesFriendSplitter', '260');
|
const storedSize = await configRepository.getString('VRCX_FavoritesFriendSplitter', '260');
|
||||||
if (typeof storedSize === 'string' && !Number.isNaN(Number(storedSize)) && Number(storedSize) > 0) {
|
const parsedSize = Number(storedSize);
|
||||||
friendSplitterSize.value = Number(storedSize);
|
if (Number.isFinite(parsedSize) && parsedSize >= 0) {
|
||||||
|
friendSplitterSize.value = parsedSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFriendSplitterResize(panelIndex, sizes) {
|
const getFriendSplitterWidthRaw = () => {
|
||||||
|
const element = friendSplitterGroupRef.value?.$el ?? friendSplitterGroupRef.value;
|
||||||
|
const width = element?.getBoundingClientRect?.().width;
|
||||||
|
return Number.isFinite(width) ? width : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFriendSplitterWidth = () => {
|
||||||
|
const width = getFriendSplitterWidthRaw();
|
||||||
|
return Number.isFinite(width) && width > 0 ? width : friendSplitterFallbackWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveDraggingPayload = (payload) => {
|
||||||
|
if (typeof payload === 'boolean') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
if (typeof payload.detail === 'boolean') {
|
||||||
|
return payload.detail;
|
||||||
|
}
|
||||||
|
if (typeof payload.dragging === 'boolean') {
|
||||||
|
return payload.dragging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Boolean(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFriendSplitterDragging = (payload) => {
|
||||||
|
const isDragging = resolveDraggingPayload(payload);
|
||||||
|
const next = friendSplitterDraggingCount.value + (isDragging ? 1 : -1);
|
||||||
|
friendSplitterDraggingCount.value = Math.max(0, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pxToPercent = (px, groupWidth, min = 0) => {
|
||||||
|
const width = groupWidth ?? getFriendSplitterWidth();
|
||||||
|
return Math.min(100, Math.max(min, (px / width) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
||||||
|
|
||||||
|
const friendSplitterDefaultSize = computed(() =>
|
||||||
|
pxToPercent(friendSplitterSize.value, friendSplitterWidth.value, 0)
|
||||||
|
);
|
||||||
|
const friendSplitterMinSize = computed(() => pxToPercent(0, friendSplitterWidth.value, 0));
|
||||||
|
const friendSplitterMaxSize = computed(() => pxToPercent(360, friendSplitterWidth.value, 0));
|
||||||
|
|
||||||
|
const handleFriendSplitterLayout = (sizes) => {
|
||||||
if (!Array.isArray(sizes) || !sizes.length) {
|
if (!Array.isArray(sizes) || !sizes.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextSize = sizes[0];
|
|
||||||
if (nextSize <= 0) {
|
if (friendSplitterDraggingCount.value === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
friendSplitterSize.value = nextSize;
|
|
||||||
configRepository.setString('VRCX_FavoritesFriendSplitter', nextSize.toString());
|
const rawWidth = getFriendSplitterWidthRaw();
|
||||||
}
|
if (!Number.isFinite(rawWidth) || rawWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSize = sizes[0];
|
||||||
|
if (!Number.isFinite(nextSize)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPx = Math.round(percentToPx(nextSize, rawWidth));
|
||||||
|
const clampedPx = Math.min(360, Math.max(0, nextPx));
|
||||||
|
friendSplitterSize.value = clampedPx;
|
||||||
|
configRepository.setString('VRCX_FavoritesFriendSplitter', clampedPx.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFriendSplitterWidth = () => {
|
||||||
|
const width = getFriendSplitterWidth();
|
||||||
|
friendSplitterWidth.value = width;
|
||||||
|
const targetSize = pxToPercent(friendSplitterSize.value, width, 0);
|
||||||
|
friendSplitterPanelRef.value?.resize?.(targetSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
updateFriendSplitterWidth();
|
||||||
|
const element = friendSplitterGroupRef.value?.$el ?? friendSplitterGroupRef.value;
|
||||||
|
if (element && typeof ResizeObserver !== 'undefined') {
|
||||||
|
friendSplitterObserver = new ResizeObserver(updateFriendSplitterWidth);
|
||||||
|
friendSplitterObserver.observe(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (friendSplitterObserver) {
|
||||||
|
friendSplitterObserver.disconnect();
|
||||||
|
friendSplitterObserver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(friendSplitterSize, (value, previous) => {
|
||||||
|
if (value === previous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (friendSplitterDraggingCount.value > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateFriendSplitterWidth();
|
||||||
|
});
|
||||||
|
|
||||||
const remoteGroupMenuKey = (key) => `remote:${key}`;
|
const remoteGroupMenuKey = (key) => `remote:${key}`;
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,19 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-splitter class="favorites-splitter" @resize-end="handleWorldSplitterResize">
|
<ResizablePanelGroup
|
||||||
<el-splitter-panel :size="worldSplitterSize" :min="0" :max="360" collapsible>
|
ref="worldSplitterGroupRef"
|
||||||
|
direction="horizontal"
|
||||||
|
class="favorites-splitter"
|
||||||
|
@layout="handleWorldSplitterLayout">
|
||||||
|
<ResizablePanel
|
||||||
|
ref="worldSplitterPanelRef"
|
||||||
|
:default-size="worldSplitterDefaultSize"
|
||||||
|
:min-size="worldSplitterMinSize"
|
||||||
|
:max-size="worldSplitterMaxSize"
|
||||||
|
:collapsed-size="0"
|
||||||
|
collapsible
|
||||||
|
:order="1">
|
||||||
<div class="favorites-groups-panel">
|
<div class="favorites-groups-panel">
|
||||||
<div class="group-section">
|
<div class="group-section">
|
||||||
<div class="group-section__header">
|
<div class="group-section__header">
|
||||||
@@ -278,8 +289,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
<el-splitter-panel>
|
<ResizableHandle with-handle @dragging="setWorldSplitterDragging" />
|
||||||
|
<ResizablePanel :order="2">
|
||||||
<div class="favorites-content">
|
<div class="favorites-content">
|
||||||
<div class="favorites-content__header">
|
<div class="favorites-content__header">
|
||||||
<div class="favorites-content__title">
|
<div class="favorites-content__title">
|
||||||
@@ -416,8 +428,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</ResizablePanel>
|
||||||
</el-splitter>
|
</ResizablePanelGroup>
|
||||||
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
|
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -450,6 +462,7 @@
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
||||||
import { useAppearanceSettingsStore, useFavoriteStore, useWorldStore } from '../../stores';
|
import { useAppearanceSettingsStore, useFavoriteStore, useWorldStore } from '../../stores';
|
||||||
import { favoriteRequest, worldRequest } from '../../api';
|
import { favoriteRequest, worldRequest } from '../../api';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
@@ -548,6 +561,12 @@
|
|||||||
|
|
||||||
const worldGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
const worldGroupVisibilityOptions = ref(['public', 'friends', 'private']);
|
||||||
const worldSplitterSize = ref(260);
|
const worldSplitterSize = ref(260);
|
||||||
|
const worldSplitterFallbackWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1200;
|
||||||
|
const worldSplitterGroupRef = ref(null);
|
||||||
|
const worldSplitterPanelRef = ref(null);
|
||||||
|
const worldSplitterWidth = ref(worldSplitterFallbackWidth);
|
||||||
|
const worldSplitterDraggingCount = ref(0);
|
||||||
|
let worldSplitterObserver = null;
|
||||||
const worldExportDialogVisible = ref(false);
|
const worldExportDialogVisible = ref(false);
|
||||||
const worldFavoriteSearch = ref('');
|
const worldFavoriteSearch = ref('');
|
||||||
const worldFavoriteSearchResults = ref([]);
|
const worldFavoriteSearchResults = ref([]);
|
||||||
@@ -595,22 +614,106 @@
|
|||||||
|
|
||||||
async function loadWorldSplitterPreferences() {
|
async function loadWorldSplitterPreferences() {
|
||||||
const storedSize = await configRepository.getString('VRCX_FavoritesWorldSplitter', '260');
|
const storedSize = await configRepository.getString('VRCX_FavoritesWorldSplitter', '260');
|
||||||
if (typeof storedSize === 'string' && !Number.isNaN(Number(storedSize)) && Number(storedSize) > 0) {
|
const parsedSize = Number(storedSize);
|
||||||
worldSplitterSize.value = Number(storedSize);
|
if (Number.isFinite(parsedSize) && parsedSize >= 0) {
|
||||||
|
worldSplitterSize.value = parsedSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorldSplitterResize(panelIndex, sizes) {
|
const getWorldSplitterWidthRaw = () => {
|
||||||
|
const element = worldSplitterGroupRef.value?.$el ?? worldSplitterGroupRef.value;
|
||||||
|
const width = element?.getBoundingClientRect?.().width;
|
||||||
|
return Number.isFinite(width) ? width : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWorldSplitterWidth = () => {
|
||||||
|
const width = getWorldSplitterWidthRaw();
|
||||||
|
return Number.isFinite(width) && width > 0 ? width : worldSplitterFallbackWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveDraggingPayload = (payload) => {
|
||||||
|
if (typeof payload === 'boolean') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
if (typeof payload.detail === 'boolean') {
|
||||||
|
return payload.detail;
|
||||||
|
}
|
||||||
|
if (typeof payload.dragging === 'boolean') {
|
||||||
|
return payload.dragging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Boolean(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setWorldSplitterDragging = (payload) => {
|
||||||
|
const isDragging = resolveDraggingPayload(payload);
|
||||||
|
const next = worldSplitterDraggingCount.value + (isDragging ? 1 : -1);
|
||||||
|
worldSplitterDraggingCount.value = Math.max(0, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pxToPercent = (px, groupWidth, min = 0) => {
|
||||||
|
const width = groupWidth ?? getWorldSplitterWidth();
|
||||||
|
return Math.min(100, Math.max(min, (px / width) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
||||||
|
|
||||||
|
const worldSplitterDefaultSize = computed(() => pxToPercent(worldSplitterSize.value, worldSplitterWidth.value, 0));
|
||||||
|
const worldSplitterMinSize = computed(() => pxToPercent(0, worldSplitterWidth.value, 0));
|
||||||
|
const worldSplitterMaxSize = computed(() => pxToPercent(360, worldSplitterWidth.value, 0));
|
||||||
|
|
||||||
|
const handleWorldSplitterLayout = (sizes) => {
|
||||||
if (!Array.isArray(sizes) || !sizes.length) {
|
if (!Array.isArray(sizes) || !sizes.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextSize = sizes[0];
|
|
||||||
if (nextSize <= 0) {
|
if (worldSplitterDraggingCount.value === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
worldSplitterSize.value = nextSize;
|
|
||||||
configRepository.setString('VRCX_FavoritesWorldSplitter', nextSize.toString());
|
const rawWidth = getWorldSplitterWidthRaw();
|
||||||
}
|
if (!Number.isFinite(rawWidth) || rawWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSize = sizes[0];
|
||||||
|
if (!Number.isFinite(nextSize)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPx = Math.round(percentToPx(nextSize, rawWidth));
|
||||||
|
const clampedPx = Math.min(360, Math.max(0, nextPx));
|
||||||
|
worldSplitterSize.value = clampedPx;
|
||||||
|
configRepository.setString('VRCX_FavoritesWorldSplitter', clampedPx.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWorldSplitterWidth = () => {
|
||||||
|
const width = getWorldSplitterWidth();
|
||||||
|
worldSplitterWidth.value = width;
|
||||||
|
const targetSize = pxToPercent(worldSplitterSize.value, width, 0);
|
||||||
|
worldSplitterPanelRef.value?.resize?.(targetSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
updateWorldSplitterWidth();
|
||||||
|
const element = worldSplitterGroupRef.value?.$el ?? worldSplitterGroupRef.value;
|
||||||
|
if (element && typeof ResizeObserver !== 'undefined') {
|
||||||
|
worldSplitterObserver = new ResizeObserver(updateWorldSplitterWidth);
|
||||||
|
worldSplitterObserver.observe(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(worldSplitterSize, (value, previous) => {
|
||||||
|
if (value === previous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (worldSplitterDraggingCount.value > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateWorldSplitterWidth();
|
||||||
|
});
|
||||||
|
|
||||||
const groupedWorldFavorites = computed(() => {
|
const groupedWorldFavorites = computed(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
@@ -1249,6 +1352,10 @@
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.removeEventListener('resize', maybeFillLocalFavoritesViewport);
|
window.removeEventListener('resize', maybeFillLocalFavoritesViewport);
|
||||||
}
|
}
|
||||||
|
if (worldSplitterObserver) {
|
||||||
|
worldSplitterObserver.disconnect();
|
||||||
|
worldSplitterObserver = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user