mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-22 16:23:50 +02:00
feat: Add searching & fullscreen mode to screenshot viewer (#627)
* Optimized search screenshots by metadata * feat: Screenshot metadata search bar * fix: Reset search when selecting a file manually * refactor: Re-do the whole search thing. Add number of results to dialog when searching * fix: Add check & error for null metadata * fix: Add sourceFile to error obj on return * fix: Fix screenshot file dialog not sending path back to JS * fix: Stop lfs parsing from dying if a value doesn't exist * fix: Fix and optimize FileStream reading of metadata for searches * fix: Reset search data and revert to normal when user clears out search box * refactor: Remove/optimize some old screenshot helper stuff - Use FileStream in ReadPNGResolution - Limit the FindChunkIndex search range used when writing metadata - Remove old ReadPNGDescription, just use filestream version now * fix: Reset metadata search state if a file is added manually * feat: Move viewer popover dialog to the fullscreen image viewer * refactor: Change how parsing errors are handled... again * refactor: Let the search carousel loop around * fix: Re-do legacy parsing /wo JObject. Fix legacy instance ids/pos. Also adds further docs to the legacy parsing for the various formats * feat: Add persistent metadata cache for search * Clean up * fix: Fix viewer dying sourceFile wasn't being included for vrcx pics * refactor: Cache the state of files with no metadata This is so we're not constantly re-processing these files with no metadata on every first search after a restart; These files won't magically gain metadata and this could cause a lot of hitching for someone that had potentially thousands of screenshots before using VRCX. * Screenshot viewer loading --------- Co-authored-by: Nekromateion <43814053+Nekromateion@users.noreply.github.com> Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
174
html/src/app.js
174
html/src/app.js
@@ -22033,20 +22033,53 @@ speechSynthesis.getVoices();
|
||||
);
|
||||
};
|
||||
|
||||
$app.methods.getAndDisplayScreenshot = function (
|
||||
path,
|
||||
needsCarouselFiles = true
|
||||
) {
|
||||
AppApi.GetScreenshotMetadata(path).then((metadata) =>
|
||||
this.displayScreenshotMetadata(metadata, needsCarouselFiles)
|
||||
);
|
||||
};
|
||||
|
||||
$app.methods.getAndDisplayLastScreenshot = function () {
|
||||
this.screenshotMetadataResetSearch();
|
||||
AppApi.GetLastScreenshot().then((path) =>
|
||||
this.getAndDisplayScreenshot(path)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function should only ever be called by .NET
|
||||
* 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} metadata - JSON string grabbed from PNG file
|
||||
* @param {string} needsCarouselFiles - Whether or not to get the last/next files for the carousel
|
||||
* @returns {void}
|
||||
*/
|
||||
$app.methods.displayScreenshotMetadata = function (metadata) {
|
||||
$app.methods.displayScreenshotMetadata = async function (
|
||||
json,
|
||||
needsCarouselFiles = true
|
||||
) {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
var json = JSON.parse(metadata);
|
||||
D.metadata = json;
|
||||
var metadata = JSON.parse(json);
|
||||
|
||||
var regex = json.fileName.match(
|
||||
// Get extra data for display dialog like resolution, file size, etc
|
||||
D.loading = true;
|
||||
var extraData = await AppApi.GetExtraScreenshotData(
|
||||
metadata.sourceFile,
|
||||
needsCarouselFiles
|
||||
);
|
||||
D.loading = false;
|
||||
var extraDataObj = JSON.parse(extraData);
|
||||
Object.assign(metadata, extraDataObj);
|
||||
|
||||
// console.log("Displaying screenshot metadata", json, "extra data", extraDataObj, "path", json.filePath)
|
||||
|
||||
D.metadata = metadata;
|
||||
|
||||
var 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) {
|
||||
@@ -22073,11 +22106,19 @@ speechSynthesis.getVoices();
|
||||
D.metadata.dateTime = Date.parse(json.creationDate);
|
||||
}
|
||||
|
||||
this.openScreenshotMetadataDialog();
|
||||
if (this.fullscreenImageDialog?.visible) {
|
||||
this.showFullscreenImageDialog(D.metadata.filePath);
|
||||
} else {
|
||||
this.openScreenshotMetadataDialog();
|
||||
}
|
||||
};
|
||||
|
||||
$app.data.screenshotMetadataDialog = {
|
||||
visible: false,
|
||||
loading: false,
|
||||
search: '',
|
||||
searchType: 'Player Name',
|
||||
searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'],
|
||||
metadata: {},
|
||||
isUploading: false
|
||||
};
|
||||
@@ -22093,30 +22134,135 @@ speechSynthesis.getVoices();
|
||||
$app.methods.showScreenshotMetadataDialog = function () {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
if (!D.metadata.filePath) {
|
||||
AppApi.GetLastScreenshot();
|
||||
this.getAndDisplayLastScreenshot();
|
||||
}
|
||||
this.openScreenshotMetadataDialog();
|
||||
};
|
||||
|
||||
$app.methods.screenshotMetadataResetSearch = function () {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
|
||||
D.search = '';
|
||||
D.searchIndex = null;
|
||||
D.searchResults = null;
|
||||
};
|
||||
|
||||
$app.data.screenshotMetadataSearchInputs = 0;
|
||||
$app.methods.screenshotMetadataSearch = function () {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
|
||||
// Don't search if user is still typing
|
||||
this.screenshotMetadataSearchInputs++;
|
||||
let current = this.screenshotMetadataSearchInputs;
|
||||
setTimeout(() => {
|
||||
if (current !== this.screenshotMetadataSearchInputs) {
|
||||
return;
|
||||
}
|
||||
this.screenshotMetadataSearchInputs = 0;
|
||||
|
||||
if (D.search === '') {
|
||||
this.screenshotMetadataResetSearch();
|
||||
if (D.metadata.filePath !== null) {
|
||||
// Re-retrieve the current screenshot metadata and get previous/next files for regular carousel directory navigation
|
||||
this.getAndDisplayScreenshot(D.metadata.filePath, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var searchType = D.searchTypes.indexOf(D.searchType); // Matches the search type enum in .NET
|
||||
D.loading = true;
|
||||
AppApi.FindScreenshotsBySearch(D.search, searchType)
|
||||
.then((json) => {
|
||||
var 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)
|
||||
this.getAndDisplayScreenshot(results[0], false);
|
||||
})
|
||||
.finally(() => {
|
||||
D.loading = false;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$app.methods.screenshotMetadataCarouselChangeSearch = function (index) {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
var searchIndex = D.searchIndex;
|
||||
var filesArr = D.searchResults;
|
||||
|
||||
if (searchIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
if (searchIndex > 0) {
|
||||
this.getAndDisplayScreenshot(filesArr[searchIndex - 1], false);
|
||||
searchIndex--;
|
||||
} else {
|
||||
this.getAndDisplayScreenshot(
|
||||
filesArr[filesArr.length - 1],
|
||||
false
|
||||
);
|
||||
searchIndex = filesArr.length - 1;
|
||||
}
|
||||
} else if (index === 2) {
|
||||
if (searchIndex < filesArr.length - 1) {
|
||||
this.getAndDisplayScreenshot(filesArr[searchIndex + 1], false);
|
||||
searchIndex++;
|
||||
} else {
|
||||
this.getAndDisplayScreenshot(filesArr[0], false);
|
||||
searchIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') {
|
||||
this.$refs.screenshotMetadataCarousel.setActiveItem(1);
|
||||
}
|
||||
|
||||
D.searchIndex = searchIndex;
|
||||
};
|
||||
|
||||
$app.methods.screenshotMetadataCarouselChange = function (index) {
|
||||
var D = this.screenshotMetadataDialog;
|
||||
var searchIndex = D.searchIndex;
|
||||
|
||||
if (searchIndex !== null) {
|
||||
this.screenshotMetadataCarouselChangeSearch(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
if (D.metadata.previousFilePath) {
|
||||
AppApi.GetScreenshotMetadata(D.metadata.previousFilePath);
|
||||
this.getAndDisplayScreenshot(D.metadata.previousFilePath);
|
||||
} else {
|
||||
AppApi.GetScreenshotMetadata(D.metadata.filePath);
|
||||
this.getAndDisplayScreenshot(D.metadata.filePath);
|
||||
}
|
||||
}
|
||||
if (index === 2) {
|
||||
if (D.metadata.nextFilePath) {
|
||||
AppApi.GetScreenshotMetadata(D.metadata.nextFilePath);
|
||||
this.getAndDisplayScreenshot(D.metadata.nextFilePath);
|
||||
} else {
|
||||
AppApi.GetScreenshotMetadata(D.metadata.filePath);
|
||||
this.getAndDisplayScreenshot(D.metadata.filePath);
|
||||
}
|
||||
}
|
||||
if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') {
|
||||
this.$refs.screenshotMetadataCarousel.setActiveItem(1);
|
||||
}
|
||||
|
||||
if (this.fullscreenImageDialog.visible) {
|
||||
// TODO
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.uploadScreenshotToGallery = function () {
|
||||
@@ -22165,8 +22311,10 @@ speechSynthesis.getVoices();
|
||||
if (this.currentlyDroppingFile === null) {
|
||||
return;
|
||||
}
|
||||
console.log('Dropped file into window: ', this.currentlyDroppingFile);
|
||||
AppApi.GetScreenshotMetadata(this.currentlyDroppingFile);
|
||||
console.log('Dropped file into viewer: ', this.currentlyDroppingFile);
|
||||
|
||||
this.screenshotMetadataResetSearch();
|
||||
this.getAndDisplayScreenshot(this.currentlyDroppingFile);
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
@@ -2535,21 +2535,30 @@ html
|
||||
|
||||
//- dialog: screenshot metadata
|
||||
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="screenshotMetadataDialog" :visible.sync="screenshotMetadataDialog.visible" :title="$t('dialog.screenshot_metadata.header')" width="1050px")
|
||||
div(v-if="screenshotMetadataDialog.visible" @dragover.prevent @dragenter.prevent @drop="handleDrop" style="-webkit-app-region: drag")
|
||||
div(v-if="screenshotMetadataDialog.visible" v-loading="screenshotMetadataDialog.loading" @dragover.prevent @dragenter.prevent @drop="handleDrop" style="-webkit-app-region: drag")
|
||||
span(style="margin-left:5px;color:#909399;font-family:monospace") {{ $t('dialog.screenshot_metadata.drag') }}
|
||||
br
|
||||
el-button(size="small" icon="el-icon-folder-opened" @click="AppApi.OpenScreenshotFileDialog()") {{ $t('dialog.screenshot_metadata.browse') }}
|
||||
el-button(size="small" icon="el-icon-picture-outline" @click="AppApi.GetLastScreenshot()") {{ $t('dialog.screenshot_metadata.last_screenshot') }}
|
||||
el-button(size="small" icon="el-icon-picture-outline" @click="getAndDisplayLastScreenshot()") {{ $t('dialog.screenshot_metadata.last_screenshot') }}
|
||||
el-button(size="small" icon="el-icon-copy-document" @click="copyImageToClipboard(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.copy_image') }}
|
||||
el-button(size="small" icon="el-icon-folder" @click="openImageFolder(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.open_folder') }}
|
||||
el-button(v-if="API.currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath" size="small" icon="el-icon-upload2" @click="uploadScreenshotToGallery") {{ $t('dialog.screenshot_metadata.upload') }}
|
||||
br
|
||||
//- Search bar input
|
||||
el-input(v-model="screenshotMetadataDialog.search" size="small" placeholder="Search" clearable style="width:200px" @input="screenshotMetadataSearch")
|
||||
//- 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 }}
|
||||
//- 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")
|
||||
br
|
||||
br
|
||||
span(v-text="screenshotMetadataDialog.metadata.fileName")
|
||||
br
|
||||
span(v-if="screenshotMetadataDialog.metadata.dateTime") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }}
|
||||
span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-left:5px")
|
||||
el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" style="margin-left:5px" v-text="screenshotMetadataDialog.metadata.fileSize")
|
||||
span(v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right:5px") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }}
|
||||
span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-right:5px")
|
||||
el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" v-text="screenshotMetadataDialog.metadata.fileSize")
|
||||
br
|
||||
location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name")
|
||||
br
|
||||
@@ -2557,24 +2566,21 @@ html
|
||||
br
|
||||
el-carousel(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange")
|
||||
el-carousel-item
|
||||
el-popover(placement="top" width="700px" trigger="click")
|
||||
span(placement="top" width="700px" trigger="click")
|
||||
img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.previousFilePath" style="width:100%;height:100%;object-fit:contain")
|
||||
img(v-lazy="screenshotMetadataDialog.metadata.previousFilePath" style="height:700px")
|
||||
el-carousel-item
|
||||
el-popover(placement="top" width="700px" trigger="click")
|
||||
span(placement="top" width="700px" trigger="click" @click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)")
|
||||
img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.filePath" style="width:100%;height:100%;object-fit:contain")
|
||||
img(v-lazy="screenshotMetadataDialog.metadata.filePath" style="height:700px")
|
||||
el-carousel-item
|
||||
el-popover(placement="top" width="700px" trigger="click")
|
||||
span(placement="top" width="700px" trigger="click")
|
||||
img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="width:100%;height:100%;object-fit:contain")
|
||||
img(v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="height:700px")
|
||||
br
|
||||
template(v-if="screenshotMetadataDialog.metadata.error")
|
||||
pre(v-text="screenshotMetadataDialog.metadata.error" style="white-space:pre-wrap;font-size:12px")
|
||||
br
|
||||
span(v-for="user in screenshotMetadataDialog.metadata.players" style="margin-top:5px")
|
||||
span.x-link(v-text="user.displayName" @click="lookupUser(user)")
|
||||
span(v-if="user.x" v-text="'('+user.x+', '+user.y+', '+user.z+')'" style="margin-left:5px;color:#909399;font-family:monospace")
|
||||
span(v-if="user.pos" v-text="'('+user.pos.x+', '+user.pos.y+', '+user.pos.z+')'" style="margin-left:5px;color:#909399;font-family:monospace")
|
||||
br
|
||||
|
||||
//- dialog: change log
|
||||
@@ -2605,10 +2611,10 @@ html
|
||||
img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url")
|
||||
|
||||
//- dialog: full screen image
|
||||
el-dialog.x-dialog(ref="fullscreenImageDialog" :before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="fullscreenImageDialog.visible" top="5vh" width="90vw")
|
||||
el-dialog.x-dialog(ref="fullscreenImageDialog" :before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="fullscreenImageDialog.visible" top="3vh" width="97vw")
|
||||
el-button(@click="copyImageUrl(fullscreenImageDialog.imageUrl)" size="mini" icon="el-icon-s-order" circle)
|
||||
el-button(type="default" size="mini" icon="el-icon-download" circle @click="downloadAndSaveImage(fullscreenImageDialog.imageUrl)" style="margin-left:5px")
|
||||
img(v-lazy="fullscreenImageDialog.imageUrl" style="width:100%;height:80vh;object-fit:contain")
|
||||
img(v-lazy="fullscreenImageDialog.imageUrl" style="width:100%;height:100vh;object-fit:contain")
|
||||
|
||||
//- dialog: open source software notice
|
||||
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" :title="$t('dialog.open_source.header')" width="650px")
|
||||
|
||||
Reference in New Issue
Block a user