Save instance emojis

This commit is contained in:
Natsumi
2025-06-26 19:25:51 +12:00
parent bbe7120380
commit 97cc2dc81a
7 changed files with 178 additions and 27 deletions

View File

@@ -257,5 +257,26 @@ namespace VRCX
return filePath;
}
public async Task<string> SaveEmojiToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Join(GetUGCPhotoLocation(ugcFolderPath), "Emoji", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Join(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return null;
try
{
await ImageCache.SaveImageToFile(url, filePath);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to save print to file");
return null;
}
return filePath;
}
}
}

View File

@@ -32,6 +32,7 @@ import {
groupRequest,
imageRequest,
instanceRequest,
inventoryRequest,
inviteMessagesRequest,
miscRequest,
notificationRequest,
@@ -119,6 +120,7 @@ import { userDialogGroupSortingOptions } from './composables/user/constants/user
import {
getPrintFileName,
getPrintLocalDate,
getEmojiFileName,
languageClass
} from './composables/user/utils';
import InteropApi from './ipc-electron/interopApi.js';
@@ -6768,6 +6770,9 @@ console.log(`isLinux: ${LINUX}`);
case 'VRCX_saveInstanceStickers':
this.saveInstanceStickers = !this.saveInstanceStickers;
break;
case 'VRCX_saveInstanceEmoji':
this.saveInstanceEmoji = !this.saveInstanceEmoji;
break;
case 'VRCX_StartAsMinimizedState':
this.isStartAsMinimizedState = !this.isStartAsMinimizedState;
break;
@@ -6808,6 +6813,11 @@ console.log(`isLinux: ${LINUX}`);
this.saveInstanceStickers
);
await configRepository.setBool(
'VRCX_saveInstanceEmoji',
this.saveInstanceEmoji
);
VRCXStorage.Set(
'VRCX_StartAsMinimizedState',
this.isStartAsMinimizedState.toString()
@@ -10954,6 +10964,75 @@ console.log(`isLinux: ${LINUX}`);
}
};
// #endregion
// #region | Emoji
$app.data.instanceInventoryCache = [];
$app.data.instanceInventoryQueue = [];
$app.data.instanceInventoryQueueWorker = null;
$app.methods.queueCheckInstanceInventory = function (inventoryId) {
if (this.instanceInventoryCache.includes(inventoryId)) {
return;
}
this.instanceInventoryCache.push(inventoryId);
if (this.instanceInventoryCache.length > 100) {
this.instanceInventoryCache.shift();
}
this.instanceInventoryQueue.push(inventoryId);
if (!this.instanceInventoryQueueWorker) {
this.instanceInventoryQueueWorker = workerTimers.setInterval(() => {
let inventoryId = this.instanceInventoryQueue.shift();
if (inventoryId) {
this.trySaveEmojiToFile(inventoryId);
}
}, 2_500);
}
};
$app.methods.trySaveEmojiToFile = async function (inventoryId) {
const args = await inventoryRequest.getInventoryItem({
inventoryId
});
if (args.json.itemType !== 'emoji') {
// Not an emoji, skip
return;
}
const userArgs = await userRequest.getCachedUser({
userId: args.json.holderId
});
const displayName = userArgs.json?.displayName ?? '';
let emoji = args.json.metadata;
emoji.name = `${displayName}_${inventoryId}`;
const emojiFileName = getEmojiFileName(emoji);
const imageUrl = args.json.metadata?.imageUrl ?? args.json.imageUrl;
const createdAt = args.json.created_at;
const monthFolder = createdAt.slice(0, 7);
const filePath = await AppApi.SaveEmojiToFile(
imageUrl,
this.ugcFolderPath,
monthFolder,
emojiFileName
);
if (filePath) {
console.log(
`Emoji saved to file: ${monthFolder}\\${emojiFileName}`
);
}
if (this.instanceInventoryQueue.length === 0) {
workerTimers.clearInterval(this.instanceInventoryQueueWorker);
this.instanceInventoryQueueWorker = null;
}
};
// #endregion
// #region | Prints
$app.methods.cropPrintsChanged = function () {
@@ -11038,6 +11117,11 @@ console.log(`isLinux: ${LINUX}`);
false
);
$app.data.saveInstanceEmoji = await configRepository.getBool(
'VRCX_saveInstanceEmoji',
false
);
$app.data.printCache = [];
$app.data.printQueue = [];
$app.data.printQueueWorker = null;

View File

@@ -260,6 +260,10 @@ export default class extends baseClass {
this.processScreenshot(gameLog.screenshotPath);
break;
case 'api-request':
if ($app.debugWebRequests) {
console.log('API Request:', gameLog.url);
}
// var userId = '';
// try {
// var url = new URL(gameLog.url);
@@ -277,23 +281,43 @@ export default class extends baseClass {
// break;
// }
if (!$app.saveInstancePrints) {
break;
if ($app.saveInstanceEmoji) {
try {
// https://api.vrchat.cloud/api/1/inventory/spawn?id=inv_75781d65-92fe-4a80-a1ff-27ee6e843b08
const url = new URL(gameLog.url);
if (
url.pathname.substring(0, 22) ===
'/api/1/inventory/spawn'
) {
const inventoryId = url.searchParams.get('id');
if (inventoryId && inventoryId.length === 40) {
$app.queueCheckInstanceInventory(
inventoryId
);
}
}
} catch (err) {
console.error(err);
}
}
try {
var printId = '';
var url = new URL(gameLog.url);
if (
url.pathname.substring(0, 14) === '/api/1/prints/'
) {
var pathArray = url.pathname.split('/');
printId = pathArray[4];
if ($app.saveInstancePrints) {
try {
let printId = '';
const url1 = new URL(gameLog.url);
if (
url1.pathname.substring(0, 14) ===
'/api/1/prints/'
) {
const pathArray = url1.pathname.split('/');
printId = pathArray[4];
}
if (printId && printId.length === 41) {
$app.queueSavePrintToFile(printId);
}
} catch (err) {
console.error(err);
}
if (printId && printId.length === 41) {
$app.queueSavePrintToFile(printId);
}
} catch (err) {
console.error(err);
}
break;
case 'avatar-change':

View File

@@ -162,7 +162,7 @@
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 1024x1024px (1:1)</span>
<br />
<br />
<div style="display: flex; align-items: center">
<div>
<el-button-group style="margin-right: 10px">
<el-button type="default" size="small" @click="refreshEmojiTable" icon="el-icon-refresh">
{{ t('dialog.gallery_icons.refresh') }}
@@ -237,7 +237,7 @@
@click="
showFullscreenImageDialog(
image.versions[image.versions.length - 1].file.url,
getEmojiFileName(image)
getEmojiName(image)
)
">
<template v-if="image.frames">
@@ -271,7 +271,7 @@
@click="
showFullscreenImageDialog(
image.versions[image.versions.length - 1].file.url,
getEmojiFileName(image)
getEmojiName(image)
)
"
size="mini"
@@ -489,6 +489,7 @@
@click="consumeInventoryBundle(item.id)"
size="mini"
icon="el-icon-plus"
style="float: right"
circle>
{{ t('dialog.gallery_icons.consume_bundle') }}
</el-button>
@@ -504,7 +505,7 @@
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
import { extractFileId } from '../../composables/shared/utils';
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../composables/user/constants/emoji';
import { getPrintFileName } from '../../composables/user/utils';
import { getPrintFileName, getEmojiFileName } from '../../composables/user/utils';
import Location from '../Location.vue';
const { t } = useI18n();
@@ -822,7 +823,7 @@
emojiAnimFps.value = parseInt(value.replace('fps', ''));
}
if (value.endsWith('loopStyle')) {
emojiAnimLoopPingPong.value = value === 'pingpong';
emojiAnimLoopPingPong.value = value.replace('loopStyle', '').toLowerCase() === 'pingpong';
}
}
}
@@ -891,13 +892,8 @@
document.getElementById('EmojiUploadButton').click();
}
function getEmojiFileName(emoji) {
if (emoji.frames) {
const loopStyle = emoji.loopStyle || 'linear';
return `${emoji.name}_${emoji.animationStyle}animationStyle_${emoji.frames}frames_${emoji.framesOverTime}fps_${loopStyle}loopStyle.png`;
} else {
return `${emoji.name}_${emoji.animationStyle}animationStyle.png`;
}
function getEmojiName(emoji) {
getEmojiFileName(emoji);
}
function generateEmojiStyle(url, fps, frameCount, loopStyle) {

View File

@@ -35,6 +35,15 @@ function getPrintFileName(print) {
return fileName;
}
function getEmojiFileName(emoji) {
if (emoji.frames) {
const loopStyle = emoji.loopStyle || 'linear';
return `${emoji.name}_${emoji.animationStyle}animationStyle_${emoji.frames}frames_${emoji.framesOverTime}fps_${loopStyle}loopStyle.png`;
} else {
return `${emoji.name}_${emoji.animationStyle}animationStyle.png`;
}
}
function getPrintLocalDate(print) {
if (print.createdAt) {
const createdAt = new Date(print.createdAt);
@@ -75,5 +84,6 @@ export {
languageClass,
getPrintFileName,
getPrintLocalDate,
getEmojiFileName,
isFriendOnline
};

View File

@@ -568,6 +568,10 @@
"header": "Save Instance Stickers To File",
"description": "Save placed stickers to your VRChat Pictures folder"
},
"save_instance_emoji_to_file": {
"header": "Save Instance Emoji To File",
"description": "Save spawned emoji to your VRChat Pictures folder"
},
"remote_database": {
"header": "Remote Avatar Database",
"enable": "Enable",

View File

@@ -871,6 +871,18 @@ mixin settingsTab
:value='saveInstanceStickers'
@change='saveVRCXWindowOption("VRCX_saveInstanceStickers")'
:long-label='true')
br
span.sub-header {{ $t('view.settings.advanced.advanced.save_instance_emoji_to_file.header') }}
el-tooltip(
placement='top'
style='margin-left: 5px'
:content='$t("view.settings.advanced.advanced.save_instance_prints_to_file.header_tooltip")')
i.el-icon-info
simple-switch(
:label='$t("view.settings.advanced.advanced.save_instance_emoji_to_file.description")'
:value='saveInstanceEmoji'
@change='saveVRCXWindowOption("VRCX_saveInstanceEmoji")'
:long-label='true')
//- "Advanced" Tab
el-tab-pane(lazy :label='$t("view.settings.category.advanced")')