Save Instance Prints To File

This commit is contained in:
Natsumi
2024-11-17 10:11:52 +13:00
parent 7956e19a10
commit 78a6dfbfe1
10 changed files with 223 additions and 82 deletions

View File

@@ -30,7 +30,6 @@ namespace VRCX
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly MD5 _hasher = MD5.Create(); private static readonly MD5 _hasher = MD5.Create();
private static bool dialogOpen;
static AppApi() static AppApi()
{ {
@@ -575,5 +574,16 @@ namespace VRCX
return null; return null;
} }
public async Task<bool> SavePrintToFile(string url, string fileName)
{
var directory = Path.Combine(GetVRChatPhotosLocation(), "Prints");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, fileName);
if (File.Exists(path))
return false;
return await ImageCache.SaveImageToFile(url, path);
}
} }
} }

View File

@@ -12,6 +12,8 @@ namespace VRCX
{ {
public partial class AppApi public partial class AppApi
{ {
private static bool dialogOpen;
/// <summary> /// <summary>
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID. /// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
/// </summary> /// </summary>

View File

@@ -6,89 +6,119 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace VRCX namespace VRCX;
internal static class ImageCache
{ {
internal static class ImageCache private static readonly string cacheLocation;
private static readonly HttpClient httpClient;
private static readonly List<string> _imageHosts =
[
"api.vrchat.cloud",
"files.vrchat.cloud",
"d348imysud55la.cloudfront.net",
"assets.vrchat.com"
];
static ImageCache()
{ {
private static readonly string cacheLocation; cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
private static readonly HttpClient httpClient; var httpClientHandler = new HttpClientHandler();
private static readonly List<string> _imageHosts = if (WebApi.ProxySet)
[ httpClientHandler.Proxy = WebApi.Proxy;
"api.vrchat.cloud",
"files.vrchat.cloud",
"d348imysud55la.cloudfront.net",
"assets.vrchat.com"
];
static ImageCache() httpClient = new HttpClient(httpClientHandler);
}
public static async Task<string> GetImage(string url, string fileId, string version)
{
var directoryLocation = Path.Combine(cacheLocation, fileId);
var fileLocation = Path.Combine(directoryLocation, $"{version}.png");
if (File.Exists(fileLocation))
{ {
cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache"); Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
var httpClientHandler = new HttpClientHandler();
if (WebApi.ProxySet)
httpClientHandler.Proxy = WebApi.Proxy;
httpClient = new HttpClient(httpClientHandler);
}
public static async Task<string> GetImage(string url, string fileId, string version)
{
var directoryLocation = Path.Combine(cacheLocation, fileId);
var fileLocation = Path.Combine(directoryLocation, $"{version}.png");
if (File.Exists(fileLocation))
{
Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
return fileLocation;
}
if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream);
}
}
var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
return fileLocation; return fileLocation;
} }
private static void CleanImageCache() if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{ {
var dirInfo = new DirectoryInfo(cacheLocation); CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000); foreach (Cookie cookie in cookies)
foreach (var folder in folders) cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{ {
folder.Delete(true); { "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream);
} }
} }
var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
return fileLocation;
}
private static void CleanImageCache()
{
var dirInfo = new DirectoryInfo(cacheLocation);
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000);
foreach (var folder in folders)
{
folder.Delete(true);
}
}
public static async Task<bool> SaveImageToFile(string url, string path)
{
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
var cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
return false;
await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
return true;
} }
} }

View File

@@ -5447,7 +5447,11 @@ speechSynthesis.getVoices();
} }
} }
this.lastLocation.playerList.forEach((ref1) => { this.lastLocation.playerList.forEach((ref1) => {
if (ref1.userId && !API.cachedUsers.has(ref1.userId)) { if (
ref1.userId &&
typeof ref1.userId === 'string' &&
!API.cachedUsers.has(ref1.userId)
) {
API.getUser({ userId: ref1.userId }); API.getUser({ userId: ref1.userId });
} }
}); });
@@ -8149,6 +8153,10 @@ speechSynthesis.getVoices();
'VRCX_StartAtWindowsStartup', 'VRCX_StartAtWindowsStartup',
this.isStartAtWindowsStartup this.isStartAtWindowsStartup
); );
await configRepository.setBool(
'VRCX_saveInstancePrints',
this.saveInstancePrints
);
VRCXStorage.Set( VRCXStorage.Set(
'VRCX_StartAsMinimizedState', 'VRCX_StartAsMinimizedState',
this.isStartAsMinimizedState.toString() this.isStartAsMinimizedState.toString()
@@ -17522,6 +17530,52 @@ speechSynthesis.getVoices();
} }
}); });
API.getPrint = function (params) {
return this.call(`prints/${params.printId}`, {
method: 'GET'
}).then((json) => {
var args = {
json,
params
};
this.$emit('PRINT', args);
return args;
});
};
API.editPrint = function (params) {
return this.call(`prints/${params.printId}`, {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('PRINT:EDIT', args);
return args;
});
};
$app.data.saveInstancePrints = await configRepository.getBool(
'VRCX_saveInstancePrints',
false
);
$app.methods.trySavePrintToFile = async function (printId) {
var print = await API.getPrint({ printId });
var imageUrl = print.json?.files?.image;
if (!imageUrl) {
console.error('Print image URL is missing', print);
return;
}
var fileName = `${printId}.png`;
var status = await AppApi.SavePrintToFile(imageUrl, fileName);
if (status) {
console.log(`Print saved to file: ${fileName}`);
}
};
// #endregion // #endregion
// #region | Emoji // #region | Emoji

View File

@@ -39,7 +39,7 @@ export default class extends baseClass {
if (this.gameLogDisabled) { if (this.gameLogDisabled) {
return; return;
} }
var userId = gameLog.userId; var userId = String(gameLog.userId || '');
if (!userId && gameLog.displayName) { if (!userId && gameLog.displayName) {
for (var ref of API.cachedUsers.values()) { for (var ref of API.cachedUsers.values()) {
if (ref.displayName === gameLog.displayName) { if (ref.displayName === gameLog.displayName) {
@@ -257,6 +257,25 @@ export default class extends baseClass {
// if (!userId) { // if (!userId) {
// break; // break;
// } // }
if (!$app.saveInstancePrints) {
break;
}
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 (printId && printId.length === 41) {
$app.trySavePrintToFile(printId);
}
} catch (err) {
console.error(err);
}
break; break;
case 'avatar-change': case 'avatar-change':
var ref = this.lastLocation.playerList.get(userId); var ref = this.lastLocation.playerList.get(userId);

View File

@@ -598,6 +598,9 @@ export default class extends baseClass {
}, },
key() { key() {
this.parse(); this.parse();
},
userid() {
this.parse();
} }
}, },
mounted() { mounted() {

View File

@@ -514,19 +514,34 @@ export default class extends baseClass {
var contentType = content.contentType; var contentType = content.contentType;
console.log('content-refresh', content); console.log('content-refresh', content);
if (contentType === 'icon') { if (contentType === 'icon') {
if ($app.galleryDialogVisible) { if (
$app.galleryDialogVisible &&
!$app.galleryDialogIconsLoading
) {
$app.refreshVRCPlusIconsTable(); $app.refreshVRCPlusIconsTable();
} }
} else if (contentType === 'gallery') { } else if (contentType === 'gallery') {
if ($app.galleryDialogVisible) { if (
$app.galleryDialogVisible &&
!$app.galleryDialogGalleryLoading
) {
$app.refreshGalleryTable(); $app.refreshGalleryTable();
} }
} else if (contentType === 'emoji') { } else if (contentType === 'emoji') {
if ($app.galleryDialogVisible) { if (
$app.galleryDialogVisible &&
!$app.galleryDialogEmojisLoading
) {
$app.refreshEmojiTable(); $app.refreshEmojiTable();
} }
} else if (contentType === 'print') { } else if (
if ($app.galleryDialogVisible) { contentType === 'print' ||
contentType === 'prints'
) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogPrintsLoading
) {
$app.refreshPrintTable(); $app.refreshPrintTable();
} }
} else if (contentType === 'avatar') { } else if (contentType === 'avatar') {

View File

@@ -433,6 +433,10 @@
"header": "Local World Persistence", "header": "Local World Persistence",
"description": "Localhost webserver (requires restart)" "description": "Localhost webserver (requires restart)"
}, },
"save_instance_prints_to_file": {
"header": "Save Instance Prints To File",
"description": "Save spawned prints to your VRChat Pictures folder"
},
"remote_database": { "remote_database": {
"header": "Remote Avatar Database", "header": "Remote Avatar Database",
"enable": "Enable", "enable": "Enable",

View File

@@ -32,7 +32,7 @@ mixin screenshotMetadata()
br br
location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name") location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name")
br br
display-name(v-if="screenshotMetadataDialog.metadata.author" :userid="screenshotMetadataDialog.metadata.author.id" :hind="screenshotMetadataDialog.metadata.author.displayName" style="color:#909399;font-family:monospace") display-name(v-if="screenshotMetadataDialog.metadata.author" :userid="screenshotMetadataDialog.metadata.author.id" :hint="screenshotMetadataDialog.metadata.author.displayName" style="color:#909399;font-family:monospace")
br 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(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange")
el-carousel-item el-carousel-item

View File

@@ -475,6 +475,10 @@ mixin settingsTab()
div.options-container-item div.options-container-item
span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.local_world_persistence.description') }} span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.local_world_persistence.description') }}
el-switch(v-model="disableWorldDatabase" :active-value="false" :inactive-value="true" @change="saveVRCXWindowOption") el-switch(v-model="disableWorldDatabase" :active-value="false" :inactive-value="true" @change="saveVRCXWindowOption")
span.sub-header {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.header') }}
div.options-container-item
span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.description') }}
el-switch(v-model="saveInstancePrints" @change="saveVRCXWindowOption")
//- Advanced | Remote Avatar Database //- Advanced | Remote Avatar Database
div.options-container div.options-container
span.header {{ $t('view.settings.advanced.advanced.remote_database.header') }} span.header {{ $t('view.settings.advanced.advanced.remote_database.header') }}