improve updater ux

This commit is contained in:
pa
2026-01-13 11:43:01 +09:00
committed by Natsumi
parent f077fcfd51
commit aab248a3af
12 changed files with 250 additions and 42 deletions

View File

@@ -2,21 +2,20 @@
<div class="x-menu-container nav-menu-container" :class="{ 'is-collapsed': isCollapsed }">
<template v-if="navLayoutReady">
<div class="nav-menu-body mt-5">
<div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog">
<el-progress
type="circle"
:width="50"
:stroke-width="3"
:percentage="updateProgress"
:format="updateProgressText"
style="padding: 7px"></el-progress>
</div>
<div v-else-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
<div v-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
<Button
variant="outline"
style="font-size: 19px; height: 36px; width: 44px; margin: 10px"
variant="ghost"
size="icon"
class="hover:bg-transparent"
style="font-size: 19px; height: 36px; margin: 10px"
@click="showVRCXUpdateDialog">
<i class="ri-download-line"></i>
<span class="relative inline-flex items-center justify-center">
<i class="ri-arrow-down-circle-line text-muted-foreground text-[20px]"></i>
<span class="absolute top-0.5 -right-1 h-1.5 w-1.5 rounded-full bg-red-500"></span>
</span>
<span v-if="!isCollapsed" class="text-[13px] text-muted-foreground">{{
t('nav_menu.update_available')
}}</span>
</Button>
</div>

View File

@@ -18,36 +18,46 @@
<br />
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
</div>
<Select
:model-value="branch"
@update:modelValue="
(v) => {
branch = v;
loadBranchVersions();
}
">
<SelectTrigger style="display: inline-flex; width: 150px; margin-right: 15px">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="b in branches" :key="b.name" :value="b.name">{{ b.name }}</SelectItem>
</SelectContent>
</Select>
<Select
:model-value="VRCXUpdateDialog.release"
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
<SelectTrigger style="display: inline-flex; width: 150px">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="item in VRCXUpdateDialog.releases" :key="item.name" :value="item.name">
{{ item.tag_name }}
</SelectItem>
</SelectContent>
</Select>
<Tabs :model-value="branch" class="w-full" @update:modelValue="handleBranchChange">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="Stable">{{ t('dialog.vrcx_updater.branch_stable') }}</TabsTrigger>
<TabsTrigger value="Nightly">{{ t('dialog.vrcx_updater.branch_nightly') }}</TabsTrigger>
</TabsList>
<TabsContent value="Nightly">
<Alert variant="destructive">
<AlertCircle class="text-muted-foreground" />
<AlertTitle>{{ t('dialog.vrcx_updater.nightly_title') }}</AlertTitle>
<AlertDescription>
{{ t('dialog.vrcx_updater.nightly_notice') }}
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
<FieldGroup class="mt-3">
<Field>
<FieldLabel>{{ t('dialog.vrcx_updater.release') }}</FieldLabel>
<FieldContent>
<Select
:model-value="VRCXUpdateDialog.release"
@update:modelValue="(v) => (VRCXUpdateDialog.release = v)">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="item in VRCXUpdateDialog.releases"
:key="item.name"
:value="item.name">
{{ item.tag_name }}
</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
</FieldGroup>
<div
v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion"
style="margin-top: 15px">
class="mt-3 text-xs text-muted-foreground">
<span>{{ t('dialog.vrcx_updater.latest_version') }}</span>
</div>
</template>
@@ -72,14 +82,17 @@
</template>
<script setup>
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { nextTick, ref, watch } from 'vue';
import { AlertCircle } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { branches } from '../../shared/constants';
import { getNextDialogIndex } from '../../shared/utils/base/ui';
import { useVRCXUpdaterStore } from '../../stores';
@@ -99,6 +112,13 @@
const { t } = useI18n();
const VRCXUpdateDialogIndex = ref(2000);
const handleBranchChange = (value) => {
if (!value || value === branch.value) {
return;
}
branch.value = value;
loadBranchVersions();
};
watch(
() => VRCXUpdateDialog,

View File

@@ -0,0 +1,16 @@
<script setup>
import { cn } from '@/lib/utils';
import { alertVariants } from '.';
const props = defineProps({
class: { type: null, required: false },
variant: { type: null, required: false }
});
</script>
<template>
<div data-slot="alert" :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="alert-description"
:class="
cn(
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="alert-title"
:class="cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
import { cva } from 'class-variance-authority';
export { default as Alert } from './Alert.vue';
export { default as AlertDescription } from './AlertDescription.vue';
export { default as AlertTitle } from './AlertTitle.vue';
export const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
{
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90'
}
},
defaultVariants: {
variant: 'default'
}
}
);

View File

@@ -0,0 +1,27 @@
<script setup>
import { TabsRoot, useForwardPropsEmits } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
defaultValue: { type: null, required: false },
orientation: { type: String, required: false },
dir: { type: String, required: false },
activationMode: { type: String, required: false },
modelValue: { type: null, required: false },
unmountOnHide: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits(['update:modelValue']);
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<TabsRoot v-slot="slotProps" data-slot="tabs" v-bind="forwarded" :class="cn('flex flex-col gap-2', props.class)">
<slot v-bind="slotProps" />
</TabsRoot>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { TabsContent } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
value: { type: [String, Number], required: true },
forceMount: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<TabsContent data-slot="tabs-content" :class="cn('flex-1 outline-none', props.class)" v-bind="delegatedProps">
<slot />
</TabsContent>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
import { TabsList } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
loop: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<TabsList
data-slot="tabs-list"
v-bind="delegatedProps"
:class="
cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
props.class
)
">
<slot />
</TabsList>
</template>

View File

@@ -0,0 +1,31 @@
<script setup>
import { TabsTrigger, useForwardProps } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
value: { type: [String, Number], required: true },
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<TabsTrigger
data-slot="tabs-trigger"
:class="
cn(
'data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
"
v-bind="forwardedProps">
<slot />
</TabsTrigger>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Tabs } from './Tabs.vue';
export { default as TabsContent } from './TabsContent.vue';
export { default as TabsList } from './TabsList.vue';
export { default as TabsTrigger } from './TabsTrigger.vue';

View File

@@ -33,6 +33,7 @@
"github": "VRCX on GitHub",
"discord": "Join our Discord",
"whats_new": "What's New?",
"update_available": "Update available",
"custom_nav": {
"header": "Custom Navigation",
"dialog_title": "Customize Navigation Menu",
@@ -1534,6 +1535,11 @@
"header": "VRCX Updater",
"latest_version": "VRCX is up to date.",
"ready_for_update": "Ready for install, restart VRCX to apply.",
"branch_stable": "Stable",
"branch_nightly": "Nightly",
"nightly_title": "Nightly builds",
"nightly_notice": "Nightly builds are for testing. For stability, use Stable.",
"release": "Release",
"download": "Download",
"install": "Install",
"cancel": "Cancel"