Custom save locations for prints & stickers (#1041)

This commit is contained in:
Thomas
2024-12-21 22:15:05 +00:00
committed by GitHub
parent aa4ab56bf7
commit 55945acf96
6 changed files with 170 additions and 34 deletions
+16 -16
View File
@@ -38,12 +38,12 @@ namespace VRCX
ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged; ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged;
ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged; ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged;
} }
public void Init() public void Init()
{ {
// Create Instance before Cef tries to bind it // Create Instance before Cef tries to bind it
} }
/// <summary> /// <summary>
/// Computes the MD5 hash of the file represented by the specified base64-encoded string. /// Computes the MD5 hash of the file represented by the specified base64-encoded string.
/// </summary> /// </summary>
@@ -68,7 +68,7 @@ namespace VRCX
{ {
using var fileMemoryStream = new MemoryStream(imageData); using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream); var image = new Bitmap(fileMemoryStream);
// for APNG, check if image is png format and less than maxSize // for APNG, check if image is png format and less than maxSize
if ((!matchingDimensions || image.Width == image.Height) && if ((!matchingDimensions || image.Width == image.Height) &&
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) && image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
@@ -78,7 +78,7 @@ namespace VRCX
{ {
return imageData; return imageData;
} }
if (image.Width > maxWidth) if (image.Width > maxWidth)
{ {
var sizingFactor = image.Width / (double)maxWidth; var sizingFactor = image.Width / (double)maxWidth;
@@ -101,14 +101,14 @@ namespace VRCX
image.Dispose(); image.Dispose();
image = newImage; image = newImage;
} }
SaveToFileToUpload(); SaveToFileToUpload();
for (int i = 0; i < 250 && imageData.Length > maxSize; i++) for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
{ {
SaveToFileToUpload(); SaveToFileToUpload();
if (imageData.Length < maxSize) if (imageData.Length < maxSize)
break; break;
int newWidth; int newWidth;
int newHeight; int newHeight;
if (image.Width > image.Height) if (image.Width > image.Height)
@@ -138,13 +138,13 @@ namespace VRCX
imageData = imageSaveMemoryStream.ToArray(); imageData = imageSaveMemoryStream.ToArray();
} }
} }
public byte[] ResizePrintImage(byte[] imageData) public byte[] ResizePrintImage(byte[] imageData)
{ {
var inputImage = ResizeImageToFitLimits(imageData, false, 1920, 1080); var inputImage = ResizeImageToFitLimits(imageData, false, 1920, 1080);
using var fileMemoryStream = new MemoryStream(inputImage); using var fileMemoryStream = new MemoryStream(inputImage);
var image = new Bitmap(fileMemoryStream); var image = new Bitmap(fileMemoryStream);
// increase size to 1920x1080 // increase size to 1920x1080
if (image.Width < 1920 || image.Height < 1080) if (image.Width < 1920 || image.Height < 1080)
{ {
@@ -180,11 +180,11 @@ namespace VRCX
graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height)); graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
image.Dispose(); image.Dispose();
image = newImage; image = newImage;
using var imageSaveMemoryStream = new MemoryStream(); using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png); image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
return imageSaveMemoryStream.ToArray(); return imageSaveMemoryStream.ToArray();
} }
/// <summary> /// <summary>
/// Computes the signature of the file represented by the specified base64-encoded string using the librsync library. /// Computes the signature of the file represented by the specified base64-encoded string using the librsync library.
@@ -283,7 +283,7 @@ namespace VRCX
{ {
MainForm.Instance.Browser.SetZoomLevel(zoomLevel); MainForm.Instance.Browser.SetZoomLevel(zoomLevel);
} }
public async Task<double> GetZoom() public async Task<double> GetZoom()
{ {
return await MainForm.Instance.Browser.GetZoomLevelAsync(); return await MainForm.Instance.Browser.GetZoomLevelAsync();
@@ -340,7 +340,7 @@ namespace VRCX
public void RestartApplication(bool isUpgrade) public void RestartApplication(bool isUpgrade)
{ {
var args = new List<string>(); var args = new List<string>();
if (isUpgrade) if (isUpgrade)
args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix); args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix);
@@ -597,7 +597,7 @@ namespace VRCX
})); }));
} }
} }
/// <summary> /// <summary>
/// Flashes the window of the main form. /// Flashes the window of the main form.
/// </summary> /// </summary>
@@ -629,7 +629,7 @@ namespace VRCX
public async Task<bool> SavePrintToFile(string url, string path, string fileName) public async Task<bool> SavePrintToFile(string url, string path, string fileName)
{ {
var folder = Path.Combine(GetVRChatPhotosLocation(), "Prints", MakeValidFileName(path)); var folder = Path.Combine(GetUGCPhotoLocation(), "Prints", MakeValidFileName(path));
Directory.CreateDirectory(folder); Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName)); var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath)) if (File.Exists(filePath))
@@ -640,7 +640,7 @@ namespace VRCX
public async Task<bool> SaveStickerToFile(string url, string path, string fileName) public async Task<bool> SaveStickerToFile(string url, string path, string fileName)
{ {
var folder = Path.Combine(GetVRChatPhotosLocation(), "Stickers", MakeValidFileName(path)); var folder = Path.Combine(GetUGCPhotoLocation(), "Stickers", MakeValidFileName(path));
Directory.CreateDirectory(folder); Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName)); var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath)) if (File.Exists(filePath))
@@ -648,7 +648,7 @@ namespace VRCX
return await ImageCache.SaveImageToFile(url, filePath); return await ImageCache.SaveImageToFile(url, filePath);
} }
public bool IsRunningUnderWine() public bool IsRunningUnderWine()
{ {
return Wine.GetIfWine(); return Wine.GetIfWine();
+96 -18
View File
@@ -6,6 +6,9 @@ using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Microsoft.Win32; using Microsoft.Win32;
using System.Threading;
using System.Windows.Forms;
using System.Threading.Tasks;
namespace VRCX namespace VRCX
{ {
@@ -34,7 +37,7 @@ namespace VRCX
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
} }
public string GetVRChatPhotosLocation() public string GetVRChatPhotosLocation()
{ {
var json = ReadConfigFile(); var json = ReadConfigFile();
@@ -50,10 +53,37 @@ namespace VRCX
} }
} }
} }
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat"); return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat");
} }
/// <summary>
/// Gets the folder the user has selected for User-Generated content such as prints / stickers from the JS side.
/// If there is no override on the folder, it returns the default VRChat Photos path.
/// </summary>
/// <returns>The UGC Photo Location.</returns>
public string GetUGCPhotoLocation(string path = "")
{
if (string.IsNullOrEmpty(path))
{
return GetVRChatPhotosLocation();
}
try
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path;
}
catch (Exception e)
{
Console.WriteLine(e);
return GetVRChatPhotosLocation();
}
}
private string GetSteamUserdataPathFromRegistry() private string GetSteamUserdataPathFromRegistry()
{ {
string steamUserdataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Steam\userdata"); string steamUserdataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Steam\userdata");
@@ -85,20 +115,20 @@ namespace VRCX
var steamUserdataPath = GetSteamUserdataPathFromRegistry(); var steamUserdataPath = GetSteamUserdataPathFromRegistry();
var screenshotPath = string.Empty; var screenshotPath = string.Empty;
var latestWriteTime = DateTime.MinValue; var latestWriteTime = DateTime.MinValue;
if (!Directory.Exists(steamUserdataPath)) if (!Directory.Exists(steamUserdataPath))
return screenshotPath; return screenshotPath;
var steamUserDirs = Directory.GetDirectories(steamUserdataPath); var steamUserDirs = Directory.GetDirectories(steamUserdataPath);
foreach (var steamUserDir in steamUserDirs) foreach (var steamUserDir in steamUserDirs)
{ {
var screenshotDir = Path.Combine(steamUserDir, @"760\remote\438100\screenshots"); var screenshotDir = Path.Combine(steamUserDir, @"760\remote\438100\screenshots");
if (!Directory.Exists(screenshotDir)) if (!Directory.Exists(screenshotDir))
continue; continue;
var lastWriteTime = File.GetLastWriteTime(screenshotDir); var lastWriteTime = File.GetLastWriteTime(screenshotDir);
if (lastWriteTime <= latestWriteTime) if (lastWriteTime <= latestWriteTime)
continue; continue;
latestWriteTime = lastWriteTime; latestWriteTime = lastWriteTime;
screenshotPath = screenshotDir; screenshotPath = screenshotDir;
} }
@@ -114,13 +144,13 @@ namespace VRCX
{ {
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer"); return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
} }
public bool OpenVrcxAppDataFolder() public bool OpenVrcxAppDataFolder()
{ {
var path = Program.AppDataDirectory; var path = Program.AppDataDirectory;
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return false; return false;
OpenFolderAndSelectItem(path, true); OpenFolderAndSelectItem(path, true);
return true; return true;
} }
@@ -130,27 +160,37 @@ namespace VRCX
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return false; return false;
OpenFolderAndSelectItem(path, true); OpenFolderAndSelectItem(path, true);
return true; return true;
} }
public bool OpenVrcPhotosFolder() public bool OpenVrcPhotosFolder()
{ {
var path = GetVRChatPhotosLocation(); var path = GetVRChatPhotosLocation();
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return false; return false;
OpenFolderAndSelectItem(path, true); OpenFolderAndSelectItem(path, true);
return true; return true;
} }
public bool OpenUGCPhotosFolder(string ugcPath = "")
{
var path = GetUGCPhotoLocation(ugcPath);
if (!Directory.Exists(path))
return false;
OpenFolderAndSelectItem(path, true);
return true;
}
public bool OpenVrcScreenshotsFolder() public bool OpenVrcScreenshotsFolder()
{ {
var path = GetVRChatScreenshotsLocation(); var path = GetVRChatScreenshotsLocation();
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return false; return false;
OpenFolderAndSelectItem(path, true); OpenFolderAndSelectItem(path, true);
return true; return true;
} }
@@ -160,7 +200,7 @@ namespace VRCX
var path = Path.Combine(Path.GetTempPath(), "VRChat", "VRChat", "Crashes"); var path = Path.Combine(Path.GetTempPath(), "VRChat", "VRChat", "Crashes");
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return false; return false;
OpenFolderAndSelectItem(path, true); OpenFolderAndSelectItem(path, true);
return true; return true;
} }
@@ -231,7 +271,7 @@ namespace VRCX
Marshal.FreeCoTaskMem(pidlFile); Marshal.FreeCoTaskMem(pidlFile);
} }
} }
public void OpenFolderAndSelectItemFallback(string path) public void OpenFolderAndSelectItemFallback(string path)
{ {
if (!File.Exists(path) && !Directory.Exists(path)) if (!File.Exists(path) && !Directory.Exists(path))
@@ -247,7 +287,45 @@ namespace VRCX
Process.Start("explorer.exe", $"/select,\"{path}\""); Process.Start("explorer.exe", $"/select,\"{path}\"");
} }
} }
/// <summary>
/// Opens a folder dialog to select a folder and pass it back to the JS side.
/// </summary>
/// <param name="defaultPath">The default path for the folder picker.</param>
public async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
{
var tcs = new TaskCompletionSource<string>();
var staThread = new Thread(() =>
{
try
{
using (var openFolderDialog = new FolderBrowserDialog())
{
openFolderDialog.InitialDirectory = Directory.Exists(defaultPath) ? defaultPath : GetVRChatPhotosLocation();
var dialogResult = openFolderDialog.ShowDialog(MainForm.nativeWindow);
if (dialogResult == DialogResult.OK)
{
tcs.SetResult(openFolderDialog.SelectedPath);
}
else
{
tcs.SetResult(defaultPath);
}
}
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
return await tcs.Task;
}
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)", private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
Regex.Escape(new string(Path.GetInvalidPathChars())))); Regex.Escape(new string(Path.GetInvalidPathChars()))));
@@ -260,7 +338,7 @@ namespace VRCX
name = name.Replace("\\", ""); name = name.Replace("\\", "");
name = _folderRegex.Replace(name, ""); name = _folderRegex.Replace(name, "");
name = _fileRegex.Replace(name, ""); name = _fileRegex.Replace(name, "");
return name; return name;
} }
} }
+2
View File
@@ -17,6 +17,7 @@ namespace VRCX
public partial class MainForm : WinformBase public partial class MainForm : WinformBase
{ {
public static MainForm Instance; public static MainForm Instance;
public static NativeWindow nativeWindow;
private static NLog.Logger jslogger = NLog.LogManager.GetLogger("Javascript"); private static NLog.Logger jslogger = NLog.LogManager.GetLogger("Javascript");
public ChromiumWebBrowser Browser; public ChromiumWebBrowser Browser;
private readonly Timer _saveTimer; private readonly Timer _saveTimer;
@@ -43,6 +44,7 @@ namespace VRCX
{ {
Instance = this; Instance = this;
InitializeComponent(); InitializeComponent();
nativeWindow = NativeWindow.FromHandle(this.Handle);
// adding a 5s delay here to avoid excessive writes to disk // adding a 5s delay here to avoid excessive writes to disk
_saveTimer = new Timer(); _saveTimer = new Timer();
+39
View File
@@ -20455,6 +20455,45 @@ speechSynthesis.getVoices();
this.noteExportDialog.loading = false; this.noteExportDialog.loading = false;
}; };
// user generated content
$app.data.ugcFolderPath = await configRepository.getString(
'VRCX_userGeneratedContentPath',
''
);
$app.data.userGeneratedContentDialog = {
visible: false
};
$app.methods.setUGCFolderPath = async function (path) {
await configRepository.setString(
'VRCX_userGeneratedContentPath',
path
);
this.ugcFolderPath = path;
};
$app.methods.resetUGCFolder = function () {
this.setUGCFolderPath('');
}
$app.methods.openUGCFolder = async function () {
await AppApi.OpenUGCPhotosFolder(this.ugcFolderPath);
};
$app.methods.openUGCFolderSelector = async function () {
var D = this.userGeneratedContentDialog;
if(D.visible)
return;
D.visible = true;
var newUGCFolder = await AppApi.OpenFolderSelectorDialog(this.ugcFolderPath);
D.visible = false;
await this.setUGCFolderPath(newUGCFolder);
};
// avatar database provider // avatar database provider
$app.data.avatarProviderDialog = { $app.data.avatarProviderDialog = {
+7
View File
@@ -516,6 +516,13 @@
"portal_spawn": "Portal Spawn:", "portal_spawn": "Portal Spawn:",
"video_play": "Video Play:", "video_play": "Video Play:",
"event": "Event:" "event": "Event:"
},
"user_generated_content": {
"header": "User Generated Content",
"folder": "Open Folder",
"description": "Open or set the folder where content such as 'Prints' and 'Stickers' are stored.",
"set_folder": "Set Folder",
"reset_override": "Reset"
} }
}, },
"photon": { "photon": {
+10
View File
@@ -506,6 +506,16 @@ 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")
//- Advanced | User Generated Content
div.options-container
span.header {{ $t('view.settings.advanced.advanced.user_generated_content.header') }}
div.options-container-item
span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.user_generated_content.description') }}
br
el-button(size="small" icon="el-icon-folder" @click="openUGCFolder()" style="margin-top:5px") {{ $t('view.settings.advanced.advanced.user_generated_content.folder') }}
el-button(size="small" icon="el-icon-folder-opened" @click="openUGCFolderSelector()") {{ $t('view.settings.advanced.advanced.user_generated_content.set_folder') }}
el-button(size="small" icon="el-icon-delete" @click="resetUGCFolder()" v-if="ugcFolderPath") {{ $t('view.settings.advanced.advanced.user_generated_content.reset_override') }}
br
span.sub-header {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.header') }} span.sub-header {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.header') }}
el-tooltip(placement="top" style="margin-left:5px" :content="$t('view.settings.advanced.advanced.save_instance_prints_to_file.header_tooltip')") 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 i.el-icon-info