replace element plus components

This commit is contained in:
pa
2026-01-15 22:38:09 +09:00
committed by Natsumi
parent bdc1d3a347
commit c430ce1b63
46 changed files with 2143 additions and 1752 deletions

View File

@@ -5,81 +5,83 @@
<DialogTitle>{{ t('dialog.favorite.header') }}</DialogTitle>
</DialogHeader>
<div v-loading="loading">
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
<Button
variant="outline"
style="width: 100%; white-space: initial"
class="my-1"
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
<Check />{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} /
{{ favoriteDialog.currentGroup.capacity }})
</Button>
</template>
<template v-else>
<Button
variant="outline"
v-for="group in groups"
:key="group.key"
style="width: 100%; white-space: initial"
class="my-1"
@click="addFavorite(group)">
{{ group.displayName }} ({{ group.count }} / {{ group.capacity }})
</Button>
</template>
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
<Button
variant="outline"
style="width: 100%; white-space: initial"
class="my-1"
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
<Check />{{ favoriteDialog.currentGroup.displayName }} ({{
favoriteDialog.currentGroup.count
}}
/ {{ favoriteDialog.currentGroup.capacity }})
</Button>
</template>
<template v-else>
<Button
variant="outline"
v-for="group in groups"
:key="group.key"
style="width: 100%; white-space: initial"
class="my-1"
@click="addFavorite(group)">
{{ group.displayName }} ({{ group.count }} / {{ group.capacity }})
</Button>
</template>
</div>
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<Button
variant="outline"
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
style="width: 100%; white-space: initial"
class="my-1"
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
<Check />{{ group }} ({{ localWorldFavGroupLength(group) }})
</Button>
<Button
variant="outline"
v-else
style="width: 100%; white-space: initial"
class="my-1"
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ localWorldFavGroupLength(group) }})
</Button>
</template>
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<Button
variant="outline"
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
style="width: 100%; white-space: initial"
class="my-1"
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
<Check />{{ group }} ({{ localWorldFavGroupLength(group) }})
</Button>
<Button
variant="outline"
v-else
style="width: 100%; white-space: initial"
class="my-1"
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ localWorldFavGroupLength(group) }})
</Button>
</template>
</div>
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
<span style="text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<Button
variant="outline"
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
style="width: 100%; white-space: initial"
class="my-1"
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
<Check />{{ group }} ({{ localAvatarFavGroupLength(group) }})
</Button>
<Button
variant="outline"
v-else
style="width: 100%; white-space: initial"
class="my-1"
:disabled="!isLocalUserVrcPlusSupporter"
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</Button>
</template>
<span style="text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<Button
variant="outline"
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
style="width: 100%; white-space: initial"
class="my-1"
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
<Check />{{ group }} ({{ localAvatarFavGroupLength(group) }})
</Button>
<Button
variant="outline"
v-else
style="width: 100%; white-space: initial"
class="my-1"
:disabled="!isLocalUserVrcPlusSupporter"
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</Button>
</template>
</div>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Check } from 'lucide-vue-next';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -4,51 +4,20 @@
<DialogHeader>
<DialogTitle>{{ t('nav_menu.custom_nav.dialog_title') }}</DialogTitle>
</DialogHeader>
<div class="custom-nav-dialog__list" v-if="localLayout.length">
<div
v-for="(entry, index) in localLayout"
:key="entry.key || entry.id"
:class="['custom-nav-entry', `custom-nav-entry--${entry.type}`]">
<template v-if="entry.type === 'item'">
<div class="custom-nav-entry__info">
<i :class="definitionsMap.get(entry.key)?.icon"></i>
<span>{{ t(definitionsMap.get(entry.key)?.labelKey || entry.key) }}</span>
</div>
<div class="custom-nav-entry__controls">
<div class="custom-nav-entry__move">
<Button
class="rounded-full w-6 h-6 text-xs"
size="icon-sm"
variant="outline"
:disabled="index === 0"
@click="handleMoveEntry(index, -1)">
<i class="ri-arrow-up-line"></i>
</Button>
<Button
class="rounded-full w-6 h-6 text-xs"
size="icon-sm"
variant="outline"
:disabled="index === localLayout.length - 1"
@click="handleMoveEntry(index, 1)">
<i class="ri-arrow-down-line"></i>
</Button>
</div>
</div>
</template>
<template v-else>
<div class="custom-nav-entry__folder-header">
<div class="custom-nav-dialog__list" v-if="localLayout.length">
<div
v-for="(entry, index) in localLayout"
:key="entry.key || entry.id"
:class="['custom-nav-entry', `custom-nav-entry--${entry.type}`]">
<template v-if="entry.type === 'item'">
<div class="custom-nav-entry__info">
<i :class="entry.icon || defaultFolderIcon"></i>
<span>{{ entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder') }}</span>
<i :class="definitionsMap.get(entry.key)?.icon"></i>
<span>{{ t(definitionsMap.get(entry.key)?.labelKey || entry.key) }}</span>
</div>
<div class="custom-nav-entry__actions">
<Button size="icon-sm w-6 h-6 text-xs" variant="outline" @click="openFolderEditor(index)">
<i class="ri-edit-box-line"></i>
{{ t('nav_menu.custom_nav.edit_folder') }}
</Button>
<div class="custom-nav-entry__controls">
<div class="custom-nav-entry__move">
<Button
class="rounded-full text-xs w-6 h-6"
class="rounded-full w-6 h-6 text-xs"
size="icon-sm"
variant="outline"
:disabled="index === 0"
@@ -56,7 +25,7 @@
<i class="ri-arrow-up-line"></i>
</Button>
<Button
class="rounded-full text-xs w-6 h-6"
class="rounded-full w-6 h-6 text-xs"
size="icon-sm"
variant="outline"
:disabled="index === localLayout.length - 1"
@@ -65,54 +34,90 @@
</Button>
</div>
</div>
</div>
<div class="custom-nav-entry__folder-items">
<template v-if="entry.items?.length">
<Badge
v-for="key in entry.items"
:key="`${entry.id}-${key}`"
variant="outline"
class="custom-nav-entry__folder-tag">
{{ t(definitionsMap.get(key)?.labelKey || key) }}
</Badge>
</template>
<span v-else class="custom-nav-entry__folder-empty">
{{ t('nav_menu.custom_nav.folder_empty') }}
</span>
</div>
</template>
</template>
<template v-else>
<div class="custom-nav-entry__folder-header">
<div class="custom-nav-entry__info">
<i :class="entry.icon || defaultFolderIcon"></i>
<span>{{
entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder')
}}</span>
</div>
<div class="custom-nav-entry__actions">
<Button
size="icon-sm w-6 h-6 text-xs"
variant="outline"
@click="openFolderEditor(index)">
<i class="ri-edit-box-line"></i>
{{ t('nav_menu.custom_nav.edit_folder') }}
</Button>
<div class="custom-nav-entry__move">
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === 0"
@click="handleMoveEntry(index, -1)">
<i class="ri-arrow-up-line"></i>
</Button>
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === localLayout.length - 1"
@click="handleMoveEntry(index, 1)">
<i class="ri-arrow-down-line"></i>
</Button>
</div>
</div>
</div>
<div class="custom-nav-entry__folder-items">
<template v-if="entry.items?.length">
<Badge
v-for="key in entry.items"
:key="`${entry.id}-${key}`"
variant="outline"
class="custom-nav-entry__folder-tag">
{{ t(definitionsMap.get(key)?.labelKey || key) }}
</Badge>
</template>
<span v-else class="custom-nav-entry__folder-empty">
{{ t('nav_menu.custom_nav.folder_empty') }}
</span>
</div>
</template>
</div>
</div>
</div>
<el-alert
v-if="invalidFolders.length"
type="warning"
:closable="false"
:title="t('nav_menu.custom_nav.invalid_folder')" />
<!-- <el-alert
v-if="invalidFolders.length"
type="warning"
:closable="false"
:title="t('nav_menu.custom_nav.invalid_folder')" /> -->
<DialogFooter>
<div class="custom-nav-dialog__footer">
<div class="custom-nav-dialog__footer-left">
<Button variant="outline" @click="openFolderEditor()">
{{ t('nav_menu.custom_nav.add_folder') }}
</Button>
<Button variant="outline" @click="handleReset">
{{ t('nav_menu.custom_nav.restore_default') }}
</Button>
<div class="custom-nav-dialog__footer">
<div class="custom-nav-dialog__footer-left">
<Button variant="outline" @click="openFolderEditor()">
{{ t('nav_menu.custom_nav.add_folder') }}
</Button>
<Button variant="outline" @click="handleReset">
{{ t('nav_menu.custom_nav.restore_default') }}
</Button>
</div>
<div class="custom-nav-dialog__footer-right">
<Button variant="secondary" @click="handleClose">
{{ t('nav_menu.custom_nav.cancel') }}
</Button>
<Button :disabled="isSaveDisabled" @click="handleSave">
{{ t('nav_menu.custom_nav.save') }}
</Button>
</div>
</div>
<div class="custom-nav-dialog__footer-right">
<Button variant="secondary" @click="handleClose">
{{ t('nav_menu.custom_nav.cancel') }}
</Button>
<Button :disabled="isSaveDisabled" @click="handleSave">
{{ t('nav_menu.custom_nav.save') }}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="folderEditor.visible">
<DialogContent class="folder-editor-dialog">
<DialogContent class="folder-editor-dialog sm:max-w-[50vw]">
<DialogHeader>
<DialogTitle>
{{
@@ -122,113 +127,151 @@
}}
</DialogTitle>
</DialogHeader>
<div class="folder-editor">
<div class="folder-editor__form">
<InputGroupField
v-model="folderEditor.data.name"
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
<IconPicker v-model="folderEditor.data.icon" class="folder-editor__icon-picker" />
</div>
<div class="folder-editor__lists">
<div class="folder-editor__column">
<div class="folder-editor__column-title">
{{ t('nav_menu.custom_nav.folder_available') }}
</div>
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
{{ t('nav_menu.custom_nav.folder_empty') }}
</div>
<el-scrollbar v-else always class="folder-editor__scroll">
<div v-for="item in folderEditorAvailableItems" :key="item.key" class="folder-editor__option">
<label class="folder-editor__option-label">
<Checkbox
:model-value="folderEditor.data.items.includes(item.key)"
@update:modelValue="(val) => toggleFolderItem(item.key, val)" />
<span>
<i :class="item.icon"></i>
{{ t(item.labelKey) }}
</span>
</label>
</div>
</el-scrollbar>
<div class="folder-editor">
<div class="folder-editor__form">
<InputGroupField
class="col-span-2"
v-model="folderEditor.data.name"
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
<InputGroupField
class="col-span-2"
v-model="folderEditor.data.icon"
:placeholder="t('nav_menu.custom_nav.folder_icon_placeholder')">
<template #trailing>
<HoverCard>
<HoverCardTrigger as-child>
<InputGroupButton
size="icon-xs"
:aria-label="t('nav_menu.custom_nav.folder_icon_placeholder')">
<LinkIcon class="size-3.5" />
</InputGroupButton>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="end" class="w-80">
<div class="text-sm leading-snug">
<div>
Find the icon you want on this site and paste its class name here, e.g.
<span class="font-mono">ri-arrow-left-up-line</span>
</div>
<div class="mt-2">
<a
class="x-link"
@click.prevent="openExternalLink('https://remixicon.com/')">
https://remixicon.com/
</a>
</div>
</div>
</HoverCardContent>
</HoverCard>
</template>
</InputGroupField>
</div>
<div class="folder-editor__column folder-editor__column--selected">
<div class="folder-editor__column-title">
{{ t('nav_menu.custom_nav.folder_selected') }}
</div>
<div v-if="!folderEditor.data.items.length" class="folder-editor__empty">
{{ t('nav_menu.custom_nav.folder_selected_empty') }}
</div>
<div
v-for="(key, index) in folderEditor.data.items"
:key="`selected-${key}`"
class="folder-editor__selected-item">
<div class="folder-editor__selected-label">
<i :class="definitionsMap.get(key)?.icon"></i>
<span>{{ t(definitionsMap.get(key)?.labelKey || key) }}</span>
<div class="folder-editor__lists">
<div class="folder-editor__column">
<div class="folder-editor__column-title">
{{ t('nav_menu.custom_nav.folder_available') }}
</div>
<div class="folder-editor__selected-actions">
<div class="custom-nav-entry__move">
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === 0"
@click="handleFolderItemMove(index, -1)">
<i class="ri-arrow-up-line"></i>
</Button>
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === folderEditor.data.items.length - 1"
@click="handleFolderItemMove(index, 1)">
<i class="ri-arrow-down-line"></i>
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
{{ t('nav_menu.custom_nav.folder_empty') }}
</div>
<ScrollArea v-else type="always" class="folder-editor__scroll">
<div
v-for="item in folderEditorAvailableItems"
:key="item.key"
class="folder-editor__option">
<label class="folder-editor__option-label">
<Checkbox
:model-value="folderEditor.data.items.includes(item.key)"
@update:modelValue="(val) => toggleFolderItem(item.key, val)" />
<span>
<i :class="item.icon"></i>
{{ t(item.labelKey) }}
</span>
</label>
</div>
</ScrollArea>
</div>
<div class="folder-editor__column folder-editor__column--selected">
<div class="folder-editor__column-title">
{{ t('nav_menu.custom_nav.folder_selected') }}
</div>
<div v-if="!folderEditor.data.items.length" class="folder-editor__empty">
{{ t('nav_menu.custom_nav.folder_selected_empty') }}
</div>
<div
v-for="(key, index) in folderEditor.data.items"
:key="`selected-${key}`"
class="folder-editor__selected-item">
<div class="folder-editor__selected-label">
<i :class="definitionsMap.get(key)?.icon"></i>
<span>{{ t(definitionsMap.get(key)?.labelKey || key) }}</span>
</div>
<div class="folder-editor__selected-actions">
<div class="custom-nav-entry__move">
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === 0"
@click="handleFolderItemMove(index, -1)">
<i class="ri-arrow-up-line"></i>
</Button>
<Button
class="rounded-full text-xs w-6 h-6"
size="icon-sm"
variant="outline"
:disabled="index === folderEditor.data.items.length - 1"
@click="handleFolderItemMove(index, 1)">
<i class="ri-arrow-down-line"></i>
</Button>
</div>
<Button size="sm" variant="outline" @click="toggleFolderItem(key, false)">
{{ t('nav_menu.custom_nav.remove_from_folder') }}
</Button>
</div>
<Button size="sm" variant="outline" @click="toggleFolderItem(key, false)">
{{ t('nav_menu.custom_nav.remove_from_folder') }}
</Button>
</div>
</div>
</div>
</div>
</div>
<DialogFooter>
<div class="folder-editor__footer">
<Button
variant="destructive"
v-if="folderEditor.isEditing"
:disabled="!canDeleteFolder"
@click="handleFolderEditorDelete">
{{ t('nav_menu.custom_nav.delete_folder') }}
</Button>
<div class="folder-editor__footer-spacer"></div>
<Button variant="secondary" class="mr-2" @click="closeFolderEditor">
{{ t('nav_menu.custom_nav.cancel') }}
</Button>
<Button :disabled="folderEditorSaveDisabled" @click="handleFolderEditorSave">
{{ t('nav_menu.custom_nav.save') }}
</Button>
</div>
<div class="folder-editor__footer">
<Button
variant="destructive"
v-if="folderEditor.isEditing"
:disabled="!canDeleteFolder"
@click="handleFolderEditorDelete">
{{ t('nav_menu.custom_nav.delete_folder') }}
</Button>
<div class="folder-editor__footer-spacer"></div>
<Button variant="secondary" class="mr-2" @click="closeFolderEditor">
{{ t('nav_menu.custom_nav.cancel') }}
</Button>
<Button :disabled="folderEditorSaveDisabled" @click="handleFolderEditorSave">
{{ t('nav_menu.custom_nav.save') }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, reactive, ref, watch } from 'vue';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { Button } from '@/components/ui/button';
import { Link as LinkIcon } from 'lucide-vue-next';
import { openExternalLink } from '@/shared/utils/common';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import { InputGroupButton, InputGroupField } from '../ui/input-group';
import { Badge } from '../ui/badge';
import { Checkbox } from '../ui/checkbox';
import { InputGroupField } from '../ui/input-group';
import { ScrollArea } from '../ui/scroll-area';
import { navDefinitions } from '../../shared/constants/ui.js';
import IconPicker from '../IconPicker.vue';
// import IconPicker from '../IconPicker.vue';
const props = defineProps({
visible: {
@@ -240,8 +283,7 @@
default: () => []
},
defaultFolderIcon: {
type: String,
default: 'ri-menu-fold-line'
type: String
}
});
@@ -258,7 +300,7 @@
type: 'folder',
id: entry.id,
name: entry.name,
icon: entry.icon || props.defaultFolderIcon,
icon: entry.icon,
items: Array.isArray(entry.items) ? [...entry.items] : []
};
}
@@ -358,14 +400,14 @@
folderEditor.data = {
id: entry.id,
name: entry.name,
icon: entry.icon || props.defaultFolderIcon,
icon: entry.icon,
items: Array.isArray(entry.items) ? [...entry.items] : []
};
} else {
folderEditor.data = {
id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`,
name: '',
icon: props.defaultFolderIcon,
icon: '',
items: []
};
}
@@ -399,6 +441,7 @@
const applyFolderChanges = () => {
const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key));
const sanitizedIcon = folderEditor.data.icon?.trim() || '';
const entries = [...localLayout.value];
if (folderEditor.isEditing) {
@@ -421,7 +464,7 @@
type: 'folder',
id: folderEditor.data.id,
name: folderEditor.data.name.trim(),
icon: folderEditor.data.icon || props.defaultFolderIcon,
icon: sanitizedIcon,
items: sanitizedItems
});
@@ -442,7 +485,7 @@
type: 'folder',
id: folderEditor.data.id,
name: folderEditor.data.name.trim(),
icon: folderEditor.data.icon || props.defaultFolderIcon,
icon: sanitizedIcon,
items: sanitizedItems
});

View File

@@ -6,64 +6,64 @@
</DialogHeader>
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ t('dialog.invite_to_group.description') }}</span>
<br />
<span>{{ t('dialog.invite_to_group.description') }}</span>
<br />
<div style="margin-top: 15px; width: 100%">
<VirtualCombobox
v-model="inviteGroupDialog.groupId"
:groups="groupPickerGroups"
:disabled="inviteGroupDialog.loading"
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
:search-placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</div>
<div style="width: 100%; margin-top: 15px">
<VirtualCombobox
v-model="inviteGroupDialog.userIds"
:groups="friendPickerGroups"
multiple
:disabled="inviteGroupDialog.loading"
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
:search-placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
:clearable="true">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
<div style="margin-top: 15px; width: 100%">
<VirtualCombobox
v-model="inviteGroupDialog.groupId"
:groups="groupPickerGroups"
:disabled="inviteGroupDialog.loading"
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
:search-placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
<span class="name" v-text="item.label"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</div>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</div>
<div style="width: 100%; margin-top: 15px">
<VirtualCombobox
v-model="inviteGroupDialog.userIds"
:groups="friendPickerGroups"
multiple
:disabled="inviteGroupDialog.loading"
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
:search-placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
:clearable="true">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</div>
</div>
<DialogFooter>
@@ -80,9 +80,9 @@
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Check as CheckIcon } from 'lucide-vue-next';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';

View File

@@ -5,69 +5,69 @@
<DialogTitle>{{ t('dialog.launch.header') }}</DialogTitle>
<DialogDescription class="sr-only">{{ t('dialog.launch.header') }}</DialogDescription>
</DialogHeader>
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.url"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.url)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
<Field v-if="launchDialog.shortUrl">
<FieldLabel>
<span class="flex items-center gap-1">
<span>{{ t('dialog.launch.short_url') }}</span>
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
<AlertTriangle />
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.url"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.url)"
><Copy
/></Button>
</TooltipWrapper>
</span>
</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.shortUrl"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.shortUrl)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.location"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.location)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
</FieldGroup>
</FieldContent>
</Field>
<Field v-if="launchDialog.shortUrl">
<FieldLabel>
<span class="flex items-center gap-1">
<span>{{ t('dialog.launch.short_url') }}</span>
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
<AlertTriangle />
</TooltipWrapper>
</span>
</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.shortUrl"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.shortUrl)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.location"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.location)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
</FieldGroup>
<DialogFooter>
<Button
class="mr-1.5"
@@ -129,8 +129,14 @@
</template>
<script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
@@ -138,6 +144,7 @@
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { AlertTriangle, Copy, MoreHorizontal } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group';

View File

@@ -6,37 +6,37 @@
</DialogHeader>
<div v-if="moderateGroupDialog.visible">
<div class="x-friend-item" style="cursor: default">
<div class="avatar">
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
<div class="x-friend-item" style="cursor: default">
<div class="avatar">
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
</div>
<div class="detail">
<span
v-if="moderateGroupDialog.userObject.id"
class="name"
:style="{ color: moderateGroupDialog.userObject.$userColour }"
v-text="moderateGroupDialog.userObject.displayName"></span>
<span v-else v-text="moderateGroupDialog.userId"></span>
</div>
</div>
<div class="detail">
<span
v-if="moderateGroupDialog.userObject.id"
class="name"
:style="{ color: moderateGroupDialog.userObject.$userColour }"
v-text="moderateGroupDialog.userObject.displayName"></span>
<span v-else v-text="moderateGroupDialog.userId"></span>
</div>
</div>
<div style="margin-top: 15px; width: 100%">
<VirtualCombobox
:model-value="moderateGroupDialog.groupId"
@update:modelValue="setGroupId"
:groups="groupPickerGroups"
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
:search-placeholder="t('dialog.moderate_group.choose_group_placeholder')"
:close-on-select="true">
<template #item="{ item, selected }">
<div class="flex w-full items-center gap-2">
<img :src="item.iconUrl" loading="lazy" class="size-5 rounded-sm" />
<span class="truncate text-sm" v-text="item.label"></span>
<span v-if="selected" class="ml-auto opacity-70"></span>
</div>
</template>
</VirtualCombobox>
</div>
<div style="margin-top: 15px; width: 100%">
<VirtualCombobox
:model-value="moderateGroupDialog.groupId"
@update:modelValue="setGroupId"
:groups="groupPickerGroups"
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
:search-placeholder="t('dialog.moderate_group.choose_group_placeholder')"
:close-on-select="true">
<template #item="{ item, selected }">
<div class="flex w-full items-center gap-2">
<img :src="item.iconUrl" loading="lazy" class="size-5 rounded-sm" />
<span class="truncate text-sm" v-text="item.label"></span>
<span v-if="selected" class="ml-auto opacity-70"></span>
</div>
</template>
</VirtualCombobox>
</div>
</div>
<DialogFooter>
@@ -54,9 +54,9 @@
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -5,208 +5,429 @@
<DialogTitle>{{ t('dialog.new_instance.header') }}</DialogTitle>
<DialogDescription class="sr-only">{{ t('dialog.new_instance.header') }}</DialogDescription>
</DialogHeader>
<TabsUnderline
v-model="newInstanceDialog.selectedTab"
:items="newInstanceTabs"
:unmount-on-hide="false"
@update:modelValue="newInstanceTabClick">
<template #Normal>
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildInstance();
}
">
<ToggleGroupItem
value="members"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
"
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
>
<ToggleGroupItem
value="plus"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
"
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
>
<ToggleGroupItem
value="public"
<TabsUnderline
v-model="newInstanceDialog.selectedTab"
:items="newInstanceTabs"
:unmount-on-hide="false"
@update:modelValue="newInstanceTabClick">
<template #Normal>
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildInstance();
}
">
<ToggleGroupItem
value="members"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-open-create'
)
"
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
>
<ToggleGroupItem
value="plus"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-plus-create'
)
"
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
>
<ToggleGroupItem
value="public"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-public-create'
) || newInstanceDialog.groupRef.privacy === 'private'
"
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.queueEnabled') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-public-create'
) || newInstanceDialog.groupRef.privacy === 'private'
'group-instance-age-gated-create'
)
"
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.queueEnabled') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
"
@update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
<FieldContent>
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="normalGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
@update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
<FieldContent>
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="normalGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.accessType === 'group' &&
newInstanceDialog.groupAccessType === 'members'
"
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<FieldContent>
<Select
multiple
:model-value="
Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []
"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in newInstanceDialog.selectedGroupRoles"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
<template v-if="newInstanceDialog.instanceCreated">
<Field>
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</template>
</FieldGroup>
</template>
<template #Legacy>
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="members">{{
t('dialog.new_instance.group_access_type_members')
}}</ToggleGroupItem>
<ToggleGroupItem value="plus">{{
t('dialog.new_instance.group_access_type_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="public">{{
t('dialog.new_instance.group_access_type_public')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.world_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.worldId"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.instance_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="sm"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.selectedTab === 'Legacy' &&
newInstanceDialog.accessType !== 'public' &&
newInstanceDialog.accessType !== 'group'
"
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.userId"
:groups="creatorPickerGroups"
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'
"
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<FieldContent>
<Select
multiple
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in newInstanceDialog.selectedGroupRoles"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
<template v-if="newInstanceDialog.instanceCreated">
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
<FieldContent>
@@ -223,220 +444,49 @@
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</template>
</FieldGroup>
</template>
<template #Legacy>
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="members">{{
t('dialog.new_instance.group_access_type_members')
}}</ToggleGroupItem>
<ToggleGroupItem value="plus">{{
t('dialog.new_instance.group_access_type_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="public">{{
t('dialog.new_instance.group_access_type_public')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.world_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.worldId"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.instance_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="sm"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.selectedTab === 'Legacy' &&
newInstanceDialog.accessType !== 'public' &&
newInstanceDialog.accessType !== 'group'
</FieldGroup>
</template>
</TabsUnderline>
<DialogFooter v-if="newInstanceDialog.selectedTab === 'Normal'">
<template v-if="newInstanceDialog.instanceCreated">
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
}}</Button>
<Button variant="outline" class="mr-2" @click="selfInvite(newInstanceDialog.location)">{{
t('dialog.new_instance.self_invite')
}}</Button>
<Button
variant="outline"
class="mr-2"
:disabled="
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
newInstanceDialog.userId !== currentUser.id
"
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.userId"
:groups="creatorPickerGroups"
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</FieldGroup>
</template>
</TabsUnderline>
<DialogFooter v-if="newInstanceDialog.selectedTab === 'Normal'">
<template v-if="newInstanceDialog.instanceCreated">
@click="showInviteDialog(newInstanceDialog.location)"
>{{ t('dialog.new_instance.invite') }}</Button
>
<template v-if="canOpenInstanceInGame">
<Button
variant="secondary"
class="mr-2"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</Button
>
<Button @click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
{{ t('dialog.new_instance.open_ingame') }}
</Button>
</template>
<template v-else>
<Button @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)">{{
t('dialog.new_instance.launch')
}}</Button>
</template>
</template>
<template v-else>
<Button @click="handleCreateNewInstance">{{ t('dialog.new_instance.create_instance') }}</Button>
</template>
</DialogFooter>
<DialogFooter v-else-if="newInstanceDialog.selectedTab === 'Legacy'">
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
}}</Button>
@@ -445,7 +495,6 @@
}}</Button>
<Button
variant="outline"
class="mr-2"
:disabled="
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
newInstanceDialog.userId !== currentUser.id
@@ -469,44 +518,7 @@
t('dialog.new_instance.launch')
}}</Button>
</template>
</template>
<template v-else>
<Button @click="handleCreateNewInstance">{{ t('dialog.new_instance.create_instance') }}</Button>
</template>
</DialogFooter>
<DialogFooter v-else-if="newInstanceDialog.selectedTab === 'Legacy'">
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
}}</Button>
<Button variant="outline" class="mr-2" @click="selfInvite(newInstanceDialog.location)">{{
t('dialog.new_instance.self_invite')
}}</Button>
<Button
variant="outline"
:disabled="
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
newInstanceDialog.userId !== currentUser.id
"
@click="showInviteDialog(newInstanceDialog.location)"
>{{ t('dialog.new_instance.invite') }}</Button
>
<template v-if="canOpenInstanceInGame">
<Button
variant="secondary"
class="mr-2"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</Button
>
<Button @click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
{{ t('dialog.new_instance.open_ingame') }}
</Button>
</template>
<template v-else>
<Button @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)">{{
t('dialog.new_instance.launch')
}}</Button>
</template>
</DialogFooter>
</DialogFooter>
</DialogContent>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
@@ -514,8 +526,15 @@
</template>
<script setup>
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Check as CheckIcon } from 'lucide-vue-next';

View File

@@ -4,79 +4,79 @@
<DialogHeader>
<DialogTitle>{{ t('dialog.boop_dialog.header') }}</DialogTitle>
</DialogHeader>
<span>{{ displayName }}</span>
<span>{{ displayName }}</span>
<br />
<br />
<br />
<br />
<div v-if="sendBoopDialog.visible" style="width: 100%">
<VirtualCombobox
v-model="emojiModel"
:groups="emojiPickerGroups"
:placeholder="t('dialog.boop_dialog.select_default_emoji')"
:search-placeholder="t('dialog.boop_dialog.select_default_emoji')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true">
<template #item="{ item, selected }">
<span v-text="item.label"></span>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</template>
</VirtualCombobox>
</div>
<div v-if="sendBoopDialog.visible" style="width: 100%">
<VirtualCombobox
v-model="emojiModel"
:groups="emojiPickerGroups"
:placeholder="t('dialog.boop_dialog.select_default_emoji')"
:search-placeholder="t('dialog.boop_dialog.select_default_emoji')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true">
<template #item="{ item, selected }">
<span v-text="item.label"></span>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</template>
</VirtualCombobox>
</div>
<br />
<br />
<br />
<br />
<div
v-if="isLocalUserVrcPlusSupporter"
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-top: 10px;
max-height: 600px;
overflow-y: auto;
">
<div
v-for="image in emojiTable"
:key="image.id"
:class="image.id === fileId ? 'x-image-selected' : ''"
style="cursor: pointer; border: 1px solid transparent; border-radius: 8px"
@click="fileId = image.id">
v-if="isLocalUserVrcPlusSupporter"
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-top: 10px;
max-height: 600px;
overflow-y: auto;
">
<div
v-if="
image.versions &&
image.versions.length > 0 &&
image.versions[image.versions.length - 1].file.url
"
class="x-popover-image"
style="padding: 8px">
<Emoji :imageUrl="image.versions[image.versions.length - 1].file.url" :size="100"></Emoji>
v-for="image in emojiTable"
:key="image.id"
:class="image.id === fileId ? 'x-image-selected' : ''"
style="cursor: pointer; border: 1px solid transparent; border-radius: 8px"
@click="fileId = image.id">
<div
v-if="
image.versions &&
image.versions.length > 0 &&
image.versions[image.versions.length - 1].file.url
"
class="x-popover-image"
style="padding: 8px">
<Emoji :imageUrl="image.versions[image.versions.length - 1].file.url" :size="100"></Emoji>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button size="sm" variant="outline" class="mr-2" @click="showGalleryPage">{{
t('dialog.boop_dialog.emoji_manager')
}}</Button>
<Button size="sm" variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.boop_dialog.cancel')
}}</Button>
<Button size="sm" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
t('dialog.boop_dialog.send')
}}</Button>
<Button size="sm" variant="outline" class="mr-2" @click="showGalleryPage">{{
t('dialog.boop_dialog.emoji_manager')
}}</Button>
<Button size="sm" variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.boop_dialog.cancel')
}}</Button>
<Button size="sm" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
t('dialog.boop_dialog.send')
}}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Check as CheckIcon } from 'lucide-vue-next';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -64,8 +64,7 @@
</template>
<Location
:location="userDialog.ref.location"
:traveling="userDialog.ref.travelingToLocation"
style="display: block; margin-top: 5px" />
:traveling="userDialog.ref.travelingToLocation" />
</div>
<div class="x-friend-list" style="flex: 1; margin-top: 10px; max-height: 150px">
<div

View File

@@ -5,86 +5,86 @@
<DialogTitle>{{ t('dialog.vrcx_updater.header') }}</DialogTitle>
</DialogHeader>
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
<template v-if="updateInProgress">
<Progress :model-value="updateProgress" class="w-full" />
<div class="mt-2 text-xs" v-text="updateProgressText()"></div>
<br />
</template>
<template v-else>
<div v-if="VRCXUpdateDialog.updatePending" style="margin-bottom: 15px">
<span>{{ pendingVRCXInstall }}</span>
<template v-if="updateInProgress">
<Progress :model-value="updateProgress" class="w-full" />
<div class="mt-2 text-xs" v-text="updateProgressText()"></div>
<br />
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
</div>
<Tabs :model-value="branch" class="w-full" @update:modelValue="handleBranchChange">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="Stable">{{ t('dialog.vrcx_updater.branch_stable') }}</TabsTrigger>
<TabsTrigger value="Nightly">{{ t('dialog.vrcx_updater.branch_nightly') }}</TabsTrigger>
</TabsList>
<TabsContent value="Nightly">
<Alert variant="destructive">
<AlertCircle class="text-muted-foreground" />
<AlertTitle>{{ t('dialog.vrcx_updater.nightly_title') }}</AlertTitle>
<AlertDescription>
{{ t('dialog.vrcx_updater.nightly_notice') }}
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
<FieldGroup class="mt-3">
<Field>
<FieldLabel>{{ t('dialog.vrcx_updater.release') }}</FieldLabel>
<FieldContent>
<Select
:model-value="VRCXUpdateDialog.release"
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="item in VRCXUpdateDialog.releases"
:key="item.name"
:value="item.name">
{{ item.tag_name }}
</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
</FieldGroup>
<div
v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion"
class="mt-3 text-xs text-muted-foreground">
<span>{{ t('dialog.vrcx_updater.latest_version') }}</span>
</div>
</template>
</template>
<template v-else>
<div v-if="VRCXUpdateDialog.updatePending" style="margin-bottom: 15px">
<span>{{ pendingVRCXInstall }}</span>
<br />
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
</div>
<Tabs :model-value="branch" class="w-full" @update:modelValue="handleBranchChange">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="Stable">{{ t('dialog.vrcx_updater.branch_stable') }}</TabsTrigger>
<TabsTrigger value="Nightly">{{ t('dialog.vrcx_updater.branch_nightly') }}</TabsTrigger>
</TabsList>
<TabsContent value="Nightly">
<Alert variant="destructive">
<AlertCircle class="text-muted-foreground" />
<AlertTitle>{{ t('dialog.vrcx_updater.nightly_title') }}</AlertTitle>
<AlertDescription>
{{ t('dialog.vrcx_updater.nightly_notice') }}
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
<FieldGroup class="mt-3">
<Field>
<FieldLabel>{{ t('dialog.vrcx_updater.release') }}</FieldLabel>
<FieldContent>
<Select
:model-value="VRCXUpdateDialog.release"
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="item in VRCXUpdateDialog.releases"
:key="item.name"
:value="item.name">
{{ item.tag_name }}
</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
</FieldGroup>
<div
v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion"
class="mt-3 text-xs text-muted-foreground">
<span>{{ t('dialog.vrcx_updater.latest_version') }}</span>
</div>
</template>
</div>
<DialogFooter>
<Button variant="secondary" class="mr-2" v-if="updateInProgress" @click="cancelUpdate">
{{ t('dialog.vrcx_updater.cancel') }}
</Button>
<Button
variant="default"
v-if="VRCXUpdateDialog.release !== pendingVRCXInstall"
:disabled="updateInProgress"
@click="installVRCXUpdate">
{{ t('dialog.vrcx_updater.download') }}
</Button>
<Button variant="default" v-if="!updateInProgress && pendingVRCXInstall" @click="restartVRCX(true)">
{{ t('dialog.vrcx_updater.install') }}
</Button>
<Button variant="secondary" class="mr-2" v-if="updateInProgress" @click="cancelUpdate">
{{ t('dialog.vrcx_updater.cancel') }}
</Button>
<Button
variant="default"
v-if="VRCXUpdateDialog.release !== pendingVRCXInstall"
:disabled="updateInProgress"
@click="installVRCXUpdate">
{{ t('dialog.vrcx_updater.download') }}
</Button>
<Button variant="default" v-if="!updateInProgress && pendingVRCXInstall" @click="restartVRCX(true)">
{{ t('dialog.vrcx_updater.install') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { AlertCircle } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';