mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
add whats new dialog
This commit is contained in:
193
src/components/onboarding/WhatsNewDialog.vue
Normal file
193
src/components/onboarding/WhatsNewDialog.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="whatsNewDialog.visible">
|
||||||
|
<DialogContent
|
||||||
|
class="border-none! bg-background/88 p-5 shadow-[0_0_0_1px_hsl(from_var(--border)_h_s_l/0.5),0_24px_48px_hsl(from_var(--background)_h_s_l/0.4)] backdrop-blur-xl backdrop-saturate-[1.4] sm:max-w-2xl"
|
||||||
|
:show-close-button="false"
|
||||||
|
@escape-key-down="handleDismiss"
|
||||||
|
@pointer-down-outside="handleDismiss"
|
||||||
|
@interact-outside.prevent>
|
||||||
|
<div class="pt-1 text-center">
|
||||||
|
<div class="mb-2 flex justify-center">
|
||||||
|
<img :src="vrcxLogo" alt="VRCX" class="size-12 rounded-xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<h2 class="m-0 text-[23px] font-bold tracking-tight">
|
||||||
|
{{ releaseTitle }}
|
||||||
|
</h2>
|
||||||
|
<Badge
|
||||||
|
v-if="releaseLabel"
|
||||||
|
variant="secondary"
|
||||||
|
class="rounded-full px-2.5 py-0.5 text-[11px] tracking-[0.08em]">
|
||||||
|
{{ releaseLabel }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
class="mt-2 h-auto gap-1 px-0 text-muted-foreground hover:text-foreground"
|
||||||
|
@click="handleViewDetails">
|
||||||
|
{{ t('onboarding.whatsnew.common.view_details') }}
|
||||||
|
<ChevronRight class="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<div
|
||||||
|
v-for="(feature, index) in whatsNewDialog.items"
|
||||||
|
:key="feature.key"
|
||||||
|
class="group flex animate-[whatsNewAppear_0.45s_ease-out_both] cursor-default flex-col rounded-[12px] border border-transparent bg-muted/55 px-3 py-3.5 text-left transition-all duration-250 hover:-translate-y-0.5 hover:border-primary/20 hover:bg-muted/80 hover:shadow-[0_4px_18px_hsl(from_var(--primary)_h_s_l/0.08)]"
|
||||||
|
:style="{ animationDelay: `${0.08 + index * 0.06}s` }">
|
||||||
|
<div
|
||||||
|
class="mb-3 flex size-10 items-center justify-center rounded-[10px] transition-all duration-250"
|
||||||
|
:style="{
|
||||||
|
background: `hsl(${resolveHue(feature.icon)} 60% 50% / 0.12)`,
|
||||||
|
color: `hsl(${resolveHue(feature.icon)} 60% 55%)`
|
||||||
|
}">
|
||||||
|
<component :is="resolveIcon(feature.icon)" class="size-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1 text-sm font-semibold">
|
||||||
|
{{ t(feature.titleKey) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-[12.5px] leading-snug text-muted-foreground">
|
||||||
|
{{ t(feature.descriptionKey) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 rounded-[12px] border border-border/60 bg-muted/30 px-3 py-3">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||||
|
<HeartHandshake class="size-4 text-muted-foreground" />
|
||||||
|
<span>{{ t('onboarding.whatsnew.common.support') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
v-for="supporter in supporters"
|
||||||
|
:key="supporter.name"
|
||||||
|
class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="min-w-20 text-sm font-medium text-foreground">
|
||||||
|
{{ supporter.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-for="link in supporter.links"
|
||||||
|
:key="link.label"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 rounded-full px-3 text-xs font-medium"
|
||||||
|
@click="openExternalLink(link.url)">
|
||||||
|
{{ link.label }}
|
||||||
|
<ExternalLink class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button class="mt-1 h-11 w-full text-sm font-semibold" size="lg" @click="handleDismiss">
|
||||||
|
<Sparkles class="size-4" />
|
||||||
|
{{ t('onboarding.whatsnew.common.got_it') }}
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, markRaw } from 'vue';
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
ExternalLink,
|
||||||
|
HeartHandshake,
|
||||||
|
LayoutDashboard,
|
||||||
|
Search,
|
||||||
|
Sparkles,
|
||||||
|
Activity,
|
||||||
|
Images
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { openExternalLink } from '../../shared/utils';
|
||||||
|
import { useVRCXUpdaterStore } from '../../stores';
|
||||||
|
import vrcxLogo from '../../../images/VRCX.png';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const vrcxUpdaterStore = useVRCXUpdaterStore();
|
||||||
|
const { whatsNewDialog } = storeToRefs(vrcxUpdaterStore);
|
||||||
|
const { closeWhatsNewDialog, openChangeLogDialogOnly } = vrcxUpdaterStore;
|
||||||
|
|
||||||
|
const releaseLabel = computed(() => whatsNewDialog.value.version || '');
|
||||||
|
const releaseTitle = computed(() => {
|
||||||
|
const releaseKey = whatsNewDialog.value.releaseKey;
|
||||||
|
return releaseKey
|
||||||
|
? t(`onboarding.whatsnew.releases.${releaseKey}.title`)
|
||||||
|
: t('onboarding.whatsnew.title');
|
||||||
|
});
|
||||||
|
|
||||||
|
const supporters = [
|
||||||
|
{
|
||||||
|
name: 'Map1en',
|
||||||
|
links: [
|
||||||
|
{ label: 'Ko-fi', url: 'https://ko-fi.com/map1en_' },
|
||||||
|
{ label: '爱发电', url: 'https://ifdian.net/a/map1en_' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Natsumi',
|
||||||
|
links: [
|
||||||
|
{ label: 'Ko-fi', url: 'https://ko-fi.com/natsumi_sama' },
|
||||||
|
{ label: 'Patreon', url: 'https://www.patreon.com/Natsumi_VRCX' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
search: markRaw(Search),
|
||||||
|
'layout-dashboard': markRaw(LayoutDashboard),
|
||||||
|
activity: markRaw(Activity),
|
||||||
|
images: markRaw(Images)
|
||||||
|
};
|
||||||
|
|
||||||
|
const hueMap = {
|
||||||
|
search: '142',
|
||||||
|
'layout-dashboard': '45',
|
||||||
|
activity: '200',
|
||||||
|
images: '280'
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveIcon(iconName) {
|
||||||
|
return iconMap[iconName] ?? Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHue(iconName) {
|
||||||
|
return hueMap[iconName] ?? '210';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleViewDetails() {
|
||||||
|
closeWhatsNewDialog();
|
||||||
|
await openChangeLogDialogOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDismiss() {
|
||||||
|
closeWhatsNewDialog();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes whatsNewAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2983,7 +2983,35 @@
|
|||||||
"whatsnew": {
|
"whatsnew": {
|
||||||
"title": "What's New in VRCX",
|
"title": "What's New in VRCX",
|
||||||
"subtitle": "Here's what changed in this update.",
|
"subtitle": "Here's what changed in this update.",
|
||||||
"cta": "Got It"
|
"cta": "Got It",
|
||||||
|
"common": {
|
||||||
|
"got_it": "Got It",
|
||||||
|
"view_details": "View detailed changelog",
|
||||||
|
"support": "Support VRCX"
|
||||||
|
},
|
||||||
|
"releases": {
|
||||||
|
"v2026_04_a": {
|
||||||
|
"title": "What's New in VRCX",
|
||||||
|
"items": {
|
||||||
|
"quick_search": {
|
||||||
|
"title": "Quick Search",
|
||||||
|
"description": "Find friends, worlds, avatars, and groups from one search."
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"description": "Build a workspace with widgets that fit how you work."
|
||||||
|
},
|
||||||
|
"activity_insights": {
|
||||||
|
"title": "Activity Insights",
|
||||||
|
"description": "See activity heatmaps, overlap, and the worlds you visit most."
|
||||||
|
},
|
||||||
|
"my_avatars": {
|
||||||
|
"title": "My Avatars",
|
||||||
|
"description": "Browse, manage, and edit your avatars in one place."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status_bar": {
|
"status_bar": {
|
||||||
|
|||||||
66
src/shared/constants/whatsNewReleases.js
Normal file
66
src/shared/constants/whatsNewReleases.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const whatsNewReleases = {
|
||||||
|
v2026_04_a: {
|
||||||
|
titleKey: 'onboarding.whatsnew.releases.v2026_04_a.title',
|
||||||
|
releaseLabel: '2026.04',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'quick_search',
|
||||||
|
icon: 'search',
|
||||||
|
titleKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.quick_search.title',
|
||||||
|
descriptionKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.quick_search.description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dashboard',
|
||||||
|
icon: 'layout-dashboard',
|
||||||
|
titleKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.dashboard.title',
|
||||||
|
descriptionKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.dashboard.description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'activity_insights',
|
||||||
|
icon: 'activity',
|
||||||
|
titleKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.activity_insights.title',
|
||||||
|
descriptionKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.activity_insights.description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'my_avatars',
|
||||||
|
icon: 'images',
|
||||||
|
titleKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.my_avatars.title',
|
||||||
|
descriptionKey:
|
||||||
|
'onboarding.whatsnew.releases.v2026_04_a.items.my_avatars.description'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getWhatsNewReleaseKey(version) {
|
||||||
|
const match = String(version || '').match(/(\d{4})\.(\d{2})(?:\.\d{2})?/);
|
||||||
|
if (!match) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `v${match[1]}_${match[2]}_a`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {{titleKey: string, releaseLabel?: string, items: Array<{key: string, icon: string, titleKey: string, descriptionKey: string}>} | null}
|
||||||
|
*/
|
||||||
|
function getWhatsNewRelease(version) {
|
||||||
|
const releaseKey = String(version || '');
|
||||||
|
if (!releaseKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return whatsNewReleases[releaseKey] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getWhatsNewRelease, getWhatsNewReleaseKey, whatsNewReleases };
|
||||||
@@ -5,6 +5,10 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
import { logWebRequest } from '../services/appConfig';
|
import { logWebRequest } from '../services/appConfig';
|
||||||
import { branches } from '../shared/constants';
|
import { branches } from '../shared/constants';
|
||||||
|
import {
|
||||||
|
getWhatsNewRelease,
|
||||||
|
getWhatsNewReleaseKey
|
||||||
|
} from '../shared/constants/whatsNewReleases';
|
||||||
import { changeLogRemoveLinks } from '../shared/utils';
|
import { changeLogRemoveLinks } from '../shared/utils';
|
||||||
|
|
||||||
import configRepository from '../services/config';
|
import configRepository from '../services/config';
|
||||||
@@ -36,6 +40,12 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
|
|||||||
buildName: '',
|
buildName: '',
|
||||||
changeLog: ''
|
changeLog: ''
|
||||||
});
|
});
|
||||||
|
const whatsNewDialog = ref({
|
||||||
|
visible: false,
|
||||||
|
version: '',
|
||||||
|
releaseKey: '',
|
||||||
|
items: []
|
||||||
|
});
|
||||||
const pendingVRCXUpdate = ref(false);
|
const pendingVRCXUpdate = ref(false);
|
||||||
const pendingVRCXInstall = ref('');
|
const pendingVRCXInstall = ref('');
|
||||||
const updateInProgress = ref(false);
|
const updateInProgress = ref(false);
|
||||||
@@ -73,7 +83,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
|
|||||||
await loadVrcxId();
|
await loadVrcxId();
|
||||||
|
|
||||||
if (await compareAppVersion()) {
|
if (await compareAppVersion()) {
|
||||||
showChangeLogDialog();
|
await showWhatsNewDialog();
|
||||||
}
|
}
|
||||||
if (autoUpdateVRCX.value !== 'Off') {
|
if (autoUpdateVRCX.value !== 'Off') {
|
||||||
await checkForVRCXUpdate();
|
await checkForVRCXUpdate();
|
||||||
@@ -134,6 +144,56 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getCurrentWhatsNewReleaseKey() {
|
||||||
|
return getWhatsNewReleaseKey(currentVersion.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function showWhatsNewDialog() {
|
||||||
|
const releaseKey = getCurrentWhatsNewReleaseKey();
|
||||||
|
const release = getWhatsNewRelease(releaseKey);
|
||||||
|
|
||||||
|
if (!release) {
|
||||||
|
whatsNewDialog.value = {
|
||||||
|
visible: false,
|
||||||
|
version: '',
|
||||||
|
releaseKey: '',
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayVersion = currentVersion.value.replace(/^VRCX\s+/, '');
|
||||||
|
|
||||||
|
whatsNewDialog.value = {
|
||||||
|
visible: true,
|
||||||
|
version: release.releaseLabel || displayVersion,
|
||||||
|
releaseKey,
|
||||||
|
items: release.items.map((item) => ({ ...item }))
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWhatsNewDialog() {
|
||||||
|
whatsNewDialog.value.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openChangeLogDialogOnly() {
|
||||||
|
changeLogDialog.value.visible = true;
|
||||||
|
if (
|
||||||
|
!changeLogDialog.value.buildName ||
|
||||||
|
!changeLogDialog.value.changeLog
|
||||||
|
) {
|
||||||
|
await checkForVRCXUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
async function loadVrcxId() {
|
async function loadVrcxId() {
|
||||||
if (!vrcxId.value) {
|
if (!vrcxId.value) {
|
||||||
vrcxId.value = crypto.randomUUID();
|
vrcxId.value = crypto.randomUUID();
|
||||||
@@ -416,6 +476,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
|
|||||||
checkingForVRCXUpdate,
|
checkingForVRCXUpdate,
|
||||||
VRCXUpdateDialog,
|
VRCXUpdateDialog,
|
||||||
changeLogDialog,
|
changeLogDialog,
|
||||||
|
whatsNewDialog,
|
||||||
pendingVRCXUpdate,
|
pendingVRCXUpdate,
|
||||||
pendingVRCXInstall,
|
pendingVRCXInstall,
|
||||||
updateInProgress,
|
updateInProgress,
|
||||||
@@ -426,6 +487,9 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
|
|||||||
setBranch,
|
setBranch,
|
||||||
|
|
||||||
compareAppVersion,
|
compareAppVersion,
|
||||||
|
showWhatsNewDialog,
|
||||||
|
closeWhatsNewDialog,
|
||||||
|
openChangeLogDialogOnly,
|
||||||
checkForVRCXUpdate,
|
checkForVRCXUpdate,
|
||||||
loadBranchVersions,
|
loadBranchVersions,
|
||||||
installVRCXUpdate,
|
installVRCXUpdate,
|
||||||
|
|||||||
@@ -83,9 +83,11 @@
|
|||||||
|
|
||||||
<SendBoopDialog></SendBoopDialog>
|
<SendBoopDialog></SendBoopDialog>
|
||||||
|
|
||||||
|
<GlobalToolsDialogs></GlobalToolsDialogs>
|
||||||
|
|
||||||
<ChangelogDialog></ChangelogDialog>
|
<ChangelogDialog></ChangelogDialog>
|
||||||
|
|
||||||
<GlobalToolsDialogs></GlobalToolsDialogs>
|
<WhatsNewDialog></WhatsNewDialog>
|
||||||
|
|
||||||
<SpotlightDialog></SpotlightDialog>
|
<SpotlightDialog></SpotlightDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -120,6 +122,7 @@
|
|||||||
import StatusBar from '../../components/StatusBar.vue';
|
import StatusBar from '../../components/StatusBar.vue';
|
||||||
import VRChatConfigDialog from '../Settings/dialogs/VRChatConfigDialog.vue';
|
import VRChatConfigDialog from '../Settings/dialogs/VRChatConfigDialog.vue';
|
||||||
import WorldImportDialog from '../Favorites/dialogs/WorldImportDialog.vue';
|
import WorldImportDialog from '../Favorites/dialogs/WorldImportDialog.vue';
|
||||||
|
import WhatsNewDialog from '../../components/onboarding/WhatsNewDialog.vue';
|
||||||
import SpotlightDialog from '../../components/onboarding/SpotlightDialog.vue';
|
import SpotlightDialog from '../../components/onboarding/SpotlightDialog.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
Reference in New Issue
Block a user