diff --git a/AppApi.cs b/AppApi.cs index 2a6d16e9..5b3961b4 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -37,6 +38,17 @@ namespace VRCX static AppApi() { Instance = new AppApi(); + + ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged; + ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged; + } + + private void OnProcessStateChanged(MonitoredProcess monitoredProcess) + { + if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver")) + return; + + CheckGameRunning(); } public string MD5File(string Blob) @@ -117,26 +129,23 @@ namespace VRCX Cef.GetGlobalCookieManager().DeleteCookies(); } - public bool[] CheckGameRunning() + public void CheckGameRunning() { var isGameRunning = false; var isSteamVRRunning = false; - if (Process.GetProcessesByName("vrchat").Length > 0) + if (ProcessMonitor.Instance.IsProcessRunning("VRChat", true)) { isGameRunning = true; } - if (Process.GetProcessesByName("vrserver").Length > 0) + if (ProcessMonitor.Instance.IsProcessRunning("vrserver", true)) { isSteamVRRunning = true; } - return new[] - { - isGameRunning, - isSteamVRRunning - }; + if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading) + MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning); } public int QuitGame() @@ -721,6 +730,9 @@ namespace VRCX public void GetScreenshotMetadata(string path) { + if (string.IsNullOrEmpty(path)) + return; + var fileName = Path.GetFileNameWithoutExtension(path); var metadata = new JObject(); if (File.Exists(path) && path.EndsWith(".png")) @@ -748,7 +760,7 @@ namespace VRCX } catch (Exception ex) { - metadata.Add("error", $"This file contains invalid LFS/SSM metadata unable to be parsed by VRCX. \n({ex.Message})\n Text: {metadataString}"); + metadata.Add("error", $"This file contains invalid LFS/SSM metadata unable to be parsed by VRCX. \n({ex.Message})\nText: {metadataString}"); } } else @@ -759,7 +771,7 @@ namespace VRCX } catch (JsonReaderException ex) { - metadata.Add("error", $"This file contains invalid metadata unable to be parsed by VRCX. \n({ex.Message})\n Text: {metadataString}"); + metadata.Add("error", $"This file contains invalid metadata unable to be parsed by VRCX. \n({ex.Message})\nText: {metadataString}"); } } } @@ -828,13 +840,56 @@ namespace VRCX } } - public void OpenImageFolder(string path) + public void OpenShortcutFolder() { - if (!File.Exists(path)) + var path = AutoAppLaunchManager.Instance.AppShortcutDirectory; + if (!Directory.Exists(path)) return; - // open folder with file highlighted - Process.Start("explorer.exe", $"/select,\"{path}\""); + OpenFolderAndSelectItem(path, true); + } + + public void OpenFolderAndSelectItem(string path, bool isFolder = false) + { + // I don't think it's quite meant for it, but SHOpenFolderAndSelectItems can open folders by passing the folder path as the item to select, as a child to itself, somehow. So we'll check to see if 'path' is a folder as well. + if (!File.Exists(path) && !Directory.Exists(path)) + return; + + var folderPath = isFolder ? path : Path.GetDirectoryName(path); + IntPtr pidlFolder; + IntPtr pidlFile; + uint psfgaoOut; + + // Convert our managed strings to PIDLs. PIDLs are essentially pointers to the actual file system objects, separate from the "display name", which is the human-readable path to the file/folder. We're parsing the display name into a PIDL here. + // The windows shell uses PIDLs to identify objects in winapi calls, so we'll need to use them to open the folder and select the file. Cool stuff! + var result = WinApi.SHParseDisplayName(folderPath, IntPtr.Zero, out pidlFolder, 0, out psfgaoOut); + if (result != 0) + { + return; + } + + result = WinApi.SHParseDisplayName(path, IntPtr.Zero, out pidlFile, 0, out psfgaoOut); + if (result != 0) + { + // Free the PIDL we allocated earlier if we failed to parse the display name of the file. + Marshal.FreeCoTaskMem(pidlFolder); + return; + } + + IntPtr[] files = { pidlFile }; + + try + { + // Open the containing folder and select our file. SHOpenFolderAndSelectItems will respect existing explorer instances, open a new one if none exist, will properly handle paths > 120 chars, and work with third-party filesystem viewers that hook into winapi calls. + // It can select multiple items, but we only need to select one. + WinApi.SHOpenFolderAndSelectItems(pidlFolder, (uint)files.Length, files, 0); + } + finally + { + // Free the PIDLs we allocated earlier + Marshal.FreeCoTaskMem(pidlFolder); + Marshal.FreeCoTaskMem(pidlFile); + } } public void FlashWindow() diff --git a/AutoAppLaunchManager.cs b/AutoAppLaunchManager.cs new file mode 100644 index 00000000..303b5662 --- /dev/null +++ b/AutoAppLaunchManager.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace VRCX +{ + /// + /// The class responsible for launching user-defined applications when VRChat opens/closes. + /// + public class AutoAppLaunchManager + { + public static AutoAppLaunchManager Instance { get; private set; } + public bool Enabled = true; + public readonly string AppShortcutDirectory; + + private DateTime startTime = DateTime.Now; + private List startedProcesses = new List(); + private static readonly byte[] shortcutSignatureBytes = { 0x4C, 0x00, 0x00, 0x00 }; // signature for ShellLinkHeader\ + + static AutoAppLaunchManager() + { + Instance = new AutoAppLaunchManager(); + } + + public AutoAppLaunchManager() + { + AppShortcutDirectory = Path.Combine(Program.AppDataDirectory, "startup"); + + if (!Directory.Exists(AppShortcutDirectory)) + { + Directory.CreateDirectory(AppShortcutDirectory); + } + + ProcessMonitor.Instance.ProcessStarted += OnProcessStarted; + ProcessMonitor.Instance.ProcessExited += OnProcessExited; + } + + private void OnProcessExited(MonitoredProcess monitoredProcess) + { + if (startedProcesses.Count == 0 || !monitoredProcess.HasName("VRChat")) + return; + + foreach (var process in startedProcesses) + { + if (!process.HasExited) + process.Kill(); + } + + startedProcesses.Clear(); + } + + private void OnProcessStarted(MonitoredProcess monitoredProcess) + { + if (!monitoredProcess.HasName("VRChat") || monitoredProcess.Process.StartTime < startTime) + return; + + if (startedProcesses.Count > 0) + { + foreach (var process in startedProcesses) + { + if (!process.HasExited) + process.Kill(); + } + + startedProcesses.Clear(); + } + + var shortcutFiles = FindShortcutFiles(AppShortcutDirectory); + + if (shortcutFiles.Length > 0) + { + foreach (var file in shortcutFiles) + { + var process = Process.Start(file); + startedProcesses.Add(process); + } + } + } + + internal void Init() + { + // What are you lookin at? + } + + internal void Exit() + { + Enabled = false; + + foreach (var process in startedProcesses) + { + if (!process.HasExited) + process.Kill(); + } + } + + /// + /// Finds windows shortcut files in a given folder. + /// + /// The folder path. + /// An array of shortcut paths. If none, then empty. + private static string[] FindShortcutFiles(string folderPath) + { + DirectoryInfo directoryInfo = new DirectoryInfo(folderPath); + FileInfo[] files = directoryInfo.GetFiles(); + List ret = new List(); + + foreach (FileInfo file in files) + { + if (IsShortcutFile(file.FullName)) + { + ret.Add(file.FullName); + } + } + + return ret.ToArray(); + } + + /// + /// Determines whether the specified file path is a shortcut by checking the file header. + /// + /// The file path. + /// true if the given file path is a shortcut, otherwise false + private static bool IsShortcutFile(string filePath) + { + byte[] headerBytes = new byte[4]; + using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + if (fileStream.Length >= 4) + { + fileStream.Read(headerBytes, 0, 4); + } + } + + return headerBytes.SequenceEqual(shortcutSignatureBytes); + } + } +} diff --git a/ProcessMonitor.cs b/ProcessMonitor.cs new file mode 100644 index 00000000..a2dc847e --- /dev/null +++ b/ProcessMonitor.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Timers; + +namespace VRCX +{ + // I don't think this applies to our use case, but I'm leaving it here for reference. + // https://stackoverflow.com/questions/2519673/process-hasexited-returns-true-even-though-process-is-running + // "When a process is started, it is assigned a PID. If the User is then prompted with the User Account Control dialog and selects 'Yes', the process is re-started and assigned a new PID." + // There's no docs for this, but Process.HasExited also seems to be checked every time the property is accessed, so it's not cached. Which means Process.Refresh() is not needed for our use case. + + /// + /// A class that monitors given processes and raises events when they are started or exited. + /// Intended to be used to monitor VRChat and VRChat-related processes. + /// + internal class ProcessMonitor + { + private readonly Dictionary monitoredProcesses; + private readonly Timer monitorProcessTimer; + + static ProcessMonitor() + { + Instance = new ProcessMonitor(); + } + + public ProcessMonitor() + { + monitoredProcesses = new Dictionary(); + + monitorProcessTimer = new Timer(); + monitorProcessTimer.Interval = 1000; + monitorProcessTimer.Elapsed += MonitorProcessTimer_Elapsed; + } + + public static ProcessMonitor Instance { get; private set; } + + /// + /// Raised when a monitored process is started. + /// + public event Action ProcessStarted; + + /// + /// Raised when a monitored process is exited. + /// + public event Action ProcessExited; + + public void Init() + { + AddProcess("vrchat"); + AddProcess("vrserver"); + monitorProcessTimer.Start(); + } + + public void Exit() + { + monitorProcessTimer.Stop(); + monitoredProcesses.Values.ToList().ForEach(x => x.ProcessExited()); + } + + private void MonitorProcessTimer_Elapsed(object sender, ElapsedEventArgs e) + { + var processesNeedingUpdate = new List(); + + // Check if any of the monitored processes have been opened or closed. + foreach (var keyValuePair in monitoredProcesses) + { + var monitoredProcess = keyValuePair.Value; + var process = monitoredProcess.Process; + var name = monitoredProcess.ProcessName; + + if (monitoredProcess.IsRunning) + { + if (monitoredProcess.Process == null || monitoredProcess.Process.HasExited) + { + monitoredProcess.ProcessExited(); + ProcessExited?.Invoke(monitoredProcess); + } + + monitoredProcess.Process.Refresh(); + } + else + { + processesNeedingUpdate.Add(monitoredProcess); + } + } + + // We do it this way so we're not constantly polling for processes if we don't actually need to (aka, all processes are already accounted for). + if (processesNeedingUpdate.Count == 0) + return; + + var processes = Process.GetProcesses(); + foreach (var monitoredProcess in processesNeedingUpdate) + { + var process = processes.FirstOrDefault(p => string.Equals(p.ProcessName, monitoredProcess.ProcessName, StringComparison.OrdinalIgnoreCase)); + if (process != null) + { + monitoredProcess.ProcessStarted(process); + ProcessStarted?.Invoke(monitoredProcess); + } + } + } + + /// + /// Checks if a process if currently being monitored and if it is running. + /// + /// The name of the process to check for. + /// If true, will manually check if the given process is running should the the monitored process not be initialized yet. + /// Whether the given process is monitored and currently running. + public bool IsProcessRunning(string processName, bool ensureCheck = false) + { + processName = processName.ToLower(); + if (monitoredProcesses.ContainsKey(processName)) + { + var process = monitoredProcesses[processName]; + + if (ensureCheck && process.Process == null) + return Process.GetProcessesByName(processName).FirstOrDefault() != null; + + return process.IsRunning; + } + + return false; + } + + /// + /// Adds a process to be monitored. + /// + /// + public void AddProcess(Process process) + { + if (monitoredProcesses.ContainsKey(process.ProcessName.ToLower())) + return; + + monitoredProcesses.Add(process.ProcessName.ToLower(), new MonitoredProcess(process)); + } + + /// + /// Adds a process to be monitored. + /// + /// + public void AddProcess(string processName) + { + if (monitoredProcesses.ContainsKey(processName.ToLower())) + { + return; + } + + monitoredProcesses.Add(processName, new MonitoredProcess(processName)); + } + + /// + /// Removes a process from being monitored. + /// + /// + public void RemoveProcess(string processName) + { + if (monitoredProcesses.ContainsKey(processName.ToLower())) + { + monitoredProcesses.Remove(processName); + } + } + } + + internal class MonitoredProcess + { + public MonitoredProcess(Process process) + { + Process = process; + ProcessName = process.ProcessName.ToLower(); + + if (!process.HasExited) + IsRunning = true; + } + + public MonitoredProcess(string processName) + { + ProcessName = processName; + IsRunning = false; + } + + public Process Process { get; private set; } + public string ProcessName { get; private set; } + public bool IsRunning { get; private set; } + + public bool HasName(string processName) + { + return ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase); + } + + public void ProcessExited() + { + IsRunning = false; + Process?.Dispose(); + Process = null; + } + + public void ProcessStarted(Process process) + { + Process = process; + ProcessName = process.ProcessName.ToLower(); + IsRunning = true; + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index f7532b28..ba6f2a33 100644 --- a/Program.cs +++ b/Program.cs @@ -78,12 +78,14 @@ namespace VRCX Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); + ProcessMonitor.Instance.Init(); SQLite.Instance.Init(); VRCXStorage.Load(); CpuMonitor.Instance.Init(); Discord.Instance.Init(); WebApi.Instance.Init(); LogWatcher.Instance.Init(); + AutoAppLaunchManager.Instance.Init(); CefService.Instance.Init(); IPCServer.Instance.Init(); @@ -93,6 +95,7 @@ namespace VRCX VRCXVR.Instance.Exit(); CefService.Instance.Exit(); + AutoAppLaunchManager.Instance.Exit(); LogWatcher.Instance.Exit(); WebApi.Instance.Exit(); @@ -100,6 +103,7 @@ namespace VRCX CpuMonitor.Instance.Exit(); VRCXStorage.Save(); SQLite.Instance.Exit(); + ProcessMonitor.Instance.Exit(); } } } \ No newline at end of file diff --git a/ScreenshotHelper.cs b/ScreenshotHelper.cs index 379a17aa..b2b26d8e 100644 --- a/ScreenshotHelper.cs +++ b/ScreenshotHelper.cs @@ -187,7 +187,11 @@ namespace VRCX var metadata = new JObject(); // lfs|2|author:usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,Natsumi-sama|world:wrld_b016712b-5ce6-4bcb-9144-c8ed089b520f,35372,pet park test|pos:-60.49379,-0.002925932,5.805772|players:usr_9d73bff9-4543-4b6f-a004-9e257869ff50,-0.85,-0.17,-0.58,Olivia.;usr_3097f91e-a816-4c7a-a625-38fbfdee9f96,12.30,13.72,0.08,Zettai Ryouiki;usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,0.68,0.32,-0.28,Natsumi-sama;usr_7525f45f-517e-442b-9abc-fbcfedb29f84,0.51,0.64,0.70,Weyoun // lfs|2|author:usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,knah|world:wrld_fb4edc80-6c48-43f2-9bd1-2fa9f1345621,35341,Luminescent Ledge|pos:8.231676,0.257298,-0.1983307|rq:2|players:usr_65b9eeeb-7c91-4ad2-8ce4-addb1c161cd6,0.74,0.59,1.57,Jakkuba;usr_6a50647f-d971-4281-90c3-3fe8caf2ba80,8.07,9.76,0.16,SopwithPup;usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,0.26,1.03,-0.28,knah;usr_7f593ad1-3e9e-4449-a623-5c1c0a8d8a78,0.15,0.60,1.46,NekOwneD + // lfs|cvr|1|author:047b30bd-089d-887c-8734-b0032df5d176,Hordini|world:2e73b387-c6d4-45e9-b998-0fd6aa122c1d,i+efec20004ef1cd8b-404003-93833f-1aee112a,Bono's Basement (Anime) (#816724)|pos:2.196716,0.01250899,-3.817466|players:5301af21-eb8d-7b36-3ef4-b623fa51c2c6,3.778407,0.01250887,-3.815876,DDAkebono;f9e5c36c-41b0-7031-1185-35b4034010c0,4.828233,0.01250893,-3.920135,Natsumi var lfs = metadataString.Split('|'); + if (lfs[1] == "cvr") + lfs = lfs.Skip(1).ToArray(); + var version = int.Parse(lfs[1]); var application = lfs[0]; metadata.Add("application", application); @@ -219,6 +223,16 @@ namespace VRCX { case "author": var author = split[1].Split(','); + if (application == "cvr") + { + metadata.Add("author", new JObject + { + { "id", "" }, + { "displayName", $"{author[1]} ({author[0]})" } + }); + break; + } + metadata.Add("author", new JObject { { "id", author[0] }, @@ -226,7 +240,17 @@ namespace VRCX }); break; case "world": - if (version == 1) + if (application == "cvr") + { + var world = split[1].Split(','); + metadata.Add("world", new JObject + { + { "id", "" }, + { "name", $"{world[2]} ({world[0]})" }, + { "instanceId", "" } + }); + } + else if (version == 1) { metadata.Add("world", new JObject { @@ -265,14 +289,28 @@ namespace VRCX foreach (var player in players) { var playerSplit = player.Split(','); - playersArray.Add(new JObject + if (application == "cvr") { - { "id", playerSplit[0] }, - { "x", playerSplit[1] }, - { "y", playerSplit[2] }, - { "z", playerSplit[3] }, - { "displayName", playerSplit[4] } - }); + playersArray.Add(new JObject + { + { "id", "" }, + { "x", playerSplit[1] }, + { "y", playerSplit[2] }, + { "z", playerSplit[3] }, + { "displayName", $"{playerSplit[4]} ({playerSplit[0]})" } + }); + } + else + { + playersArray.Add(new JObject + { + { "id", playerSplit[0] }, + { "x", playerSplit[1] }, + { "y", playerSplit[2] }, + { "z", playerSplit[3] }, + { "displayName", playerSplit[4] } + }); + } } metadata.Add("players", playersArray); diff --git a/VRCX.csproj b/VRCX.csproj index 982cd898..99bd1e81 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -84,12 +84,14 @@ + + diff --git a/WinApi.cs b/WinApi.cs index e1550dd8..641948a1 100644 --- a/WinApi.cs +++ b/WinApi.cs @@ -19,5 +19,9 @@ namespace VRCX [DllImport("user32.dll", SetLastError = true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + public static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); + [DllImport("shell32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, uint dwFlags); } } diff --git a/html/src/app.js b/html/src/app.js index 81ca96f3..fa679f39 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -4885,6 +4885,7 @@ speechSynthesis.getVoices(); this.checkForVRCXUpdate(); } }); + AppApi.CheckGameRunning(); API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); API.$on('SHOW_WORLD_DIALOG_SHORTNAME', (tag) => this.verifyShortName('', tag) @@ -5045,45 +5046,12 @@ speechSynthesis.getVoices(); this.nextClearVRCXCacheCheck = this.clearVRCXCacheFrequency; this.clearVRCXCache(); } - AppApi.CheckGameRunning().then( - ([isGameRunning, isSteamVRRunning]) => { - this.updateOpenVR(isGameRunning, isSteamVRRunning); - if (isGameRunning !== this.isGameRunning) { - this.isGameRunning = isGameRunning; - if (isGameRunning) { - API.currentUser.$online_for = Date.now(); - API.currentUser.$offline_for = ''; - } else { - configRepository.setBool( - 'isGameNoVR', - this.isGameNoVR - ); - API.currentUser.$online_for = ''; - API.currentUser.$offline_for = Date.now(); - this.autoVRChatCacheManagement(); - this.checkIfGameCrashed(); - this.ipcTimeout = 0; - } - this.lastLocationReset(); - this.clearNowPlaying(); - this.updateVRLastLocation(); - workerTimers.setTimeout( - () => this.checkVRChatDebugLogging(), - 60000 - ); - this.nextDiscordUpdate = 0; - } - if (isSteamVRRunning !== this.isSteamVRRunning) { - this.isSteamVRRunning = isSteamVRRunning; - } - if (--this.nextDiscordUpdate <= 0) { - this.nextDiscordUpdate = 7; - if (this.discordActive) { - this.updateDiscord(); - } - } + if (--this.nextDiscordUpdate <= 0) { + this.nextDiscordUpdate = 7; + if (this.discordActive) { + this.updateDiscord(); } - ); + } } } catch (err) { API.isRefreshFriendsLoading = false; @@ -5092,6 +5060,43 @@ speechSynthesis.getVoices(); workerTimers.setTimeout(() => this.updateLoop(), 500); }; + $app.methods.updateIsGameRunning = function ( + isGameRunning, + isSteamVRRunning + ) { + console.log( + `updateIsGameRunning isGameRunning:${isGameRunning} isSteamVRRunning:${isSteamVRRunning}` + ); + if (isGameRunning !== this.isGameRunning) { + this.isGameRunning = isGameRunning; + if (isGameRunning) { + API.currentUser.$online_for = Date.now(); + API.currentUser.$offline_for = ''; + } else { + configRepository.setBool('isGameNoVR', this.isGameNoVR); + API.currentUser.$online_for = ''; + API.currentUser.$offline_for = Date.now(); + this.autoVRChatCacheManagement(); + this.checkIfGameCrashed(); + this.ipcTimeout = 0; + } + this.lastLocationReset(); + this.clearNowPlaying(); + this.updateVRLastLocation(); + workerTimers.setTimeout( + () => this.checkVRChatDebugLogging(), + 60000 + ); + this.nextDiscordUpdate = 0; + console.log('isGameRunning changed', isGameRunning); + } + if (isSteamVRRunning !== this.isSteamVRRunning) { + this.isSteamVRRunning = isSteamVRRunning; + console.log('isSteamVRRunning changed', isSteamVRRunning); + } + this.updateOpenVR(); + }; + $app.data.debug = false; $app.data.debugWebRequests = false; $app.data.debugWebSocket = false; @@ -9631,10 +9636,12 @@ speechSynthesis.getVoices(); case 'openvr-init': this.isGameNoVR = false; configRepository.setBool('isGameNoVR', this.isGameNoVR); + this.updateOpenVR(); break; case 'desktop-mode': this.isGameNoVR = true; configRepository.setBool('isGameNoVR', this.isGameNoVR); + this.updateOpenVR(); break; case 'udon-exception': console.log('UdonException', gameLog.data); @@ -13384,7 +13391,7 @@ speechSynthesis.getVoices(); this.updateVRConfigVars(); this.updateVRLastLocation(); AppApi.ExecuteVrOverlayFunction('notyClear', ''); - this.updateOpenVR(this.isGameRunning, this.isSteamVRRunning); + this.updateOpenVR(); }; $app.methods.saveSortFavoritesOption = function () { this.getLocalWorldFavorites(); @@ -13523,6 +13530,7 @@ speechSynthesis.getVoices(); if (!this.timeoutHudOverlay) { AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); } + this.updateOpenVR(); }; $app.data.logResourceLoad = configRepository.getBool( 'VRCX_logResourceLoad' @@ -13973,12 +13981,12 @@ speechSynthesis.getVoices(); }); }; - $app.methods.updateOpenVR = function (isGameRunning, isSteamVRRunning) { + $app.methods.updateOpenVR = function () { if ( this.openVR && !this.isGameNoVR && - isSteamVRRunning && - (isGameRunning || this.openVRAlways) + this.isSteamVRRunning && + (this.isGameRunning || this.openVRAlways) ) { var hmdOverlay = false; if ( @@ -15006,6 +15014,9 @@ speechSynthesis.getVoices(); }); $app.methods.showUserDialog = function (userId) { + if (!userId) { + return; + } this.$nextTick(() => adjustDialogZ(this.$refs.userDialog.$el)); var D = this.userDialog; D.id = userId; @@ -20586,6 +20597,12 @@ speechSynthesis.getVoices(); this.VRChatConfigFile.screenshot_res_width = res.width; }; + // Auto Launch Shortcuts + + $app.methods.openShortcutFolder = function () { + AppApi.OpenShortcutFolder(); + }; + // Screenshot Helper $app.methods.saveScreenshotHelper = function () { @@ -20747,7 +20764,7 @@ speechSynthesis.getVoices(); }; $app.methods.openImageFolder = function (path) { - AppApi.OpenImageFolder(path).then(() => { + AppApi.OpenFolderAndSelectItem(path).then(() => { this.$message({ message: 'Opened image folder', type: 'success' @@ -20804,6 +20821,7 @@ speechSynthesis.getVoices(); this.progressPieFilter ); this.updateVRLastLocation(); + this.updateOpenVR(); }; $app.methods.showYouTubeApiDialog = function () { diff --git a/html/src/index.pug b/html/src/index.pug index 7eabc6d5..782d7764 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1418,6 +1418,9 @@ html el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") {{ $t('view.settings.advanced.advanced.launch_options') }} el-button(size="small" icon="el-icon-picture" @click="showScreenshotMetadataDialog()") {{ $t('view.settings.advanced.advanced.screenshot_metadata') }} + el-button(size="small" icon="el-icon-folder" @click="openShortcutFolder()") {{ $t('view.settings.advanced.advanced.auto_launch') }} + el-tooltip(placement="top" style="margin-left:5px" :content="$t('view.settings.advanced.advanced.auto_launch_tooltip')") + i.el-icon-warning div.options-container span.sub-header {{ $t('view.settings.advanced.advanced.primary_password.header') }} div.options-container-item @@ -3880,7 +3883,7 @@ html img(v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="height:700px") br template(v-if="screenshotMetadataDialog.metadata.error") - pre(v-text="screenshotMetadataDialog.metadata.error") + pre(v-text="screenshotMetadataDialog.metadata.error" style="white-space:pre-wrap;font-size:12px") br span(v-for="user in screenshotMetadataDialog.metadata.players" style="margin-top:5px") span.x-link(v-text="user.displayName" @click="lookupUser(user)") diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index e831b04d..80352fc0 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -354,6 +354,8 @@ "header": "Advanced", "launch_options": "Launch Options", "screenshot_metadata": "Screenshot Metadata", + "auto_launch": "Auto-Launch Folder", + "auto_launch_tooltip": "To auto-launch apps with VRChat, place shortcuts in this folder.", "pending_offline": { "header": "Pending Offline", "description": "Delay before marking user as offline (fixes false positives)",