diff --git a/Dotnet/AppApi/AppApi.cs b/Dotnet/AppApi/AppApi.cs index 2ae8283f..f6fda847 100644 --- a/Dotnet/AppApi/AppApi.cs +++ b/Dotnet/AppApi/AppApi.cs @@ -38,12 +38,12 @@ namespace VRCX ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged; ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged; } - + public void Init() { // Create Instance before Cef tries to bind it } - + /// /// Computes the MD5 hash of the file represented by the specified base64-encoded string. /// @@ -68,7 +68,7 @@ namespace VRCX { using var fileMemoryStream = new MemoryStream(imageData); var image = new Bitmap(fileMemoryStream); - + // for APNG, check if image is png format and less than maxSize if ((!matchingDimensions || image.Width == image.Height) && image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) && @@ -78,7 +78,7 @@ namespace VRCX { return imageData; } - + if (image.Width > maxWidth) { var sizingFactor = image.Width / (double)maxWidth; @@ -101,14 +101,14 @@ namespace VRCX image.Dispose(); image = newImage; } - + SaveToFileToUpload(); for (int i = 0; i < 250 && imageData.Length > maxSize; i++) { SaveToFileToUpload(); if (imageData.Length < maxSize) break; - + int newWidth; int newHeight; if (image.Width > image.Height) @@ -138,13 +138,13 @@ namespace VRCX imageData = imageSaveMemoryStream.ToArray(); } } - + public byte[] ResizePrintImage(byte[] imageData) { var inputImage = ResizeImageToFitLimits(imageData, false, 1920, 1080); using var fileMemoryStream = new MemoryStream(inputImage); var image = new Bitmap(fileMemoryStream); - + // increase size to 1920x1080 if (image.Width < 1920 || image.Height < 1080) { @@ -180,11 +180,11 @@ namespace VRCX graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height)); image.Dispose(); image = newImage; - + using var imageSaveMemoryStream = new MemoryStream(); image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png); return imageSaveMemoryStream.ToArray(); - } + } /// /// 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); } - + public async Task GetZoom() { return await MainForm.Instance.Browser.GetZoomLevelAsync(); @@ -340,7 +340,7 @@ namespace VRCX public void RestartApplication(bool isUpgrade) { var args = new List(); - + if (isUpgrade) args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix); @@ -597,7 +597,7 @@ namespace VRCX })); } } - + /// /// Flashes the window of the main form. /// @@ -629,7 +629,7 @@ namespace VRCX public async Task 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); var filePath = Path.Combine(folder, MakeValidFileName(fileName)); if (File.Exists(filePath)) @@ -640,7 +640,7 @@ namespace VRCX public async Task 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); var filePath = Path.Combine(folder, MakeValidFileName(fileName)); if (File.Exists(filePath)) @@ -648,7 +648,7 @@ namespace VRCX return await ImageCache.SaveImageToFile(url, filePath); } - + public bool IsRunningUnderWine() { return Wine.GetIfWine(); diff --git a/Dotnet/AppApi/Folders.cs b/Dotnet/AppApi/Folders.cs index 6e5f9a13..8fb6f5ea 100644 --- a/Dotnet/AppApi/Folders.cs +++ b/Dotnet/AppApi/Folders.cs @@ -6,6 +6,9 @@ using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.Win32; +using System.Threading; +using System.Windows.Forms; +using System.Threading.Tasks; namespace VRCX { @@ -34,7 +37,7 @@ namespace VRCX return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; } - + public string GetVRChatPhotosLocation() { var json = ReadConfigFile(); @@ -50,10 +53,37 @@ namespace VRCX } } } - + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat"); } - + + /// + /// 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. + /// + /// The UGC Photo Location. + 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() { string steamUserdataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Steam\userdata"); @@ -85,20 +115,20 @@ namespace VRCX var steamUserdataPath = GetSteamUserdataPathFromRegistry(); var screenshotPath = string.Empty; var latestWriteTime = DateTime.MinValue; - if (!Directory.Exists(steamUserdataPath)) + if (!Directory.Exists(steamUserdataPath)) return screenshotPath; - + var steamUserDirs = Directory.GetDirectories(steamUserdataPath); foreach (var steamUserDir in steamUserDirs) { var screenshotDir = Path.Combine(steamUserDir, @"760\remote\438100\screenshots"); if (!Directory.Exists(screenshotDir)) continue; - + var lastWriteTime = File.GetLastWriteTime(screenshotDir); if (lastWriteTime <= latestWriteTime) continue; - + latestWriteTime = lastWriteTime; screenshotPath = screenshotDir; } @@ -114,13 +144,13 @@ namespace VRCX { return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer"); } - + public bool OpenVrcxAppDataFolder() { var path = Program.AppDataDirectory; if (!Directory.Exists(path)) return false; - + OpenFolderAndSelectItem(path, true); return true; } @@ -130,27 +160,37 @@ namespace VRCX var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; if (!Directory.Exists(path)) return false; - + OpenFolderAndSelectItem(path, true); return true; } - + public bool OpenVrcPhotosFolder() { var path = GetVRChatPhotosLocation(); if (!Directory.Exists(path)) return false; - + OpenFolderAndSelectItem(path, 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() { var path = GetVRChatScreenshotsLocation(); if (!Directory.Exists(path)) return false; - + OpenFolderAndSelectItem(path, true); return true; } @@ -160,7 +200,7 @@ namespace VRCX var path = Path.Combine(Path.GetTempPath(), "VRChat", "VRChat", "Crashes"); if (!Directory.Exists(path)) return false; - + OpenFolderAndSelectItem(path, true); return true; } @@ -231,7 +271,7 @@ namespace VRCX Marshal.FreeCoTaskMem(pidlFile); } } - + public void OpenFolderAndSelectItemFallback(string path) { if (!File.Exists(path) && !Directory.Exists(path)) @@ -247,7 +287,45 @@ namespace VRCX Process.Start("explorer.exe", $"/select,\"{path}\""); } } - + + /// + /// Opens a folder dialog to select a folder and pass it back to the JS side. + /// + /// The default path for the folder picker. + public async Task OpenFolderSelectorDialog(string defaultPath = "") + { + var tcs = new TaskCompletionSource(); + 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}]+)", Regex.Escape(new string(Path.GetInvalidPathChars())))); @@ -260,7 +338,7 @@ namespace VRCX name = name.Replace("\\", ""); name = _folderRegex.Replace(name, ""); name = _fileRegex.Replace(name, ""); - + return name; } } diff --git a/Dotnet/MainForm.cs b/Dotnet/MainForm.cs index bfde0a1e..97120f94 100644 --- a/Dotnet/MainForm.cs +++ b/Dotnet/MainForm.cs @@ -17,6 +17,7 @@ namespace VRCX public partial class MainForm : WinformBase { public static MainForm Instance; + public static NativeWindow nativeWindow; private static NLog.Logger jslogger = NLog.LogManager.GetLogger("Javascript"); public ChromiumWebBrowser Browser; private readonly Timer _saveTimer; @@ -43,6 +44,7 @@ namespace VRCX { Instance = this; InitializeComponent(); + nativeWindow = NativeWindow.FromHandle(this.Handle); // adding a 5s delay here to avoid excessive writes to disk _saveTimer = new Timer(); diff --git a/html/src/app.js b/html/src/app.js index d7d19cfd..8256a004 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -20455,6 +20455,45 @@ speechSynthesis.getVoices(); 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 $app.data.avatarProviderDialog = { diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index cf56af4a..6c871934 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -516,6 +516,13 @@ "portal_spawn": "Portal Spawn:", "video_play": "Video Play:", "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": { diff --git a/html/src/mixins/tabs/settings.pug b/html/src/mixins/tabs/settings.pug index d0290841..62fcb063 100644 --- a/html/src/mixins/tabs/settings.pug +++ b/html/src/mixins/tabs/settings.pug @@ -506,6 +506,16 @@ 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") + //- 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') }} 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