feat: add breadcrumb components and main dialog layout functionality

This commit is contained in:
pa
2026-01-20 17:30:23 +09:00
committed by Natsumi
parent 0b636df330
commit b2bd7693bb
21 changed files with 3768 additions and 3471 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,210 @@
<script setup>
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@/components/ui/breadcrumb';
import { useAvatarStore, useGroupStore, useUiStore, useUserStore, useWorldStore } from '@/stores';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { computed } from 'vue';
import AvatarDialog from './AvatarDialog/AvatarDialog.vue';
import GroupDialog from './GroupDialog/GroupDialog.vue';
import UserDialog from './UserDialog/UserDialog.vue';
import WorldDialog from './WorldDialog/WorldDialog.vue';
const avatarStore = useAvatarStore();
const groupStore = useGroupStore();
const uiStore = useUiStore();
const userStore = useUserStore();
const worldStore = useWorldStore();
const isOpen = computed({
get: () =>
userStore.userDialog.visible ||
worldStore.worldDialog.visible ||
avatarStore.avatarDialog.visible ||
groupStore.groupDialog.visible,
set: (value) => {
if (!value) {
userStore.userDialog.visible = false;
worldStore.worldDialog.visible = false;
avatarStore.avatarDialog.visible = false;
groupStore.groupDialog.visible = false;
uiStore.clearDialogCrumbs();
}
}
});
const dialogCrumbs = computed(() => uiStore.dialogCrumbs);
const activeCrumb = computed(() => dialogCrumbs.value[dialogCrumbs.value.length - 1] || null);
const activeType = computed(() => {
if (activeCrumb.value?.type) {
return activeCrumb.value.type;
}
if (userStore.userDialog.visible) {
return 'user';
}
if (worldStore.worldDialog.visible) {
return 'world';
}
if (avatarStore.avatarDialog.visible) {
return 'avatar';
}
if (groupStore.groupDialog.visible) {
return 'group';
}
return null;
});
const activeComponent = computed(() => {
switch (activeType.value) {
case 'user':
return UserDialog;
case 'world':
return WorldDialog;
case 'avatar':
return AvatarDialog;
case 'group':
return GroupDialog;
default:
return null;
}
});
const dialogClass = computed(() => {
switch (activeType.value) {
case 'world':
return 'x-dialog x-world-dialog translate-y-0 sm:max-w-235';
case 'avatar':
return 'x-dialog x-avatar-dialog sm:max-w-235 translate-y-0';
case 'group':
return 'x-dialog x-group-dialog group-body translate-y-0 sm:max-w-235';
case 'user':
default:
return 'x-dialog x-user-dialog sm:max-w-235 translate-y-0';
}
});
const shouldShowBreadcrumbs = computed(() => dialogCrumbs.value.length > 1);
const shouldCollapseBreadcrumbs = computed(() => dialogCrumbs.value.length > 4);
const middleBreadcrumbs = computed(() => {
if (!shouldCollapseBreadcrumbs.value) {
return [];
}
return dialogCrumbs.value.slice(1, -2);
});
const handleBreadcrumbClick = (index) => {
const item = dialogCrumbs.value[index];
if (!item) {
return;
}
uiStore.jumpDialogCrumb(index);
if (item.type === 'user') {
userStore.showUserDialog(item.id, { skipBreadcrumb: true });
return;
}
if (item.type === 'world') {
worldStore.showWorldDialog(item.id, null, { skipBreadcrumb: true });
return;
}
if (item.type === 'avatar') {
avatarStore.showAvatarDialog(item.id, { skipBreadcrumb: true });
return;
}
if (item.type === 'group') {
groupStore.showGroupDialog(item.id, { skipBreadcrumb: true });
}
};
</script>
<template>
<Dialog v-model:open="isOpen">
<DialogContent :class="dialogClass" style="top: 10vh" :show-close-button="false">
<Breadcrumb v-if="shouldShowBreadcrumbs" class="mb-2">
<BreadcrumbList>
<template v-if="shouldCollapseBreadcrumbs">
<BreadcrumbItem>
<BreadcrumbLink as-child>
<button
type="button"
class="max-w-40 truncate text-left"
@click="handleBreadcrumbClick(0)">
{{ dialogCrumbs[0]?.label || dialogCrumbs[0]?.id }}
</button>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger class="flex items-center gap-1">
<BreadcrumbEllipsis class="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
v-for="(crumb, index) in middleBreadcrumbs"
:key="`${crumb.type}-${crumb.id}`"
@click="handleBreadcrumbClick(index + 1)">
{{ crumb.label || crumb.id }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink as-child>
<button
type="button"
class="max-w-40 truncate text-left"
@click="handleBreadcrumbClick(dialogCrumbs.length - 2)">
{{
dialogCrumbs[dialogCrumbs.length - 2]?.label ||
dialogCrumbs[dialogCrumbs.length - 2]?.id
}}
</button>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage class="max-w-40 truncate">
{{
dialogCrumbs[dialogCrumbs.length - 1]?.label ||
dialogCrumbs[dialogCrumbs.length - 1]?.id
}}
</BreadcrumbPage>
</BreadcrumbItem>
</template>
<template v-else>
<template v-for="(crumb, index) in dialogCrumbs" :key="`${crumb.type}-${crumb.id}`">
<BreadcrumbItem>
<BreadcrumbLink v-if="index < dialogCrumbs.length - 1" as-child>
<button
type="button"
class="max-w-40 truncate text-left"
@click="handleBreadcrumbClick(index)">
{{ crumb.label || crumb.id }}
</button>
</BreadcrumbLink>
<BreadcrumbPage v-else class="max-w-40 truncate">
{{ crumb.label || crumb.id }}
</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator v-if="index < dialogCrumbs.length - 1" />
</template>
</template>
</BreadcrumbList>
</Breadcrumb>
<component :is="activeComponent" v-if="activeComponent" />
</DialogContent>
</Dialog>
</template>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
<script setup>
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<nav aria-label="breadcrumb" data-slot="breadcrumb" :class="props.class">
<slot />
</nav>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { MoreHorizontal } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
:class="cn('flex size-9 items-center justify-center', props.class)">
<slot>
<MoreHorizontal class="size-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<li data-slot="breadcrumb-item" :class="cn('inline-flex items-center gap-1.5', props.class)">
<slot />
</li>
</template>
@@ -0,0 +1,20 @@
<script setup>
import { Primitive } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: 'a' },
class: { type: null, required: false }
});
</script>
<template>
<Primitive
data-slot="breadcrumb-link"
:as="as"
:as-child="asChild"
:class="cn('hover:text-foreground transition-colors', props.class)">
<slot />
</Primitive>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<ol
data-slot="breadcrumb-list"
:class="
cn('text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5', props.class)
">
<slot />
</ol>
</template>
@@ -0,0 +1,18 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('text-foreground font-normal', props.class)">
<slot />
</span>
</template>
@@ -0,0 +1,20 @@
<script setup>
import { ChevronRight } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:size-3.5', props.class)">
<slot>
<ChevronRight />
</slot>
</li>
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
export { default as BreadcrumbList } from './BreadcrumbList.vue';
export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';
@@ -5,12 +5,13 @@ import { useAppearanceSettingsStore } from '../stores';
import configRepository from '../service/config'; import configRepository from '../service/config';
export function useAuthenticatedLayoutResizable() { export function useMainLayoutResizable() {
const asideMaxPx = 500; const asideMaxPx = 500;
const appearanceStore = useAppearanceSettingsStore(); const appearanceStore = useAppearanceSettingsStore();
const { setAsideWidth } = appearanceStore; const { setAsideWidth } = appearanceStore;
const { asideWidth, isSideBarTabShow, isNavCollapsed } = storeToRefs(appearanceStore); const { asideWidth, isSideBarTabShow, isNavCollapsed } =
storeToRefs(appearanceStore);
const fallbackWidth = const fallbackWidth =
typeof window !== 'undefined' && window.innerWidth typeof window !== 'undefined' && window.innerWidth
@@ -63,7 +64,9 @@ export function useAuthenticatedLayoutResizable() {
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth; const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
const isAsideCollapsed = (layout) => const isAsideCollapsed = (layout) =>
Array.isArray(layout) && layout.length >= 2 && layout[layout.length - 1] <= 1; Array.isArray(layout) &&
layout.length >= 2 &&
layout[layout.length - 1] <= 1;
const asideDefaultSize = computed(() => const asideDefaultSize = computed(() =>
pxToPercent(asideWidth.value, undefined, 0) pxToPercent(asideWidth.value, undefined, 0)
@@ -170,6 +173,7 @@ export function useAuthenticatedLayoutResizable() {
asidePanelRef, asidePanelRef,
asideDefaultSize, asideDefaultSize,
asideMaxSize, asideMaxSize,
asideMaxPx,
mainDefaultSize, mainDefaultSize,
handleLayout, handleLayout,
setIsDragging, setIsDragging,
+2 -2
View File
@@ -2,7 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router';
import { watchState } from './../service/watchState'; import { watchState } from './../service/watchState';
import AuthenticatedLayout from '../views/Layout/AuthenticatedLayout.vue'; import MainLayout from '../views/Layout/MainLayout.vue';
import Charts from './../views/Charts/Charts.vue'; import Charts from './../views/Charts/Charts.vue';
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue'; import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue'; import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
@@ -31,7 +31,7 @@ const routes = [
}, },
{ {
path: '/', path: '/',
component: AuthenticatedLayout, component: MainLayout,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
children: [ children: [
{ path: '', redirect: { name: 'feed' } }, { path: '', redirect: { name: 'feed' } },
+27 -1
View File
@@ -18,9 +18,12 @@ import { database } from '../service/database';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { useAvatarProviderStore } from './avatarProvider'; import { useAvatarProviderStore } from './avatarProvider';
import { useFavoriteStore } from './favorite'; import { useFavoriteStore } from './favorite';
import { useGroupStore } from './group';
import { useModalStore } from './modal'; import { useModalStore } from './modal';
import { useUiStore } from './ui';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { useVRCXUpdaterStore } from './vrcxUpdater'; import { useVRCXUpdaterStore } from './vrcxUpdater';
import { useWorldStore } from './world';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
import webApiService from '../service/webapi'; import webApiService from '../service/webapi';
@@ -31,7 +34,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
const vrcxUpdaterStore = useVRCXUpdaterStore(); const vrcxUpdaterStore = useVRCXUpdaterStore();
const advancedSettingsStore = useAdvancedSettingsStore(); const advancedSettingsStore = useAdvancedSettingsStore();
const userStore = useUserStore(); const userStore = useUserStore();
const worldStore = useWorldStore();
const groupStore = useGroupStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
const uiStore = useUiStore();
const { t } = useI18n(); const { t } = useI18n();
let cachedAvatarModerations = new Map(); let cachedAvatarModerations = new Map();
@@ -172,8 +178,22 @@ export const useAvatarStore = defineStore('Avatar', () => {
* @param {string} avatarId * @param {string} avatarId
* @returns * @returns
*/ */
function showAvatarDialog(avatarId) { function showAvatarDialog(avatarId, options = {}) {
const D = avatarDialog.value; const D = avatarDialog.value;
if (
!avatarDialog.value.visible &&
!userStore.userDialog.visible &&
!worldStore.worldDialog.visible &&
!groupStore.groupDialog.visible
) {
uiStore.clearDialogCrumbs();
}
if (!options.skipBreadcrumb) {
uiStore.pushDialogCrumb('avatar', avatarId);
}
userStore.userDialog.visible = false;
worldStore.worldDialog.visible = false;
groupStore.groupDialog.visible = false;
D.visible = true; D.visible = true;
D.loading = true; D.loading = true;
D.id = avatarId; D.id = avatarId;
@@ -201,6 +221,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
const ref2 = cachedAvatars.get(avatarId); const ref2 = cachedAvatars.get(avatarId);
if (typeof ref2 !== 'undefined') { if (typeof ref2 !== 'undefined') {
D.ref = ref2; D.ref = ref2;
uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id);
updateVRChatAvatarCache(); updateVRChatAvatarCache();
if ( if (
ref2.releaseStatus !== 'public' && ref2.releaseStatus !== 'public' &&
@@ -215,6 +236,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
.then((args) => { .then((args) => {
const ref = applyAvatar(args.json); const ref = applyAvatar(args.json);
D.ref = ref; D.ref = ref;
uiStore.setDialogCrumbLabel(
'avatar',
D.id,
D.ref?.name || D.id
);
getAvatarGallery(avatarId); getAvatarGallery(avatarId);
updateVRChatAvatarCache(); updateVRChatAvatarCache();
if (/quest/.test(ref.tags)) { if (/quest/.test(ref.tags)) {
+26 -1
View File
@@ -16,11 +16,14 @@ import {
} from '../shared/utils'; } from '../shared/utils';
import { database } from '../service/database.js'; import { database } from '../service/database.js';
import { groupDialogFilterOptions } from '../shared/constants/'; import { groupDialogFilterOptions } from '../shared/constants/';
import { useAvatarStore } from './avatar';
import { useGameStore } from './game'; import { useGameStore } from './game';
import { useInstanceStore } from './instance'; import { useInstanceStore } from './instance';
import { useModalStore } from './modal'; import { useModalStore } from './modal';
import { useNotificationStore } from './notification'; import { useNotificationStore } from './notification';
import { useUiStore } from './ui';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { useWorldStore } from './world';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
import configRepository from '../service/config'; import configRepository from '../service/config';
@@ -31,8 +34,11 @@ export const useGroupStore = defineStore('Group', () => {
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const gameStore = useGameStore(); const gameStore = useGameStore();
const userStore = useUserStore(); const userStore = useUserStore();
const worldStore = useWorldStore();
const avatarStore = useAvatarStore();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
const uiStore = useUiStore();
const { t } = useI18n(); const { t } = useI18n();
let cachedGroups = new Map(); let cachedGroups = new Map();
@@ -124,10 +130,24 @@ export const useGroupStore = defineStore('Group', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
function showGroupDialog(groupId) { function showGroupDialog(groupId, options = {}) {
if (!groupId) { if (!groupId) {
return; return;
} }
if (
!groupDialog.value.visible &&
!userStore.userDialog.visible &&
!worldStore.worldDialog.visible &&
!avatarStore.avatarDialog.visible
) {
uiStore.clearDialogCrumbs();
}
if (!options.skipBreadcrumb) {
uiStore.pushDialogCrumb('group', groupId);
}
userStore.userDialog.visible = false;
worldStore.worldDialog.visible = false;
avatarStore.avatarDialog.visible = false;
const D = groupDialog.value; const D = groupDialog.value;
D.visible = true; D.visible = true;
D.loading = true; D.loading = true;
@@ -161,6 +181,11 @@ export const useGroupStore = defineStore('Group', () => {
if (groupId === args.ref.id) { if (groupId === args.ref.id) {
D.loading = false; D.loading = false;
D.ref = args.ref; D.ref = args.ref;
uiStore.setDialogCrumbLabel(
'group',
D.id,
D.ref?.name || D.id
);
D.inGroup = args.ref.membershipStatus === 'member'; D.inGroup = args.ref.membershipStatus === 'member';
D.ownerDisplayName = args.ref.ownerId; D.ownerDisplayName = args.ref.ownerId;
userRequest userRequest
+55 -1
View File
@@ -27,6 +27,7 @@ export const useUiStore = defineStore('Ui', () => {
const notifiedMenus = ref([]); const notifiedMenus = ref([]);
const shiftHeld = ref(false); const shiftHeld = ref(false);
const trayIconNotify = ref(false); const trayIconNotify = ref(false);
const dialogCrumbs = ref([]);
watch(ctrlR, (isPressed) => { watch(ctrlR, (isPressed) => {
if (isPressed) { if (isPressed) {
@@ -58,6 +59,54 @@ export const useUiStore = defineStore('Ui', () => {
} }
}); });
function pushDialogCrumb(type, id, label = '') {
if (!type || !id) {
return;
}
const items = dialogCrumbs.value;
const last = items[items.length - 1];
if (last && last.type === type && last.id === id) {
if (label && last.label !== label) {
last.label = label;
}
return;
}
const existingIndex = items.findIndex(
(item) => item.type === type && item.id === id
);
if (existingIndex !== -1) {
items.splice(existingIndex + 1);
if (label) {
items[existingIndex].label = label;
}
return;
}
items.push({ type, id, label: label || id });
}
function setDialogCrumbLabel(type, id, label) {
if (!type || !id || !label) {
return;
}
const item = dialogCrumbs.value.find(
(entry) => entry.type === type && entry.id === id
);
if (item) {
item.label = label;
}
}
function jumpDialogCrumb(index) {
if (index < 0 || index >= dialogCrumbs.value.length) {
return;
}
dialogCrumbs.value.splice(index + 1);
}
function clearDialogCrumbs() {
dialogCrumbs.value = [];
}
// Make sure file drops outside of the screenshot manager don't navigate to the file path dropped. // Make sure file drops outside of the screenshot manager don't navigate to the file path dropped.
// This issue persists on prompts created with prompt(), unfortunately. Not sure how to fix that. // This issue persists on prompts created with prompt(), unfortunately. Not sure how to fix that.
document.body.addEventListener('drop', function (e) { document.body.addEventListener('drop', function (e) {
@@ -133,10 +182,15 @@ export const useUiStore = defineStore('Ui', () => {
return { return {
notifiedMenus, notifiedMenus,
shiftHeld, shiftHeld,
dialogCrumbs,
notifyMenu, notifyMenu,
removeNotify, removeNotify,
showConsole, showConsole,
updateTrayIconNotify updateTrayIconNotify,
pushDialogCrumb,
setDialogCrumbLabel,
jumpDialogCrumb,
clearDialogCrumbs
}; };
}); });
+23 -1
View File
@@ -48,6 +48,7 @@ import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon'; import { usePhotonStore } from './photon';
import { useSearchStore } from './search'; import { useSearchStore } from './search';
import { useSharedFeedStore } from './sharedFeed'; import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useWorldStore } from './world'; import { useWorldStore } from './world';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
@@ -68,6 +69,7 @@ export const useUserStore = defineStore('User', () => {
const groupStore = useGroupStore(); const groupStore = useGroupStore();
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const worldStore = useWorldStore(); const worldStore = useWorldStore();
const uiStore = useUiStore();
const moderationStore = useModerationStore(); const moderationStore = useModerationStore();
const photonStore = usePhotonStore(); const photonStore = usePhotonStore();
const sharedFeedStore = useSharedFeedStore(); const sharedFeedStore = useSharedFeedStore();
@@ -310,6 +312,7 @@ export const useUserStore = defineStore('User', () => {
customUserTags.clear(); customUserTags.clear();
state.notes.clear(); state.notes.clear();
subsetOfLanguages.value = []; subsetOfLanguages.value = [];
uiStore.clearDialogCrumbs();
} }
}, },
{ flush: 'sync' } { flush: 'sync' }
@@ -755,7 +758,7 @@ export const useUserStore = defineStore('User', () => {
* *
* @param {string} userId * @param {string} userId
*/ */
function showUserDialog(userId) { function showUserDialog(userId, options = {}) {
if ( if (
!userId || !userId ||
typeof userId !== 'string' || typeof userId !== 'string' ||
@@ -763,6 +766,20 @@ export const useUserStore = defineStore('User', () => {
) { ) {
return; return;
} }
if (
!userDialog.value.visible &&
!worldStore.worldDialog.visible &&
!avatarStore.avatarDialog.visible &&
!groupStore.groupDialog.visible
) {
uiStore.clearDialogCrumbs();
}
if (!options.skipBreadcrumb) {
uiStore.pushDialogCrumb('user', userId);
}
worldStore.worldDialog.visible = false;
avatarStore.avatarDialog.visible = false;
groupStore.groupDialog.visible = false;
const D = userDialog.value; const D = userDialog.value;
D.id = userId; D.id = userId;
D.treeData = {}; D.treeData = {};
@@ -846,6 +863,11 @@ export const useUserStore = defineStore('User', () => {
if (args.ref.id === D.id) { if (args.ref.id === D.id) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
D.ref = args.ref; D.ref = args.ref;
uiStore.setDialogCrumbLabel(
'user',
D.id,
D.ref?.displayName || D.id
);
D.friend = friendStore.friends.get(D.id); D.friend = friendStore.friends.get(D.id);
D.isFriend = Boolean(D.friend); D.isFriend = Boolean(D.friend);
D.note = String(D.ref.note || ''); D.note = String(D.ref.note || '');
+26 -1
View File
@@ -14,9 +14,12 @@ import {
} from '../shared/utils'; } from '../shared/utils';
import { instanceRequest, miscRequest, worldRequest } from '../api'; import { instanceRequest, miscRequest, worldRequest } from '../api';
import { database } from '../service/database'; import { database } from '../service/database';
import { useAvatarStore } from './avatar';
import { useFavoriteStore } from './favorite'; import { useFavoriteStore } from './favorite';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance'; import { useInstanceStore } from './instance';
import { useLocationStore } from './location'; import { useLocationStore } from './location';
import { useUiStore } from './ui';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
@@ -25,6 +28,9 @@ export const useWorldStore = defineStore('World', () => {
const favoriteStore = useFavoriteStore(); const favoriteStore = useFavoriteStore();
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const userStore = useUserStore(); const userStore = useUserStore();
const avatarStore = useAvatarStore();
const groupStore = useGroupStore();
const uiStore = useUiStore();
const { t } = useI18n(); const { t } = useI18n();
const worldDialog = reactive({ const worldDialog = reactive({
@@ -71,12 +77,26 @@ export const useWorldStore = defineStore('World', () => {
* @param {string} tag * @param {string} tag
* @param {string} shortName * @param {string} shortName
*/ */
function showWorldDialog(tag, shortName = null) { function showWorldDialog(tag, shortName = null, options = {}) {
const D = worldDialog; const D = worldDialog;
const L = parseLocation(tag); const L = parseLocation(tag);
if (L.worldId === '') { if (L.worldId === '') {
return; return;
} }
if (
!worldDialog.visible &&
!userStore.userDialog.visible &&
!avatarStore.avatarDialog.visible &&
!groupStore.groupDialog.visible
) {
uiStore.clearDialogCrumbs();
}
if (!options.skipBreadcrumb) {
uiStore.pushDialogCrumb('world', L.worldId);
}
userStore.userDialog.visible = false;
avatarStore.avatarDialog.visible = false;
groupStore.groupDialog.visible = false;
L.shortName = shortName; L.shortName = shortName;
D.id = L.worldId; D.id = L.worldId;
D.$location = L; D.$location = L;
@@ -141,6 +161,11 @@ export const useWorldStore = defineStore('World', () => {
if (D.id === args.ref.id) { if (D.id === args.ref.id) {
D.loading = false; D.loading = false;
D.ref = args.ref; D.ref = args.ref;
uiStore.setDialogCrumbLabel(
'world',
D.id,
D.ref?.name || D.id
);
D.isFavorite = favoriteStore.getCachedFavoritesByObjectId( D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(
D.id D.id
); );
@@ -18,10 +18,7 @@
<ResizablePanelGroup <ResizablePanelGroup
ref="panelGroupRef" ref="panelGroupRef"
direction="horizontal" direction="horizontal"
:class="[ :class="['group/main-layout flex-1 h-full min-w-0', { 'aside-collapsed': isAsideCollapsedStatic }]"
'group/main-layout flex-1 h-full min-w-0',
{ 'aside-collapsed': isAsideCollapsedStatic }
]"
@layout="handleLayout"> @layout="handleLayout">
<template #default="{ layout }"> <template #default="{ layout }">
<ResizablePanel :default-size="mainDefaultSize" :order="1"> <ResizablePanel :default-size="mainDefaultSize" :order="1">
@@ -46,7 +43,8 @@
:max-size="asideMaxSize" :max-size="asideMaxSize"
:collapsed-size="0" :collapsed-size="0"
collapsible collapsible
:order="2"> :order="2"
:style="{ maxWidth: `${asideMaxPx}px` }">
<Sidebar></Sidebar> <Sidebar></Sidebar>
</ResizablePanel> </ResizablePanel>
</template> </template>
@@ -56,13 +54,7 @@
</SidebarProvider> </SidebarProvider>
<!-- ## Dialogs ## --> <!-- ## Dialogs ## -->
<UserDialog></UserDialog> <MainDialogContainer></MainDialogContainer>
<WorldDialog></WorldDialog>
<AvatarDialog></AvatarDialog>
<GroupDialog></GroupDialog>
<GroupMemberModerationDialog></GroupMemberModerationDialog> <GroupMemberModerationDialog></GroupMemberModerationDialog>
@@ -102,16 +94,15 @@
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
import { SidebarInset, SidebarProvider } from '../../components/ui/sidebar'; import { SidebarInset, SidebarProvider } from '../../components/ui/sidebar';
import { useAppearanceSettingsStore } from '../../stores'; import { useAppearanceSettingsStore } from '../../stores';
import { useAuthenticatedLayoutResizable } from '../../composables/useAuthenticatedLayoutResizable'; import { useMainLayoutResizable } from '../../composables/useMainLayoutResizable';
import { watchState } from '../../service/watchState'; import { watchState } from '../../service/watchState';
import AvatarDialog from '../../components/dialogs/AvatarDialog/AvatarDialog.vue';
import AvatarImportDialog from '../Favorites/dialogs/AvatarImportDialog.vue'; import AvatarImportDialog from '../Favorites/dialogs/AvatarImportDialog.vue';
import ChangelogDialog from '../Settings/dialogs/ChangelogDialog.vue'; import ChangelogDialog from '../Settings/dialogs/ChangelogDialog.vue';
import ChooseFavoriteGroupDialog from '../../components/dialogs/ChooseFavoriteGroupDialog.vue'; import ChooseFavoriteGroupDialog from '../../components/dialogs/ChooseFavoriteGroupDialog.vue';
import MainDialogContainer from '../../components/dialogs/MainDialogContainer.vue';
import FriendImportDialog from '../Favorites/dialogs/FriendImportDialog.vue'; import FriendImportDialog from '../Favorites/dialogs/FriendImportDialog.vue';
import FullscreenImagePreview from '../../components/FullscreenImagePreview.vue'; import FullscreenImagePreview from '../../components/FullscreenImagePreview.vue';
import GroupDialog from '../../components/dialogs/GroupDialog/GroupDialog.vue';
import GroupMemberModerationDialog from '../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue'; import GroupMemberModerationDialog from '../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue';
import InviteGroupDialog from '../../components/dialogs/InviteGroupDialog.vue'; import InviteGroupDialog from '../../components/dialogs/InviteGroupDialog.vue';
import LaunchDialog from '../../components/dialogs/LaunchDialog.vue'; import LaunchDialog from '../../components/dialogs/LaunchDialog.vue';
@@ -121,9 +112,7 @@
import PrimaryPasswordDialog from '../Settings/dialogs/PrimaryPasswordDialog.vue'; import PrimaryPasswordDialog from '../Settings/dialogs/PrimaryPasswordDialog.vue';
import SendBoopDialog from '../../components/dialogs/SendBoopDialog.vue'; import SendBoopDialog from '../../components/dialogs/SendBoopDialog.vue';
import Sidebar from '../Sidebar/Sidebar.vue'; import Sidebar from '../Sidebar/Sidebar.vue';
import UserDialog from '../../components/dialogs/UserDialog/UserDialog.vue';
import VRChatConfigDialog from '../Settings/dialogs/VRChatConfigDialog.vue'; import VRChatConfigDialog from '../Settings/dialogs/VRChatConfigDialog.vue';
import WorldDialog from '../../components/dialogs/WorldDialog/WorldDialog.vue';
import WorldImportDialog from '../Favorites/dialogs/WorldImportDialog.vue'; import WorldImportDialog from '../Favorites/dialogs/WorldImportDialog.vue';
const router = useRouter(); const router = useRouter();
@@ -180,16 +169,15 @@
const { const {
asideDefaultSize, asideDefaultSize,
asideMaxSize, asideMaxSize,
asideMaxPx,
mainDefaultSize, mainDefaultSize,
handleLayout, handleLayout,
setIsDragging, setIsDragging,
isAsideCollapsed, isAsideCollapsed,
isSideBarTabShow isSideBarTabShow
} = useAuthenticatedLayoutResizable(); } = useMainLayoutResizable();
const isAsideCollapsedStatic = computed( const isAsideCollapsedStatic = computed(() => !isSideBarTabShow.value || asideWidth.value === 0);
() => !isSideBarTabShow.value || asideWidth.value === 0
);
watch( watch(
() => watchState.isLoggedIn, () => watchState.isLoggedIn,