feat: add tools tab

This commit is contained in:
pa
2025-08-27 13:04:37 +09:00
committed by Natsumi
parent d92818edea
commit 1b9fd75f97
16 changed files with 439 additions and 118 deletions

View File

@@ -628,6 +628,7 @@
:label="t('view.settings.appearance.user_dialog.vrcx_memos')"
:value="!hideUserMemos"
@change="setHideUserMemos" />
<!-- redirect to tools tab -->
<div class="options-container-item">
<span class="name">{{
t('view.settings.appearance.user_dialog.export_vrcx_memos_into_vrchat_notes')
@@ -641,6 +642,7 @@
>{{ t('view.settings.appearance.user_dialog.export_notes') }}</el-button
>
</div>
<!-- redirect to tools tab end -->
</div>
<!--//- Appearance | Friend Log-->
<div class="options-container">
@@ -1264,6 +1266,7 @@
<!--//- "Pictures" Tab-->
<el-tab-pane lazy :label="t('view.settings.category.pictures')">
<!-- redirect to tools tab -->
<div class="options-container" style="margin-top: 0">
<span class="header">{{ t('view.settings.category.pictures') }}</span>
<div class="options-container-item" style="margin-top: 15px">
@@ -1274,6 +1277,7 @@
</el-button-group>
</div>
</div>
<!-- redirect to tools tab end -->
<div class="options-container">
<span class="header">{{ t('view.settings.pictures.pictures.open_folder') }}</span>
@@ -1845,9 +1849,7 @@
</el-tab-pane>
</el-tabs>
<OpenSourceSoftwareNoticeDialog :ossDialog.sync="ossDialog" />
<NoteExportDialog :isNoteExportDialogVisible.sync="isNoteExportDialogVisible" />
<NotificationPositionDialog :isNotificationPositionDialogVisible.sync="isNotificationPositionDialogVisible" />
<ScreenshotMetadataDialog :screenshotMetadataDialog="screenshotMetadataDialog" />
<RegistryBackupDialog />
<YouTubeApiDialog :isYouTubeApiDialogVisible.sync="isYouTubeApiDialogVisible" />
<FeedFiltersDialog :feedFiltersDialogMode.sync="feedFiltersDialogMode" />
@@ -1886,9 +1888,7 @@
} from '../../stores';
import { photonEventTableTypeFilterList } from '../../shared/constants';
import OpenSourceSoftwareNoticeDialog from './dialogs/OpenSourceSoftwareNoticeDialog.vue';
import NoteExportDialog from './dialogs/NoteExportDialog.vue';
import NotificationPositionDialog from './dialogs/NotificationPositionDialog.vue';
import ScreenshotMetadataDialog from './dialogs/ScreenshotMetadataDialog.vue';
import RegistryBackupDialog from './dialogs/RegistryBackupDialog.vue';
import YouTubeApiDialog from './dialogs/YouTubeApiDialog.vue';
import ChangelogDialog from './dialogs/ChangelogDialog.vue';
@@ -2179,20 +2179,10 @@
]);
const ossDialog = ref(false);
const isNoteExportDialogVisible = ref(false);
const feedFiltersDialogMode = ref('');
const isNotificationPositionDialogVisible = ref(false);
const isYouTubeApiDialogVisible = ref(false);
const screenshotMetadataDialog = ref({
visible: false,
loading: false,
search: '',
searchType: 'Player Name',
searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'],
metadata: {},
isUploading: false
});
const zoomLevel = ref(100);
@@ -2224,16 +2214,18 @@
feedFiltersDialogMode.value = 'wrist';
}
// redirect to tools tab
function showNoteExportDialog() {
isNoteExportDialogVisible.value = true;
menuActiveIndex.value = 'tools';
}
function showNotificationPositionDialog() {
isNotificationPositionDialogVisible.value = true;
}
// redirect to tools tab
function showScreenshotMetadataDialog() {
screenshotMetadataDialog.value.visible = true;
menuActiveIndex.value = 'tools';
}
function openVrcxAppDataFolder() {

View File

@@ -1,201 +0,0 @@
<template>
<safe-dialog
class="x-dialog"
:visible="isNoteExportDialogVisible"
:title="t('dialog.note_export.header')"
width="1000px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.note_export.description1') }} <br />
{{ t('dialog.note_export.description2') }} <br />
{{ t('dialog.note_export.description3') }} <br />
{{ t('dialog.note_export.description4') }} <br />
{{ t('dialog.note_export.description5') }} <br />
{{ t('dialog.note_export.description6') }} <br />
{{ t('dialog.note_export.description7') }} <br />
{{ t('dialog.note_export.description8') }} <br />
</div>
<el-button size="small" :disabled="loading" style="margin-top: 10px" @click="updateNoteExportDialog">
{{ t('dialog.note_export.refresh') }}
</el-button>
<el-button size="small" :disabled="loading" style="margin-top: 10px" @click="exportNoteExport">
{{ t('dialog.note_export.export') }}
</el-button>
<el-button v-if="loading" size="small" style="margin-top: 10px" @click="cancelNoteExport">
{{ t('dialog.note_export.cancel') }}
</el-button>
<span v-if="loading" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
{{ t('dialog.note_export.progress') }} {{ progress }}/{{ progressTotal }}
</span>
<template v-if="errors">
<el-button size="small" @click="errors = ''">
{{ t('dialog.note_export.clear_errors') }}
</el-button>
<h2 style="font-weight: bold; margin: 0">
{{ t('dialog.note_export.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="errors"></pre>
</template>
<data-tables v-loading="loading" v-bind="noteExportTable" style="margin-top: 10px">
<el-table-column :label="t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
<template slot-scope="scope">
<el-popover placement="right" height="500px" trigger="hover">
<img slot="reference" v-lazy="userImage(scope.row.ref)" class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.ref)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))" />
</el-popover>
</template>
</el-table-column>
<el-table-column :label="t('table.import.name')" width="170" prop="name">
<template slot-scope="scope">
<span class="x-link" @click="showUserDialog(scope.row.id)" v-text="scope.row.name"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.note')" prop="memo">
<template slot-scope="scope">
<el-input
v-model="scope.row.memo"
type="textarea"
maxlength="256"
show-word-limit
:rows="2"
:autosize="{ minRows: 1, maxRows: 10 }"
size="mini"
resize="none"></el-input>
</template>
</el-table-column>
<el-table-column :label="t('table.import.skip_export')" width="90" align="right">
<template slot-scope="scope">
<el-button
type="text"
icon="el-icon-close"
size="mini"
@click="removeFromNoteExportTable(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import * as workerTimers from 'worker-timers';
import { miscRequest } from '../../../api';
import { removeFromArray, userImage, userImageFull } from '../../../shared/utils';
import { useFriendStore, useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const { friends } = storeToRefs(useFriendStore());
const { showUserDialog } = useUserStore();
const { showFullscreenImageDialog } = useGalleryStore();
const props = defineProps({
isNoteExportDialogVisible: {
type: Boolean
}
});
const noteExportTable = ref({
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table'
});
const progress = ref(0);
const progressTotal = ref(0);
const loading = ref(false);
const errors = ref('');
watch(
() => props.isNoteExportDialogVisible,
(newVal) => {
if (newVal) {
initData();
}
}
);
function initData() {
noteExportTable.value.data = [];
progress.value = 0;
progressTotal.value = 0;
loading.value = false;
errors.value = '';
}
const emit = defineEmits(['update:isNoteExportDialogVisible']);
function updateNoteExportDialog() {
const data = [];
friends.value.forEach((ctx) => {
const newMemo = ctx.memo.replace(/[\r\n]/g, ' ');
if (ctx.memo && ctx.ref && ctx.ref.note !== newMemo.slice(0, 256)) {
data.push({
id: ctx.id,
name: ctx.name,
memo: newMemo,
ref: ctx.ref
});
}
});
noteExportTable.value.data = data;
}
async function exportNoteExport() {
let ctx;
loading.value = true;
const data = [...noteExportTable.value.data].reverse();
progressTotal.value = data.length;
try {
for (let i = data.length - 1; i >= 0; i--) {
if (props.isNoteExportDialogVisible && loading.value) {
ctx = data[i];
await miscRequest.saveNote({
targetUserId: ctx.id,
note: ctx.memo.slice(0, 256)
});
removeFromArray(noteExportTable.value.data, ctx);
progress.value++;
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 5000);
});
}
}
} catch (err) {
errors.value = `Name: ${ctx?.name}\n${err}\n\n`;
} finally {
progress.value = 0;
progressTotal.value = 0;
loading.value = false;
}
}
function cancelNoteExport() {
loading.value = false;
}
function removeFromNoteExportTable(ref) {
removeFromArray(noteExportTable.value.data, ref);
}
function closeDialog() {
emit('update:isNoteExportDialogVisible', false);
}
</script>

View File

@@ -1,539 +0,0 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="screenshotMetadataDialog.visible"
:title="t('dialog.screenshot_metadata.header')"
width="1050px"
top="10vh">
<div
v-if="screenshotMetadataDialog.visible"
v-loading="screenshotMetadataDialog.loading"
style="-webkit-app-region: drag"
@dragover.prevent
@dragenter.prevent
@drop="handleDrop">
<span style="margin-left: 5px; color: #909399; font-family: monospace">{{
t('dialog.screenshot_metadata.drag')
}}</span>
<br />
<br />
<el-button size="small" icon="el-icon-folder-opened" @click="getAndDisplayScreenshotFromFile">{{
t('dialog.screenshot_metadata.browse')
}}</el-button>
<el-button size="small" icon="el-icon-picture-outline" @click="getAndDisplayLastScreenshot">{{
t('dialog.screenshot_metadata.last_screenshot')
}}</el-button>
<el-button
size="small"
icon="el-icon-copy-document"
@click="copyImageToClipboard(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.copy_image') }}</el-button
>
<el-button
size="small"
icon="el-icon-folder"
@click="openImageFolder(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.open_folder') }}</el-button
>
<el-button
v-if="currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath"
size="small"
icon="el-icon-upload2"
@click="uploadScreenshotToGallery"
>{{ t('dialog.screenshot_metadata.upload') }}</el-button
>
<el-button
v-if="screenshotMetadataDialog.metadata.filePath"
size="small"
icon="el-icon-delete"
@click="deleteMetadata(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.delete_metadata') }}</el-button
>
<br />
<br />
<!-- Search bar input -->
<el-input
v-model="screenshotMetadataDialog.search"
size="small"
placeholder="Search"
clearable
style="width: 200px"
@input="screenshotMetadataSearch" />
<!-- Search type dropdown -->
<el-select
v-model="screenshotMetadataDialog.searchType"
size="small"
placeholder="Search Type"
style="width: 150px; margin-left: 10px"
@change="screenshotMetadataSearch">
<el-option
v-for="type in screenshotMetadataDialog.searchTypes"
:key="type"
:label="type"
:value="type" />
</el-select>
<!-- Search index/total label -->
<template v-if="screenshotMetadataDialog.searchIndex !== null">
<span style="white-space: pre-wrap; font-size: 12px; margin-left: 10px">{{
screenshotMetadataDialog.searchIndex + 1 + '/' + screenshotMetadataDialog.searchResults.length
}}</span>
</template>
<br />
<br />
<span v-text="screenshotMetadataDialog.metadata.fileName"></span>
<br />
<template v-if="screenshotMetadataDialog.metadata.note">
<span v-text="screenshotMetadataDialog.metadata.note"></span>
<br />
</template>
<span v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right: 5px">{{
formatDateFilter(screenshotMetadataDialog.metadata.dateTime, 'long')
}}</span>
<span
v-if="screenshotMetadataDialog.metadata.fileResolution"
style="margin-right: 5px"
v-text="screenshotMetadataDialog.metadata.fileResolution"></span>
<el-tag v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini">{{
screenshotMetadataDialog.metadata.fileSize
}}</el-tag>
<br />
<Location
v-if="screenshotMetadataDialog.metadata.world"
:location="screenshotMetadataDialog.metadata.world.instanceId"
:hint="screenshotMetadataDialog.metadata.world.name" />
<DisplayName
v-if="screenshotMetadataDialog.metadata.author"
:userid="screenshotMetadataDialog.metadata.author.id"
:hint="screenshotMetadataDialog.metadata.author.displayName"
style="color: #909399; font-family: monospace" />
<br />
<el-carousel
ref="screenshotMetadataCarouselRef"
:interval="0"
:initial-index="1"
indicator-position="none"
arrow="always"
height="600px"
style="margin-top: 10px"
@change="screenshotMetadataCarouselChange">
<el-carousel-item>
<span placement="top" width="700px" trigger="click">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.previousFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
</el-carousel-item>
<el-carousel-item>
<span
placement="top"
width="700px"
trigger="click"
@click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.filePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
</el-carousel-item>
<el-carousel-item>
<span placement="top" width="700px" trigger="click">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.nextFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
</el-carousel-item>
</el-carousel>
<br />
<template v-if="screenshotMetadataDialog.metadata.error">
<pre
style="white-space: pre-wrap; font-size: 12px"
v-text="screenshotMetadataDialog.metadata.error"></pre>
<br />
</template>
<span v-for="user in screenshotMetadataDialog.metadata.players" :key="user.id" style="margin-top: 5px">
<span class="x-link" @click="lookupUser(user)" v-text="user.displayName"></span>
<span
v-if="user.pos"
style="margin-left: 5px; color: #909399; font-family: monospace"
v-text="'(' + user.pos.x + ', ' + user.pos.y + ', ' + user.pos.z + ')'"></span>
<br />
</span>
</div>
</safe-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { vrcPlusImageRequest } from '../../../api';
import { useGalleryStore, useUserStore, useVrcxStore } from '../../../stores';
import { formatDateFilter } from '../../../shared/utils';
const { showFullscreenImageDialog, handleGalleryImageAdd } = useGalleryStore();
const { currentlyDroppingFile } = storeToRefs(useVrcxStore());
const { currentUser } = storeToRefs(useUserStore());
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const userStore = useUserStore();
const { lookupUser } = userStore;
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
const props = defineProps({
screenshotMetadataDialog: {
type: Object,
required: true
}
});
watch(
() => props.screenshotMetadataDialog.visible,
(newVal) => {
if (newVal) {
if (!props.screenshotMetadataDialog.metadata.filePath) {
getAndDisplayLastScreenshot();
}
window.addEventListener('keyup', handleComponentKeyup);
} else {
window.removeEventListener('keyup', handleComponentKeyup);
}
}
);
const screenshotMetadataSearchInputs = ref(0);
const screenshotMetadataCarouselRef = ref(null);
const handleComponentKeyup = (event) => {
const carouselNavigation = { ArrowLeft: 0, ArrowRight: 2 }[event.key];
if (typeof carouselNavigation !== 'undefined' && props.screenshotMetadataDialog?.visible) {
screenshotMetadataCarouselChange(carouselNavigation);
}
};
function handleDrop(event) {
if (currentlyDroppingFile.value === null) {
return;
}
console.log('Dropped file into viewer: ', currentlyDroppingFile.value);
screenshotMetadataResetSearch();
getAndDisplayScreenshot(currentlyDroppingFile.value);
event.preventDefault();
}
async function getAndDisplayScreenshotFromFile() {
let filePath = '';
// eslint-disable-next-line no-undef
if (LINUX) {
filePath = await window.electron.openFileDialog(); // PNG filter is applied in main.js
} else {
filePath = await AppApi.OpenFileSelectorDialog(
await AppApi.GetVRChatPhotosLocation(),
'.png',
'PNG Files (*.png)|*.png'
);
}
if (filePath === '') {
return;
}
screenshotMetadataResetSearch();
getAndDisplayScreenshot(filePath);
}
function getAndDisplayLastScreenshot() {
screenshotMetadataResetSearch();
AppApi.GetLastScreenshot().then((path) => {
if (!path) {
return;
}
getAndDisplayScreenshot(path);
});
}
function copyImageToClipboard(path) {
if (!path) {
return;
}
AppApi.CopyImageToClipboard(path).then(() => {
$message({
message: 'Image copied to clipboard',
type: 'success'
});
});
}
function openImageFolder(path) {
if (!path) {
return;
}
AppApi.OpenFolderAndSelectItem(path).then(() => {
$message({
message: 'Opened image folder',
type: 'success'
});
});
}
function deleteMetadata(path) {
if (!path) {
return;
}
AppApi.DeleteScreenshotMetadata(path).then((result) => {
if (!result) {
$message({
message: t('message.screenshot_metadata.delete_failed'),
type: 'error'
});
return;
}
$message({
message: t('message.screenshot_metadata.deleted'),
type: 'success'
});
const D = props.screenshotMetadataDialog;
getAndDisplayScreenshot(D.metadata.filePath, true);
});
}
function uploadScreenshotToGallery() {
const D = props.screenshotMetadataDialog;
if (D.metadata.fileSizeBytes > 10000000) {
$message({
message: t('message.file.too_large'),
type: 'error'
});
return;
}
D.isUploading = true;
AppApi.GetFileBase64(D.metadata.filePath)
.then((base64Body) => {
vrcPlusImageRequest
.uploadGalleryImage(base64Body)
.then((args) => {
handleGalleryImageAdd(args);
$message({
message: t('message.gallery.uploaded'),
type: 'success'
});
return args;
})
.finally(() => {
D.isUploading = false;
});
})
.catch((err) => {
$message({
message: t('message.gallery.failed'),
type: 'error'
});
console.error(err);
D.isUploading = false;
});
}
function screenshotMetadataSearch() {
const D = props.screenshotMetadataDialog;
// Don't search if user is still typing
screenshotMetadataSearchInputs.value++;
let current = screenshotMetadataSearchInputs.value;
setTimeout(() => {
if (current !== screenshotMetadataSearchInputs.value) {
return;
}
screenshotMetadataSearchInputs.value = 0;
if (D.search === '') {
screenshotMetadataResetSearch();
if (D.metadata.filePath !== null) {
// Re-retrieve the current screenshot metadata and get previous/next files for regular carousel directory navigation
getAndDisplayScreenshot(D.metadata.filePath, true);
}
return;
}
const searchType = D.searchTypes.indexOf(D.searchType); // Matches the search type enum in .NET
D.loading = true;
AppApi.FindScreenshotsBySearch(D.search, searchType)
.then((json) => {
const results = JSON.parse(json);
if (results.length === 0) {
D.metadata = {};
D.metadata.error = 'No results found';
D.searchIndex = null;
D.searchResults = null;
return;
}
D.searchIndex = 0;
D.searchResults = results;
// console.log("Search results", results)
getAndDisplayScreenshot(results[0], false);
})
.finally(() => {
D.loading = false;
});
}, 500);
}
function screenshotMetadataCarouselChange(index) {
const D = props.screenshotMetadataDialog;
const searchIndex = D.searchIndex;
if (searchIndex !== null) {
screenshotMetadataCarouselChangeSearch(index);
return;
}
if (index === 0) {
if (D.metadata.previousFilePath) {
getAndDisplayScreenshot(D.metadata.previousFilePath);
} else {
getAndDisplayScreenshot(D.metadata.filePath);
}
}
if (index === 2) {
if (D.metadata.nextFilePath) {
getAndDisplayScreenshot(D.metadata.nextFilePath);
} else {
getAndDisplayScreenshot(D.metadata.filePath);
}
}
if (typeof screenshotMetadataCarouselRef.value !== 'undefined') {
screenshotMetadataCarouselRef.value.setActiveItem(1);
}
if (fullscreenImageDialog.value.visible) {
// TODO
}
}
function screenshotMetadataResetSearch() {
const D = props.screenshotMetadataDialog;
D.search = '';
D.searchIndex = null;
D.searchResults = null;
}
function screenshotMetadataCarouselChangeSearch(index) {
const D = props.screenshotMetadataDialog;
let searchIndex = D.searchIndex;
const filesArr = D.searchResults;
if (searchIndex === null) {
return;
}
if (index === 0) {
if (searchIndex > 0) {
getAndDisplayScreenshot(filesArr[searchIndex - 1], false);
searchIndex--;
} else {
getAndDisplayScreenshot(filesArr[filesArr.length - 1], false);
searchIndex = filesArr.length - 1;
}
} else if (index === 2) {
if (searchIndex < filesArr.length - 1) {
getAndDisplayScreenshot(filesArr[searchIndex + 1], false);
searchIndex++;
} else {
getAndDisplayScreenshot(filesArr[0], false);
searchIndex = 0;
}
}
if (typeof screenshotMetadataCarouselRef.value !== 'undefined') {
screenshotMetadataCarouselRef.value.setActiveItem(1);
}
D.searchIndex = searchIndex;
}
async function getAndDisplayScreenshot(path, needsCarouselFiles = true) {
const metadata = await AppApi.GetScreenshotMetadata(path);
displayScreenshotMetadata(metadata, needsCarouselFiles);
}
/**
* Function receives an unmodified json string grabbed from the screenshot file
* Error checking and and verification of data is done in .NET already; In the case that the data/file is invalid, a JSON object with the token "error" will be returned containing a description of the problem.
* Example: {"error":"Invalid file selected. Please select a valid VRChat screenshot."}
* See docs/screenshotMetadata.json for schema
* @param {string} json - JSON string grabbed from PNG file
* @param {boolean} needsCarouselFiles - Whether or not to get the last/next files for the carousel
* @returns {Promise<void>}
*/
async function displayScreenshotMetadata(json, needsCarouselFiles = true) {
let time;
let date;
const D = props.screenshotMetadataDialog;
D.metadata.author = {};
D.metadata.world = {};
D.metadata.players = [];
D.metadata.creationDate = '';
D.metadata.application = '';
const metadata = JSON.parse(json);
if (!metadata?.sourceFile) {
D.metadata = {};
D.metadata.error = 'Invalid file selected. Please select a valid VRChat screenshot.';
return;
}
// Get extra data for display dialog like resolution, file size, etc
D.loading = true;
const extraData = await AppApi.GetExtraScreenshotData(metadata.sourceFile, needsCarouselFiles);
D.loading = false;
const extraDataObj = JSON.parse(extraData);
Object.assign(metadata, extraDataObj);
// console.log("Displaying screenshot metadata", json, "extra data", extraDataObj, "path", json.filePath)
D.metadata = metadata;
const regex = metadata.fileName.match(
/VRChat_((\d{3,})x(\d{3,})_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{1,})|(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{3})_(\d{3,})x(\d{3,}))/
);
if (regex) {
if (typeof regex[2] !== 'undefined' && regex[4].length === 4) {
// old format
// VRChat_3840x2160_2022-02-02_03-21-39.771
date = `${regex[4]}-${regex[5]}-${regex[6]}`;
time = `${regex[7]}:${regex[8]}:${regex[9]}`;
D.metadata.dateTime = Date.parse(`${date} ${time}`);
// D.metadata.resolution = `${regex[2]}x${regex[3]}`;
} else if (typeof regex[11] !== 'undefined' && regex[11].length === 4) {
// new format
// VRChat_2023-02-16_10-39-25.274_3840x2160
date = `${regex[11]}-${regex[12]}-${regex[13]}`;
time = `${regex[14]}:${regex[15]}:${regex[16]}`;
D.metadata.dateTime = Date.parse(`${date} ${time}`);
// D.metadata.resolution = `${regex[18]}x${regex[19]}`;
}
}
if (metadata.timestamp) {
D.metadata.dateTime = Date.parse(metadata.timestamp);
}
if (!D.metadata.dateTime) {
D.metadata.dateTime = Date.parse(metadata.creationDate);
}
if (fullscreenImageDialog.value.visible) {
showFullscreenImageDialog(D.metadata.filePath);
}
}
</script>