Skip to content

Commit

Permalink
Save Instance Prints To File
Browse files Browse the repository at this point in the history
Natsumi-sama committed Nov 16, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 4841478 commit 6f85c76
Showing 10 changed files with 215 additions and 74 deletions.
12 changes: 11 additions & 1 deletion Dotnet/AppApi/AppApi.cs
Original file line number Diff line number Diff line change
@@ -30,7 +30,6 @@ public partial class AppApi

private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly MD5 _hasher = MD5.Create();
private static bool dialogOpen;

static AppApi()
{
@@ -575,5 +574,16 @@ public string GetFileBase64(string path)

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);
}
}
}
2 changes: 2 additions & 0 deletions Dotnet/AppApi/Screenshot.cs
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ namespace VRCX
{
public partial class AppApi
{
private static bool dialogOpen;

/// <summary>
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
/// </summary>
160 changes: 95 additions & 65 deletions Dotnet/ImageCache.cs
Original file line number Diff line number Diff line change
@@ -6,89 +6,119 @@
using System.Net.Http;
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"
];
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()
{
cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
var httpClientHandler = new HttpClientHandler();
if (WebApi.ProxySet)
httpClientHandler.Proxy = WebApi.Proxy;
static ImageCache()
{
cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
var httpClientHandler = new HttpClientHandler();
if (WebApi.ProxySet)
httpClientHandler.Proxy = WebApi.Proxy;

httpClient = new HttpClient(httpClientHandler);
}
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");
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 (File.Exists(fileLocation))
{
Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
return fileLocation;
}

if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
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 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 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)
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
Headers =
{
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using (var response = await httpClient.SendAsync(request))
{ "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))
{
response.EnsureSuccessStatusCode();
await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream);
}
await response.Content.CopyToAsync(fileStream);
}
}

var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();

return fileLocation;
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);
}
}

private static void CleanImageCache()
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)
{
var dirInfo = new DirectoryInfo(cacheLocation);
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000);
foreach (var folder in folders)
Headers =
{
folder.Delete(true);
{ "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;
}
}
56 changes: 55 additions & 1 deletion html/src/app.js
Original file line number Diff line number Diff line change
@@ -5458,7 +5458,11 @@ speechSynthesis.getVoices();
}
}
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 });
}
});
@@ -8174,6 +8178,10 @@ speechSynthesis.getVoices();
'VRCX_StartAtWindowsStartup',
this.isStartAtWindowsStartup
);
await configRepository.setBool(
'VRCX_saveInstancePrints',
this.saveInstancePrints
);
VRCXStorage.Set(
'VRCX_StartAsMinimizedState',
this.isStartAsMinimizedState.toString()
@@ -17547,6 +17555,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
// #region | Emoji

21 changes: 20 additions & 1 deletion html/src/classes/gameLog.js
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ export default class extends baseClass {
if (this.gameLogDisabled) {
return;
}
var userId = gameLog.userId;
var userId = String(gameLog.userId || '');
if (!userId && gameLog.displayName) {
for (var ref of API.cachedUsers.values()) {
if (ref.displayName === gameLog.displayName) {
@@ -257,6 +257,25 @@ export default class extends baseClass {
// if (!userId) {
// 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;
case 'avatar-change':
var ref = this.lastLocation.playerList.get(userId);
3 changes: 3 additions & 0 deletions html/src/classes/uiComponents.js
Original file line number Diff line number Diff line change
@@ -598,6 +598,9 @@ export default class extends baseClass {
},
key() {
this.parse();
},
userid() {
this.parse();
}
},
mounted() {
25 changes: 20 additions & 5 deletions html/src/classes/websocket.js
Original file line number Diff line number Diff line change
@@ -514,19 +514,34 @@ export default class extends baseClass {
var contentType = content.contentType;
console.log('content-refresh', content);
if (contentType === 'icon') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogIconsLoading
) {
$app.refreshVRCPlusIconsTable();
}
} else if (contentType === 'gallery') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogGalleryLoading
) {
$app.refreshGalleryTable();
}
} else if (contentType === 'emoji') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogEmojisLoading
) {
$app.refreshEmojiTable();
}
} else if (contentType === 'print') {
if ($app.galleryDialogVisible) {
} else if (
contentType === 'print' ||
contentType === 'prints'
) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogPrintsLoading
) {
$app.refreshPrintTable();
}
} else if (contentType === 'avatar') {
4 changes: 4 additions & 0 deletions html/src/localization/en/en.json
Original file line number Diff line number Diff line change
@@ -433,6 +433,10 @@
"header": "Local World Persistence",
"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": {
"header": "Remote Avatar Database",
"enable": "Enable",
2 changes: 1 addition & 1 deletion html/src/mixins/dialogs/screenshotMetadata.pug
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ mixin screenshotMetadata()
br
location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name")
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
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
4 changes: 4 additions & 0 deletions html/src/mixins/tabs/settings.pug
Original file line number Diff line number Diff line change
@@ -475,6 +475,10 @@ mixin settingsTab()
div.options-container-item
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")
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
div.options-container
span.header {{ $t('view.settings.advanced.advanced.remote_database.header') }}

0 comments on commit 6f85c76

Please sign in to comment.