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

@@ -381,7 +381,7 @@
import AvatarProviderDialog from '../../dialogs/AvatarProviderDialog.vue';
import PhotonSettings from '../PhotonSettings.vue';
import RegistryBackupDialog from '../../dialogs/RegistryBackupDialog.vue';
import RegistryBackupDialog from '../../../Tools/dialogs/RegistryBackupDialog.vue';
import SimpleSwitch from '../SimpleSwitch.vue';
import TranslationApiDialog from '../../dialogs/TranslationApiDialog.vue';
import YouTubeApiDialog from '../../dialogs/YouTubeApiDialog.vue';

View File

@@ -1,279 +0,0 @@
<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 './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>