fix mutuals chart by filtering out invalid user IDs

This commit is contained in:
pa
2026-02-05 20:54:21 +09:00
parent cb0c241580
commit d3e44523bd
4 changed files with 40 additions and 16 deletions

View File

@@ -337,7 +337,7 @@
SquarePen,
UserCheck
} from 'lucide-vue-next';
import { computed, defineAsyncComponent, ref } from 'vue';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Card } from '@/components/ui/card';
import { storeToRefs } from 'pinia';
@@ -350,6 +350,7 @@
import { useVrcxStore } from '../../stores/vrcx';
import AutoChangeStatusDialog from './dialogs/AutoChangeStatusDialog.vue';
import configRepository from '../../service/config.js';
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
@@ -357,7 +358,7 @@
const ExportDiscordNamesDialog = defineAsyncComponent(() => import('./dialogs/ExportDiscordNamesDialog.vue'));
const ExportFriendsListDialog = defineAsyncComponent(() => import('./dialogs/ExportFriendsListDialog.vue'));
const ExportAvatarsListDialog = defineAsyncComponent(() => import('./dialogs/ExportAvatarsListDialog.vue'));
const RegistryBackupDialog = defineAsyncComponent(() => import('../Settings/dialogs/RegistryBackupDialog.vue'));
const RegistryBackupDialog = defineAsyncComponent(() => import('./dialogs/RegistryBackupDialog.vue'));
const { t } = useI18n();
const router = useRouter();
@@ -368,6 +369,7 @@
const { showVRChatConfig } = useAdvancedSettingsStore();
const { showLaunchOptions } = useLaunchStore();
const { showRegistryBackupDialog } = useVrcxStore();
const toolsCategoryCollapsedConfigKey = 'VRCX_toolsCategoryCollapsed';
const categoryCollapsed = ref({
group: false,
@@ -401,8 +403,22 @@
const toggleCategory = (category) => {
categoryCollapsed.value[category] = !categoryCollapsed.value[category];
configRepository.setString(toolsCategoryCollapsedConfigKey, JSON.stringify(categoryCollapsed.value));
};
onMounted(async () => {
const storedValue = await configRepository.getString(toolsCategoryCollapsedConfigKey, '{}');
try {
const parsed = JSON.parse(storedValue);
categoryCollapsed.value = {
...categoryCollapsed.value,
...parsed
};
} catch {
// ignore invalid stored value and keep defaults
}
});
const showEditInviteMessageDialog = () => {
isEditInviteMessagesDialogVisible.value = true;
};

View File

@@ -0,0 +1,279 @@
<template>
<Dialog :open="isRegistryBackupDialogVisible" @update:open="(open) => !open && closeAndClearDialog()">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.registry_backup.header') }}</DialogTitle>
</DialogHeader>
<div style="margin-top: 10px">
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 12px">
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.auto_backup') }}</span>
<Switch :model-value="vrcRegistryAutoBackup" @update:modelValue="setVrcRegistryAutoBackup" />
</div>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
margin-top: 5px;
">
<span class="name" style="margin-right: 24px">{{
t('dialog.registry_backup.ask_to_restore')
}}</span>
<Switch :model-value="vrcRegistryAskRestore" @update:modelValue="setVrcRegistryAskRestore" />
</div>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="false"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 10px">
<Button size="sm" variant="destructive" @click="deleteVrcRegistry">{{
t('dialog.registry_backup.reset')
}}</Button>
<div class="flex gap-2">
<Button size="sm" variant="outline" @click="promptVrcRegistryBackupName">{{
t('dialog.registry_backup.backup')
}}</Button>
<Button size="sm" variant="outline" @click="restoreVrcRegistryFromFile">{{
t('dialog.registry_backup.restore_from_file')
}}</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import { useAdvancedSettingsStore, useModalStore, useVrcxStore } from '../../../stores';
import { downloadAndSaveJson, removeFromArray } from '../../../shared/utils';
import { Switch } from '../../../components/ui/switch';
import { createColumns } from '../../Settings/dialogs/registryBackupColumns.jsx';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
import configRepository from '../../../service/config';
const { backupVrcRegistry } = useVrcxStore();
const { isRegistryBackupDialogVisible } = storeToRefs(useVrcxStore());
const { vrcRegistryAutoBackup, vrcRegistryAskRestore } = storeToRefs(useAdvancedSettingsStore());
const { setVrcRegistryAutoBackup, setVrcRegistryAskRestore } = useAdvancedSettingsStore();
const modalStore = useModalStore();
const { t } = useI18n();
const registryBackupTable = ref({
data: [],
layout: 'table'
});
const tableStyle = { maxHeight: '320px' };
const rows = computed(() =>
Array.isArray(registryBackupTable.value?.data) ? registryBackupTable.value.data.slice() : []
);
const columns = computed(() =>
createColumns({
onRestore: restoreVrcRegistryBackup,
onSaveToFile: saveVrcRegistryBackupToFile,
onDelete: deleteVrcRegistryBackup
})
);
const { table } = useVrcxVueTable({
persistKey: 'registryBackupDialog',
get data() {
return rows.value;
},
columns: columns.value,
getRowId: (row) => String(row?.name ?? ''),
enablePagination: false,
initialSorting: [{ id: 'date', desc: true }]
});
watch(
() => isRegistryBackupDialogVisible.value,
(newVal) => {
if (newVal) {
updateRegistryBackupDialog();
}
}
);
async function updateRegistryBackupDialog() {
const backupsJson = await configRepository.getString('VRCX_VRChatRegistryBackups');
registryBackupTable.value.data = JSON.parse(backupsJson || '[]');
}
function restoreVrcRegistryBackup(row) {
modalStore
.confirm({
description: 'Continue? Restore Backup',
title: 'Confirm'
})
.then(({ ok }) => {
if (!ok) {
return;
}
const data = JSON.stringify(row.data);
AppApi.SetVRChatRegistry(data)
.then(() => {
toast.success('VRC registry settings restored');
})
.catch((e) => {
console.error(e);
toast.error(`Failed to restore VRC registry settings, check console for full error: ${e}`);
});
})
.catch(() => {});
}
function saveVrcRegistryBackupToFile(row) {
downloadAndSaveJson(row.name, row.data);
}
async function deleteVrcRegistryBackup(row) {
const backups = registryBackupTable.value.data;
removeFromArray(backups, row);
await configRepository.setString('VRCX_VRChatRegistryBackups', JSON.stringify(backups));
await updateRegistryBackupDialog();
}
function deleteVrcRegistry() {
modalStore
.confirm({
description: 'Continue? Delete VRC Registry Settings',
title: 'Confirm'
})
.then(({ ok }) => {
if (!ok) {
return;
}
AppApi.DeleteVRChatRegistryFolder().then(() => {
toast.success('VRC registry settings deleted');
});
})
.catch(() => {});
}
async function handleBackupVrcRegistry(name) {
await backupVrcRegistry(name);
await updateRegistryBackupDialog();
}
function promptVrcRegistryBackupName() {
modalStore
.prompt({
title: 'Backup Name',
description: 'Enter a name for the backup',
inputValue: 'Backup',
pattern: /\S+/,
errorMessage: 'Name is required'
})
.then(({ ok, value }) => {
if (!ok) return;
handleBackupVrcRegistry(value);
})
.catch(() => {});
}
async function openJsonFileSelectorDialogElectron() {
return new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = function (event) {
const target = /** @type {HTMLInputElement | null} */ (event.target);
const file = target?.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = function () {
fileInput.remove();
resolve(reader.result);
};
reader.readAsText(file);
} else {
fileInput.remove();
resolve(null);
}
};
fileInput.click();
});
}
async function restoreVrcRegistryFromFile() {
const filePath = await AppApi.OpenFileSelectorDialog(null, '.json', 'JSON Files (*.json)|*.json');
if (WINDOWS) {
if (filePath === '') {
return;
}
}
let json;
if (LINUX) {
json = await openJsonFileSelectorDialogElectron();
} else {
json = await AppApi.ReadVrcRegJsonFile(filePath);
}
try {
const data = JSON.parse(json);
if (!data || typeof data !== 'object') {
throw new Error('Invalid JSON');
}
// quick check to make sure it's a valid registry backup
for (const key in data) {
const value = data[key];
if (typeof value !== 'object' || typeof value.type !== 'number' || typeof value.data === 'undefined') {
throw new Error('Invalid JSON');
}
}
AppApi.SetVRChatRegistry(json)
.then(() => {
toast.success('VRC registry settings restored');
})
.catch((e) => {
console.error(e);
toast.error(`Failed to restore VRC registry settings, check console for full error: ${e}`);
});
} catch {
toast.error('Invalid JSON');
}
}
function clearVrcRegistryDialog() {
registryBackupTable.value.data = [];
}
function closeAndClearDialog() {
closeDialog();
// TODO: Element Plus had a distinct @closed event after animation.
// If you ever need exact timing, wrap DialogContent with a Transition and call clear on after-leave.
clearVrcRegistryDialog();
}
function closeDialog() {
isRegistryBackupDialogVisible.value = false;
}
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>