replace el-dropdown

This commit is contained in:
pa
2026-01-08 22:41:05 +09:00
committed by Natsumi
parent 3d37cebefc
commit 4eabeb11e0
21 changed files with 799 additions and 683 deletions

View File

@@ -1326,6 +1326,8 @@ i.x-status-icon.red {
.options-container-item { .options-container-item {
font-size: 12px; font-size: 12px;
margin-top: 5px; margin-top: 5px;
display: flex;
align-items: center;
} }
.options-container-item .name { .options-container-item .name {

View File

@@ -54,64 +54,60 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button <div class="flex justify-end">
:disabled="!checkCanInvite(launchDialog.location)" <Button
@click="showInviteDialog(launchDialog.location)"> class="mr-1.5"
{{ t('dialog.launch.invite') }} variant="outline"
</el-button> :disabled="!checkCanInvite(launchDialog.location)"
<template v-if="canOpenInstanceInGame"> @click="showInviteDialog(launchDialog.location)">
<el-button {{ t('dialog.launch.invite') }}
</Button>
<Button
v-if="canOpenInstanceInGame"
variant="outline"
:disabled="!launchDialog.secureOrShortName" :disabled="!launchDialog.secureOrShortName"
@click="handleAttachGame(launchDialog.location, launchDialog.shortName)"> @click="handleAttachGame(launchDialog.location, launchDialog.shortName)">
{{ t('dialog.launch.open_ingame') }} {{ t('dialog.launch.open_ingame') }}
</el-button> </Button>
<el-dropdown <Button
split-button v-else
type="primary" variant="outline"
:disabled="!launchDialog.secureOrShortName"
@click="handleLaunchDefault(launchDialog.location, launchDialog.shortName)"
@command="(cmd) => handleLaunchCommand(cmd, launchDialog.location, launchDialog.shortName)">
{{ launchModeLabel }}
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="launchDialog.desktop ? 'vr' : 'desktop'">
{{
launchDialog.desktop
? t('dialog.launch.launch')
: t('dialog.launch.start_as_desktop')
}}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button
class="mr-1.25" class="mr-1.25"
:disabled="!launchDialog.secureOrShortName" :disabled="!launchDialog.secureOrShortName"
@click="selfInvite(launchDialog.location, launchDialog.shortName)"> @click="selfInvite(launchDialog.location, launchDialog.shortName)">
{{ t('dialog.launch.self_invite') }} {{ t('dialog.launch.self_invite') }}
</el-button> </Button>
<el-dropdown <ButtonGroup>
split-button <Button
type="primary" :disabled="!launchDialog.secureOrShortName"
:disabled="!launchDialog.secureOrShortName" @click="handleLaunchDefault(launchDialog.location, launchDialog.shortName)">
@click="handleLaunchDefault(launchDialog.location, launchDialog.shortName)" {{ launchModeLabel }}
@command="(cmd) => handleLaunchCommand(cmd, launchDialog.location, launchDialog.shortName)"> </Button>
{{ launchModeLabel }} <DropdownMenu>
<template #dropdown> <DropdownMenuTrigger as-child>
<el-dropdown-menu> <Button size="icon" :disabled="!launchDialog.secureOrShortName" aria-label="More options">
<el-dropdown-item :command="launchDialog.desktop ? 'vr' : 'desktop'"> <MoreHorizontal class="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-52">
<DropdownMenuItem
@click="
handleLaunchCommand(
launchDialog.desktop ? 'vr' : 'desktop',
launchDialog.location,
launchDialog.shortName
)
">
{{ {{
launchDialog.desktop launchDialog.desktop
? t('dialog.launch.launch') ? t('dialog.launch.launch')
: t('dialog.launch.start_as_desktop') : t('dialog.launch.start_as_desktop')
}} }}
</el-dropdown-item> </DropdownMenuItem>
</el-dropdown-menu> </DropdownMenuContent>
</template> </DropdownMenu>
</el-dropdown> </ButtonGroup>
</template> </div>
</template> </template>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" /> <InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</el-dialog> </el-dialog>
@@ -119,8 +115,17 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { CopyDocument, Warning } from '@element-plus/icons-vue'; import { CopyDocument, Warning } from '@element-plus/icons-vue';
import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { MoreHorizontal } from 'lucide-vue-next';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@@ -1,12 +1,12 @@
<script setup> <script setup>
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { buttonGroupVariants } from '.'; import { buttonGroupVariants } from '.';
const props = defineProps({ const props = defineProps({
class: { type: null, required: false }, class: { type: null, required: false },
orientation: { type: null, required: false } orientation: { type: null, required: false }
}); });
</script> </script>
<template> <template>

View File

@@ -1,16 +1,16 @@
<script setup> <script setup>
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core'; import { reactiveOmit } from '@vueuse/core';
const props = defineProps({ const props = defineProps({
orientation: { type: String, required: false, default: 'vertical' }, orientation: { type: String, required: false, default: 'vertical' },
decorative: { type: Boolean, required: false }, decorative: { type: Boolean, required: false },
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },
as: { type: null, required: false }, as: { type: null, required: false },
class: { type: null, required: false } class: { type: null, required: false }
}); });
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
</script> </script>
<template> <template>

View File

@@ -1,13 +1,13 @@
<script setup> <script setup>
import { Primitive } from 'reka-ui'; import { Primitive } from 'reka-ui';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const props = defineProps({ const props = defineProps({
class: { type: null, required: false }, class: { type: null, required: false },
orientation: { type: null, required: false }, orientation: { type: null, required: false },
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: 'div' } as: { type: null, required: false, default: 'div' }
}); });
</script> </script>
<template> <template>

View File

@@ -1,26 +1,26 @@
<script setup> <script setup>
import { SelectRoot, useForwardPropsEmits } from "reka-ui"; import { SelectRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps({ const props = defineProps({
open: { type: Boolean, required: false }, open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false }, defaultOpen: { type: Boolean, required: false },
defaultValue: { type: null, required: false }, defaultValue: { type: null, required: false },
modelValue: { type: null, required: false }, modelValue: { type: null, required: false },
by: { type: [String, Function], required: false }, by: { type: [String, Function], required: false },
dir: { type: String, required: false }, dir: { type: String, required: false },
multiple: { type: Boolean, required: false }, multiple: { type: Boolean, required: false },
autocomplete: { type: String, required: false }, autocomplete: { type: String, required: false },
disabled: { type: Boolean, required: false }, disabled: { type: Boolean, required: false },
name: { type: String, required: false }, name: { type: String, required: false },
required: { type: Boolean, required: false }, required: { type: Boolean, required: false }
}); });
const emits = defineEmits(["update:modelValue", "update:open"]); const emits = defineEmits(['update:modelValue', 'update:open']);
const forwarded = useForwardPropsEmits(props, emits); const forwarded = useForwardPropsEmits(props, emits);
</script> </script>
<template> <template>
<SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded"> <SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded">
<slot v-bind="slotProps" /> <slot v-bind="slotProps" />
</SelectRoot> </SelectRoot>
</template> </template>

View File

@@ -1,82 +1,72 @@
<script setup> <script setup>
import { reactiveOmit } from "@vueuse/core"; import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits } from 'reka-ui';
import { import { cn } from '@/lib/utils';
SelectContent, import { reactiveOmit } from '@vueuse/core';
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { SelectScrollDownButton, SelectScrollUpButton } from ".";
defineOptions({ import { SelectScrollDownButton, SelectScrollUpButton } from '.';
inheritAttrs: false,
});
const props = defineProps({ defineOptions({
forceMount: { type: Boolean, required: false }, inheritAttrs: false
position: { type: String, required: false, default: "popper" }, });
bodyLock: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"closeAutoFocus",
"escapeKeyDown",
"pointerDownOutside",
]);
const delegatedProps = reactiveOmit(props, "class"); const props = defineProps({
forceMount: { type: Boolean, required: false },
position: { type: String, required: false, default: 'popper' },
bodyLock: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(['closeAutoFocus', 'escapeKeyDown', 'pointerDownOutside']);
const forwarded = useForwardPropsEmits(delegatedProps, emits); const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script> </script>
<template> <template>
<SelectPortal> <SelectPortal>
<SelectContent <SelectContent
data-slot="select-content" data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class=" :class="
cn( cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-10000 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' && position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class, props.class
) )
" ">
> <SelectScrollUpButton />
<SelectScrollUpButton /> <SelectViewport
<SelectViewport :class="
:class=" cn(
cn( 'p-1',
'p-1', position === 'popper' &&
position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1'
'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1', )
) ">
" <slot />
> </SelectViewport>
<slot /> <SelectScrollDownButton />
</SelectViewport> </SelectContent>
<SelectScrollDownButton /> </SelectPortal>
</SelectContent>
</SelectPortal>
</template> </template>

View File

@@ -1,20 +1,20 @@
<script setup> <script setup>
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'; import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui';
import { ChevronDown } from 'lucide-vue-next'; import { ChevronDown } from 'lucide-vue-next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core'; import { reactiveOmit } from '@vueuse/core';
const props = defineProps({ const props = defineProps({
disabled: { type: Boolean, required: false }, disabled: { type: Boolean, required: false },
reference: { type: null, required: false }, reference: { type: null, required: false },
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },
as: { type: null, required: false }, as: { type: null, required: false },
class: { type: null, required: false }, class: { type: null, required: false },
size: { type: String, required: false, default: 'default' } size: { type: String, required: false, default: 'default' }
}); });
const delegatedProps = reactiveOmit(props, 'class', 'size'); const delegatedProps = reactiveOmit(props, 'class', 'size');
const forwardedProps = useForwardProps(delegatedProps); const forwardedProps = useForwardProps(delegatedProps);
</script> </script>
<template> <template>

View File

@@ -1,17 +1,17 @@
<script setup> <script setup>
import { Separator } from 'reka-ui'; import { Separator } from 'reka-ui';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core'; import { reactiveOmit } from '@vueuse/core';
const props = defineProps({ const props = defineProps({
orientation: { type: String, required: false, default: 'horizontal' }, orientation: { type: String, required: false, default: 'horizontal' },
decorative: { type: Boolean, required: false, default: true }, decorative: { type: Boolean, required: false, default: true },
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },
as: { type: null, required: false }, as: { type: null, required: false },
class: { type: null, required: false } class: { type: null, required: false }
}); });
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
</script> </script>
<template> <template>

View File

@@ -9,65 +9,43 @@
</template> </template>
</el-checkbox-group> </el-checkbox-group>
<el-dropdown trigger="click" size="small"> <div class="flex items-center gap-2">
<el-button size="small"> <Select
<span v-if="avatarExportFavoriteGroup"> :model-value="avatarExportFavoriteGroupSelection"
{{ avatarExportFavoriteGroup.displayName }} ({{ avatarExportFavoriteGroup.count }}/{{ @update:modelValue="handleAvatarExportFavoriteGroupSelect">
avatarExportFavoriteGroup.capacity <SelectTrigger size="sm">
}}) <SelectValue placeholder="All Favorites" />
<el-icon class="el-icon--right"><ArrowDown /></el-icon> </SelectTrigger>
</span> <SelectContent>
<span v-else> <SelectGroup>
All Favorites <SelectItem :value="AVATAR_EXPORT_ALL_VALUE">All Favorites</SelectItem>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem
</span> v-for="groupAPI in favoriteAvatarGroups"
</el-button> :key="groupAPI.name"
<template #dropdown> :value="groupAPI.name">
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectAvatarExportGroup(null)">
All Favorites
</el-dropdown-item>
<template v-for="groupAPI in favoriteAvatarGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin-left: 10px"> <Select
<el-button size="small"> :model-value="avatarExportLocalFavoriteGroupSelection"
<span v-if="avatarExportLocalFavoriteGroup"> @update:modelValue="handleAvatarExportLocalFavoriteGroupSelect"
{{ avatarExportLocalFavoriteGroup }} ({{ style="margin-left: 10px">
localAvatarFavGroupLength(avatarExportLocalFavoriteGroup) <SelectTrigger size="sm">
}}) <SelectValue placeholder="Select Group" />
<el-icon class="el-icon--right"><ArrowDown /></el-icon> </SelectTrigger>
</span> <SelectContent>
<span v-else> <SelectGroup>
Select Group <SelectItem :value="AVATAR_EXPORT_NONE_VALUE">None</SelectItem>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarExportLocalGroup(null)">
None
</el-dropdown-item>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarExportLocalGroup(group)">
{{ group }} ({{ localAvatarFavGroupLength(group) }}) {{ group }} ({{ localAvatarFavGroupLength(group) }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown> </div>
<br /> <br />
<el-input <el-input
v-model="avatarExportContent" v-model="avatarExportContent"
@@ -82,8 +60,8 @@
</template> </template>
<script setup> <script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -114,6 +92,12 @@
const avatarExportContent = ref(''); const avatarExportContent = ref('');
const avatarExportFavoriteGroup = ref(null); const avatarExportFavoriteGroup = ref(null);
const avatarExportLocalFavoriteGroup = ref(null); const avatarExportLocalFavoriteGroup = ref(null);
const AVATAR_EXPORT_ALL_VALUE = '__all__';
const AVATAR_EXPORT_NONE_VALUE = '__none__';
const avatarExportFavoriteGroupSelection = ref(AVATAR_EXPORT_ALL_VALUE);
const avatarExportLocalFavoriteGroupSelection = ref(AVATAR_EXPORT_NONE_VALUE);
const exportSelectedOptions = ref(['ID', 'Name']); const exportSelectedOptions = ref(['ID', 'Name']);
const exportSelectOptions = ref([ const exportSelectOptions = ref([
{ label: 'ID', value: 'id' }, { label: 'ID', value: 'id' },
@@ -144,8 +128,29 @@
function showAvatarExportDialog() { function showAvatarExportDialog() {
avatarExportFavoriteGroup.value = null; avatarExportFavoriteGroup.value = null;
avatarExportLocalFavoriteGroup.value = null; avatarExportLocalFavoriteGroup.value = null;
avatarExportFavoriteGroupSelection.value = AVATAR_EXPORT_ALL_VALUE;
avatarExportLocalFavoriteGroupSelection.value = AVATAR_EXPORT_NONE_VALUE;
updateAvatarExportDialog(); updateAvatarExportDialog();
} }
function handleAvatarExportFavoriteGroupSelect(value) {
avatarExportFavoriteGroupSelection.value = value;
if (value === AVATAR_EXPORT_ALL_VALUE) {
selectAvatarExportGroup(null);
return;
}
const group = favoriteAvatarGroups.value.find((g) => g.name === value) || null;
selectAvatarExportGroup(group);
}
function handleAvatarExportLocalFavoriteGroupSelect(value) {
avatarExportLocalFavoriteGroupSelection.value = value;
if (value === AVATAR_EXPORT_NONE_VALUE) {
selectAvatarExportLocalGroup(null);
return;
}
selectAvatarExportLocalGroup(value);
}
function handleCopyAvatarExportData(event) { function handleCopyAvatarExportData(event) {
if (event.target.tagName === 'TEXTAREA') { if (event.target.tagName === 'TEXTAREA') {
event.target.select(); event.target.select();
@@ -161,11 +166,24 @@
}); });
} }
function updateAvatarExportDialog() { function updateAvatarExportDialog() {
const formatter = function (str) { const needsCsvQuotes = (text) => {
if (/[\x00-\x1f,"]/.test(str) === true) { for (let i = 0; i < text.length; i++) {
return `"${str.replace(/"/g, '""')}"`; if (text.charCodeAt(i) < 0x20) {
return true;
}
} }
return str; return text.includes(',') || text.includes('"');
};
const formatter = function (value) {
if (value === null || typeof value === 'undefined') {
return '';
}
const text = String(value);
if (needsCsvQuotes(text)) {
return `"${text.replace(/"/g, '""')}"`;
}
return text;
}; };
const propsForQuery = exportSelectOptions.value const propsForQuery = exportSelectOptions.value
.filter((option) => exportSelectedOptions.value.includes(option.label)) .filter((option) => exportSelectedOptions.value.includes(option.label))
@@ -205,8 +223,8 @@
favoriteAvatars.value.forEach((ref) => { favoriteAvatars.value.forEach((ref) => {
lines.push(resText(ref.ref)); lines.push(resText(ref.ref));
}); });
for (let i = 0; i < localAvatarFavoritesList.length; ++i) { for (let i = 0; i < localAvatarFavoritesList.value.length; ++i) {
const avatarId = localAvatarFavoritesList[i]; const avatarId = localAvatarFavoritesList.value[i];
const ref = cachedAvatars.get(avatarId); const ref = cachedAvatars.get(avatarId);
if (typeof ref !== 'undefined') { if (typeof ref !== 'undefined') {
lines.push(resText(ref)); lines.push(resText(ref));
@@ -218,11 +236,15 @@
function selectAvatarExportGroup(group) { function selectAvatarExportGroup(group) {
avatarExportFavoriteGroup.value = group; avatarExportFavoriteGroup.value = group;
avatarExportLocalFavoriteGroup.value = null; avatarExportLocalFavoriteGroup.value = null;
avatarExportFavoriteGroupSelection.value = group?.name ?? AVATAR_EXPORT_ALL_VALUE;
avatarExportLocalFavoriteGroupSelection.value = AVATAR_EXPORT_NONE_VALUE;
updateAvatarExportDialog(); updateAvatarExportDialog();
} }
function selectAvatarExportLocalGroup(group) { function selectAvatarExportLocalGroup(group) {
avatarExportLocalFavoriteGroup.value = group; avatarExportLocalFavoriteGroup.value = group;
avatarExportFavoriteGroup.value = null; avatarExportFavoriteGroup.value = null;
avatarExportLocalFavoriteGroupSelection.value = group ?? AVATAR_EXPORT_NONE_VALUE;
avatarExportFavoriteGroupSelection.value = AVATAR_EXPORT_ALL_VALUE;
updateAvatarExportDialog(); updateAvatarExportDialog();
} }
</script> </script>

View File

@@ -29,57 +29,43 @@
style="margin-top: 10px"></el-input> style="margin-top: 10px"></el-input>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px"> <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div> <div>
<el-dropdown trigger="click" size="small" style="margin-right: 5px" @click.stop> <div class="flex items-center gap-2">
<el-button size="small"> <Select
<span v-if="avatarImportDialog.avatarImportFavoriteGroup"> :model-value="avatarImportFavoriteGroupSelection"
{{ avatarImportDialog.avatarImportFavoriteGroup.displayName }} ({{ @update:modelValue="handleAvatarImportGroupSelect"
avatarImportDialog.avatarImportFavoriteGroup.count style="margin-right: 5px">
}}/{{ avatarImportDialog.avatarImportFavoriteGroup.capacity }}) <SelectTrigger size="sm">
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
</span> </SelectTrigger>
<span v-else> <SelectContent>
{{ t('dialog.avatar_import.select_group_placeholder') }} <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem
</span> v-for="groupAPI in favoriteAvatarGroups"
</el-button> :key="groupAPI.name"
<template #dropdown> :value="groupAPI.name"
<el-dropdown-menu> :disabled="groupAPI.count >= groupAPI.capacity">
<template v-for="groupAPI in favoriteAvatarGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectAvatarImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown>
<el-dropdown trigger="click" size="small"> <Select
<el-button size="small"> :model-value="avatarImportLocalFavoriteGroupSelection"
<span v-if="avatarImportDialog.avatarImportLocalFavoriteGroup"> @update:modelValue="handleAvatarImportLocalGroupSelect"
{{ avatarImportDialog.avatarImportLocalFavoriteGroup }} ({{ style="margin-left: 10px">
localAvatarFavGroupLength(avatarImportDialog.avatarImportLocalFavoriteGroup) <SelectTrigger size="sm">
}}) <SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
<el-icon class="el-icon--right"><ArrowDown /></el-icon> </SelectTrigger>
</span> <SelectContent>
<span v-else> <SelectGroup>
{{ t('dialog.avatar_import.select_group_placeholder') }} <SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarImportLocalGroup(group)">
{{ group }} ({{ localAvatarFavGroupLength(group) }}) {{ group }} ({{ localAvatarFavGroupLength(group) }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown> </div>
<span v-if="avatarImportDialog.avatarImportFavoriteGroup" style="margin-left: 5px"> <span v-if="avatarImportDialog.avatarImportFavoriteGroup" style="margin-left: 5px">
{{ avatarImportTable.data.length }} / {{ avatarImportTable.data.length }} /
{{ {{
@@ -176,8 +162,9 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, Close, Loading } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Close, Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -209,6 +196,9 @@
importProgressTotal: 0 importProgressTotal: 0
}); });
const avatarImportFavoriteGroupSelection = ref('');
const avatarImportLocalFavoriteGroupSelection = ref('');
const avatarImportTable = ref({ const avatarImportTable = ref({
data: [], data: [],
tableProps: { tableProps: {
@@ -305,11 +295,26 @@
function selectAvatarImportGroup(group) { function selectAvatarImportGroup(group) {
avatarImportDialog.value.avatarImportLocalFavoriteGroup = null; avatarImportDialog.value.avatarImportLocalFavoriteGroup = null;
avatarImportDialog.value.avatarImportFavoriteGroup = group; avatarImportDialog.value.avatarImportFavoriteGroup = group;
avatarImportFavoriteGroupSelection.value = group?.name ?? '';
avatarImportLocalFavoriteGroupSelection.value = '';
} }
function selectAvatarImportLocalGroup(group) { function selectAvatarImportLocalGroup(group) {
avatarImportDialog.value.avatarImportFavoriteGroup = null; avatarImportDialog.value.avatarImportFavoriteGroup = null;
avatarImportDialog.value.avatarImportLocalFavoriteGroup = group; avatarImportDialog.value.avatarImportLocalFavoriteGroup = group;
avatarImportFavoriteGroupSelection.value = '';
avatarImportLocalFavoriteGroupSelection.value = group ?? '';
}
function handleAvatarImportGroupSelect(value) {
avatarImportFavoriteGroupSelection.value = value;
const group = favoriteAvatarGroups.value.find((g) => g.name === value) ?? null;
selectAvatarImportGroup(group);
}
function handleAvatarImportLocalGroupSelect(value) {
avatarImportLocalFavoriteGroupSelection.value = value;
selectAvatarImportLocalGroup(value || null);
} }
function cancelAvatarImport() { function cancelAvatarImport() {

View File

@@ -5,34 +5,22 @@
:title="t('dialog.friend_export.header')" :title="t('dialog.friend_export.header')"
width="650px" width="650px"
destroy-on-close> destroy-on-close>
<el-dropdown trigger="click" size="small"> <Select :model-value="friendExportFavoriteGroupSelection" @update:modelValue="handleFriendExportGroupSelect">
<el-button size="small"> <SelectTrigger size="sm">
<span v-if="friendExportFavoriteGroup"> <SelectValue placeholder="All Favorites" />
{{ friendExportFavoriteGroup.displayName }} ({{ friendExportFavoriteGroup.count }}/{{ </SelectTrigger>
friendExportFavoriteGroup.capacity <SelectContent>
}}) <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem :value="FRIEND_EXPORT_ALL_VALUE">All Favorites</SelectItem>
</span> <SelectItem v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name" :value="groupAPI.name">
<span v-else {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
>All Favorites <el-icon class="el-icon--right"><ArrowDown /></el-icon </SelectItem>
></span> </SelectGroup>
</el-button> </SelectContent>
<template #dropdown> </Select>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectFriendExportGroup(null)">
All Favorites
</el-dropdown-item>
<template v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectFriendExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<br /> <br />
<el-input <el-input
v-model="friendExportContent" v-model="friendExportContent"
type="textarea" type="textarea"
@@ -46,8 +34,8 @@
</template> </template>
<script setup> <script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -68,6 +56,8 @@
const { favoriteFriends, favoriteFriendGroups } = storeToRefs(useFavoriteStore()); const { favoriteFriends, favoriteFriendGroups } = storeToRefs(useFavoriteStore());
const friendExportFavoriteGroup = ref(null); const friendExportFavoriteGroup = ref(null);
const FRIEND_EXPORT_ALL_VALUE = '__all__';
const friendExportFavoriteGroupSelection = ref(FRIEND_EXPORT_ALL_VALUE);
const friendExportContent = ref(''); const friendExportContent = ref('');
const isDialogVisible = computed({ const isDialogVisible = computed({
@@ -90,9 +80,20 @@
function showFriendExportDialog() { function showFriendExportDialog() {
friendExportFavoriteGroup.value = null; friendExportFavoriteGroup.value = null;
friendExportFavoriteGroupSelection.value = FRIEND_EXPORT_ALL_VALUE;
updateFriendExportDialog(); updateFriendExportDialog();
} }
function handleFriendExportGroupSelect(value) {
friendExportFavoriteGroupSelection.value = value;
if (value === FRIEND_EXPORT_ALL_VALUE) {
selectFriendExportGroup(null);
return;
}
const group = favoriteFriendGroups.value.find((g) => g.name === value) || null;
selectFriendExportGroup(group);
}
function handleCopyFriendExportData(event) { function handleCopyFriendExportData(event) {
if (event.target.tagName === 'TEXTAREA') { if (event.target.tagName === 'TEXTAREA') {
event.target.select(); event.target.select();
@@ -109,18 +110,31 @@
} }
function updateFriendExportDialog() { function updateFriendExportDialog() {
const _ = function (str) { const needsCsvQuotes = (text) => {
if (/[\x00-\x1f,"]/.test(str) === true) { for (let i = 0; i < text.length; i++) {
return `"${str.replace(/"/g, '""')}"`; if (text.charCodeAt(i) < 0x20) {
return true;
}
} }
return str; return text.includes(',') || text.includes('"');
};
const formatter = function (value) {
if (value === null || typeof value === 'undefined') {
return '';
}
const text = String(value);
if (needsCsvQuotes(text)) {
return `"${text.replace(/"/g, '""')}"`;
}
return text;
}; };
const lines = ['UserID,Name']; const lines = ['UserID,Name'];
favoriteFriendGroups.value.forEach((group) => { favoriteFriendGroups.value.forEach((group) => {
if (!friendExportFavoriteGroup.value || friendExportFavoriteGroup.value === group) { if (!friendExportFavoriteGroup.value || friendExportFavoriteGroup.value === group) {
favoriteFriends.value.forEach((ref) => { favoriteFriends.value.forEach((ref) => {
if (group.key === ref.groupKey) { if (group.key === ref.groupKey) {
lines.push(`${_(ref.id)},${_(ref.name)}`); lines.push(`${formatter(ref.id)},${formatter(ref.name)}`);
} }
}); });
} }
@@ -130,6 +144,7 @@
function selectFriendExportGroup(group) { function selectFriendExportGroup(group) {
friendExportFavoriteGroup.value = group; friendExportFavoriteGroup.value = group;
friendExportFavoriteGroupSelection.value = group?.name ?? FRIEND_EXPORT_ALL_VALUE;
updateFriendExportDialog(); updateFriendExportDialog();
} }
</script> </script>

View File

@@ -29,32 +29,24 @@
style="margin-top: 10px" /> style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px"> <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div> <div>
<el-dropdown trigger="click" size="small"> <Select
<el-button size="small"> :model-value="friendImportFavoriteGroupSelection"
<span v-if="friendImportDialog.friendImportFavoriteGroup"> @update:modelValue="handleFriendImportGroupSelect">
{{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{ <SelectTrigger size="sm">
friendImportDialog.friendImportFavoriteGroup.count <SelectValue :placeholder="t('dialog.friend_import.select_group_placeholder')" />
}}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }}) </SelectTrigger>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectContent>
</span> <SelectGroup>
<span v-else <SelectItem
>{{ t('dialog.friend_import.select_group_placeholder') }} v-for="groupAPI in favoriteFriendGroups"
<el-icon class="el-icon--right"><ArrowDown /></el-icon :key="groupAPI.name"
></span> :value="groupAPI.name"
</el-button> :disabled="groupAPI.count >= groupAPI.capacity">
<template #dropdown> {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
<el-dropdown-menu> </SelectItem>
<template v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name"> </SelectGroup>
<el-dropdown-item </SelectContent>
style="display: block; margin: 10px 0" </Select>
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectFriendImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px"> <span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px">
{{ friendImportTable.data.length }} / {{ friendImportTable.data.length }} /
{{ {{
@@ -123,8 +115,9 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, Close, Loading } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Close, Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -155,6 +148,8 @@
importProgressTotal: 0 importProgressTotal: 0
}); });
const friendImportFavoriteGroupSelection = ref('');
const friendImportTable = ref({ const friendImportTable = ref({
data: [], data: [],
tableProps: { tableProps: {
@@ -182,6 +177,8 @@
friendImportDialogIndex.value = getNextDialogIndex(); friendImportDialogIndex.value = getNextDialogIndex();
clearFriendImportTable(); clearFriendImportTable();
resetFriendImport(); resetFriendImport();
friendImportFavoriteGroupSelection.value =
friendImportDialog.value.friendImportFavoriteGroup?.name ?? '';
if (friendImportDialogInput.value) { if (friendImportDialogInput.value) {
friendImportDialog.value.input = friendImportDialogInput.value; friendImportDialog.value.input = friendImportDialogInput.value;
processFriendImportList(); processFriendImportList();
@@ -191,6 +188,14 @@
} }
); );
function handleFriendImportGroupSelect(value) {
friendImportFavoriteGroupSelection.value = value;
const group = favoriteFriendGroups.value.find((g) => g.name === value) || null;
if (group) {
selectFriendImportGroup(group);
}
}
function cancelFriendImport() { function cancelFriendImport() {
friendImportDialog.value.loading = false; friendImportDialog.value.loading = false;
} }
@@ -204,6 +209,7 @@
} }
function selectFriendImportGroup(group) { function selectFriendImportGroup(group) {
friendImportDialog.value.friendImportFavoriteGroup = group; friendImportDialog.value.friendImportFavoriteGroup = group;
friendImportFavoriteGroupSelection.value = group?.name ?? '';
} }
async function importFriendImportTable() { async function importFriendImportTable() {
const D = friendImportDialog.value; const D = friendImportDialog.value;

View File

@@ -9,61 +9,38 @@
</template> </template>
</el-checkbox-group> </el-checkbox-group>
<el-dropdown trigger="click" size="small"> <div class="flex items-center gap-2">
<el-button size="small"> <Select :model-value="worldExportFavoriteGroupSelection" @update:modelValue="handleWorldExportGroupSelect">
<span v-if="worldExportFavoriteGroup"> <SelectTrigger size="sm">
{{ worldExportFavoriteGroup.displayName }} ({{ worldExportFavoriteGroup.count }}/{{ <SelectValue placeholder="All Favorites" />
worldExportFavoriteGroup.capacity </SelectTrigger>
}}) <SelectContent>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectGroup>
</span> <SelectItem :value="WORLD_EXPORT_ALL_VALUE">None</SelectItem>
<span v-else> <SelectItem v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name" :value="groupAPI.name">
All Favorites
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectWorldExportGroup(null)">
None
</el-dropdown-item>
<template v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin-left: 10px"> <Select
<el-button size="small"> :model-value="worldExportLocalFavoriteGroupSelection"
<span v-if="worldExportLocalFavoriteGroup"> @update:modelValue="handleWorldExportLocalGroupSelect"
{{ worldExportLocalFavoriteGroup }} ({{ localWorldFavGroupLength(worldExportLocalFavoriteGroup) }}) style="margin-left: 10px">
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectTrigger size="sm">
</span> <SelectValue placeholder="Select Group" />
<span v-else> </SelectTrigger>
Select Group <SelectContent>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectGroup>
</span> <SelectItem :value="WORLD_EXPORT_NONE_VALUE">None</SelectItem>
</el-button> <SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectWorldExportLocalGroup(null)">
None
</el-dropdown-item>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldExportLocalGroup(group)">
{{ group }} ({{ localWorldFavorites[group].length }}) {{ group }} ({{ localWorldFavorites[group].length }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown> </div>
<br /> <br />
@@ -80,8 +57,8 @@
</template> </template>
<script setup> <script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -112,6 +89,12 @@
const worldExportContent = ref(''); const worldExportContent = ref('');
const worldExportFavoriteGroup = ref(null); const worldExportFavoriteGroup = ref(null);
const worldExportLocalFavoriteGroup = ref(null); const worldExportLocalFavoriteGroup = ref(null);
const WORLD_EXPORT_ALL_VALUE = '__all__';
const WORLD_EXPORT_NONE_VALUE = '__none__';
const worldExportFavoriteGroupSelection = ref(WORLD_EXPORT_ALL_VALUE);
const worldExportLocalFavoriteGroupSelection = ref(WORLD_EXPORT_NONE_VALUE);
// Storage of selected filtering options for model and world export // Storage of selected filtering options for model and world export
const exportSelectedOptions = ref(['ID', 'Name']); const exportSelectedOptions = ref(['ID', 'Name']);
const exportSelectOptions = ref([ const exportSelectOptions = ref([
@@ -143,9 +126,30 @@
function showWorldExportDialog() { function showWorldExportDialog() {
worldExportFavoriteGroup.value = null; worldExportFavoriteGroup.value = null;
worldExportLocalFavoriteGroup.value = null; worldExportLocalFavoriteGroup.value = null;
worldExportFavoriteGroupSelection.value = WORLD_EXPORT_ALL_VALUE;
worldExportLocalFavoriteGroupSelection.value = WORLD_EXPORT_NONE_VALUE;
updateWorldExportDialog(); updateWorldExportDialog();
} }
function handleWorldExportGroupSelect(value) {
worldExportFavoriteGroupSelection.value = value;
if (value === WORLD_EXPORT_ALL_VALUE) {
selectWorldExportGroup(null);
return;
}
const group = favoriteWorldGroups.value.find((g) => g.name === value) || null;
selectWorldExportGroup(group);
}
function handleWorldExportLocalGroupSelect(value) {
worldExportLocalFavoriteGroupSelection.value = value;
if (value === WORLD_EXPORT_NONE_VALUE) {
selectWorldExportLocalGroup(null);
return;
}
selectWorldExportLocalGroup(value);
}
function handleCopyWorldExportData(event) { function handleCopyWorldExportData(event) {
if (event.target.tagName === 'TEXTAREA') { if (event.target.tagName === 'TEXTAREA') {
event.target.select(); event.target.select();
@@ -221,12 +225,16 @@
function selectWorldExportGroup(group) { function selectWorldExportGroup(group) {
worldExportFavoriteGroup.value = group; worldExportFavoriteGroup.value = group;
worldExportLocalFavoriteGroup.value = null; worldExportLocalFavoriteGroup.value = null;
worldExportFavoriteGroupSelection.value = group?.name ?? WORLD_EXPORT_ALL_VALUE;
worldExportLocalFavoriteGroupSelection.value = WORLD_EXPORT_NONE_VALUE;
updateWorldExportDialog(); updateWorldExportDialog();
} }
function selectWorldExportLocalGroup(group) { function selectWorldExportLocalGroup(group) {
worldExportLocalFavoriteGroup.value = group; worldExportLocalFavoriteGroup.value = group;
worldExportFavoriteGroup.value = null; worldExportFavoriteGroup.value = null;
worldExportLocalFavoriteGroupSelection.value = group ?? WORLD_EXPORT_NONE_VALUE;
worldExportFavoriteGroupSelection.value = WORLD_EXPORT_ALL_VALUE;
updateWorldExportDialog(); updateWorldExportDialog();
} }
</script> </script>

View File

@@ -30,57 +30,42 @@
style="margin-top: 10px"></el-input> style="margin-top: 10px"></el-input>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px"> <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div> <div>
<el-dropdown trigger="click" size="small" style="margin-right: 5px" @click.stop> <div class="flex items-center gap-2">
<el-button size="small"> <Select
<span v-if="worldImportDialog.worldImportFavoriteGroup"> :model-value="worldImportFavoriteGroupSelection"
{{ worldImportDialog.worldImportFavoriteGroup.displayName }} @update:modelValue="handleWorldImportGroupSelect">
({{ worldImportDialog.worldImportFavoriteGroup.count }}/{{ <SelectTrigger size="sm">
worldImportDialog.worldImportFavoriteGroup.capacity <SelectValue :placeholder="t('dialog.world_import.select_vrchat_group_placeholder')" />
}}) </SelectTrigger>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectContent>
</span> <SelectGroup>
<span v-else> <SelectItem
{{ t('dialog.world_import.select_vrchat_group_placeholder') }} v-for="groupAPI in favoriteWorldGroups"
<el-icon class="el-icon--right"><ArrowDown /></el-icon> :key="groupAPI.name"
</span> :value="groupAPI.name"
</el-button> :disabled="groupAPI.count >= groupAPI.capacity">
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectWorldImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown>
<el-dropdown trigger="click" size="small" @click.stop> <Select
<el-button size="small"> :model-value="worldImportLocalFavoriteGroupSelection"
<span v-if="worldImportDialog.worldImportLocalFavoriteGroup"> @update:modelValue="handleWorldImportLocalGroupSelect"
{{ worldImportDialog.worldImportLocalFavoriteGroup }} style="margin-left: 10px">
({{ localWorldFavGroupLength(worldImportDialog.worldImportLocalFavoriteGroup) }}) <SelectTrigger size="sm">
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectValue :placeholder="t('dialog.world_import.select_local_group_placeholder')" />
</span> </SelectTrigger>
<span v-else> <SelectContent>
{{ t('dialog.world_import.select_local_group_placeholder') }} <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldImportLocalGroup(group)">
{{ group }} ({{ localWorldFavGroupLength(group) }}) {{ group }} ({{ localWorldFavGroupLength(group) }})
</el-dropdown-item> </SelectItem>
</template> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown> </div>
<span v-if="worldImportDialog.worldImportFavoriteGroup" style="margin-left: 5px"> <span v-if="worldImportDialog.worldImportFavoriteGroup" style="margin-left: 5px">
{{ worldImportTable.data.length }} / {{ worldImportTable.data.length }} /
{{ {{
@@ -172,8 +157,9 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, Close, Loading } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Close, Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -209,6 +195,9 @@
importProgressTotal: 0 importProgressTotal: 0
}); });
const worldImportFavoriteGroupSelection = ref('');
const worldImportLocalFavoriteGroupSelection = ref('');
const worldImportTable = ref({ const worldImportTable = ref({
data: [], data: [],
tableProps: { tableProps: {
@@ -302,11 +291,26 @@
function selectWorldImportGroup(group) { function selectWorldImportGroup(group) {
worldImportDialog.value.worldImportLocalFavoriteGroup = null; worldImportDialog.value.worldImportLocalFavoriteGroup = null;
worldImportDialog.value.worldImportFavoriteGroup = group; worldImportDialog.value.worldImportFavoriteGroup = group;
worldImportFavoriteGroupSelection.value = group?.name ?? '';
worldImportLocalFavoriteGroupSelection.value = '';
} }
function selectWorldImportLocalGroup(group) { function selectWorldImportLocalGroup(group) {
worldImportDialog.value.worldImportFavoriteGroup = null; worldImportDialog.value.worldImportFavoriteGroup = null;
worldImportDialog.value.worldImportLocalFavoriteGroup = group; worldImportDialog.value.worldImportLocalFavoriteGroup = group;
worldImportFavoriteGroupSelection.value = '';
worldImportLocalFavoriteGroupSelection.value = group ?? '';
}
function handleWorldImportGroupSelect(value) {
worldImportFavoriteGroupSelection.value = value;
const group = favoriteWorldGroups.value.find((g) => g.name === value) ?? null;
selectWorldImportGroup(group);
}
function handleWorldImportLocalGroupSelect(value) {
worldImportLocalFavoriteGroupSelection.value = value;
selectWorldImportLocalGroup(value || null);
} }
function cancelWorldImport() { function cancelWorldImport() {

View File

@@ -71,24 +71,24 @@
v-loading="isSearchWorldLoading" v-loading="isSearchWorldLoading"
:label="t('view.search.world.header')" :label="t('view.search.world.header')"
style="min-height: 60px"> style="min-height: 60px">
<el-dropdown <Select
size="small" :model-value="searchWorldCategoryIndex"
trigger="click" @update:modelValue="handleSearchWorldCategorySelect"
style="margin-bottom: 15px" style="margin-bottom: 15px">
@command="(row) => searchWorld(row)"> <SelectTrigger size="sm">
<el-button size="small" <SelectValue :placeholder="t('view.search.world.category')" />
>{{ t('view.search.world.category') }} <el-icon class="el-icon--right"><ArrowDown /></el-icon </SelectTrigger>
></el-button> <SelectContent>
<template #dropdown> <SelectGroup>
<el-dropdown-menu> <SelectItem
<el-dropdown-item
v-for="row in cachedConfig.dynamicWorldRows" v-for="row in cachedConfig.dynamicWorldRows"
:key="row.index" :key="row.index"
:command="row" :value="row.index">
v-text="row.name"></el-dropdown-item> {{ row.name }}
</el-dropdown-menu> </SelectItem>
</template> </SelectGroup>
</el-dropdown> </SelectContent>
</Select>
<el-checkbox v-model="searchWorldLabs" style="margin-left: 10px">{{ <el-checkbox v-model="searchWorldLabs" style="margin-left: 10px">{{
t('view.search.world.community_lab') t('view.search.world.community_lab')
}}</el-checkbox> }}</el-checkbox>
@@ -135,30 +135,25 @@
style="min-height: 60px"> style="min-height: 60px">
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<el-dropdown <Select
v-if="avatarRemoteDatabaseProviderList.length > 1" v-if="avatarRemoteDatabaseProviderList.length > 1"
trigger="click" :model-value="avatarRemoteDatabaseProvider"
size="small" @update:modelValue="setAvatarProvider"
style="margin-right: 5px" style="margin-right: 5px">
@click.stop> <SelectTrigger size="sm">
<el-button size="small" <SelectValue :placeholder="t('view.search.avatar.search_provider')" />
>{{ t('view.search.avatar.search_provider') }} </SelectTrigger>
<el-icon class="el-icon--right"><ArrowDown /></el-icon <SelectContent>
></el-button> <SelectGroup>
<template #dropdown> <SelectItem
<el-dropdown-menu>
<el-dropdown-item
v-for="provider in avatarRemoteDatabaseProviderList" v-for="provider in avatarRemoteDatabaseProviderList"
:key="provider" :key="provider"
@click="setAvatarProvider(provider)"> :value="provider">
<el-icon v-if="provider === avatarRemoteDatabaseProvider" class="el-icon--left"
><Check
/></el-icon>
{{ provider }} {{ provider }}
</el-dropdown-item> </SelectItem>
</el-dropdown-menu> </SelectGroup>
</template> </SelectContent>
</el-dropdown> </Select>
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')"> <TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')">
<el-button <el-button
type="default" type="default"
@@ -311,7 +306,8 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, Back, Check, Delete, Refresh, Right } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Back, Delete, Refresh, Right } from '@element-plus/icons-vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group'; import { ButtonGroup } from '@/components/ui/button-group';
import { ref } from 'vue'; import { ref } from 'vue';
@@ -368,6 +364,8 @@
const searchWorldOption = ref(''); const searchWorldOption = ref('');
const searchWorldLabs = ref(false); const searchWorldLabs = ref(false);
const searchWorldParams = ref({}); const searchWorldParams = ref({});
const searchWorldCategoryIndex = ref(null);
const searchWorldResults = ref([]); const searchWorldResults = ref([]);
const searchAvatarFilter = ref(''); const searchAvatarFilter = ref('');
@@ -436,6 +434,7 @@
function searchWorld(ref) { function searchWorld(ref) {
searchWorldOption.value = ''; searchWorldOption.value = '';
searchWorldCategoryIndex.value = ref?.index ?? null;
const params = { const params = {
n: 10, n: 10,
offset: 0 offset: 0
@@ -502,6 +501,12 @@
moreSearchWorld(); moreSearchWorld();
} }
function handleSearchWorldCategorySelect(index) {
searchWorldCategoryIndex.value = index;
const row = cachedConfig.value?.dynamicWorldRows?.find((r) => r.index === index);
searchWorld(row || {});
}
function moreSearchWorld(go) { function moreSearchWorld(go) {
const params = searchWorldParams.value; const params = searchWorldParams.value;
if (go) { if (go) {

View File

@@ -34,6 +34,7 @@
.simple-switch { .simple-switch {
font-size: 12px; font-size: 12px;
display: flex; display: flex;
align-items: center;
} }
.simple-switch > .name { .simple-switch > .name {
width: 225px; width: 225px;

View File

@@ -4,45 +4,33 @@
<span class="header">{{ t('view.settings.appearance.appearance.header') }}</span> <span class="header">{{ t('view.settings.appearance.appearance.header') }}</span>
<div class="options-container-item"> <div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.language') }}</span> <span class="name">{{ t('view.settings.appearance.appearance.language') }}</span>
<el-dropdown trigger="click" size="small" @click.stop> <Select :model-value="appLanguage" @update:modelValue="changeAppLanguage">
<el-button size="small"> <SelectTrigger size="sm">
<span> <SelectValue :placeholder="getLanguageName(appLanguage)" />
{{ getLanguageName(appLanguage) }} <el-icon class="el-icon--right"> <ArrowDown /></el-icon </SelectTrigger>
></span> <SelectContent>
</el-button> <SelectGroup>
<template #dropdown> <SelectItem v-for="language in languageCodes" :key="language" :value="language">
<el-dropdown-menu> {{ getLanguageName(language) }}
<el-dropdown-item </SelectItem>
v-for="language in languageCodes" </SelectGroup>
:key="language" </SelectContent>
:class="{ 'is-active': appLanguage === language }" </Select>
@click="changeAppLanguage(language)"
v-text="getLanguageName(language)" />
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
<div class="options-container-item"> <div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.theme_mode') }}</span> <span class="name">{{ t('view.settings.appearance.appearance.theme_mode') }}</span>
<el-dropdown trigger="click" size="small" @click.stop> <Select :model-value="themeMode" @update:modelValue="saveThemeMode">
<el-button size="small"> <SelectTrigger size="sm">
<span <SelectValue :placeholder="t(`view.settings.appearance.appearance.theme_mode_${themeMode}`)" />
>{{ t(`view.settings.appearance.appearance.theme_mode_${themeMode}`) }} </SelectTrigger>
<el-icon class="el-icon--right"><ArrowDown /></el-icon <SelectContent>
></span> <SelectGroup>
</el-button> <SelectItem v-for="(config, themeKey) in THEME_CONFIG" :key="themeKey" :value="themeKey">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(config, themeKey) in THEME_CONFIG"
:key="themeKey"
@click="saveThemeMode(themeKey)"
:class="{ 'is-active': themeMode === themeKey }">
{{ t(`view.settings.appearance.appearance.theme_mode_${themeKey}`) }} {{ t(`view.settings.appearance.appearance.theme_mode_${themeKey}`) }}
</el-dropdown-item> </SelectItem>
</el-dropdown-menu> </SelectGroup>
</template> </SelectContent>
</el-dropdown> </Select>
</div> </div>
<div v-if="!isLinux" class="options-container-item"> <div v-if="!isLinux" class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.zoom') }}</span> <span class="name">{{ t('view.settings.appearance.appearance.zoom') }}</span>
@@ -314,7 +302,7 @@
:label="t('view.settings.appearance.user_colors.random_colors_from_user_id')" :label="t('view.settings.appearance.user_colors.random_colors_from_user_id')"
:value="randomUserColours" :value="randomUserColours"
@change="updateTrustColor('', '', true)"></simple-switch> @change="updateTrustColor('', '', true)"></simple-switch>
<div class="options-container-item"> <div>
<div> <div>
<el-color-picker <el-color-picker
:model-value="trustColor.untrusted" :model-value="trustColor.untrusted"
@@ -384,8 +372,9 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, ArrowRight, Notebook } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { computed, onBeforeUnmount, ref } from 'vue'; import { computed, onBeforeUnmount, ref } from 'vue';
import { ArrowRight, Notebook } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@@ -230,22 +230,21 @@
</div> </div>
<div class="options-container-item"> <div class="options-container-item">
<span class="name">{{ t('view.settings.notifications.notifications.text_to_speech.tts_voice') }}</span> <span class="name">{{ t('view.settings.notifications.notifications.text_to_speech.tts_voice') }}</span>
<el-dropdown trigger="click" size="small" @command="(voice) => changeTTSVoice(voice)"> <Select
<el-button size="small" :disabled="notificationTTS === 'Never'"> :model-value="ttsVoiceIndex"
<span :disabled="notificationTTS === 'Never'"
>{{ getTTSVoiceName() }} <el-icon style="margin-left: 5px"><ArrowDown /></el-icon @update:modelValue="(v) => (ttsVoiceIndex = v)">
></span> <SelectTrigger size="sm">
</el-button> <SelectValue :placeholder="getTTSVoiceName()" />
<template #dropdown> </SelectTrigger>
<el-dropdown-menu> <SelectContent>
<el-dropdown-item <SelectGroup>
v-for="(voice, index) in TTSvoices" <SelectItem v-for="(voice, index) in TTSvoices" :key="index" :value="index">
:key="index" {{ voice.name }}
:command="index" </SelectItem>
v-text="voice.name" /> </SelectGroup>
</el-dropdown-menu> </SelectContent>
</template> </Select>
</el-dropdown>
</div> </div>
<simple-switch <simple-switch
:label="t('view.settings.notifications.notifications.text_to_speech.use_memo_nicknames')" :label="t('view.settings.notifications.notifications.text_to_speech.use_memo_nicknames')"
@@ -274,14 +273,15 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, ChatSquare, Rank, Timer, VideoPlay } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ChatSquare, Rank, Timer, VideoPlay } from '@element-plus/icons-vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAdvancedSettingsStore, useNotificationsSettingsStore, useVrStore } from '../../../../stores'; import { useAdvancedSettingsStore, useNotificationsSettingsStore, useVrStore } from '../../../../stores';
import { Slider } from '../../../../components/ui/slider';
import { ToggleGroup, ToggleGroupItem } from '../../../../components/ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from '../../../../components/ui/toggle-group';
import { Slider } from '../../../../components/ui/slider';
import FeedFiltersDialog from '../../dialogs/FeedFiltersDialog.vue'; import FeedFiltersDialog from '../../dialogs/FeedFiltersDialog.vue';
import NotificationPositionDialog from '../../dialogs/NotificationPositionDialog.vue'; import NotificationPositionDialog from '../../dialogs/NotificationPositionDialog.vue';
@@ -345,6 +345,19 @@
} }
}); });
const ttsVoiceIndex = computed({
get: () => {
const currentName = getTTSVoiceName();
const idx = TTSvoices.value.findIndex((v) => v?.name === currentName);
return idx >= 0 ? idx : null;
},
set: (value) => {
if (typeof value === 'number') {
changeTTSVoice(value);
}
}
});
function showNotyFeedFiltersDialog() { function showNotyFeedFiltersDialog() {
feedFiltersDialogMode.value = 'noty'; feedFiltersDialogMode.value = 'noty';
} }

View File

@@ -7,23 +7,18 @@
@close="closeDialog"> @close="closeDialog">
<div class="options-container-item"> <div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.bio_language') }}</span> <span class="name">{{ t('view.settings.appearance.appearance.bio_language') }}</span>
<el-dropdown trigger="click" size="small" style="float: right" @click.stop> <Select :model-value="bioLanguage" @update:modelValue="setBioLanguage">
<el-button size="small"> <SelectTrigger size="sm" style="float: right">
<span> <SelectValue :placeholder="String(getLanguageName(bioLanguage) || bioLanguage || '')" />
{{ getLanguageName(bioLanguage) || bioLanguage }} </SelectTrigger>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectContent>
</span> <SelectGroup>
</el-button> <SelectItem v-for="language in languageCodes" :key="language" :value="language">
<template #dropdown> {{ getLanguageName(language) }}
<el-dropdown-menu> </SelectItem>
<el-dropdown-item </SelectGroup>
v-for="language in languageCodes" </SelectContent>
:key="language" </Select>
@click="setBioLanguage(language)"
v-text="getLanguageName(language)" />
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
<br /> <br />
<el-form label-position="top" label-width="120px" size="small" style="margin-bottom: 12px"> <el-form label-position="top" label-width="120px" size="small" style="margin-bottom: 12px">
@@ -102,8 +97,8 @@
</template> </template>
<script setup> <script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@@ -71,81 +71,69 @@
<div style="display: inline-block; margin-top: 10px"> <div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.camera_resolution') }}</span> <span>{{ t('dialog.config_json.camera_resolution') }}</span>
<br /> <br />
<el-dropdown <Select
size="small" :model-value="vrchatCameraResolutionKey"
trigger="click" @update:modelValue="(v) => (vrchatCameraResolutionKey = v)">
style="margin-top: 5px" <SelectTrigger size="sm" style="margin-top: 5px">
@command="(command) => setVRChatCameraResolution(command)"> <SelectValue :placeholder="getVRChatCameraResolution()" />
<el-button size="small"> </SelectTrigger>
<span> <SelectContent>
<span v-text="getVRChatCameraResolution()"></span> <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem
</span> v-for="row in VRChatCameraResolutions"
</el-button> :key="row.name"
<template #dropdown> :value="getVRChatResolutionKey(row)">
<el-dropdown-menu> {{ row.name }}
<el-dropdown-item v-for="row in VRChatCameraResolutions" :key="row.index" :command="row">{{ </SelectItem>
row.name </SelectGroup>
}}</el-dropdown-item> </SelectContent>
</el-dropdown-menu> </Select>
</template>
</el-dropdown>
</div> </div>
<br /> <br />
<div style="display: inline-block; margin-top: 10px"> <div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.spout_resolution') }}</span> <span>{{ t('dialog.config_json.spout_resolution') }}</span>
<br /> <br />
<el-dropdown <Select
size="small" :model-value="vrchatSpoutResolutionKey"
trigger="click" @update:modelValue="(v) => (vrchatSpoutResolutionKey = v)">
style="margin-top: 5px" <SelectTrigger size="sm" style="margin-top: 5px">
@command="(command) => setVRChatSpoutResolution(command)"> <SelectValue :placeholder="getVRChatSpoutResolution()" />
<el-button size="small"> </SelectTrigger>
<span> <SelectContent>
<span v-text="getVRChatSpoutResolution()"></span> <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="row in VRChatScreenshotResolutions" v-for="row in VRChatScreenshotResolutions"
:key="row.index" :key="row.name"
:command="row" :value="getVRChatResolutionKey(row)">
>{{ row.name }}</el-dropdown-item {{ row.name }}
> </SelectItem>
</el-dropdown-menu> </SelectGroup>
</template> </SelectContent>
</el-dropdown> </Select>
</div> </div>
<br /> <br />
<div style="display: inline-block; margin-top: 10px"> <div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.screenshot_resolution') }}</span> <span>{{ t('dialog.config_json.screenshot_resolution') }}</span>
<br /> <br />
<el-dropdown <Select
size="small" :model-value="vrchatScreenshotResolutionKey"
trigger="click" @update:modelValue="(v) => (vrchatScreenshotResolutionKey = v)">
style="margin-top: 5px" <SelectTrigger size="sm" style="margin-top: 5px">
@command="(command) => setVRChatScreenshotResolution(command)"> <SelectValue :placeholder="getVRChatScreenshotResolution()" />
<el-button size="small"> </SelectTrigger>
<span> <SelectContent>
<span v-text="getVRChatScreenshotResolution()"></span> <SelectGroup>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <SelectItem
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="row in VRChatScreenshotResolutions" v-for="row in VRChatScreenshotResolutions"
:key="row.index" :key="row.name"
:command="row" :value="getVRChatResolutionKey(row)">
>{{ row.name }}</el-dropdown-item {{ row.name }}
> </SelectItem>
</el-dropdown-menu> </SelectGroup>
</template> </SelectContent>
</el-dropdown> </Select>
</div> </div>
<br /> <br />
@@ -181,7 +169,8 @@
</template> </template>
<script setup> <script setup>
import { ArrowDown, Delete, FolderDelete, FolderOpened, Refresh } from '@element-plus/icons-vue'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Delete, FolderDelete, FolderOpened, Refresh } from '@element-plus/icons-vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -199,52 +188,54 @@
const { t } = useI18n(); const { t } = useI18n();
const VRChatConfigFile = ref({}); const VRChatConfigFile = ref(/** @type {Record<string, any>} */ ({}));
// it's a object // it's a object
const VRChatConfigList = ref({ const VRChatConfigList = ref(
cache_size: { /** @type {Record<string, any>} */ ({
name: t('dialog.config_json.max_cache_size'), cache_size: {
default: '30', name: t('dialog.config_json.max_cache_size'),
type: 'number', default: '30',
min: 30 type: 'number',
}, min: 30
cache_expiry_delay: { },
name: t('dialog.config_json.cache_expiry_delay'), cache_expiry_delay: {
default: '30', name: t('dialog.config_json.cache_expiry_delay'),
type: 'number', default: '30',
min: 30 type: 'number',
}, min: 30
cache_directory: { },
name: t('dialog.config_json.cache_directory'), cache_directory: {
default: '%AppData%\\..\\LocalLow\\VRChat\\VRChat', name: t('dialog.config_json.cache_directory'),
folderBrowser: true default: '%AppData%\\..\\LocalLow\\VRChat\\VRChat',
}, folderBrowser: true
picture_output_folder: { },
name: t('dialog.config_json.picture_directory'), picture_output_folder: {
// my pictures folder name: t('dialog.config_json.picture_directory'),
default: `%UserProfile%\\Pictures\\VRChat`, // my pictures folder
folderBrowser: true default: `%UserProfile%\\Pictures\\VRChat`,
}, folderBrowser: true
// dynamic_bone_max_affected_transform_count: { },
// name: 'Dynamic Bones Limit Max Transforms (0 disable all transforms)', // dynamic_bone_max_affected_transform_count: {
// default: '32', // name: 'Dynamic Bones Limit Max Transforms (0 disable all transforms)',
// type: 'number', // default: '32',
// min: 0 // type: 'number',
// }, // min: 0
// dynamic_bone_max_collider_check_count: { // },
// name: 'Dynamic Bones Limit Max Collider Collisions (0 disable all colliders)', // dynamic_bone_max_collider_check_count: {
// default: '8', // name: 'Dynamic Bones Limit Max Collider Collisions (0 disable all colliders)',
// type: 'number', // default: '8',
// min: 0 // type: 'number',
// }, // min: 0
fpv_steadycam_fov: { // },
name: t('dialog.config_json.fpv_steadycam_fov'), fpv_steadycam_fov: {
default: '50', name: t('dialog.config_json.fpv_steadycam_fov'),
type: 'number', default: '50',
min: 30, type: 'number',
max: 110 min: 30,
} max: 110
}); }
})
);
const loading = ref(false); const loading = ref(false);
@@ -263,6 +254,68 @@
return VRChatConfigFile.value.cache_size || VRChatTotalCacheSize.value; return VRChatConfigFile.value.cache_size || VRChatTotalCacheSize.value;
}); });
const VRCHAT_RESOLUTION_DEFAULT_KEY = '__default__';
function getVRChatResolutionKey(row) {
const width = Number(row?.width);
const height = Number(row?.height);
if (width > 0 && height > 0) {
return `${width}x${height}`;
}
return VRCHAT_RESOLUTION_DEFAULT_KEY;
}
const vrchatCameraResolutionKey = computed({
get: () => {
const width = Number(VRChatConfigFile.value.camera_res_width);
const height = Number(VRChatConfigFile.value.camera_res_height);
if (width > 0 && height > 0) {
return `${width}x${height}`;
}
return VRCHAT_RESOLUTION_DEFAULT_KEY;
},
set: (value) => {
const row = VRChatCameraResolutions.find((r) => getVRChatResolutionKey(r) === value);
if (row) {
setVRChatCameraResolution(row);
}
}
});
const vrchatSpoutResolutionKey = computed({
get: () => {
const width = Number(VRChatConfigFile.value.camera_spout_res_width);
const height = Number(VRChatConfigFile.value.camera_spout_res_height);
if (width > 0 && height > 0) {
return `${width}x${height}`;
}
return VRCHAT_RESOLUTION_DEFAULT_KEY;
},
set: (value) => {
const row = VRChatScreenshotResolutions.find((r) => getVRChatResolutionKey(r) === value);
if (row) {
setVRChatSpoutResolution(row);
}
}
});
const vrchatScreenshotResolutionKey = computed({
get: () => {
const width = Number(VRChatConfigFile.value.screenshot_res_width);
const height = Number(VRChatConfigFile.value.screenshot_res_height);
if (width > 0 && height > 0) {
return `${width}x${height}`;
}
return VRCHAT_RESOLUTION_DEFAULT_KEY;
},
set: (value) => {
const row = VRChatScreenshotResolutions.find((r) => getVRChatResolutionKey(r) === value);
if (row) {
setVRChatScreenshotResolution(row);
}
}
});
function showDeleteAllVRChatCacheConfirm() { function showDeleteAllVRChatCacheConfirm() {
ElMessageBox.confirm(`Continue? Delete all VRChat cache`, 'Confirm', { ElMessageBox.confirm(`Continue? Delete all VRChat cache`, 'Confirm', {
confirmButtonText: 'Confirm', confirmButtonText: 'Confirm',
@@ -354,8 +407,11 @@
delete VRChatConfigFile.value[item]; delete VRChatConfigFile.value[item];
} else if (typeof VRChatConfigFile.value[item] === 'boolean' && VRChatConfigFile.value[item] === false) { } else if (typeof VRChatConfigFile.value[item] === 'boolean' && VRChatConfigFile.value[item] === false) {
delete VRChatConfigFile.value[item]; delete VRChatConfigFile.value[item];
} else if (typeof VRChatConfigFile.value[item] === 'string' && !isNaN(VRChatConfigFile.value[item])) { } else if (typeof VRChatConfigFile.value[item] === 'string') {
VRChatConfigFile.value[item] = parseInt(VRChatConfigFile.value[item], 10); const parsed = parseInt(VRChatConfigFile.value[item], 10);
if (!Number.isNaN(parsed)) {
VRChatConfigFile.value[item] = parsed;
}
} }
} }
WriteVRChatConfigFile(); WriteVRChatConfigFile();