refactor: reorganize settings tabs

This commit is contained in:
pa
2026-03-17 16:40:41 +09:00
parent f582135303
commit 91e7e8e1b6
15 changed files with 714 additions and 708 deletions

View File

@@ -4,27 +4,30 @@
<span class="text-lg font-semibold text-foreground">{{ t('view.settings.header') }}</span>
</div>
<TabsUnderline
default-value="general"
default-value="system"
:items="settingsTabs"
:unmount-on-hide="false"
fill>
<template #general>
<GeneralTab />
<template #system>
<SystemTab />
</template>
<template #appearance>
<AppearanceTab />
<template #interface>
<InterfaceTab />
</template>
<template #social>
<SocialTab />
</template>
<template #notifications>
<NotificationsTab />
</template>
<template #wrist-overlay>
<WristOverlayTab />
<template #vr>
<VrTab />
</template>
<template #discord>
<DiscordPresenceTab />
<template #media>
<MediaTab />
</template>
<template #pictures>
<PicturesTab />
<template #integrations>
<IntegrationsTab />
</template>
<template #advanced>
<AdvancedTab />
@@ -39,21 +42,23 @@
import { useI18n } from 'vue-i18n';
import AdvancedTab from './components/Tabs/AdvancedTab.vue';
import AppearanceTab from './components/Tabs/AppearanceTab.vue';
import DiscordPresenceTab from './components/Tabs/DiscordPresenceTab.vue';
import GeneralTab from './components/Tabs/GeneralTab.vue';
import InterfaceTab from './components/Tabs/InterfaceTab.vue';
import IntegrationsTab from './components/Tabs/IntegrationsTab.vue';
import MediaTab from './components/Tabs/MediaTab.vue';
import NotificationsTab from './components/Tabs/NotificationsTab.vue';
import PicturesTab from './components/Tabs/PicturesTab.vue';
import WristOverlayTab from './components/Tabs/WristOverlayTab.vue';
import SocialTab from './components/Tabs/SocialTab.vue';
import SystemTab from './components/Tabs/SystemTab.vue';
import VrTab from './components/Tabs/VrTab.vue';
const { t } = useI18n();
const settingsTabs = computed(() => [
{ value: 'general', label: t('view.settings.category.general') },
{ value: 'appearance', label: t('view.settings.category.appearance') },
{ value: 'system', label: t('view.settings.category.system') },
{ value: 'interface', label: t('view.settings.category.interface') },
{ value: 'social', label: t('view.settings.category.social') },
{ value: 'notifications', label: t('view.settings.category.notifications') },
{ value: 'wrist-overlay', label: t('view.settings.category.wrist_overlay') },
{ value: 'discord', label: t('view.settings.category.discord_presence') },
{ value: 'pictures', label: t('view.settings.category.pictures') },
{ value: 'vr', label: t('view.settings.category.vr') },
{ value: 'media', label: t('view.settings.category.media') },
{ value: 'integrations', label: t('view.settings.category.integrations') },
{ value: 'advanced', label: t('view.settings.category.advanced') }
]);

View File

@@ -22,7 +22,7 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.vrcx_settings.header')">
<SettingsGroup :title="t('view.settings.advanced_groups.security.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.primary_password.header')"
:description="t('view.settings.advanced.advanced.primary_password.description')">
<Switch
@@ -30,13 +30,6 @@
:disabled="!enablePrimaryPassword"
@update:modelValue="enablePrimaryPasswordChange" />
</SettingsItem>
<template v-if="branch === 'Nightly'">
<SettingsItem :label="t('view.settings.advanced.advanced.anonymous_error_reporting.header')"
:description="t('view.settings.advanced.advanced.anonymous_error_reporting.description')">
<Switch :model-value="sentryErrorReporting" @update:modelValue="setSentryErrorReporting()" />
</SettingsItem>
</template>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.general.logging.header')">
@@ -64,36 +57,6 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.profile.game_info.header')">
<div class="px-1 py-1">
<div class="flex-1 cursor-pointer" @click="getVisits">
<span class="block truncate font-medium text-sm leading-[18px]">{{
t('view.profile.game_info.online_users')
}}</span>
<span v-if="visits" class="block truncate text-xs text-muted-foreground">{{
t('view.profile.game_info.user_online', { count: visits })
}}</span>
<span v-else class="block truncate text-xs text-muted-foreground">{{
t('view.profile.game_info.refresh')
}}</span>
</div>
</div>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.remote_database.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.remote_database.enable')">
<Switch
:model-value="avatarRemoteDatabase"
@update:modelValue="setAvatarRemoteDatabase(!avatarRemoteDatabase)" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.remote_database.avatar_database_provider')">
<Button size="sm" variant="outline" @click="showAvatarProviderDialog">{{
t('view.settings.advanced.advanced.remote_database.avatar_database_provider')
}}</Button>
</SettingsItem>
</SettingsGroup>
<template v-if="!isLinux">
<SettingsGroup :title="t('view.settings.advanced.advanced.app_launcher.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.app_launcher.folder')">
@@ -121,50 +84,6 @@
</SettingsGroup>
</template>
<SettingsGroup :title="t('view.settings.advanced.advanced.youtube_api.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.youtube_api.enable')"
:description="t('view.settings.advanced.advanced.youtube_api.enable_tooltip')">
<Switch :model-value="youTubeApi" @update:modelValue="changeYouTubeApi('VRCX_youtubeAPI')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.youtube_api.youtube_api_key')">
<Button size="sm" variant="outline" @click="showYouTubeApiDialog">{{
t('view.settings.advanced.advanced.youtube_api.youtube_api_key')
}}</Button>
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.translation_api.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.translation_api.enable')"
:description="t('view.settings.advanced.advanced.translation_api.enable_tooltip')">
<Switch :model-value="translationApi" @update:modelValue="changeTranslationAPI('VRCX_translationAPI')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.translation_api.translation_api_key')">
<Button size="sm" variant="outline" @click="showTranslationApiDialog">
<Languages class="h-4 w-4 mr-1.5" />
{{ t('view.settings.advanced.advanced.translation_api.translation_api_key') }}
</Button>
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.video_progress_pie.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.video_progress_pie.enable')"
:description="t('view.settings.advanced.advanced.video_progress_pie.enable_tooltip')">
<Switch
:model-value="progressPie"
:disabled="!openVR"
@update:modelValue="changeYouTubeApi('VRCX_progressPie')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.video_progress_pie.dance_world_only')">
<Switch
:model-value="progressPieFilter"
:disabled="!openVR"
@update:modelValue="changeYouTubeApi('VRCX_progressPieFilter')" />
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.launch_commands.header')">
<SettingsItem
:label="t('view.settings.advanced.advanced.launch_commands.show_confirmation_on_switch_avatar_enable')"
@@ -224,7 +143,7 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.sqlite_table_size.header')">
<SettingsGroup :title="t('view.settings.advanced_groups.database.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.sqlite_table_size.refresh')">
<Button size="sm" variant="outline" @click="getSqliteTableSizes">{{
t('view.settings.advanced.advanced.sqlite_table_size.refresh')
@@ -245,9 +164,7 @@
<span>{{ t('view.settings.advanced.advanced.sqlite_table_size.video_play') }} <span v-text="sqliteTableSizes.videoPlay"></span></span>
<span>{{ t('view.settings.advanced.advanced.sqlite_table_size.event') }} <span v-text="sqliteTableSizes.event"></span></span>
</div>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.advanced.advanced.database_cleanup.header')">
<SettingsItem
:label="t('view.settings.advanced.advanced.database_cleanup.auto_cleanup')"
:description="t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_description')">
@@ -320,41 +237,64 @@
</DialogContent>
</Dialog>
<SettingsGroup :title="t('view.profile.config_json')">
<SettingsGroup :title="t('view.settings.advanced_groups.diagnostics.header')">
<SettingsGroup :title="t('view.profile.game_info.header')">
<div class="px-1 py-1">
<div class="flex-1 cursor-pointer" @click="getVisits">
<span class="block truncate font-medium text-sm leading-[18px]">{{
t('view.profile.game_info.online_users')
}}</span>
<span v-if="visits" class="block truncate text-xs text-muted-foreground">{{
t('view.profile.game_info.user_online', { count: visits })
}}</span>
<span v-else class="block truncate text-xs text-muted-foreground">{{
t('view.profile.game_info.refresh')
}}</span>
</div>
</div>
</SettingsGroup>
<div class="flex items-center gap-2">
<TooltipWrapper side="top" :content="t('view.profile.refresh_tooltip')">
<Button class="rounded-full" size="icon-sm" variant="outline" @click="refreshConfigTreeData()">
<RefreshCcw />
</Button>
</TooltipWrapper>
<TooltipWrapper side="top" :content="t('view.profile.clear_results_tooltip')">
<Button class="rounded-full" size="icon-sm" variant="outline" @click="configTreeData = {}">
<Trash2 />
</Button>
</TooltipWrapper>
</div>
<vue-json-pretty
v-if="Object.keys(configTreeData).length > 0"
:data="configTreeData"
:deep="2"
:theme="isDarkMode ? 'dark' : 'light'"
:height="800"
:dynamic-height="false"
virtual
show-icon />
<SettingsGroup :title="t('view.profile.config_json')">
<div class="flex items-center gap-2">
<TooltipWrapper side="top" :content="t('view.profile.refresh_tooltip')">
<Button class="rounded-full" size="icon-sm" variant="outline" @click="refreshConfigTreeData()">
<RefreshCcw />
</Button>
</TooltipWrapper>
<TooltipWrapper side="top" :content="t('view.profile.clear_results_tooltip')">
<Button class="rounded-full" size="icon-sm" variant="outline" @click="configTreeData = {}">
<Trash2 />
</Button>
</TooltipWrapper>
</div>
<vue-json-pretty
v-if="Object.keys(configTreeData).length > 0"
:data="configTreeData"
:deep="2"
:theme="isDarkMode ? 'dark' : 'light'"
:height="800"
:dynamic-height="false"
virtual
show-icon />
</SettingsGroup>
</SettingsGroup>
<template v-if="branch === 'Nightly'">
<SettingsGroup :title="t('view.settings.advanced_groups.nightly.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.anonymous_error_reporting.header')"
:description="t('view.settings.advanced.advanced.anonymous_error_reporting.description')">
<Switch :model-value="sentryErrorReporting" @update:modelValue="setSentryErrorReporting()" />
</SettingsItem>
</SettingsGroup>
</template>
<RegistryBackupDialog />
<YouTubeApiDialog v-model:isYouTubeApiDialogVisible="isYouTubeApiDialogVisible" />
<TranslationApiDialog v-model:isTranslationApiDialogVisible="isTranslationApiDialogVisible" />
<AvatarProviderDialog v-model:isAvatarProviderDialogVisible="isAvatarProviderDialogVisible" />
<PhotonSettings v-if="photonLoggingEnabled" />
</div>
</template>
<script setup>
import { Languages, RefreshCcw, Trash2, TriangleAlert } from 'lucide-vue-next';
import { RefreshCcw, Trash2, TriangleAlert } from 'lucide-vue-next';
import { computed, reactive, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
@@ -370,17 +310,14 @@
useAdvancedSettingsStore,
useAppearanceSettingsStore,
useAuthStore,
useAvatarProviderStore,
useAvatarStore,
useGeneralSettingsStore,
useGroupStore,
useInstanceStore,
useNotificationsSettingsStore,
usePhotonStore,
useUiStore,
useUserStore,
useVRCXUpdaterStore,
useVrStore,
useWorldStore
} from '@/stores';
import { authRequest, queryRequest } from '@/api';
@@ -388,19 +325,15 @@
import { clearVRCXCache } from '@/coordinators/vrcxCoordinator';
import { openExternalLink } from '@/shared/utils';
import AvatarProviderDialog from '../../dialogs/AvatarProviderDialog.vue';
import PhotonSettings from '../PhotonSettings.vue';
import RegistryBackupDialog from '../../../Tools/dialogs/RegistryBackupDialog.vue';
import TranslationApiDialog from '../../dialogs/TranslationApiDialog.vue';
import YouTubeApiDialog from '../../dialogs/YouTubeApiDialog.vue';
import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
import { TooltipWrapper } from '@/components/ui/tooltip';
const { t } = useI18n();
const advancedSettingsStore = useAdvancedSettingsStore();
const notificationsSettingsStore = useNotificationsSettingsStore();
const { updateVRLastLocation, updateOpenVR } = useVrStore();
const { enablePrimaryPasswordChange } = useAuthStore();
const { cachedConfig } = storeToRefs(useAuthStore());
const { showConsole } = useUiStore();
@@ -424,7 +357,6 @@
const { photonLoggingEnabled } = storeToRefs(usePhotonStore());
const { branch } = storeToRefs(useVRCXUpdaterStore());
const { openVR } = storeToRefs(notificationsSettingsStore);
const { isDarkMode } = storeToRefs(useAppearanceSettingsStore());
@@ -434,14 +366,9 @@
vrcQuitFix,
autoSweepVRChatCache,
selfInviteOverride,
avatarRemoteDatabase,
enableAppLauncher,
enableAppLauncherAutoClose,
enableAppLauncherRunProcessOnce,
youTubeApi,
translationApi,
progressPie,
progressPieFilter,
showConfirmationOnSwitchAvatar,
gameLogDisabled,
sqliteTableSizes,
@@ -455,7 +382,6 @@
setVrcQuitFix,
setAutoSweepVRChatCache,
setSelfInviteOverride,
setAvatarRemoteDatabase,
setEnableAppLauncher,
setEnableAppLauncherAutoClose,
setEnableAppLauncherRunProcessOnce,
@@ -463,16 +389,10 @@
getSqliteTableSizes,
setAvatarAutoCleanup,
purgeAvatarFeedData,
showVRChatConfig,
promptAutoClearVRCXCacheFrequency,
setSentryErrorReporting
} = advancedSettingsStore;
const { isAvatarProviderDialogVisible } = storeToRefs(useAvatarProviderStore());
const { showAvatarProviderDialog } = useAvatarProviderStore();
const isYouTubeApiDialogVisible = ref(false);
const isTranslationApiDialogVisible = ref(false);
const configTreeData = ref({});
const visits = ref(0);
const selectedPurgePeriod = ref('180');
@@ -505,20 +425,6 @@
AppApi.OpenShortcutFolder();
}
/**
*
*/
function showYouTubeApiDialog() {
isYouTubeApiDialogVisible.value = true;
}
/**
*
*/
function showTranslationApiDialog() {
isTranslationApiDialogVisible.value = true;
}
/**
*
*/
@@ -531,32 +437,6 @@
cacheSize.cachedInstances = cachedInstances.size;
}
/**
*
* @param configKey
*/
async function changeYouTubeApi(configKey = '') {
if (configKey === 'VRCX_youtubeAPI') {
advancedSettingsStore.setYouTubeApi();
} else if (configKey === 'VRCX_progressPie') {
advancedSettingsStore.setProgressPie();
} else if (configKey === 'VRCX_progressPieFilter') {
advancedSettingsStore.setProgressPieFilter();
}
updateVRLastLocation();
updateOpenVR();
}
/**
*
* @param configKey
*/
async function changeTranslationAPI(configKey = '') {
if (configKey === 'VRCX_translationAPI') {
advancedSettingsStore.setTranslationApi();
}
}
/**
*
*/

View File

@@ -1,5 +1,6 @@
<template>
<div class="flex flex-col gap-10 py-2">
<!-- Discord Rich Presence -->
<SettingsGroup :title="t('view.settings.discord_presence.discord_presence.header')">
<template #description>
<p class="m-0">{{ t('view.settings.discord_presence.discord_presence.description') }}</p>
@@ -94,21 +95,83 @@
" />
</SettingsItem>
</SettingsGroup>
<!-- Translation API -->
<SettingsGroup :title="t('view.settings.advanced.advanced.translation_api.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.translation_api.enable')"
:description="t('view.settings.advanced.advanced.translation_api.enable_tooltip')">
<Switch :model-value="translationApi" @update:modelValue="changeTranslationAPI('VRCX_translationAPI')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.translation_api.translation_api_key')">
<Button size="sm" variant="outline" @click="showTranslationApiDialog">
<Languages class="h-4 w-4 mr-1.5" />
{{ t('view.settings.advanced.advanced.translation_api.translation_api_key') }}
</Button>
</SettingsItem>
</SettingsGroup>
<!-- YouTube API -->
<SettingsGroup :title="t('view.settings.advanced.advanced.youtube_api.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.youtube_api.enable')"
:description="t('view.settings.advanced.advanced.youtube_api.enable_tooltip')">
<Switch :model-value="youTubeApi" @update:modelValue="changeYouTubeApi('VRCX_youtubeAPI')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.youtube_api.youtube_api_key')">
<Button size="sm" variant="outline" @click="showYouTubeApiDialog">{{
t('view.settings.advanced.advanced.youtube_api.youtube_api_key')
}}</Button>
</SettingsItem>
</SettingsGroup>
<!-- Remote Database -->
<SettingsGroup :title="t('view.settings.advanced.advanced.remote_database.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.remote_database.enable')">
<Switch
:model-value="avatarRemoteDatabase"
@update:modelValue="setAvatarRemoteDatabase(!avatarRemoteDatabase)" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.remote_database.avatar_database_provider')">
<Button size="sm" variant="outline" @click="showAvatarProviderDialog">{{
t('view.settings.advanced.advanced.remote_database.avatar_database_provider')
}}</Button>
</SettingsItem>
</SettingsGroup>
<TranslationApiDialog v-model:isTranslationApiDialogVisible="isTranslationApiDialogVisible" />
<YouTubeApiDialog v-model:isYouTubeApiDialogVisible="isYouTubeApiDialogVisible" />
<AvatarProviderDialog v-model:isAvatarProviderDialogVisible="isAvatarProviderDialogVisible" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Languages } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useAdvancedSettingsStore, useDiscordPresenceSettingsStore } from '@/stores';
import {
useAdvancedSettingsStore,
useAvatarProviderStore,
useDiscordPresenceSettingsStore,
useVrStore
} from '@/stores';
import AvatarProviderDialog from '../../dialogs/AvatarProviderDialog.vue';
import TranslationApiDialog from '../../dialogs/TranslationApiDialog.vue';
import YouTubeApiDialog from '../../dialogs/YouTubeApiDialog.vue';
import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
const { t } = useI18n();
const advancedSettingsStore = useAdvancedSettingsStore();
const { updateVRLastLocation, updateOpenVR } = useVrStore();
const {
setDiscordActive,
setDiscordInstance,
@@ -132,5 +195,57 @@
discordWorldNameAsDiscordStatus
} = storeToRefs(useDiscordPresenceSettingsStore());
const { showVRChatConfig } = useAdvancedSettingsStore();
const { showVRChatConfig } = advancedSettingsStore;
const {
avatarRemoteDatabase,
youTubeApi,
translationApi
} = storeToRefs(advancedSettingsStore);
const {
setAvatarRemoteDatabase
} = advancedSettingsStore;
const { isAvatarProviderDialogVisible } = storeToRefs(useAvatarProviderStore());
const { showAvatarProviderDialog } = useAvatarProviderStore();
const isYouTubeApiDialogVisible = ref(false);
const isTranslationApiDialogVisible = ref(false);
/**
*
*/
function showYouTubeApiDialog() {
isYouTubeApiDialogVisible.value = true;
}
/**
*
*/
function showTranslationApiDialog() {
isTranslationApiDialogVisible.value = true;
}
/**
*
* @param configKey
*/
async function changeYouTubeApi(configKey = '') {
if (configKey === 'VRCX_youtubeAPI') {
advancedSettingsStore.setYouTubeApi();
}
updateVRLastLocation();
updateOpenVR();
}
/**
*
* @param configKey
*/
async function changeTranslationAPI(configKey = '') {
if (configKey === 'VRCX_translationAPI') {
advancedSettingsStore.setTranslationApi();
}
}
</script>

View File

@@ -134,7 +134,7 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.appearance.sorting_tables.header')">
<SettingsGroup :title="t('view.settings.interface.lists_tables.header')">
<SettingsItem :label="t('view.settings.appearance.appearance.sort_favorite_by')">
<RadioGroup
:model-value="sortFavorites ? 'true' : 'false'"
@@ -264,31 +264,6 @@
<SettingsItem :label="t('view.settings.appearance.user_dialog.vrcx_memos')">
<Switch :model-value="!hideUserMemos" @update:modelValue="setHideUserMemos" />
</SettingsItem>
<SettingsItem
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown')"
:description="t('view.settings.appearance.user_dialog.recent_action_cooldown_description')">
<Switch :model-value="recentActionCooldownEnabled" @update:modelValue="setRecentActionCooldownEnabled" />
</SettingsItem>
<SettingsItem
v-if="recentActionCooldownEnabled"
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown_minutes')">
<NumberField
:model-value="recentActionCooldownMinutes"
:min="1"
:max="1440"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="w-32"
@update:modelValue="setRecentActionCooldownMinutes">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.appearance.friend_log.header')">
@@ -360,7 +335,7 @@
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { CheckIcon, ChevronDown } from 'lucide-vue-next';
import { useAppearanceSettingsStore, useFavoriteStore, useGeneralSettingsStore, useVrStore } from '@/stores';
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Switch } from '@/components/ui/switch';
import { getLanguageName, languageCodes } from '@/localization';
@@ -380,7 +355,6 @@
const { t } = useI18n();
const appearanceSettingsStore = useAppearanceSettingsStore();
const generalSettingsStore = useGeneralSettingsStore();
const { saveOpenVROption, updateVRConfigVars } = useVrStore();
const {
@@ -432,15 +406,7 @@
setAppCjkFontPack
} = appearanceSettingsStore;
const {
recentActionCooldownEnabled,
recentActionCooldownMinutes
} = storeToRefs(generalSettingsStore);
const {
setRecentActionCooldownEnabled,
setRecentActionCooldownMinutes
} = generalSettingsStore;
const trustColorEntries = [
{

View File

@@ -14,172 +14,6 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.notifications.notifications.steamvr_notifications.header')">
<SettingsItem
:label="t('view.settings.notifications.notifications.desktop_notifications.when_to_display')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="overlayToast"
:disabled="
(!overlayNotifications || !openVR) &&
!xsNotifications &&
!ovrtHudNotifications &&
!ovrtWristNotifications
"
@update:model-value="
setOverlayToast($event);
saveOpenVROption();
">
<ToggleGroupItem value="Never">{{
t('view.settings.notifications.notifications.conditions.never')
}}</ToggleGroupItem>
<ToggleGroupItem value="Game Running">{{
t('view.settings.notifications.notifications.conditions.inside_vrchat')
}}</ToggleGroupItem>
<ToggleGroupItem value="Game Closed">{{
t('view.settings.notifications.notifications.conditions.outside_vrchat')
}}</ToggleGroupItem>
<ToggleGroupItem value="Always">{{
t('view.settings.notifications.notifications.conditions.always')
}}</ToggleGroupItem>
</ToggleGroup>
</SettingsItem>
<SettingsItem
:label="t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')">
<Switch
:model-value="openVR"
@update:modelValue="
setOpenVR();
saveOpenVROption();
" />
</SettingsItem>
<template v-if="openVR">
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.overlay_notifications')
">
<Switch
:model-value="overlayNotifications"
:disabled="!openVR"
@update:modelValue="
setOverlayNotifications();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_position')
">
<Button
size="sm"
variant="outline"
:disabled="!overlayNotifications || !openVR"
@click="showNotificationPositionDialog"
>{{
t('view.settings.notifications.notifications.steamvr_notifications.notification_position')
}}</Button
>
</SettingsItem>
</template>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_opacity')
">
<div class="w-75 max-w-full pt-1">
<Slider v-model="notificationOpacityValue" :min="0" :max="100" />
</div>
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_timeout')
">
<Button
size="sm"
variant="outline"
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
@click="promptNotificationTimeout"
>{{
t('view.settings.notifications.notifications.steamvr_notifications.notification_timeout')
}}</Button
>
</SettingsItem>
<SettingsItem
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')">
<Switch
:model-value="imageNotifications"
@update:modelValue="
setImageNotifications();
saveOpenVROption();
" />
</SettingsItem>
<template v-if="!isLinux">
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.xsoverlay_notifications')
">
<Switch
:model-value="xsNotifications"
@update:modelValue="
setXsNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
<template v-else>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.wayvr_notifications')
">
<Switch
:model-value="xsNotifications"
@update:modelValue="
setXsNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
<template v-if="!isLinux">
<SettingsItem
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
)
">
<Switch
:model-value="ovrtHudNotifications"
@update:modelValue="
setOvrtHudNotifications();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
)
">
<Switch
:model-value="ovrtWristNotifications"
@update:modelValue="
setOvrtWristNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.notifications.notifications.desktop_notifications.header')">
<SettingsItem
:label="t('view.settings.notifications.notifications.desktop_notifications.when_to_display')">
@@ -294,7 +128,6 @@
</div>
</SettingsGroup>
<NotificationPositionDialog v-model:isNotificationPositionDialogVisible="isNotificationPositionDialogVisible" />
<FeedFiltersDialog v-model:feedFiltersDialogMode="feedFiltersDialogMode" />
</div>
</template>
@@ -303,7 +136,6 @@
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
@@ -312,31 +144,19 @@
import { useI18n } from 'vue-i18n';
import {
useAdvancedSettingsStore,
useNotificationStore,
useNotificationsSettingsStore,
useVrStore
useNotificationsSettingsStore
} from '@/stores';
import FeedFiltersDialog from '../../dialogs/FeedFiltersDialog.vue';
import NotificationPositionDialog from '../../dialogs/NotificationPositionDialog.vue';
import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
const { t } = useI18n();
const notificationsSettingsStore = useNotificationsSettingsStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const { saveOpenVROption } = useVrStore();
const {
overlayToast,
openVR,
overlayNotifications,
xsNotifications,
ovrtHudNotifications,
ovrtWristNotifications,
imageNotifications,
desktopToast,
afkDesktopToast,
notificationTTS,
@@ -346,42 +166,19 @@
TTSvoices
} = storeToRefs(notificationsSettingsStore);
const { notificationOpacity } = storeToRefs(advancedSettingsStore);
const {
setOverlayToast,
setOpenVR,
setOverlayNotifications,
setXsNotifications,
setOvrtHudNotifications,
setOvrtWristNotifications,
setImageNotifications,
setDesktopToast,
setAfkDesktopToast,
setNotificationTTSNickName,
getTTSVoiceName,
changeTTSVoice,
saveNotificationTTS,
testNotificationTTS,
promptNotificationTimeout
testNotificationTTS
} = notificationsSettingsStore;
const { testNotification } = useNotificationStore();
const { setNotificationOpacity } = advancedSettingsStore;
const feedFiltersDialogMode = ref('');
const isNotificationPositionDialogVisible = ref(false);
const isLinux = computed(() => LINUX);
const notificationOpacityValue = computed({
get: () => [notificationOpacity.value],
set: (value) => {
const next = value?.[0];
if (typeof next === 'number') {
setNotificationOpacity(next);
}
}
});
const ttsVoiceIndex = computed({
get: () => {
@@ -402,11 +199,4 @@
function showNotyFeedFiltersDialog() {
feedFiltersDialogMode.value = 'noty';
}
/**
*
*/
function showNotificationPositionDialog() {
isNotificationPositionDialogVisible.value = true;
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div class="flex flex-col gap-10 py-2">
<SettingsGroup :title="t('view.settings.social.interaction.header')">
<SettingsItem
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown')"
:description="t('view.settings.appearance.user_dialog.recent_action_cooldown_description')">
<Switch :model-value="recentActionCooldownEnabled" @update:modelValue="setRecentActionCooldownEnabled" />
</SettingsItem>
<SettingsItem
v-if="recentActionCooldownEnabled"
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown_minutes')">
<NumberField
:model-value="recentActionCooldownMinutes"
:min="1"
:max="1440"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="w-32"
@update:modelValue="setRecentActionCooldownMinutes">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</SettingsItem>
</SettingsGroup>
<SettingsGroup>
<template #description>
<div class="flex items-center gap-1.5">
<span class="text-base font-semibold text-foreground">{{ t('view.settings.general.favorites.header') }}</span>
<TooltipWrapper side="top" :content="t('view.settings.general.favorites.header_tooltip')">
<Info class="size-3 text-muted-foreground cursor-help" />
</TooltipWrapper>
</div>
</template>
<Select
:model-value="localFavoriteFriendsGroups"
multiple
@update:modelValue="setLocalFavoriteFriendsGroups">
<SelectTrigger>
<SelectValue :placeholder="t('view.settings.general.favorites.group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in favoriteFriendGroups" :key="group.key" :value="group.key">
{{ group.displayName }}
</SelectItem>
</SelectGroup>
<template v-if="localFriendFavoriteGroups.length">
<SelectSeparator />
<SelectGroup>
<SelectItem
v-for="group in localFriendFavoriteGroups"
:key="'local:' + group"
:value="'local:' + group">
{{ group }}
</SelectItem>
</SelectGroup>
</template>
</SelectContent>
</Select>
</SettingsGroup>
</div>
</template>
<script setup>
import { Switch } from '@/components/ui/switch';
import { Info } from 'lucide-vue-next';
import {
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput
} from '@/components/ui/number-field';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useFavoriteStore, useGeneralSettingsStore } from '@/stores';
import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
import { TooltipWrapper } from '@/components/ui/tooltip';
const { t } = useI18n();
const generalSettingsStore = useGeneralSettingsStore();
const favoriteStore = useFavoriteStore();
const {
recentActionCooldownEnabled,
recentActionCooldownMinutes,
localFavoriteFriendsGroups
} = storeToRefs(generalSettingsStore);
const {
setRecentActionCooldownEnabled,
setRecentActionCooldownMinutes,
setLocalFavoriteFriendsGroups
} = generalSettingsStore;
const { favoriteFriendGroups, localFriendFavoriteGroups } = storeToRefs(favoriteStore);
</script>

View File

@@ -121,44 +121,6 @@
</SettingsItem>
</SettingsGroup>
<SettingsGroup>
<template #description>
<div class="flex items-center gap-1.5">
<span class="text-base font-semibold text-foreground">{{ t('view.settings.general.favorites.header') }}</span>
<TooltipWrapper side="top" :content="t('view.settings.general.favorites.header_tooltip')">
<Info class="size-3 text-muted-foreground cursor-help" />
</TooltipWrapper>
</div>
</template>
<Select
:model-value="localFavoriteFriendsGroups"
multiple
@update:modelValue="setLocalFavoriteFriendsGroups">
<SelectTrigger>
<SelectValue :placeholder="t('view.settings.general.favorites.group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in favoriteFriendGroups" :key="group.key" :value="group.key">
{{ group.displayName }}
</SelectItem>
</SelectGroup>
<template v-if="localFriendFavoriteGroups.length">
<SelectSeparator />
<SelectGroup>
<SelectItem
v-for="group in localFriendFavoriteGroups"
:key="'local:' + group"
:value="'local:' + group">
{{ group }}
</SelectItem>
</SelectGroup>
</template>
</SelectContent>
</Select>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.general.contributors.header')">
<div>
<img
@@ -198,21 +160,11 @@
import { computed, defineAsyncComponent, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Info } from 'lucide-vue-next';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { useFavoriteStore, useGeneralSettingsStore, useVRCXUpdaterStore } from '@/stores';
import { useGeneralSettingsStore, useVRCXUpdaterStore } from '@/stores';
import { links } from '@/shared/constants';
import { openExternalLink } from '@/shared/utils';
@@ -223,15 +175,13 @@
const generalSettingsStore = useGeneralSettingsStore();
const vrcxUpdaterStore = useVRCXUpdaterStore();
const favoriteStore = useFavoriteStore();
const {
isStartAtWindowsStartup,
isStartAsMinimizedState,
isCloseToTray,
disableGpuAcceleration,
disableVrOverlayGpuAcceleration,
localFavoriteFriendsGroups
disableVrOverlayGpuAcceleration
} = storeToRefs(generalSettingsStore);
const {
@@ -240,12 +190,9 @@
setIsCloseToTray,
setDisableGpuAcceleration,
setDisableVrOverlayGpuAcceleration,
setLocalFavoriteFriendsGroups,
promptProxySettings
} = generalSettingsStore;
const { favoriteFriendGroups, localFriendFavoriteGroups } = storeToRefs(favoriteStore);
const { appVersion, autoUpdateVRCX, latestAppVersion, noUpdater } = storeToRefs(vrcxUpdaterStore);
const { setAutoUpdateVRCX, checkForVRCXUpdate, showVRCXUpdateDialog, showChangeLogDialog } = vrcxUpdaterStore;

View File

@@ -0,0 +1,332 @@
<template>
<div class="flex flex-col gap-10 py-2">
<!-- VR Core -->
<SettingsGroup :title="t('view.settings.vr.vr_core.header')">
<SettingsItem
:label="t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')">
<Switch
:model-value="openVR"
@update:modelValue="
setOpenVR();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem
:label="t('view.settings.wrist_overlay.steamvr_wrist_overlay.start_overlay_with')">
<RadioGroup
:model-value="openVRAlways ? 'true' : 'false'"
:disabled="!openVR"
class="gap-2 flex"
@update:modelValue="handleOpenVRAlwaysRadio">
<div class="flex items-center space-x-2">
<RadioGroupItem id="openVRAlways-false" value="false" />
<label for="openVRAlways-false">{{ 'VRChat' }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="openVRAlways-true" value="true" />
<label for="openVRAlways-true">{{ 'SteamVR' }}</label>
</div>
</RadioGroup>
</SettingsItem>
<template v-if="!isLinux">
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.xsoverlay_notifications')
">
<Switch
:model-value="xsNotifications"
@update:modelValue="
setXsNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
<template v-else>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.wayvr_notifications')
">
<Switch
:model-value="xsNotifications"
@update:modelValue="
setXsNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
<template v-if="!isLinux">
<SettingsItem
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
)
">
<Switch
:model-value="ovrtHudNotifications"
@update:modelValue="
setOvrtHudNotifications();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
)
">
<Switch
:model-value="ovrtWristNotifications"
@update:modelValue="
setOvrtWristNotifications();
saveOpenVROption();
" />
</SettingsItem>
</template>
</SettingsGroup>
<!-- VR Notifications -->
<SettingsGroup :title="t('view.settings.vr.vr_notifications.header')">
<SettingsItem
:label="t('view.settings.notifications.notifications.desktop_notifications.when_to_display')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="overlayToast"
:disabled="
(!overlayNotifications || !openVR) &&
!xsNotifications &&
!ovrtHudNotifications &&
!ovrtWristNotifications
"
@update:model-value="
setOverlayToast($event);
saveOpenVROption();
">
<ToggleGroupItem value="Never">{{
t('view.settings.notifications.notifications.conditions.never')
}}</ToggleGroupItem>
<ToggleGroupItem value="Game Running">{{
t('view.settings.notifications.notifications.conditions.inside_vrchat')
}}</ToggleGroupItem>
<ToggleGroupItem value="Game Closed">{{
t('view.settings.notifications.notifications.conditions.outside_vrchat')
}}</ToggleGroupItem>
<ToggleGroupItem value="Always">{{
t('view.settings.notifications.notifications.conditions.always')
}}</ToggleGroupItem>
</ToggleGroup>
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.overlay_notifications')
">
<Switch
:model-value="overlayNotifications"
:disabled="!openVR"
@update:modelValue="
setOverlayNotifications();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_position')
">
<Button
size="sm"
variant="outline"
:disabled="!overlayNotifications || !openVR"
@click="showNotificationPositionDialog"
>{{
t('view.settings.notifications.notifications.steamvr_notifications.notification_position')
}}</Button
>
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_opacity')
">
<div class="w-75 max-w-full pt-1">
<Slider v-model="notificationOpacityValue" :min="0" :max="100" />
</div>
</SettingsItem>
<SettingsItem
:label="
t('view.settings.notifications.notifications.steamvr_notifications.notification_timeout')
">
<Button
size="sm"
variant="outline"
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
@click="promptNotificationTimeout"
>{{
t('view.settings.notifications.notifications.steamvr_notifications.notification_timeout')
}}</Button
>
</SettingsItem>
<SettingsItem
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')">
<Switch
:model-value="imageNotifications"
@update:modelValue="
setImageNotifications();
saveOpenVROption();
" />
</SettingsItem>
</SettingsGroup>
<!-- Wrist Overlay -->
<WristOverlaySettings @open-feed-filters="showWristFeedFiltersDialog" />
<!-- VR Extras -->
<SettingsGroup :title="t('view.settings.vr.vr_extras.header')">
<SettingsItem :label="t('view.settings.advanced.advanced.video_progress_pie.header')"
:description="t('view.settings.advanced.advanced.video_progress_pie.enable_tooltip')">
<Switch
:model-value="progressPie"
:disabled="!openVR"
@update:modelValue="changeYouTubeApi('VRCX_progressPie')" />
</SettingsItem>
<SettingsItem :label="t('view.settings.advanced.advanced.video_progress_pie.dance_world_only')">
<Switch
:model-value="progressPieFilter"
:disabled="!openVR"
@update:modelValue="changeYouTubeApi('VRCX_progressPieFilter')" />
</SettingsItem>
</SettingsGroup>
<NotificationPositionDialog v-model:isNotificationPositionDialogVisible="isNotificationPositionDialogVisible" />
<FeedFiltersDialog v-model:feedFiltersDialogMode="feedFiltersDialogMode" />
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import {
useAdvancedSettingsStore,
useNotificationsSettingsStore,
useVrStore,
useWristOverlaySettingsStore
} from '@/stores';
import FeedFiltersDialog from '../../dialogs/FeedFiltersDialog.vue';
import NotificationPositionDialog from '../../dialogs/NotificationPositionDialog.vue';
import WristOverlaySettings from '../WristOverlaySettings.vue';
import SettingsGroup from '../SettingsGroup.vue';
import SettingsItem from '../SettingsItem.vue';
const { t } = useI18n();
const notificationsSettingsStore = useNotificationsSettingsStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const wristOverlaySettingsStore = useWristOverlaySettingsStore();
const { saveOpenVROption } = useVrStore();
const { updateVRLastLocation, updateOpenVR } = useVrStore();
const {
overlayToast,
openVR,
overlayNotifications,
xsNotifications,
ovrtHudNotifications,
ovrtWristNotifications,
imageNotifications
} = storeToRefs(notificationsSettingsStore);
const { notificationOpacity } = storeToRefs(advancedSettingsStore);
const { openVRAlways } = storeToRefs(wristOverlaySettingsStore);
const { setOpenVRAlways } = wristOverlaySettingsStore;
const {
progressPie,
progressPieFilter
} = storeToRefs(advancedSettingsStore);
const {
setOverlayToast,
setOpenVR,
setOverlayNotifications,
setXsNotifications,
setOvrtHudNotifications,
setOvrtWristNotifications,
setImageNotifications,
promptNotificationTimeout
} = notificationsSettingsStore;
const { setNotificationOpacity } = advancedSettingsStore;
const isNotificationPositionDialogVisible = ref(false);
const feedFiltersDialogMode = ref('');
const isLinux = computed(() => LINUX);
const notificationOpacityValue = computed({
get: () => [notificationOpacity.value],
set: (value) => {
const next = value?.[0];
if (typeof next === 'number') {
setNotificationOpacity(next);
}
}
});
/**
*
*/
function showNotificationPositionDialog() {
isNotificationPositionDialogVisible.value = true;
}
/**
*
*/
function showWristFeedFiltersDialog() {
feedFiltersDialogMode.value = 'wrist';
}
/**
*
* @param value
*/
function handleOpenVRAlwaysRadio(value) {
const nextValue = value === 'true';
if (nextValue !== openVRAlways.value) {
setOpenVRAlways();
saveOpenVROption();
}
}
/**
*
* @param configKey
*/
async function changeYouTubeApi(configKey = '') {
if (configKey === 'VRCX_progressPie') {
advancedSettingsStore.setProgressPie();
} else if (configKey === 'VRCX_progressPieFilter') {
advancedSettingsStore.setProgressPieFilter();
}
updateVRLastLocation();
updateOpenVR();
}
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
<WristOverlaySettings @open-feed-filters="showWristFeedFiltersDialog" />
<FeedFiltersDialog v-model:feedFiltersDialogMode="feedFiltersDialogMode" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import FeedFiltersDialog from '../../dialogs/FeedFiltersDialog.vue';
import WristOverlaySettings from '../WristOverlaySettings.vue';
const feedFiltersDialogMode = ref('');
function showWristFeedFiltersDialog() {
feedFiltersDialogMode.value = 'wrist';
}
</script>

View File

@@ -1,99 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
discordStore: {
setDiscordActive: vi.fn(),
setDiscordInstance: vi.fn(),
setDiscordHideInvite: vi.fn(),
setDiscordJoinButton: vi.fn(),
setDiscordHideImage: vi.fn(),
setDiscordShowPlatform: vi.fn(),
setDiscordWorldIntegration: vi.fn(),
setDiscordWorldNameAsDiscordStatus: vi.fn(),
saveDiscordOption: vi.fn(),
discordActive: { __v_isRef: true, value: true },
discordInstance: { __v_isRef: true, value: true },
discordHideInvite: { __v_isRef: true, value: false },
discordJoinButton: { __v_isRef: true, value: true },
discordHideImage: { __v_isRef: true, value: false },
discordShowPlatform: { __v_isRef: true, value: true },
discordWorldIntegration: { __v_isRef: true, value: true },
discordWorldNameAsDiscordStatus: { __v_isRef: true, value: false }
},
showVRChatConfig: vi.fn()
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../../../stores', () => ({
useDiscordPresenceSettingsStore: () => mocks.discordStore,
useAdvancedSettingsStore: () => ({
showVRChatConfig: (...a) => mocks.showVRChatConfig(...a)
})
}));
vi.mock('@/components/ui/switch', () => ({
Switch: {
props: ['modelValue', 'disabled'],
emits: ['update:modelValue'],
template:
'<button data-testid="switch" :data-disabled="disabled || undefined" @click="$emit(\'update:modelValue\', !modelValue)"><slot /></button>'
}
}));
vi.mock('../../SettingsGroup.vue', () => ({
default: { template: '<div><slot /><slot name="description" /></div>' }
}));
vi.mock('../../SettingsItem.vue', () => ({
default: {
props: ['label', 'description'],
template: '<div :data-label="label"><slot /></div>'
}
}));
import DiscordPresenceTab from '../DiscordPresenceTab.vue';
describe('DiscordPresenceTab.vue', () => {
beforeEach(() => {
mocks.discordStore.discordActive.value = true;
mocks.discordStore.discordInstance.value = true;
mocks.discordStore.setDiscordActive.mockClear();
mocks.discordStore.saveDiscordOption.mockClear();
mocks.showVRChatConfig.mockClear();
});
it('opens VRChat config and handles switch changes', async () => {
const wrapper = mount(DiscordPresenceTab);
const tooltipRow = wrapper
.findAll('p')
.find((node) =>
node
.text()
.includes(
'view.settings.discord_presence.discord_presence.enable_tooltip'
)
);
expect(tooltipRow).toBeTruthy();
await tooltipRow.trigger('click');
expect(mocks.showVRChatConfig).toHaveBeenCalledTimes(1);
const switches = wrapper.findAll('[data-testid="switch"]');
await switches[0].trigger('click');
expect(mocks.discordStore.setDiscordActive).toHaveBeenCalledTimes(1);
expect(mocks.discordStore.saveDiscordOption).toHaveBeenCalled();
});
it('passes disabled state to dependent switches when discord is disabled', () => {
mocks.discordStore.discordActive.value = false;
const wrapper = mount(DiscordPresenceTab);
const switches = wrapper.findAll('[data-testid="switch"]');
const worldIntegrationSwitch = switches[1];
expect(worldIntegrationSwitch?.attributes('data-disabled')).toBe('true');
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
vi.mock('../../WristOverlaySettings.vue', () => ({
default: {
emits: ['open-feed-filters'],
template:
'<button data-testid="open-filters" @click="$emit(\'open-feed-filters\')">open</button>'
}
}));
vi.mock('../../../dialogs/FeedFiltersDialog.vue', () => ({
default: {
props: ['feedFiltersDialogMode'],
template:
'<div data-testid="feed-dialog" :data-mode="feedFiltersDialogMode" />'
}
}));
import WristOverlayTab from '../WristOverlayTab.vue';
describe('WristOverlayTab.vue', () => {
it('sets feed dialog mode to wrist when child emits open-feed-filters', async () => {
const wrapper = mount(WristOverlayTab);
expect(
wrapper.get('[data-testid="feed-dialog"]').attributes('data-mode')
).toBe('');
await wrapper.get('[data-testid="open-filters"]').trigger('click');
expect(
wrapper.get('[data-testid="feed-dialog"]').attributes('data-mode')
).toBe('wrist');
});
});

View File

@@ -17,15 +17,6 @@
>
</SettingsItem>
<SettingsItem :label="t('view.settings.wrist_overlay.steamvr_wrist_overlay.steamvr_overlay')">
<Switch
:model-value="openVR"
@update:modelValue="
setOpenVR();
saveOpenVROption();
" />
</SettingsItem>
<SettingsItem :label="t('view.settings.wrist_overlay.steamvr_wrist_overlay.wrist_feed_overlay')">
<Switch
:model-value="overlayWrist"
@@ -46,24 +37,6 @@
" />
</SettingsItem>
<SettingsItem
:label="t('view.settings.wrist_overlay.steamvr_wrist_overlay.start_overlay_with')">
<RadioGroup
:model-value="openVRAlways ? 'true' : 'false'"
:disabled="!openVR"
class="gap-2 flex"
@update:modelValue="handleOpenVRAlwaysRadio">
<div class="flex items-center space-x-2">
<RadioGroupItem id="openVRAlways-false" value="false" />
<label for="openVRAlways-false">{{ 'VRChat' }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="openVRAlways-true" value="true" />
<label for="openVRAlways-true">{{ 'SteamVR' }}</label>
</div>
</RadioGroup>
</SettingsItem>
<SettingsItem :label="t('view.settings.wrist_overlay.steamvr_wrist_overlay.overlay_button')">
<RadioGroup
:model-value="overlaybutton ? 'true' : 'false'"
@@ -196,10 +169,10 @@
const { openVR } = storeToRefs(notificationsSettingsStore);
const {
overlayWrist,
hidePrivateFromFeed,
openVRAlways,
overlaybutton,
overlayHand,
vrBackgroundEnabled,
@@ -210,12 +183,9 @@
pcUptimeOnFeed
} = storeToRefs(wristOverlaySettingsStore);
const { setOpenVR } = notificationsSettingsStore;
const {
setOverlayWrist,
setHidePrivateFromFeed,
setOpenVRAlways,
setOverlaybutton,
setOverlayHand,
setVrBackgroundEnabled,
@@ -228,18 +198,6 @@
const { saveOpenVROption } = useVrStore();
/**
*
* @param value
*/
function handleOpenVRAlwaysRadio(value) {
const nextValue = value === 'true';
if (nextValue !== openVRAlways.value) {
setOpenVRAlways();
saveOpenVROption();
}
}
/**
*
* @param value

View File

@@ -9,7 +9,6 @@ const mocks = vi.hoisted(() => ({
wristStore: {
overlayWrist: { value: true },
hidePrivateFromFeed: { value: false },
openVRAlways: { value: false },
overlaybutton: { value: false },
overlayHand: { value: '1' },
vrBackgroundEnabled: { value: false },
@@ -20,7 +19,6 @@ const mocks = vi.hoisted(() => ({
pcUptimeOnFeed: { value: false },
setOverlayWrist: vi.fn(),
setHidePrivateFromFeed: vi.fn(),
setOpenVRAlways: vi.fn(),
setOverlaybutton: vi.fn(),
setOverlayHand: vi.fn(),
setVrBackgroundEnabled: vi.fn(),
@@ -95,10 +93,9 @@ describe('WristOverlaySettings.vue', () => {
beforeEach(() => {
mocks.notificationsStore.openVR.value = true;
mocks.wristStore.overlayWrist.value = true;
mocks.wristStore.openVRAlways.value = false;
mocks.wristStore.overlaybutton.value = false;
mocks.notificationsStore.setOpenVR.mockClear();
mocks.wristStore.setOpenVRAlways.mockClear();
mocks.wristStore.setOverlayWrist.mockClear();
mocks.wristStore.setHidePrivateFromFeed.mockClear();
mocks.wristStore.setOverlaybutton.mockClear();
mocks.wristStore.setOverlayHand.mockClear();
mocks.saveOpenVROption.mockClear();
@@ -107,32 +104,33 @@ describe('WristOverlaySettings.vue', () => {
it('emits open-feed-filters and handles switch/radio/toggle updates', async () => {
const wrapper = mount(WristOverlaySettings);
// Feed filters button emits event
await wrapper.get('[data-testid="filters-btn"]').trigger('click');
expect(wrapper.emitted('open-feed-filters')).toBeTruthy();
// First switch is now overlayWrist (SteamVR Overlay moved to VrTab)
const switches = wrapper.findAll('[data-testid="switch"]');
await switches[0].trigger('click');
expect(mocks.notificationsStore.setOpenVR).toHaveBeenCalledTimes(1);
expect(mocks.wristStore.setOverlayWrist).toHaveBeenCalledTimes(1);
expect(mocks.saveOpenVROption).toHaveBeenCalled();
// First (and only) radio group is now overlay button (Start Overlay With moved to VrTab)
const radioGroups = wrapper.findAll('[data-testid="radio-group"]');
await radioGroups[0].get('[data-testid="radio-true"]').trigger('click');
expect(mocks.wristStore.setOpenVRAlways).toHaveBeenCalledTimes(1);
await radioGroups[1].get('[data-testid="radio-true"]').trigger('click');
expect(mocks.wristStore.setOverlaybutton).toHaveBeenCalledTimes(1);
// Toggle group for overlay hand
await wrapper.get('[data-testid="toggle-right"]').trigger('click');
expect(mocks.wristStore.setOverlayHand).toHaveBeenCalledWith('2');
});
it('does not toggle openVRAlways when the value is unchanged', async () => {
mocks.wristStore.openVRAlways.value = true;
it('does not toggle overlaybutton when the value is unchanged', async () => {
mocks.wristStore.overlaybutton.value = true;
const wrapper = mount(WristOverlaySettings);
const firstRadio = wrapper.findAll('[data-testid="radio-group"]')[0];
await firstRadio.get('[data-testid="radio-true"]').trigger('click');
expect(mocks.wristStore.setOpenVRAlways).not.toHaveBeenCalled();
expect(mocks.wristStore.setOverlaybutton).not.toHaveBeenCalled();
});
});