improve appearance settings ui

This commit is contained in:
pa
2026-03-16 11:12:37 +09:00
parent 1bac1e34d6
commit 8e3c1e0054
3 changed files with 192 additions and 169 deletions
@@ -0,0 +1,18 @@
<template>
<div class="flex flex-col gap-3.5">
<h3 v-if="title" class="text-base font-semibold pl-0.5 text-foreground m-0">{{ title }}</h3>
<Card class="p-0">
<CardContent class="flex flex-col gap-1 py-4.5 px-5.5">
<slot />
</CardContent>
</Card>
</div>
</template>
<script setup>
import { Card, CardContent } from '@/components/ui/card';
defineProps({
title: { type: String, default: '' }
});
</script>
@@ -0,0 +1,18 @@
<template>
<div class="flex items-center justify-between gap-6 min-h-8">
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<span class="text-sm leading-snug text-foreground">{{ label }}</span>
<span v-if="description" class="text-xs leading-tight text-muted-foreground">{{ description }}</span>
</div>
<div class="flex items-center gap-2 shrink-0">
<slot />
</div>
</div>
</template>
<script setup>
defineProps({
label: { type: String, required: true },
description: { type: String, default: '' }
});
</script>
@@ -1,9 +1,7 @@
<template> <template>
<div> <div class="flex flex-col gap-10 py-2">
<div class="options-container mt-0"> <SettingsGroup :title="t('view.settings.appearance.appearance.header')">
<span class="header">{{ t('view.settings.appearance.appearance.header') }}</span> <SettingsItem :label="t('view.settings.appearance.appearance.language')">
<div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.language') }}</span>
<Select :model-value="appLanguage" @update:modelValue="changeAppLanguage"> <Select :model-value="appLanguage" @update:modelValue="changeAppLanguage">
<SelectTrigger size="sm"> <SelectTrigger size="sm">
<SelectValue :placeholder="appLanguageDisplayName" /> <SelectValue :placeholder="appLanguageDisplayName" />
@@ -16,12 +14,9 @@
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </SettingsItem>
<div class="options-container-item"> <SettingsItem :label="t('view.settings.appearance.appearance.font_family')">
<span class="name">
{{ t('view.settings.appearance.appearance.font_family') }}
</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="min-w-[180px] justify-between font-normal"> <Button variant="outline" size="sm" class="min-w-[180px] justify-between font-normal">
@@ -74,9 +69,9 @@
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </SettingsItem>
<div v-if="!isLinux" class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.zoom') }}</span> <SettingsItem v-if="!isLinux" :label="t('view.settings.appearance.appearance.zoom')">
<NumberField <NumberField
v-model="zoomLevel" v-model="zoomLevel"
:step="1" :step="1"
@@ -89,56 +84,61 @@
<NumberFieldIncrement /> <NumberFieldIncrement />
</NumberFieldContent> </NumberFieldContent>
</NumberField> </NumberField>
</div> </SettingsItem>
<simple-switch
:label="t('view.settings.appearance.appearance.show_notification_icon_dot')" <SettingsItem :label="t('view.settings.appearance.appearance.show_notification_icon_dot')">
:value="notificationIconDot" <Switch
@change=" :model-value="notificationIconDot"
setNotificationIconDot(); @update:modelValue="
saveOpenVROption(); setNotificationIconDot();
" /> saveOpenVROption();
<simple-switch " />
:label="t('view.settings.appearance.appearance.vrcplus_profile_icons')" </SettingsItem>
:value="displayVRCPlusIconsAsAvatar"
@change=" <SettingsItem :label="t('view.settings.appearance.appearance.vrcplus_profile_icons')">
setDisplayVRCPlusIconsAsAvatar(); <Switch
saveOpenVROption(); :model-value="displayVRCPlusIconsAsAvatar"
" /> @update:modelValue="
</div> setDisplayVRCPlusIconsAsAvatar();
<div class="options-container"> saveOpenVROption();
<span class="header">{{ t('view.settings.appearance.display.header') }}</span> " />
<simple-switch </SettingsItem>
:label="t('view.settings.appearance.appearance.show_instance_id')" </SettingsGroup>
:value="showInstanceIdInLocation"
@change="setShowInstanceIdInLocation" /> <SettingsGroup :title="t('view.settings.appearance.display.header')">
<simple-switch <SettingsItem :label="t('view.settings.appearance.appearance.show_instance_id')">
:label="t('view.settings.appearance.appearance.nicknames')" <Switch :model-value="showInstanceIdInLocation" @update:modelValue="setShowInstanceIdInLocation" />
:value="!hideNicknames" </SettingsItem>
@change="
setHideNicknames(); <SettingsItem :label="t('view.settings.appearance.appearance.nicknames')">
saveOpenVROption(); <Switch
" /> :model-value="!hideNicknames"
<simple-switch @update:modelValue="
:label="t('view.settings.appearance.appearance.age_gated_instances')" setHideNicknames();
:value="isAgeGatedInstancesVisible" saveOpenVROption();
@change="setIsAgeGatedInstancesVisible" /> " />
<simple-switch </SettingsItem>
:label="t('view.settings.appearance.appearance.striped_data_table_mode')"
:value="isDataTableStriped" <SettingsItem :label="t('view.settings.appearance.appearance.age_gated_instances')">
@change="toggleStripedDataTable" /> <Switch
<simple-switch :model-value="isAgeGatedInstancesVisible"
:label="t('view.settings.appearance.appearance.toggle_pointer_on_hover')" @update:modelValue="setIsAgeGatedInstancesVisible" />
:value="showPointerOnHover" </SettingsItem>
@change="togglePointerOnHover" />
</div> <SettingsItem :label="t('view.settings.appearance.appearance.striped_data_table_mode')">
<div class="options-container"> <Switch :model-value="isDataTableStriped" @update:modelValue="toggleStripedDataTable" />
<span class="header">{{ t('view.settings.appearance.sorting_tables.header') }}</span> </SettingsItem>
<div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.sort_favorite_by') }}</span> <SettingsItem :label="t('view.settings.appearance.appearance.toggle_pointer_on_hover')">
<Switch :model-value="showPointerOnHover" @update:modelValue="togglePointerOnHover" />
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.appearance.sorting_tables.header')">
<SettingsItem :label="t('view.settings.appearance.appearance.sort_favorite_by')">
<RadioGroup <RadioGroup
:model-value="sortFavorites ? 'true' : 'false'" :model-value="sortFavorites ? 'true' : 'false'"
class="gap-2 flex" class="gap-2 flex"
style="margin-top: 8px"
@update:modelValue="handleSortFavoritesRadio"> @update:modelValue="handleSortFavoritesRadio">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem id="sortFavorites-false" value="false" /> <RadioGroupItem id="sortFavorites-false" value="false" />
@@ -153,13 +153,12 @@
</label> </label>
</div> </div>
</RadioGroup> </RadioGroup>
</div> </SettingsItem>
<div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.sort_instance_users_by') }}</span> <SettingsItem :label="t('view.settings.appearance.appearance.sort_instance_users_by')">
<RadioGroup <RadioGroup
:model-value="instanceUsersSortAlphabetical ? 'true' : 'false'" :model-value="instanceUsersSortAlphabetical ? 'true' : 'false'"
class="gap-2 flex" class="gap-2 flex"
style="margin-top: 8px"
@update:modelValue="handleInstanceUsersSortAlphabeticalRadio"> @update:modelValue="handleInstanceUsersSortAlphabeticalRadio">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem id="instanceUsersSortAlphabetical-false" value="false" /> <RadioGroupItem id="instanceUsersSortAlphabetical-false" value="false" />
@@ -174,10 +173,9 @@
</label> </label>
</div> </div>
</RadioGroup> </RadioGroup>
</div> </SettingsItem>
<div class="options-container-item"> <SettingsItem :label="t('view.settings.appearance.appearance.table_page_sizes')">
<span class="name">{{ t('view.settings.appearance.appearance.table_page_sizes') }}</span>
<Popover v-model:open="tablePageSizesOpen"> <Popover v-model:open="tablePageSizesOpen">
<ListboxRoot v-model="tablePageSizesModel" highlight-on-hover multiple> <ListboxRoot v-model="tablePageSizesModel" highlight-on-hover multiple>
<PopoverAnchor class="inline-flex w-75"> <PopoverAnchor class="inline-flex w-75">
@@ -222,23 +220,21 @@
</PopoverContent> </PopoverContent>
</ListboxRoot> </ListboxRoot>
</Popover> </Popover>
</div> </SettingsItem>
<div class="options-container-item"> <SettingsItem :label="t('view.settings.appearance.appearance.table_entries_settings')">
<Button size="sm" variant="outline" @click="showTableLimitsDialog">{{ <Button size="sm" variant="outline" @click="showTableLimitsDialog">{{
t('view.settings.appearance.appearance.table_entries_settings') t('view.settings.appearance.appearance.table_entries_settings')
}}</Button> }}</Button>
</div> </SettingsItem>
</div> </SettingsGroup>
<TableLimitsDialog /> <TableLimitsDialog />
<div class="options-container">
<span class="header">{{ t('view.settings.appearance.timedate.header') }}</span> <SettingsGroup :title="t('view.settings.appearance.timedate.header')">
<div class="options-container-item"> <SettingsItem :label="t('view.settings.appearance.timedate.time_format')">
<span class="name">{{ t('view.settings.appearance.timedate.time_format') }}</span>
<RadioGroup <RadioGroup
:model-value="dtHour12 ? 'true' : 'false'" :model-value="dtHour12 ? 'true' : 'false'"
class="gap-2 flex" class="gap-2 flex"
style="margin-top: 8px"
@update:modelValue="handleDtHour12Radio"> @update:modelValue="handleDtHour12Radio">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem id="dtHour12-true" value="true" /> <RadioGroupItem id="dtHour12-true" value="true" />
@@ -253,101 +249,45 @@
</label> </label>
</div> </div>
</RadioGroup> </RadioGroup>
</div> </SettingsItem>
<simple-switch
:label="t('view.settings.appearance.timedate.force_iso_date_format')"
:value="dtIsoFormat"
@change="setDtIsoFormat" />
</div>
<div class="options-container"> <SettingsItem :label="t('view.settings.appearance.timedate.force_iso_date_format')">
<span class="header">{{ t('view.settings.appearance.user_dialog.header') }}</span> <Switch :model-value="dtIsoFormat" @update:modelValue="setDtIsoFormat" />
<simple-switch </SettingsItem>
:label="t('view.settings.appearance.user_dialog.vrchat_notes')" </SettingsGroup>
:value="!hideUserNotes"
@change="setHideUserNotes" /> <SettingsGroup :title="t('view.settings.appearance.user_dialog.header')">
<simple-switch <SettingsItem :label="t('view.settings.appearance.user_dialog.vrchat_notes')">
:label="t('view.settings.appearance.user_dialog.vrcx_memos')" <Switch :model-value="!hideUserNotes" @update:modelValue="setHideUserNotes" />
:value="!hideUserMemos" </SettingsItem>
@change="setHideUserMemos" />
</div> <SettingsItem :label="t('view.settings.appearance.user_dialog.vrcx_memos')">
<div class="options-container"> <Switch :model-value="!hideUserMemos" @update:modelValue="setHideUserMemos" />
<span class="header">{{ t('view.settings.appearance.friend_log.header') }}</span> </SettingsItem>
<simple-switch </SettingsGroup>
:label="t('view.settings.appearance.friend_log.hide_unfriends')"
:value="hideUnfriends" <SettingsGroup :title="t('view.settings.appearance.friend_log.header')">
@change="setHideUnfriends"></simple-switch> <SettingsItem :label="t('view.settings.appearance.friend_log.hide_unfriends')">
</div> <Switch :model-value="hideUnfriends" @update:modelValue="setHideUnfriends" />
<div class="options-container"> </SettingsItem>
<span class="header">{{ t('view.settings.appearance.user_colors.header') }}</span> </SettingsGroup>
<simple-switch
:label="t('view.settings.appearance.user_colors.random_colors_from_user_id')" <SettingsGroup :title="t('view.settings.appearance.user_colors.header')">
:value="randomUserColours" <SettingsItem :label="t('view.settings.appearance.user_colors.random_colors_from_user_id')">
@change="updateTrustColor('', '', true)"></simple-switch> <Switch :model-value="randomUserColours" @update:modelValue="updateTrustColor('', '', true)" />
<div class="flex flex-col gap-1"> </SettingsItem>
<div> <div class="settings-item">
<span class="x-tag-untrusted">{{ <div class="flex flex-col gap-2 py-2">
t('view.settings.appearance.user_colors.trust_levels.visitor') <div v-for="colorEntry in trustColorEntries" :key="colorEntry.key" class="flex items-center gap-3">
}}</span> <span :class="colorEntry.tagClass">{{ t(colorEntry.labelKey) }}</span>
<PresetColorPicker <PresetColorPicker
:model-value="trustColor.untrusted" :model-value="trustColor[colorEntry.key]"
:presets="['#CCCCCC']" :presets="colorEntry.presets"
@change="updateTrustColor('untrusted', $event)" /> @change="updateTrustColor(colorEntry.key, $event)" />
</div> </div>
<div>
<span class="x-tag-basic">{{
t('view.settings.appearance.user_colors.trust_levels.new_user')
}}</span>
<PresetColorPicker
:model-value="trustColor.basic"
:presets="['#1778ff']"
@change="updateTrustColor('basic', $event)" />
</div>
<div>
<span class="x-tag-known">{{ t('view.settings.appearance.user_colors.trust_levels.user') }}</span>
<PresetColorPicker
:model-value="trustColor.known"
:presets="['#2bcf5c']"
@change="updateTrustColor('known', $event)" />
</div>
<div>
<span class="x-tag-trusted">{{
t('view.settings.appearance.user_colors.trust_levels.known_user')
}}</span>
<PresetColorPicker
:model-value="trustColor.trusted"
:presets="['#ff7b42']"
@change="updateTrustColor('trusted', $event)" />
</div>
<div>
<span class="x-tag-veteran">{{
t('view.settings.appearance.user_colors.trust_levels.trusted_user')
}}</span>
<PresetColorPicker
:model-value="trustColor.veteran"
:presets="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
@change="updateTrustColor('veteran', $event)" />
</div>
<div>
<span class="x-tag-vip">{{
t('view.settings.appearance.user_colors.trust_levels.vrchat_team')
}}</span>
<PresetColorPicker
:model-value="trustColor.vip"
:presets="['#ff2626']"
@change="updateTrustColor('vip', $event)" />
</div>
<div>
<span class="x-tag-troll">{{
t('view.settings.appearance.user_colors.trust_levels.nuisance')
}}</span>
<PresetColorPicker
:model-value="trustColor.troll"
:presets="['#782f2f']"
@change="updateTrustColor('troll', $event)" />
</div> </div>
</div> </div>
</div> </SettingsGroup>
</div> </div>
</template> </template>
@@ -397,6 +337,7 @@
import { CheckIcon, ChevronDown } from 'lucide-vue-next'; import { CheckIcon, ChevronDown } from 'lucide-vue-next';
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores'; import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Switch } from '@/components/ui/switch';
import { getLanguageName, languageCodes } from '@/localization'; import { getLanguageName, languageCodes } from '@/localization';
import { APP_CJK_FONT_PACKS, APP_FONT_CONFIG, APP_FONT_DEFAULT_KEY, APP_FONT_FAMILIES } from '@/shared/constants'; import { APP_CJK_FONT_PACKS, APP_FONT_CONFIG, APP_FONT_DEFAULT_KEY, APP_FONT_FAMILIES } from '@/shared/constants';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -408,7 +349,8 @@
import TableLimitsDialog from '@/components/dialogs/TableLimitsDialog.vue'; import TableLimitsDialog from '@/components/dialogs/TableLimitsDialog.vue';
import { saveSortFavoritesOption } from '@/coordinators/favoriteCoordinator'; import { saveSortFavoritesOption } from '@/coordinators/favoriteCoordinator';
import SimpleSwitch from '../SimpleSwitch.vue'; import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
const { t } = useI18n(); const { t } = useI18n();
@@ -464,6 +406,51 @@
setAppCjkFontPack setAppCjkFontPack
} = appearanceSettingsStore; } = appearanceSettingsStore;
const trustColorEntries = [
{
key: 'untrusted',
tagClass: 'x-tag-untrusted',
labelKey: 'view.settings.appearance.user_colors.trust_levels.visitor',
presets: ['#CCCCCC']
},
{
key: 'basic',
tagClass: 'x-tag-basic',
labelKey: 'view.settings.appearance.user_colors.trust_levels.new_user',
presets: ['#1778ff']
},
{
key: 'known',
tagClass: 'x-tag-known',
labelKey: 'view.settings.appearance.user_colors.trust_levels.user',
presets: ['#2bcf5c']
},
{
key: 'trusted',
tagClass: 'x-tag-trusted',
labelKey: 'view.settings.appearance.user_colors.trust_levels.known_user',
presets: ['#ff7b42']
},
{
key: 'veteran',
tagClass: 'x-tag-veteran',
labelKey: 'view.settings.appearance.user_colors.trust_levels.trusted_user',
presets: ['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']
},
{
key: 'vip',
tagClass: 'x-tag-vip',
labelKey: 'view.settings.appearance.user_colors.trust_levels.vrchat_team',
presets: ['#ff2626']
},
{
key: 'troll',
tagClass: 'x-tag-troll',
labelKey: 'view.settings.appearance.user_colors.trust_levels.nuisance',
presets: ['#782f2f']
}
];
const fontDropdownDisplayText = computed(() => { const fontDropdownDisplayText = computed(() => {
if (appFontFamily.value === 'custom') { if (appFontFamily.value === 'custom') {
return t('view.settings.appearance.appearance.font_family_custom'); return t('view.settings.appearance.appearance.font_family_custom');