Delete screenshot metadata

This commit is contained in:
Natsumi
2025-08-09 14:22:39 +12:00
parent eb0843646b
commit d40d0af21f
8 changed files with 250 additions and 102 deletions

View File

@@ -125,4 +125,41 @@ public partial class AppApi
return lastScreenshot;
}
public bool DeleteScreenshotMetadata(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path) || !path.EndsWith(".png"))
return false;
try
{
ScreenshotHelper.DeleteTextMetadata(path, true);
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to delete screenshot metadata for {0}", path);
return false;
}
}
public void DeleteAllScreenshotMetadata()
{
var path = GetVRChatPhotosLocation();
if (!Directory.Exists(path))
return;
var imageFiles = Directory.GetFiles(path, "*.png", SearchOption.AllDirectories);
foreach (var file in imageFiles)
{
try
{
ScreenshotHelper.DeleteTextMetadata(file, true);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to delete screenshot metadata for {0}", file);
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -43,7 +43,7 @@ public class PNGFile : IDisposable
ReadAndCacheMetadata();
var chunk = metadataChunkCache.FirstOrDefault((chunk) => chunkTypeFilter.HasFlag(chunk.ChunkTypeEnum));
if (chunk.IsZero())
if (chunk == null || chunk.IsZero())
return null;
return chunk;
@@ -60,7 +60,7 @@ public class PNGFile : IDisposable
public PNGChunk? GetChunkReverse(PNGChunkTypeFilter chunkTypeFilter)
{
var chunk = ReadChunkReverse(chunkTypeFilter);
if (chunk != null && chunk.IsZero())
if (chunk == null || chunk.IsZero())
return null;
return chunk;

View File

@@ -36,7 +36,7 @@ namespace VRCX
/// If true, the function searches from the end of the file using a reverse search bruteforce method.
/// </param>
/// <returns>The text associated with the specified keyword, or null if not found.</returns>
public static string ReadTextChunk(string keyword, PNGFile pngFile, bool legacySearch = false)
public static string? ReadTextChunk(string keyword, PNGFile pngFile, bool legacySearch = false)
{
// Search for legacy text chunks created by old vrchat mods
if (legacySearch)

View File

@@ -582,6 +582,13 @@
"header": "Save Instance Emoji To File",
"description": "Save spawned emoji to your VRChat Pictures folder"
},
"delete_all_screenshot_metadata": {
"button": "Delete Screenshot Metadata",
"ask": "Are you sure you want to delete all VRC & VRCX screenshot metadata from your VRChat Pictures folder?",
"confirm": "Deleting all screenshot metadata is a very destructive action and cannot be undone! This includes all prints, sub folders and other unrelated images in your VRChat Pictures folder, if these photos are precious to you, please make a backup before proceeding.",
"confirm_yes": "Yes",
"confirm_no": "No"
},
"remote_database": {
"header": "Remote Avatar Database",
"enable": "Enable",
@@ -1533,6 +1540,7 @@
"last_screenshot": "Last Screenshot",
"copy_image": "Copy Image",
"open_folder": "Open Folder",
"delete_metadata": "Delete Metadata",
"upload": "Upload"
},
"registry_backup": {
@@ -1692,6 +1700,10 @@
},
"friend": {
"load_failed": "Failed to load friends list, logging out"
},
"screenshot_metadata": {
"deleted": "Screenshot metadata deleted",
"delete_failed": "Failed to delete screenshot metadata"
}
},
"prompt": {

View File

@@ -512,6 +512,71 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
);
}
function askDeleteAllScreenshotMetadata() {
$app.$confirm(
t(
'view.settings.advanced.advanced.delete_all_screenshot_metadata.ask'
),
{
confirmButtonText: t(
'view.settings.advanced.advanced.delete_all_screenshot_metadata.confirm_yes'
),
cancelButtonText: t(
'view.settings.advanced.advanced.delete_all_screenshot_metadata.confirm_no'
),
type: 'warning',
showInput: false,
callback: async (action) => {
if (action === 'confirm') {
deleteAllScreenshotMetadata();
}
}
}
);
}
function deleteAllScreenshotMetadata() {
$app.$confirm(
t(
'view.settings.advanced.advanced.delete_all_screenshot_metadata.confirm'
),
{
confirmButtonText: t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_confirm'
),
cancelButtonText: t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_cancel'
),
type: 'warning',
showInput: false,
callback: async (action) => {
if (action === 'confirm') {
const msgBox = $app.$message({
message: 'Batch metadata removal in progress...',
type: 'warning',
duration: 0
});
try {
await AppApi.DeleteAllScreenshotMetadata();
$app.$message({
message: 'Batch metadata removal complete',
type: 'success'
});
} catch (err) {
console.error(err);
$app.$message({
message: `Batch metadata removal failed: ${err}`,
type: 'error'
});
} finally {
msgBox.close();
}
}
}
}
);
}
function resetUGCFolder() {
setUGCFolderPath('');
}
@@ -647,6 +712,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
showVRChatConfig,
promptAutoClearVRCXCacheFrequency,
setSaveInstanceEmoji,
setVrcRegistryAutoBackup
setVrcRegistryAutoBackup,
askDeleteAllScreenshotMetadata
};
});

View File

@@ -290,6 +290,8 @@ declare global {
searchType?: number
): Promise<string>;
GetLastScreenshot(): Promise<string>;
DeleteScreenshotMetadata(path: string): Promise<boolean>;
DeleteAllScreenshotMetadata(): Promise<void>;
// Moderations
GetVRChatModerations(

View File

@@ -1285,6 +1285,9 @@
:value="screenshotHelperCopyToClipboard"
@change="setScreenshotHelperCopyToClipboard()"
:long-label="true" />
<el-button size="small" icon="el-icon-delete" @click="askDeleteAllScreenshotMetadata()">{{
t('view.settings.advanced.advanced.delete_all_screenshot_metadata.button')
}}</el-button>
</div>
<div class="options-container">
@@ -2113,7 +2116,8 @@
openUGCFolderSelector,
showVRChatConfig,
promptAutoClearVRCXCacheFrequency,
setSaveInstanceEmoji
setSaveInstanceEmoji,
askDeleteAllScreenshotMetadata
} = advancedSettingsStore;
const instanceTypes = ref([

View File

@@ -42,6 +42,13 @@
@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 />
@@ -279,6 +286,26 @@
});
});
}
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) {