mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
Linux: SteamVR overlay support (#1299)
* fix: open folder and select item on linux * feat: linux wrist overlay * feat: linux hmd overlay * feat: replace unix sockets with shm on linux * fix: reduce linux wrist overlay fps * fix: hide electron offscreen windows * fix: destroy electron offscreen windows when not in use * fix: open folder and select item on linux * feat: cpu, uptime and device monitoring on linux * feat: native wayland gl context with x11 fallback on linux * fix: use platform agnostic wording for common folders * fix: crash dumps folder button on linux * fix: enable missing VR notification options on linux * fix: update cef, eslint config to include updated AppApiVr names * merge: rebase linux VR changes to upstream * Clean up * Load custom file contents rather than path Fixes loading custom file in debug mode * fix: call SetVR on linux as well * fix: AppApiVrElectron init, properly create and dispose of shm * Handle avatar history error * Lint * Change overlay dispose logic * macOS DOTNET_ROOT * Remove moving dotnet bin * Fix * fix: init overlay on SteamVR restart * Fix fetching empty instance, fix user dialog not fetching * Trim direct access inputs * Make icon higher res, because mac build would fail 😂 * macOS fixes * will it build? that's the question * fix: ensure offscreen windows are ready before vrinit * will it build? that's the question * will it build? that's the question * meow * one, more, time * Fix crash and overlay ellipsis * a --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
34
.github/workflows/github_actions.yml
vendored
34
.github/workflows/github_actions.yml
vendored
@@ -112,6 +112,38 @@ jobs:
|
||||
name: Electron-AppImage
|
||||
path: 'build/VRCX_${{ needs.set_version.outputs.version }}.AppImage'
|
||||
|
||||
build_macos:
|
||||
runs-on: macos-latest
|
||||
needs: [set_version, build_dotnet_linux, build_node]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
echo "${{ needs.set_version.outputs.version }}" > Version
|
||||
cat Version
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Restore dependencies
|
||||
run: npm ci
|
||||
- name: Build Electron-html
|
||||
run: npm run prod-linux
|
||||
- name: Download Electron dotnet artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Electron-Release
|
||||
path: build/Electron
|
||||
- name: Build macOS .dmg
|
||||
run: npm run build-electron
|
||||
- name: Upload Electron macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Electron-MacApp
|
||||
path: 'build/VRCX_${{ needs.set_version.outputs.version }}.dmg'
|
||||
|
||||
create_setup:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -122,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
echo "!define PRODUCT_VERSION_FROM_FILE \"${{ needs.set_version.outputs.date }}.0\"" > Installer/version_define.nsh
|
||||
echo "!define PRODUCT_VERSION_FROM_FILE \"${{ needs.set_version.outputs.date }}.0\"" > Installer/version_define.nsh
|
||||
- name: Install 7-zip and makensis
|
||||
run: sudo apt update && sudo apt install -y p7zip-full nsis nsis-pluginapi
|
||||
- name: Set plugin permissions
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AppApiVr
|
||||
public class AppApiVrCef : AppApiVr
|
||||
{
|
||||
public static readonly AppApiVr Instance;
|
||||
|
||||
static AppApiVr()
|
||||
static AppApiVrCef()
|
||||
{
|
||||
Instance = new AppApiVr();
|
||||
Instance = new AppApiVrCef();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
public override void Init()
|
||||
{
|
||||
// Create Instance before Cef tries to bind it
|
||||
}
|
||||
|
||||
public void VrInit()
|
||||
public override void VrInit()
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
|
||||
}
|
||||
|
||||
public void ToggleSystemMonitor(bool enabled)
|
||||
public override void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitor.Instance.Start(enabled);
|
||||
SystemMonitorCef.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
/// <returns>The current CPU usage as a percentage.</returns>
|
||||
public float CpuUsage()
|
||||
public override float CpuUsage()
|
||||
{
|
||||
return SystemMonitor.Instance.CpuUsage;
|
||||
return SystemMonitorCef.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -45,7 +43,7 @@ namespace VRCX
|
||||
/// Each sub-array contains the type of device and its current state
|
||||
/// </summary>
|
||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||
public string[][] GetVRDevices()
|
||||
public override string[][] GetVRDevices()
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
@@ -54,36 +52,42 @@ namespace VRCX
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||
public double GetUptime()
|
||||
public override double GetUptime()
|
||||
{
|
||||
return SystemMonitor.Instance.UpTime;
|
||||
return SystemMonitorCef.Instance.UpTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
/// <returns>The current language of the operating system.</returns>
|
||||
public string CurrentCulture()
|
||||
public override string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public string CustomVrScriptPath()
|
||||
|
||||
public override string CustomVrScript()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsRunningUnderWine()
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException("GetExecuteVrFeedFunctionQueue is not implemented in AppApiVrCef.");
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException("GetExecuteVrOverlayFunctionQueue is not implemented in AppApiVrCef.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,22 +96,22 @@ namespace VRCX
|
||||
});
|
||||
}
|
||||
|
||||
public string CustomCssPath()
|
||||
public string CustomCss()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string CustomScriptPath()
|
||||
public string CustomScript()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string CurrentCulture()
|
||||
|
||||
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public abstract partial class AppApiVr
|
||||
{
|
||||
public static AppApiVr Instance;
|
||||
public abstract void Init();
|
||||
public abstract void VrInit();
|
||||
public abstract void ToggleSystemMonitor(bool enabled);
|
||||
public abstract float CpuUsage();
|
||||
public abstract string[][] GetVRDevices();
|
||||
public abstract double GetUptime();
|
||||
public abstract string CurrentCulture();
|
||||
public abstract string CustomVrScript();
|
||||
public abstract bool IsRunningUnderWine();
|
||||
public abstract List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||
public abstract List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||
}
|
||||
@@ -9,27 +9,30 @@ namespace VRCX
|
||||
public partial class AppApiElectron : AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
|
||||
public override void ShowDevTools()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
|
||||
}
|
||||
|
||||
|
||||
public override void RefreshVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void RestartVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void SetZoom(double zoomLevel)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override async Task<double> GetZoom()
|
||||
{
|
||||
return 1;
|
||||
@@ -47,23 +50,25 @@ namespace VRCX
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public override void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
|
||||
}
|
||||
|
||||
|
||||
public override void FocusWindow()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void ChangeTheme(int value)
|
||||
{
|
||||
}
|
||||
@@ -71,7 +76,7 @@ namespace VRCX
|
||||
public override void DoFunny()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override string GetClipboard()
|
||||
{
|
||||
var process = new Process
|
||||
@@ -102,7 +107,7 @@ namespace VRCX
|
||||
public override void SetStartup(bool enabled)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void CopyImageToClipboard(string path)
|
||||
{
|
||||
if (!File.Exists(path) ||
|
||||
@@ -113,7 +118,7 @@ namespace VRCX
|
||||
!path.EndsWith(".bmp") &&
|
||||
!path.EndsWith(".webp")))
|
||||
return;
|
||||
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -124,7 +129,7 @@ namespace VRCX
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
try
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
@@ -138,16 +143,16 @@ namespace VRCX
|
||||
public override void FlashWindow()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void SetUserAgent()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AppApiVrElectron : AppApiVr
|
||||
{
|
||||
static AppApiVrElectron()
|
||||
{
|
||||
Instance = new AppApiVrElectron();
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public override void VrInit()
|
||||
{
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
var list = new List<KeyValuePair<string, string>>();
|
||||
while (Program.VRCXVRInstance.GetExecuteVrFeedFunctionQueue().TryDequeue(out var item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
var list = new List<KeyValuePair<string, string>>();
|
||||
while (Program.VRCXVRInstance.GetExecuteVrOverlayFunctionQueue().TryDequeue(out var item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public override void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitorElectron.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
/// <returns>The current CPU usage as a percentage.</returns>
|
||||
public override float CpuUsage()
|
||||
{
|
||||
return SystemMonitorElectron.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of arrays containing information about the connected VR devices.
|
||||
/// Each sub-array contains the type of device and its current state
|
||||
/// </summary>
|
||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||
public override string[][] GetVRDevices()
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||
public override double GetUptime()
|
||||
{
|
||||
return SystemMonitorElectron.Instance.UpTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
/// <returns>The current language of the operating system.</returns>
|
||||
public override string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public override string CustomVrScript()
|
||||
{
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||
if (File.Exists(filePath))
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,7 @@ namespace VRCX
|
||||
),
|
||||
IsLocal = true
|
||||
});
|
||||
|
||||
// cefSettings.CefCommandLineArgs.Add("allow-universal-access-from-files");
|
||||
|
||||
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
|
||||
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
|
||||
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace VRCX
|
||||
repository.Register("Discord", Discord.Instance);
|
||||
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
|
||||
}
|
||||
|
||||
|
||||
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
|
||||
{
|
||||
repository.NameConverter = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class IPCClient
|
||||
public class IPCClient
|
||||
{
|
||||
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
|
||||
private readonly NamedPipeServerStream _ipcServer;
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class IPCServer
|
||||
public class IPCServer
|
||||
{
|
||||
public static readonly IPCServer Instance;
|
||||
public static readonly List<IPCClient> Clients = new List<IPCClient>();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,9 @@ using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SystemMonitor
|
||||
public class SystemMonitorCef
|
||||
{
|
||||
public static readonly SystemMonitor Instance;
|
||||
public static readonly SystemMonitorCef Instance;
|
||||
public float CpuUsage;
|
||||
public double UpTime;
|
||||
private bool _enabled;
|
||||
@@ -22,9 +22,9 @@ namespace VRCX
|
||||
private Thread _thread;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static SystemMonitor()
|
||||
static SystemMonitorCef()
|
||||
{
|
||||
Instance = new SystemMonitor();
|
||||
Instance = new SystemMonitorCef();
|
||||
}
|
||||
|
||||
public void Start(bool enabled)
|
||||
@@ -5,6 +5,7 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -24,7 +25,7 @@ using Device4 = SharpDX.Direct3D11.Device4;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class VRCXVR : VRCXVRInterface
|
||||
public class VRCXVRCef : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
@@ -57,12 +58,12 @@ namespace VRCX
|
||||
private bool _wristOverlayWasActive;
|
||||
|
||||
|
||||
static VRCXVR()
|
||||
static VRCXVRCef()
|
||||
{
|
||||
Instance = new VRCXVR();
|
||||
Instance = new VRCXVRCef();
|
||||
}
|
||||
|
||||
public VRCXVR()
|
||||
public VRCXVRCef()
|
||||
{
|
||||
_deviceListLock = new ReaderWriterLockSlim();
|
||||
_deviceList = new List<string[]>();
|
||||
@@ -90,7 +91,7 @@ namespace VRCX
|
||||
public override void Restart()
|
||||
{
|
||||
Exit();
|
||||
Instance = new VRCXVR();
|
||||
Instance = new VRCXVRCef();
|
||||
Instance.Init();
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||
}
|
||||
@@ -834,6 +835,11 @@ namespace VRCX
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
if (_wristOverlay == null) return;
|
||||
@@ -842,6 +848,11 @@ namespace VRCX
|
||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
if (_hmdOverlay == null) return;
|
||||
@@ -5,6 +5,7 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -761,6 +762,11 @@ namespace VRCX
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
@@ -769,6 +775,11 @@ namespace VRCX
|
||||
Restart();
|
||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GLContextWayland
|
||||
{
|
||||
// Wayland
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern IntPtr wl_display_connect(string name);
|
||||
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern void wl_display_disconnect(IntPtr display);
|
||||
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern int wl_display_roundtrip(IntPtr display);
|
||||
|
||||
// EGL
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglGetDisplay(IntPtr display_id);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglInitialize(IntPtr display, out int major, out int minor);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglTerminate(IntPtr display);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglChooseConfig(IntPtr display, int[] attrib_list, IntPtr[] configs, int config_size, out int num_config);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglCreatePbufferSurface(IntPtr display, IntPtr config, int[] attrib_list);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglCreateContext(IntPtr display, IntPtr config, IntPtr share_context, int[] attrib_list);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglDestroySurface(IntPtr display, IntPtr surface);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglDestroyContext(IntPtr display, IntPtr context);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern int eglGetError();
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglQueryString(IntPtr display, int name);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glGetString(int name);
|
||||
|
||||
// EGL constants
|
||||
private const int EGL_SURFACE_TYPE = 0x3033;
|
||||
private const int EGL_PBUFFER_BIT = 0x0001;
|
||||
private const int EGL_RENDERABLE_TYPE = 0x3040;
|
||||
private const int EGL_OPENGL_BIT = 0x0008;
|
||||
private const int EGL_OPENGL_ES2_BIT = 0x0004;
|
||||
private const int EGL_RED_SIZE = 0x3024;
|
||||
private const int EGL_GREEN_SIZE = 0x3023;
|
||||
private const int EGL_BLUE_SIZE = 0x3022;
|
||||
private const int EGL_ALPHA_SIZE = 0x3021;
|
||||
private const int EGL_DEPTH_SIZE = 0x3025;
|
||||
private const int EGL_NONE = 0x3038;
|
||||
private const int EGL_WIDTH = 0x3057;
|
||||
private const int EGL_HEIGHT = 0x3056;
|
||||
private const int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
private const int EGL_VENDOR = 0x3053;
|
||||
private const int EGL_VERSION = 0x3054;
|
||||
private const int EGL_EXTENSIONS = 0x3055;
|
||||
|
||||
// OpenGL constants
|
||||
private const int GL_VENDOR = 0x1F00;
|
||||
private const int GL_RENDERER = 0x1F01;
|
||||
private const int GL_VERSION = 0x1F02;
|
||||
|
||||
private static IntPtr waylandDisplay = IntPtr.Zero;
|
||||
private static IntPtr eglDisplay = IntPtr.Zero;
|
||||
private static IntPtr eglContext = IntPtr.Zero;
|
||||
private static IntPtr eglSurface = IntPtr.Zero;
|
||||
|
||||
public static bool Initialise()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Connect to Wayland display
|
||||
waylandDisplay = wl_display_connect(null);
|
||||
if (waylandDisplay == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to connect to Wayland display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform initial roundtrip to ensure connection is established
|
||||
wl_display_roundtrip(waylandDisplay);
|
||||
|
||||
// Get EGL display
|
||||
eglDisplay = eglGetDisplay(waylandDisplay);
|
||||
if (eglDisplay == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to get EGL display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize EGL
|
||||
if (!eglInitialize(eglDisplay, out int major, out int minor))
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize EGL. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"EGL version: {major}.{minor}");
|
||||
|
||||
// Choose EGL config for offscreen rendering
|
||||
int[] configAttribs = {
|
||||
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
IntPtr[] configs = new IntPtr[10];
|
||||
if (!eglChooseConfig(eglDisplay, configAttribs, configs, 10, out int numConfigs) || numConfigs == 0)
|
||||
{
|
||||
Console.WriteLine($"Failed to choose EGL config. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Found {numConfigs} EGL configs");
|
||||
IntPtr config = configs[0];
|
||||
|
||||
// Create a minimal pbuffer surface (offscreen)
|
||||
int[] pbufferAttribs = {
|
||||
EGL_WIDTH, 1,
|
||||
EGL_HEIGHT, 1,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
eglSurface = eglCreatePbufferSurface(eglDisplay, config, pbufferAttribs);
|
||||
if (eglSurface == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine($"Failed to create EGL pbuffer surface. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create OpenGL context
|
||||
int[] contextAttribs = {
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
eglContext = eglCreateContext(eglDisplay, config, IntPtr.Zero, contextAttribs);
|
||||
if (eglContext == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine($"Failed to create EGL context. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make context current
|
||||
if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext))
|
||||
{
|
||||
Console.WriteLine($"Failed to make EGL context current. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine("OpenGL context created successfully (Wayland/EGL offscreen)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
if (eglDisplay != IntPtr.Zero)
|
||||
{
|
||||
if (eglContext != IntPtr.Zero)
|
||||
{
|
||||
eglMakeCurrent(eglDisplay, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
eglDestroyContext(eglDisplay, eglContext);
|
||||
}
|
||||
if (eglSurface != IntPtr.Zero)
|
||||
eglDestroySurface(eglDisplay, eglSurface);
|
||||
|
||||
eglTerminate(eglDisplay);
|
||||
}
|
||||
|
||||
if (waylandDisplay != IntPtr.Zero)
|
||||
{
|
||||
wl_display_disconnect(waylandDisplay);
|
||||
}
|
||||
|
||||
waylandDisplay = IntPtr.Zero;
|
||||
eglDisplay = IntPtr.Zero;
|
||||
eglContext = IntPtr.Zero;
|
||||
eglSurface = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GLContextX11
|
||||
{
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XOpenDisplay(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XDefaultScreen(IntPtr display);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glXChooseVisual(IntPtr display, int screen, int[] attribList);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint border_width, ulong border, ulong background);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XMapWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XUnmapWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glXCreateContext(IntPtr display, IntPtr visual, IntPtr shareList, bool direct);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern bool glXMakeCurrent(IntPtr display, IntPtr drawable, IntPtr context);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern void glXDestroyContext(IntPtr display, IntPtr context);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XDestroyWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XCloseDisplay(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XFlush(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XRootWindow(IntPtr display, int screen);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XStoreName(IntPtr display, IntPtr window, string window_name);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XChangeWindowAttributes(IntPtr display, IntPtr window, ulong valuemask, ref XSetWindowAttributes attributes);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XMoveWindow(IntPtr display, IntPtr window, int x, int y);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct XSetWindowAttributes
|
||||
{
|
||||
public IntPtr background_pixmap;
|
||||
public ulong background_pixel;
|
||||
public IntPtr border_pixmap;
|
||||
public ulong border_pixel;
|
||||
public int bit_gravity;
|
||||
public int win_gravity;
|
||||
public int backing_store;
|
||||
public ulong backing_planes;
|
||||
public ulong backing_pixel;
|
||||
public bool save_under;
|
||||
public long event_mask;
|
||||
public long do_not_propagate_mask;
|
||||
public bool override_redirect;
|
||||
public IntPtr colormap;
|
||||
public IntPtr cursor;
|
||||
}
|
||||
|
||||
private const ulong CWOverrideRedirect = 0x00000200;
|
||||
private const ulong CWBackPixel = 0x00000002;
|
||||
private const ulong CWBorderPixel = 0x00000008;
|
||||
|
||||
private static IntPtr display = IntPtr.Zero;
|
||||
private static IntPtr window = IntPtr.Zero;
|
||||
private static IntPtr context = IntPtr.Zero;
|
||||
private static bool windowVisible = false;
|
||||
|
||||
public static bool Initialise()
|
||||
{
|
||||
try
|
||||
{
|
||||
display = XOpenDisplay(IntPtr.Zero);
|
||||
if (display == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("No X11 display available");
|
||||
return false;
|
||||
}
|
||||
|
||||
int screen = XDefaultScreen(display);
|
||||
|
||||
// Request a visual that supports OpenGL
|
||||
int[] attribs = { 4, 1, 5, 1, 12, 1, 0 }; // GLX_RGBA, GLX_DOUBLEBUFFER, GLX_DEPTH_SIZE, 1, None
|
||||
IntPtr visual = glXChooseVisual(display, screen, attribs);
|
||||
if (visual == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to find a GLX visual");
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr root = XRootWindow(display, screen);
|
||||
|
||||
// Create window off-screen and very small to minimize visibility
|
||||
window = XCreateSimpleWindow(display, root, -10000, -10000, 1, 1, 0, 0, 0);
|
||||
if (window == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to create X11 window");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set window attributes to make it invisible
|
||||
XSetWindowAttributes attrs = new XSetWindowAttributes();
|
||||
attrs.override_redirect = true; // Bypass window manager
|
||||
attrs.background_pixel = 0;
|
||||
attrs.border_pixel = 0;
|
||||
|
||||
XChangeWindowAttributes(display, window, CWOverrideRedirect | CWBackPixel | CWBorderPixel, ref attrs);
|
||||
|
||||
// Give it a name that indicates it's a hidden OpenGL context
|
||||
XStoreName(display, window, "Hidden OpenGL Context");
|
||||
|
||||
context = glXCreateContext(display, visual, IntPtr.Zero, true);
|
||||
if (context == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to create GLX context");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!glXMakeCurrent(display, window, context))
|
||||
{
|
||||
Console.WriteLine("Failed to make GLX context current");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't map the window at all - keep it completely hidden
|
||||
// The GLX context will work without the window being visible
|
||||
XFlush(display);
|
||||
|
||||
Console.WriteLine("OpenGL context created successfully (X11/GLX)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
if (display != IntPtr.Zero)
|
||||
{
|
||||
if (context != IntPtr.Zero)
|
||||
glXDestroyContext(display, context);
|
||||
if (window != IntPtr.Zero)
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
display = IntPtr.Zero;
|
||||
window = IntPtr.Zero;
|
||||
context = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Valve.VR;
|
||||
|
||||
public class GLTextureWriter : IDisposable
|
||||
{
|
||||
private uint _textureId;
|
||||
private readonly int _width, _height;
|
||||
private byte[] _buffer;
|
||||
|
||||
// OpenGL P/Invoke declarations
|
||||
[DllImport("libGL.so.1", EntryPoint = "glGenTextures")]
|
||||
private static extern void glGenTextures(int n, out uint textures);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glBindTexture")]
|
||||
private static extern void glBindTexture(uint target, uint texture);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexParameteri")]
|
||||
private static extern void glTexParameteri(uint target, uint pname, int param);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexImage2D")]
|
||||
private static extern void glTexImage2D(uint target, int level, int internalformat, int width, int height, int border, uint format, uint type, IntPtr pixels);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexSubImage2D")]
|
||||
private static extern void glTexSubImage2D(uint target, int level, int xoffset, int yoffset, int width, int height, uint format, uint type, byte[] pixels);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glDeleteTextures")]
|
||||
private static extern void glDeleteTextures(int n, ref uint textures);
|
||||
|
||||
// OpenGL constants
|
||||
private const uint GL_TEXTURE_2D = 0x0DE1;
|
||||
private const uint GL_TEXTURE_MIN_FILTER = 0x2801;
|
||||
private const uint GL_TEXTURE_MAG_FILTER = 0x2800;
|
||||
private const uint GL_TEXTURE_WRAP_S = 0x2802;
|
||||
private const uint GL_TEXTURE_WRAP_T = 0x2803;
|
||||
private const uint GL_LINEAR = 0x2601;
|
||||
private const uint GL_CLAMP_TO_EDGE = 0x812F;
|
||||
private const uint GL_RGBA = 0x1908;
|
||||
private const uint GL_BGRA = 0x80E1;
|
||||
private const uint GL_UNSIGNED_BYTE = 0x1401;
|
||||
|
||||
public GLTextureWriter(int width, int height)
|
||||
{
|
||||
_width = width;
|
||||
_height = height;
|
||||
_buffer = new byte[width * height * 4]; // 4 bytes per pixel (RGBA)
|
||||
InitTexture();
|
||||
}
|
||||
|
||||
private void InitTexture()
|
||||
{
|
||||
glGenTextures(1, out _textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (int)GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (int)GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (int)GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (int)GL_CLAMP_TO_EDGE);
|
||||
|
||||
// Allocate texture storage with no initial data.
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, (int)GL_RGBA, _width, _height, 0,
|
||||
GL_BGRA, GL_UNSIGNED_BYTE, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies image data into the internal buffer.
|
||||
/// </summary>
|
||||
public void WriteImageToBuffer(byte[] data)
|
||||
{
|
||||
if (data.Length != _buffer.Length)
|
||||
throw new ArgumentException("Data size does not match texture size.");
|
||||
|
||||
System.Buffer.BlockCopy(data, 0, _buffer, 0, _buffer.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the OpenGL texture with the current buffer.
|
||||
/// </summary>
|
||||
public void UpdateTexture()
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _width, _height,
|
||||
GL_BGRA, GL_UNSIGNED_BYTE, _buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Texture_t structure for use with OpenVR.
|
||||
/// </summary>
|
||||
public Texture_t AsTextureT()
|
||||
{
|
||||
return new Texture_t
|
||||
{
|
||||
handle = (IntPtr)_textureId,
|
||||
eType = ETextureType.OpenGL,
|
||||
eColorSpace = EColorSpace.Auto
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_textureId != 0)
|
||||
{
|
||||
glDeleteTextures(1, ref _textureId);
|
||||
_textureId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SystemMonitorElectron
|
||||
{
|
||||
public static readonly SystemMonitorElectron Instance;
|
||||
public float CpuUsage;
|
||||
public double UpTime;
|
||||
private bool _enabled;
|
||||
private long _lastTotalTime;
|
||||
private long _lastIdleTime;
|
||||
private bool _firstReading = true; // Add this flag
|
||||
private Thread _thread;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static SystemMonitorElectron()
|
||||
{
|
||||
Instance = new SystemMonitorElectron();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public void Start(bool enabled)
|
||||
{
|
||||
if (enabled == _enabled)
|
||||
return;
|
||||
|
||||
_enabled = enabled;
|
||||
if (enabled)
|
||||
StartThread();
|
||||
else
|
||||
Exit();
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
{
|
||||
CpuUsage = 0;
|
||||
UpTime = 0;
|
||||
_firstReading = true; // Reset flag
|
||||
try
|
||||
{
|
||||
if (_thread != null)
|
||||
{
|
||||
_thread.Interrupt();
|
||||
_thread.Join();
|
||||
_thread = null;
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void StartThread()
|
||||
{
|
||||
Exit();
|
||||
|
||||
try
|
||||
{
|
||||
ReadCpuInfo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Failed to initialize CPU monitoring: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
_thread = new Thread(ThreadProc)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
private void ThreadProc()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_enabled)
|
||||
{
|
||||
UpdateMetrics();
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"SystemMonitor thread exception: {ex}");
|
||||
}
|
||||
|
||||
Exit();
|
||||
}
|
||||
|
||||
private void UpdateMetrics()
|
||||
{
|
||||
try
|
||||
{
|
||||
CpuUsage = GetCpuUsage();
|
||||
UpTime = GetUptime();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error updating metrics: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private float GetCpuUsage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cpuInfo = ReadCpuInfo();
|
||||
if (cpuInfo == null) return 0;
|
||||
|
||||
var totalTime = cpuInfo.Value.TotalTime;
|
||||
var idleTime = cpuInfo.Value.IdleTime;
|
||||
|
||||
// Skip the first reading to establish baseline
|
||||
if (_firstReading)
|
||||
{
|
||||
_lastTotalTime = totalTime;
|
||||
_lastIdleTime = idleTime;
|
||||
_firstReading = false;
|
||||
return 0; // Return 0 for first reading
|
||||
}
|
||||
|
||||
var totalTimeDiff = totalTime - _lastTotalTime;
|
||||
var idleTimeDiff = idleTime - _lastIdleTime;
|
||||
|
||||
if (totalTimeDiff > 0)
|
||||
{
|
||||
var cpuUsage = 100.0f * (1.0f - (float)idleTimeDiff / totalTimeDiff);
|
||||
_lastTotalTime = totalTime;
|
||||
_lastIdleTime = idleTime;
|
||||
|
||||
// Clamp to reasonable range
|
||||
return Math.Max(0, Math.Min(100, cpuUsage));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading CPU usage: {ex}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private double GetUptime()
|
||||
{
|
||||
try
|
||||
{
|
||||
var uptimeContent = File.ReadAllText("/proc/uptime");
|
||||
var parts = uptimeContent.Split(' ');
|
||||
if (parts.Length > 0 && double.TryParse(parts[0], out var uptimeSeconds))
|
||||
{
|
||||
return TimeSpan.FromSeconds(uptimeSeconds).TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading uptime: {ex}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private (long TotalTime, long IdleTime)? ReadCpuInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var statContent = File.ReadAllText("/proc/stat");
|
||||
var lines = statContent.Split('\n');
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("cpu "))
|
||||
{
|
||||
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 5)
|
||||
{
|
||||
// CPU time values: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice
|
||||
var user = long.Parse(parts[1]);
|
||||
var nice = long.Parse(parts[2]);
|
||||
var system = long.Parse(parts[3]);
|
||||
var idle = long.Parse(parts[4]);
|
||||
var iowait = parts.Length > 5 ? long.Parse(parts[5]) : 0;
|
||||
var irq = parts.Length > 6 ? long.Parse(parts[6]) : 0;
|
||||
var softirq = parts.Length > 7 ? long.Parse(parts[7]) : 0;
|
||||
var steal = parts.Length > 8 ? long.Parse(parts[8]) : 0;
|
||||
|
||||
var totalTime = user + nice + system + idle + iowait + irq + softirq + steal;
|
||||
return (totalTime, idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading /proc/stat: {ex}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
@@ -0,0 +1,921 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using Valve.VR;
|
||||
using System.Numerics;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class VRCXVRElectron : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly float[] _rotation = { 0f, 0f, 0f };
|
||||
private static readonly float[] _translation = { 0f, 0f, 0f };
|
||||
private static readonly float[] _translationLeft = { -7f / 100f, -5f / 100f, 6f / 100f };
|
||||
private static readonly float[] _translationRight = { 7f / 100f, -5f / 100f, 6f / 100f };
|
||||
private static readonly float[] _rotationLeft = { 90f * (float)(Math.PI / 180f), 90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
|
||||
private static readonly float[] _rotationRight = { -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
|
||||
private readonly List<string[]> _deviceList;
|
||||
private readonly ReaderWriterLockSlim _deviceListLock;
|
||||
private bool _active;
|
||||
private bool _menuButton;
|
||||
private int _overlayHand;
|
||||
private GLTextureWriter _wristOverlayTextureWriter;
|
||||
private GLTextureWriter _hmdOverlayTextureWriter;
|
||||
private Thread _thread;
|
||||
private DateTime _nextOverlayUpdate;
|
||||
|
||||
private ulong _hmdOverlayHandle;
|
||||
private bool _hmdOverlayActive;
|
||||
private bool _hmdOverlayWasActive;
|
||||
|
||||
private ulong _wristOverlayHandle;
|
||||
private bool _wristOverlayActive;
|
||||
private bool _wristOverlayWasActive;
|
||||
|
||||
private const string WRIST_OVERLAY_SHM_PATH = "/dev/shm/vrcx_wrist_overlay";
|
||||
private const string HMD_OVERLAY_SHM_PATH = "/dev/shm/vrcx_hmd_overlay";
|
||||
private const int WRIST_FRAME_WIDTH = 512;
|
||||
private const int WRIST_FRAME_HEIGHT = 512;
|
||||
private const int WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4; // RGBA
|
||||
private byte[] wristFrameBuffer = new byte[WRIST_FRAME_SIZE];
|
||||
private const int HMD_FRAME_WIDTH = 1024;
|
||||
private const int HMD_FRAME_HEIGHT = 1024;
|
||||
private const int HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4; // RGBA
|
||||
private byte[] hmdFrameBuffer = new byte[HMD_FRAME_SIZE];
|
||||
private MemoryMappedFile _wristOverlayMMF;
|
||||
private MemoryMappedViewAccessor _wristOverlayAccessor;
|
||||
private MemoryMappedFile _hmdOverlayMMF;
|
||||
private MemoryMappedViewAccessor _hmdOverlayAccessor;
|
||||
private readonly ConcurrentQueue<KeyValuePair<string, string>> _wristFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||
private readonly ConcurrentQueue<KeyValuePair<string, string>> _hmdFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||
|
||||
static VRCXVRElectron()
|
||||
{
|
||||
Instance = new VRCXVRElectron();
|
||||
}
|
||||
|
||||
public VRCXVRElectron()
|
||||
{
|
||||
_deviceListLock = new ReaderWriterLockSlim();
|
||||
_deviceList = new List<string[]>();
|
||||
_thread = new Thread(ThreadLoop)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE
|
||||
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
|
||||
public override void Init()
|
||||
{
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
var thread = _thread;
|
||||
_thread = null;
|
||||
thread?.Interrupt();
|
||||
thread?.Join();
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
|
||||
public override void Restart()
|
||||
{
|
||||
Exit();
|
||||
Instance = new VRCXVRElectron();
|
||||
Instance.Init();
|
||||
//MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||
}
|
||||
|
||||
private void SetupTextures()
|
||||
{
|
||||
bool contextInitialised = false;
|
||||
|
||||
// Check if we're running on Wayland
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ||
|
||||
Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower() == "wayland")
|
||||
{
|
||||
contextInitialised = GLContextWayland.Initialise();
|
||||
}
|
||||
else
|
||||
{
|
||||
contextInitialised = GLContextX11.Initialise();
|
||||
}
|
||||
|
||||
if (!contextInitialised)
|
||||
{
|
||||
contextInitialised = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ?
|
||||
GLContextX11.Initialise() : GLContextWayland.Initialise();
|
||||
}
|
||||
|
||||
if (!contextInitialised)
|
||||
{
|
||||
throw new Exception("Failed to initialise OpenGL context");
|
||||
}
|
||||
|
||||
_wristOverlayTextureWriter = new GLTextureWriter(512, 512);
|
||||
_wristOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
_hmdOverlayTextureWriter = new GLTextureWriter(1024, 1024);
|
||||
_hmdOverlayTextureWriter.UpdateTexture();
|
||||
}
|
||||
|
||||
private void UpgradeDevice()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public byte[] GetLatestWristOverlayFrame()
|
||||
{
|
||||
if (_wristOverlayAccessor == null) return null;
|
||||
byte ready = _wristOverlayAccessor.ReadByte(0);
|
||||
if (ready == 1)
|
||||
{
|
||||
_wristOverlayAccessor.ReadArray(1, wristFrameBuffer, 0, WRIST_FRAME_SIZE);
|
||||
_wristOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||
return wristFrameBuffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] GetLatestHmdOverlayFrame()
|
||||
{
|
||||
if (_hmdOverlayAccessor == null) return null;
|
||||
byte ready = _hmdOverlayAccessor.ReadByte(0);
|
||||
if (ready == 1)
|
||||
{
|
||||
_hmdOverlayAccessor.ReadArray(1, hmdFrameBuffer, 0, HMD_FRAME_SIZE);
|
||||
_hmdOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||
return hmdFrameBuffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void FlipImageVertically(byte[] imageData, int width, int height)
|
||||
{
|
||||
int stride = width * 4; // 4 bytes per pixel (RGBA)
|
||||
byte[] tempRow = new byte[stride];
|
||||
|
||||
for (int y = 0; y < height / 2; y++)
|
||||
{
|
||||
int topIndex = y * stride;
|
||||
int bottomIndex = (height - 1 - y) * stride;
|
||||
|
||||
// Swap rows
|
||||
Buffer.BlockCopy(imageData, topIndex, tempRow, 0, stride);
|
||||
Buffer.BlockCopy(imageData, bottomIndex, imageData, topIndex, stride);
|
||||
Buffer.BlockCopy(tempRow, 0, imageData, bottomIndex, stride);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThreadLoop()
|
||||
{
|
||||
var active = false;
|
||||
var e = new VREvent_t();
|
||||
var nextInit = DateTime.MinValue;
|
||||
var nextDeviceUpdate = DateTime.MinValue;
|
||||
_nextOverlayUpdate = DateTime.MinValue;
|
||||
var overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||
var overlayVisible1 = false;
|
||||
var overlayVisible2 = false;
|
||||
var dashboardHandle = 0UL;
|
||||
|
||||
while (_thread != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_active)
|
||||
Thread.Sleep(1);
|
||||
else
|
||||
Thread.Sleep(32);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
|
||||
if (_active)
|
||||
{
|
||||
var system = OpenVR.System;
|
||||
if (system == null)
|
||||
{
|
||||
if (DateTime.UtcNow.CompareTo(nextInit) <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var _err = EVRInitError.None;
|
||||
system = OpenVR.Init(ref _err, EVRApplicationType.VRApplication_Background);
|
||||
nextInit = DateTime.UtcNow.AddSeconds(5);
|
||||
if (system == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
active = true;
|
||||
SetupTextures();
|
||||
}
|
||||
|
||||
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
|
||||
{
|
||||
var type = (EVREventType)e.eventType;
|
||||
if (type == EVREventType.VREvent_Quit)
|
||||
{
|
||||
active = false;
|
||||
IsHmdAfk = false;
|
||||
OpenVR.Shutdown();
|
||||
nextInit = DateTime.UtcNow.AddSeconds(10);
|
||||
system = null;
|
||||
|
||||
_wristOverlayHandle = 0;
|
||||
_hmdOverlayHandle = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (system != null)
|
||||
{
|
||||
if (DateTime.UtcNow.CompareTo(nextDeviceUpdate) >= 0)
|
||||
{
|
||||
overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||
UpdateDevices(system, ref overlayIndex);
|
||||
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||
{
|
||||
_nextOverlayUpdate = DateTime.UtcNow.AddSeconds(10);
|
||||
}
|
||||
|
||||
nextDeviceUpdate = DateTime.UtcNow.AddSeconds(0.1);
|
||||
}
|
||||
|
||||
var overlay = OpenVR.Overlay;
|
||||
if (overlay != null)
|
||||
{
|
||||
var dashboardVisible = overlay.IsDashboardVisible();
|
||||
//var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
|
||||
//if (err != EVROverlayError.None &&
|
||||
// dashboardHandle != 0)
|
||||
//{
|
||||
// overlay.DestroyOverlay(dashboardHandle);
|
||||
// dashboardHandle = 0;
|
||||
// logger.Error(err);
|
||||
//}
|
||||
|
||||
if (_wristOverlayActive)
|
||||
{
|
||||
var err = ProcessOverlay1(overlay, ref _wristOverlayHandle, ref overlayVisible1,
|
||||
dashboardVisible, overlayIndex);
|
||||
if (err != EVROverlayError.None &&
|
||||
_wristOverlayHandle != 0)
|
||||
{
|
||||
overlay.DestroyOverlay(_wristOverlayHandle);
|
||||
_wristOverlayHandle = 0;
|
||||
logger.Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (_hmdOverlayActive)
|
||||
{
|
||||
var err = ProcessOverlay2(overlay, ref _hmdOverlayHandle, ref overlayVisible2,
|
||||
dashboardVisible);
|
||||
if (err != EVROverlayError.None &&
|
||||
_hmdOverlayHandle != 0)
|
||||
{
|
||||
overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||
_hmdOverlayHandle = 0;
|
||||
logger.Error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (active)
|
||||
{
|
||||
active = false;
|
||||
IsHmdAfk = false;
|
||||
OpenVR.Shutdown();
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
|
||||
public override void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
_active = active;
|
||||
_hmdOverlayActive = hmdOverlay;
|
||||
_wristOverlayActive = wristOverlay;
|
||||
_menuButton = menuButton;
|
||||
_overlayHand = overlayHand;
|
||||
|
||||
if (_hmdOverlayActive != _hmdOverlayWasActive && _hmdOverlayHandle != 0)
|
||||
{
|
||||
OpenVR.Overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||
_hmdOverlayHandle = 0;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
}
|
||||
|
||||
_hmdOverlayWasActive = _hmdOverlayActive;
|
||||
|
||||
if (_wristOverlayActive != _wristOverlayWasActive && _wristOverlayHandle != 0)
|
||||
{
|
||||
OpenVR.Overlay.DestroyOverlay(_wristOverlayHandle);
|
||||
_wristOverlayHandle = 0;
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
}
|
||||
|
||||
_wristOverlayWasActive = _wristOverlayActive;
|
||||
|
||||
if (!_active)
|
||||
{
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
//_wristOverlay.Reload();
|
||||
//_hmdOverlay.Reload();
|
||||
}
|
||||
|
||||
public override string[][] GetDevices()
|
||||
{
|
||||
_deviceListLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _deviceList.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDevices(CVRSystem system, ref uint overlayIndex)
|
||||
{
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
var state = new VRControllerState_t();
|
||||
var poses = new TrackedDevicePose_t[OpenVR.k_unMaxTrackedDeviceCount];
|
||||
system.GetDeviceToAbsoluteTrackingPose(ETrackingUniverseOrigin.TrackingUniverseStanding, 0, poses);
|
||||
for (var i = 0u; i < OpenVR.k_unMaxTrackedDeviceCount; ++i)
|
||||
{
|
||||
var devClass = system.GetTrackedDeviceClass(i);
|
||||
switch (devClass)
|
||||
{
|
||||
case ETrackedDeviceClass.HMD:
|
||||
var success = system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state));
|
||||
if (!success)
|
||||
break; // this fails while SteamVR overlay is open
|
||||
|
||||
var prox = state.ulButtonPressed & (1UL << ((int)EVRButtonId.k_EButton_ProximitySensor));
|
||||
var isHmdAfk = prox == 0;
|
||||
if (isHmdAfk != IsHmdAfk)
|
||||
{
|
||||
IsHmdAfk = isHmdAfk;
|
||||
Program.AppApiInstance.CheckGameRunning();
|
||||
}
|
||||
|
||||
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
|
||||
var headsetBatteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref headsetErr);
|
||||
if (headsetErr != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
// Headset has no battery, skip displaying it
|
||||
break;
|
||||
}
|
||||
|
||||
var headset = new[]
|
||||
{
|
||||
"headset",
|
||||
system.IsTrackedDeviceConnected(i)
|
||||
? "connected"
|
||||
: "disconnected",
|
||||
// Currently neither VD or SteamLink report charging state
|
||||
"discharging",
|
||||
(headsetBatteryPercentage * 100).ToString(),
|
||||
poses[i].eTrackingResult.ToString()
|
||||
};
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Add(headset);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
break;
|
||||
case ETrackedDeviceClass.Controller:
|
||||
case ETrackedDeviceClass.GenericTracker:
|
||||
case ETrackedDeviceClass.TrackingReference:
|
||||
{
|
||||
var err = ETrackedPropertyError.TrackedProp_Success;
|
||||
var batteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref err);
|
||||
if (err != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
batteryPercentage = 1f;
|
||||
}
|
||||
|
||||
err = ETrackedPropertyError.TrackedProp_Success;
|
||||
var isCharging = system.GetBoolTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceIsCharging_Bool, ref err);
|
||||
if (err != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
isCharging = false;
|
||||
}
|
||||
|
||||
sb.Clear();
|
||||
system.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_TrackingSystemName_String, sb, (uint)sb.Capacity, ref err);
|
||||
var isOculus = sb.ToString().IndexOf("oculus", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
// Oculus : B/Y, Bit 1, Mask 2
|
||||
// Oculus : A/X, Bit 7, Mask 128
|
||||
// Vive : Menu, Bit 1, Mask 2,
|
||||
// Vive : Grip, Bit 2, Mask 4
|
||||
var role = system.GetControllerRoleForTrackedDeviceIndex(i);
|
||||
if (role == ETrackedControllerRole.LeftHand || role == ETrackedControllerRole.RightHand)
|
||||
{
|
||||
if (_overlayHand == 0 ||
|
||||
(_overlayHand == 1 && role == ETrackedControllerRole.LeftHand) ||
|
||||
(_overlayHand == 2 && role == ETrackedControllerRole.RightHand))
|
||||
{
|
||||
if (system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state)) &&
|
||||
(state.ulButtonPressed & (_menuButton ? 2u : isOculus ? 128u : 4u)) != 0)
|
||||
{
|
||||
_nextOverlayUpdate = DateTime.MinValue;
|
||||
if (role == ETrackedControllerRole.LeftHand)
|
||||
{
|
||||
Array.Copy(_translationLeft, _translation, 3);
|
||||
Array.Copy(_rotationLeft, _rotation, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_translationRight, _translation, 3);
|
||||
Array.Copy(_rotationRight, _rotation, 3);
|
||||
}
|
||||
|
||||
overlayIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var type = string.Empty;
|
||||
if (devClass == ETrackedDeviceClass.Controller)
|
||||
{
|
||||
if (role == ETrackedControllerRole.LeftHand)
|
||||
{
|
||||
type = "leftController";
|
||||
}
|
||||
else if (role == ETrackedControllerRole.RightHand)
|
||||
{
|
||||
type = "rightController";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "controller";
|
||||
}
|
||||
}
|
||||
else if (devClass == ETrackedDeviceClass.GenericTracker)
|
||||
{
|
||||
type = "tracker";
|
||||
}
|
||||
else if (devClass == ETrackedDeviceClass.TrackingReference)
|
||||
{
|
||||
type = "base";
|
||||
}
|
||||
|
||||
var item = new[]
|
||||
{
|
||||
type,
|
||||
system.IsTrackedDeviceConnected(i)
|
||||
? "connected"
|
||||
: "disconnected",
|
||||
isCharging
|
||||
? "charging"
|
||||
: "discharging",
|
||||
(batteryPercentage * 100).ToString(),
|
||||
poses[i].eTrackingResult.ToString()
|
||||
};
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Add(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessDashboard(CVROverlay overlay, ref ulong dashboardHandle, bool dashboardVisible)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (dashboardHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX", ref dashboardHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
ulong thumbnailHandle = 0;
|
||||
err = overlay.CreateDashboardOverlay("VRCX", "VRCX", ref dashboardHandle, ref thumbnailHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
var iconPath = Path.Join(Program.BaseDirectory, "VRCX.png");
|
||||
err = overlay.SetOverlayFromFile(thumbnailHandle, iconPath);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(dashboardHandle, 1.5f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(dashboardHandle, VROverlayInputMethod.Mouse);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var e = new VREvent_t();
|
||||
|
||||
while (overlay.PollNextOverlayEvent(dashboardHandle, ref e, (uint)Marshal.SizeOf(e)))
|
||||
{
|
||||
var type = (EVREventType)e.eventType;
|
||||
if (type == EVREventType.VREvent_MouseMove)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseMoveEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), false, CefEventFlags.None);
|
||||
}
|
||||
else if (type == EVREventType.VREvent_MouseButtonDown)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, false, 1, CefEventFlags.LeftMouseButton);
|
||||
}
|
||||
else if (type == EVREventType.VREvent_MouseButtonUp)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, true, 1, CefEventFlags.None);
|
||||
}
|
||||
}
|
||||
|
||||
if (dashboardVisible)
|
||||
{
|
||||
//var texture = new Texture_t
|
||||
//{
|
||||
// handle = _texture1.NativePointer
|
||||
//};
|
||||
//err = overlay.SetOverlayTexture(dashboardHandle, ref texture);
|
||||
//if (err != EVROverlayError.None)
|
||||
//{
|
||||
// return err;
|
||||
//}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessOverlay1(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible, uint overlayIndex)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (overlayHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX1", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
err = overlay.CreateOverlay("VRCX1", "VRCX1", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
_wristOverlayMMF = MemoryMappedFile.CreateFromFile(WRIST_OVERLAY_SHM_PATH, FileMode.Open, null, WRIST_FRAME_SIZE + 1);
|
||||
_wristOverlayAccessor = _wristOverlayMMF.CreateViewAccessor();
|
||||
}
|
||||
}
|
||||
|
||||
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||
{
|
||||
// http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices
|
||||
// Scaling-Rotation-Translation
|
||||
var m = Matrix4x4.CreateScale(0.25f);
|
||||
m *= Matrix4x4.CreateRotationX(_rotation[0]);
|
||||
m *= Matrix4x4.CreateRotationY(_rotation[1]);
|
||||
m *= Matrix4x4.CreateRotationZ(_rotation[2]);
|
||||
m *= Matrix4x4.CreateTranslation(new Vector3(_translation[0], _translation[1], _translation[2]));
|
||||
var hm34 = new HmdMatrix34_t
|
||||
{
|
||||
m0 = m.M11,
|
||||
m1 = m.M21,
|
||||
m2 = m.M31,
|
||||
m3 = m.M41,
|
||||
m4 = m.M12,
|
||||
m5 = m.M22,
|
||||
m6 = m.M32,
|
||||
m7 = m.M42,
|
||||
m8 = m.M13,
|
||||
m9 = m.M23,
|
||||
m10 = m.M33,
|
||||
m11 = m.M43
|
||||
};
|
||||
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, overlayIndex, ref hm34);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dashboardVisible &&
|
||||
DateTime.UtcNow.CompareTo(_nextOverlayUpdate) <= 0)
|
||||
{
|
||||
if (_wristOverlayTextureWriter != null)
|
||||
{
|
||||
byte[] imageData = GetLatestWristOverlayFrame();
|
||||
if (imageData != null)
|
||||
{
|
||||
FlipImageVertically(imageData, WRIST_FRAME_WIDTH, WRIST_FRAME_HEIGHT);
|
||||
_wristOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||
_wristOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
Texture_t texture = _wristOverlayTextureWriter.AsTextureT();
|
||||
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!overlayVisible)
|
||||
{
|
||||
err = overlay.ShowOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (overlayVisible)
|
||||
{
|
||||
err = overlay.HideOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessOverlay2(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (overlayHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX2", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
err = overlay.CreateOverlay("VRCX2", "VRCX2", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
var m = Matrix4x4.CreateScale(1f);
|
||||
m *= Matrix4x4.CreateTranslation(0, -0.3f, -1.5f);
|
||||
var hm34 = new HmdMatrix34_t
|
||||
{
|
||||
m0 = m.M11,
|
||||
m1 = m.M21,
|
||||
m2 = m.M31,
|
||||
m3 = m.M41,
|
||||
m4 = m.M12,
|
||||
m5 = m.M22,
|
||||
m6 = m.M32,
|
||||
m7 = m.M42,
|
||||
m8 = m.M13,
|
||||
m9 = m.M23,
|
||||
m10 = m.M33,
|
||||
m11 = m.M43
|
||||
};
|
||||
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, OpenVR.k_unTrackedDeviceIndex_Hmd, ref hm34);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
_hmdOverlayMMF = MemoryMappedFile.CreateFromFile(HMD_OVERLAY_SHM_PATH, FileMode.Open, null, HMD_FRAME_SIZE + 1);
|
||||
_hmdOverlayAccessor = _hmdOverlayMMF.CreateViewAccessor();
|
||||
}
|
||||
}
|
||||
|
||||
if (!dashboardVisible)
|
||||
{
|
||||
if (_hmdOverlayTextureWriter != null)
|
||||
{
|
||||
byte[] imageData = GetLatestHmdOverlayFrame();
|
||||
if (imageData != null)
|
||||
{
|
||||
FlipImageVertically(imageData, HMD_FRAME_WIDTH, HMD_FRAME_HEIGHT);
|
||||
_hmdOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||
_hmdOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
Texture_t texture = _hmdOverlayTextureWriter.AsTextureT();
|
||||
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!overlayVisible)
|
||||
{
|
||||
err = overlay.ShowOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (overlayVisible)
|
||||
{
|
||||
err = overlay.HideOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
return _wristFeedFunctionQueue;
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||
// if (_wristOverlay.IsLoading)
|
||||
// Restart();
|
||||
|
||||
_wristFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
return _hmdFeedFunctionQueue;
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||
// if (_hmdOverlay.IsLoading)
|
||||
// Restart();
|
||||
|
||||
_hmdFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
namespace VRCX;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
public abstract class VRCXVRInterface
|
||||
{
|
||||
public bool IsHmdAfk;
|
||||
|
||||
public abstract void Init();
|
||||
public abstract void Exit();
|
||||
public abstract void Refresh();
|
||||
@@ -12,4 +14,6 @@ public abstract class VRCXVRInterface
|
||||
public abstract string[][] GetDevices();
|
||||
public abstract void ExecuteVrFeedFunction(string function, string json);
|
||||
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
||||
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||
}
|
||||
@@ -24,10 +24,9 @@ namespace VRCX
|
||||
public static string Version { get; private set; }
|
||||
public static bool LaunchDebug;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
#if !LINUX
|
||||
public static VRCXVRInterface VRCXVRInstance { get; private set; }
|
||||
#endif
|
||||
public static AppApi AppApiInstance { get; private set; }
|
||||
public static AppApiVr AppApiVrInstance { get; private set; }
|
||||
|
||||
private static void SetProgramDirectories()
|
||||
{
|
||||
@@ -74,7 +73,7 @@ namespace VRCX
|
||||
try
|
||||
{
|
||||
var versionFile = File.ReadAllText(Path.Join(BaseDirectory, "Version")).Trim();
|
||||
|
||||
|
||||
// look for trailing git hash "-22bcd96" to indicate nightly build
|
||||
var version = versionFile.Split('-');
|
||||
if (version.Length > 0 && version[^1].Length == 7)
|
||||
@@ -234,7 +233,8 @@ namespace VRCX
|
||||
SQLiteLegacy.Instance.Init();
|
||||
AppApiInstance = new AppApiCef();
|
||||
|
||||
AppApiVr.Instance.Init();
|
||||
AppApiVrInstance = new AppApiVrCef();
|
||||
AppApiVrInstance.Init();
|
||||
ProcessMonitor.Instance.Init();
|
||||
Discord.Instance.Init();
|
||||
WebApi.Instance.Init();
|
||||
@@ -246,7 +246,7 @@ namespace VRCX
|
||||
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
|
||||
VRCXVRInstance = new VRCXVRLegacy();
|
||||
else
|
||||
VRCXVRInstance = new VRCXVR();
|
||||
VRCXVRInstance = new VRCXVRCef();
|
||||
VRCXVRInstance.Init();
|
||||
|
||||
Application.Run(new MainForm());
|
||||
@@ -260,7 +260,7 @@ namespace VRCX
|
||||
WebApi.Instance.Exit();
|
||||
|
||||
Discord.Instance.Exit();
|
||||
SystemMonitor.Instance.Exit();
|
||||
SystemMonitorCef.Instance.Exit();
|
||||
VRCXStorage.Instance.Save();
|
||||
SQLiteLegacy.Instance.Exit();
|
||||
ProcessMonitor.Instance.Exit();
|
||||
@@ -285,6 +285,9 @@ namespace VRCX
|
||||
|
||||
AppApiInstance = new AppApiElectron();
|
||||
// ProcessMonitor.Instance.Init();
|
||||
|
||||
VRCXVRInstance = new VRCXVRElectron();
|
||||
VRCXVRInstance.Init();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@
|
||||
<Compile Remove="Cef\**" />
|
||||
<Content Remove="AppApi\Cef\**" />
|
||||
<Compile Remove="AppApi\Cef\**" />
|
||||
<Content Remove="Overlay\**" />
|
||||
<Compile Remove="Overlay\**" />
|
||||
<Content Remove="Overlay\Cef\**" />
|
||||
<Compile Remove="Overlay\Cef\**" />
|
||||
<Content Remove="DBMerger\**" />
|
||||
<Compile Remove="DBMerger\**" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace VRCX
|
||||
catch (UriFormatException)
|
||||
{
|
||||
VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty);
|
||||
VRCXStorage.Instance.Flush();
|
||||
const string message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI.";
|
||||
#if !LINUX
|
||||
System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
|
||||
BIN
VRCX.png
BIN
VRCX.png
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 31 KiB |
11
package.json
11
package.json
@@ -139,10 +139,19 @@
|
||||
},
|
||||
"maintainer": "rs189 <35667100+rs189@users.noreply.github.com>",
|
||||
"description": "Friendship management tool for VRChat"
|
||||
},
|
||||
"mac": {
|
||||
"artifactName": "VRCX_Version.${ext}",
|
||||
"target": [
|
||||
"dmg"
|
||||
],
|
||||
"icon": "VRCX.png",
|
||||
"category": "public.app-category.utilities",
|
||||
"executableName": "VRCX"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"hazardous": "^0.3.0",
|
||||
"node-api-dotnet": "^0.9.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,88 +5,101 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const DOTNET_VERSION = '9.0.7';
|
||||
const DOTNET_RUNTIME_URL = `https://builds.dotnet.microsoft.com/dotnet/Runtime/${DOTNET_VERSION}/dotnet-runtime-${DOTNET_VERSION}-linux-x64.tar.gz`;
|
||||
const DOTNET_RUNTIME_DIR = path.join(__dirname, '..', 'build', 'Electron', 'dotnet-runtime');
|
||||
const DOTNET_BIN_DIR = path.join(DOTNET_RUNTIME_DIR, 'bin');
|
||||
const DOTNET_RUNTIME_DIR = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'Electron',
|
||||
'dotnet-runtime'
|
||||
);
|
||||
|
||||
async function downloadFile(url, targetPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(targetPath);
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download, status code: ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(resolve);
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to download, status code: ${response.statusCode}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(resolve);
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
fs.unlink(targetPath, () => reject(err));
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(targetPath, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function extractTarGz(tarGzPath, extractDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tar = spawnSync('tar', ['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
const tar = spawnSync(
|
||||
'tar',
|
||||
['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'],
|
||||
{
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
|
||||
if (tar.status === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`tar extraction failed with status ${tar.status}`));
|
||||
reject(
|
||||
new Error(`tar extraction failed with status ${tar.status}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.platform !== 'linux') {
|
||||
console.log('Skipping .NET runtime download on non-Linux platform');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Downloading .NET ${DOTNET_VERSION} runtime...`);
|
||||
|
||||
|
||||
if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
|
||||
fs.mkdirSync(DOTNET_RUNTIME_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(DOTNET_BIN_DIR)) {
|
||||
fs.mkdirSync(DOTNET_BIN_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const tarGzPath = path.join(DOTNET_RUNTIME_DIR, 'dotnet-runtime.tar.gz');
|
||||
|
||||
|
||||
try {
|
||||
// Download .NET runtime
|
||||
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath);
|
||||
console.log('Download completed');
|
||||
|
||||
|
||||
// Extract .NET runtime to a temporary directory first
|
||||
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
|
||||
if (!fs.existsSync(tempExtractDir)) {
|
||||
fs.mkdirSync(tempExtractDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
console.log('Extracting .NET runtime...');
|
||||
await extractTarGz(tarGzPath, tempExtractDir);
|
||||
console.log('Extraction completed');
|
||||
|
||||
|
||||
// Clean up tar.gz file
|
||||
fs.unlinkSync(tarGzPath);
|
||||
console.log('Cleanup completed');
|
||||
|
||||
// Move dotnet executable to bin directory
|
||||
|
||||
// Ensure the dotnet executable is executable
|
||||
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
|
||||
const targetDotnet = path.join(DOTNET_BIN_DIR, 'dotnet');
|
||||
|
||||
if (fs.existsSync(extractedDotnet)) {
|
||||
fs.renameSync(extractedDotnet, targetDotnet);
|
||||
fs.chmodSync(targetDotnet, 0o755);
|
||||
console.log('Moved dotnet executable to bin directory');
|
||||
}
|
||||
|
||||
fs.chmodSync(extractedDotnet, 0o755);
|
||||
|
||||
// Move all other files to the root of dotnet-runtime
|
||||
const files = fs.readdirSync(tempExtractDir);
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(tempExtractDir, file);
|
||||
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
|
||||
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
if (fs.lstatSync(sourcePath).isDirectory()) {
|
||||
// Remove existing directory and move new one
|
||||
@@ -96,16 +109,16 @@ async function main() {
|
||||
fs.unlinkSync(targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fs.renameSync(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
|
||||
// Clean up temp directory
|
||||
fs.rmSync(tempExtractDir, { recursive: true, force: true });
|
||||
|
||||
console.log(`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`);
|
||||
console.log(`dotnet executable available at: ${targetDotnet}`);
|
||||
|
||||
|
||||
console.log(
|
||||
`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
@@ -116,4 +129,4 @@ if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { downloadFile, extractTarGz };
|
||||
module.exports = { downloadFile, extractTarGz };
|
||||
|
||||
@@ -7,19 +7,35 @@ const {
|
||||
Tray,
|
||||
Menu,
|
||||
dialog,
|
||||
Notification
|
||||
Notification,
|
||||
nativeImage
|
||||
} = require('electron');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
// Include bundled .NET runtime
|
||||
const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
|
||||
const bundledDotnet = path.join(bundledDotNetPath, 'bin', 'dotnet');
|
||||
//app.disableHardwareAcceleration();
|
||||
|
||||
if (fs.existsSync(bundledDotnet)) {
|
||||
process.env.DOTNET_ROOT = bundledDotNetPath;
|
||||
process.env.PATH = `${path.dirname(bundledDotnet)}:${process.env.PATH}`;
|
||||
if (process.platform === 'linux') {
|
||||
// Include bundled .NET runtime
|
||||
const bundledDotNetPath = path.join(
|
||||
process.resourcesPath,
|
||||
'dotnet-runtime'
|
||||
);
|
||||
if (fs.existsSync(bundledDotNetPath)) {
|
||||
process.env.DOTNET_ROOT = bundledDotNetPath;
|
||||
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
const dotnetPath = path.join('/usr/local/share/dotnet');
|
||||
const dotnetPathArm = path.join('/usr/local/share/dotnet/x64');
|
||||
if (fs.existsSync(dotnetPathArm)) {
|
||||
process.env.DOTNET_ROOT = dotnetPathArm;
|
||||
process.env.PATH = `${dotnetPathArm}:${process.env.PATH}`;
|
||||
} else if (fs.existsSync(dotnetPath)) {
|
||||
process.env.DOTNET_ROOT = dotnetPath;
|
||||
process.env.PATH = `${dotnetPath}:${process.env.PATH}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDotNetInstalled()) {
|
||||
@@ -30,9 +46,11 @@ if (!isDotNetInstalled()) {
|
||||
);
|
||||
app.quit();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let isOverlayActive = false;
|
||||
let appIsQuitting = false;
|
||||
|
||||
// Get launch arguments
|
||||
let appImagePath = process.env.APPIMAGE;
|
||||
const args = process.argv.slice(1);
|
||||
@@ -51,6 +69,24 @@ require(path.join(rootDir, 'build/Electron/VRCX-Electron.cjs'));
|
||||
const InteropApi = require('./InteropApi');
|
||||
const interopApi = new InteropApi();
|
||||
|
||||
const WRIST_FRAME_WIDTH = 512;
|
||||
const WRIST_FRAME_HEIGHT = 512;
|
||||
const WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4;
|
||||
const WRIST_SHM_PATH = '/dev/shm/vrcx_wrist_overlay';
|
||||
|
||||
function createWristOverlayWindowShm() {
|
||||
fs.writeFileSync(WRIST_SHM_PATH, Buffer.alloc(WRIST_FRAME_SIZE + 1));
|
||||
}
|
||||
|
||||
const HMD_FRAME_WIDTH = 1024;
|
||||
const HMD_FRAME_HEIGHT = 1024;
|
||||
const HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4;
|
||||
const HMD_SHM_PATH = '/dev/shm/vrcx_hmd_overlay';
|
||||
|
||||
function createHmdOverlayWindowShm() {
|
||||
fs.writeFileSync(HMD_SHM_PATH, Buffer.alloc(HMD_FRAME_SIZE + 1));
|
||||
}
|
||||
|
||||
const version = getVersion();
|
||||
interopApi.getDotNetObject('ProgramElectron').PreInit(version, args);
|
||||
interopApi.getDotNetObject('VRCXStorage').Load();
|
||||
@@ -61,6 +97,10 @@ interopApi.getDotNetObject('Discord').Init();
|
||||
interopApi.getDotNetObject('WebApi').Init();
|
||||
interopApi.getDotNetObject('LogWatcher').Init();
|
||||
|
||||
interopApi.getDotNetObject('IPCServer').Init();
|
||||
interopApi.getDotNetObject('SystemMonitorElectron').Init();
|
||||
interopApi.getDotNetObject('AppApiVrElectron').Init();
|
||||
|
||||
ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => {
|
||||
return interopApi.callMethod(className, methodName, args);
|
||||
});
|
||||
@@ -142,6 +182,45 @@ ipcMain.handle('app:restart', () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('app:getWristOverlayWindow', () => {
|
||||
if (wristOverlayWindow && wristOverlayWindow.webContents) {
|
||||
return !wristOverlayWindow.webContents.isLoading() &&
|
||||
wristOverlayWindow.webContents.isPainting();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ipcMain.handle('app:getHmdOverlayWindow', () => {
|
||||
if (hmdOverlayWindow && hmdOverlayWindow.webContents) {
|
||||
return !hmdOverlayWindow.webContents.isLoading() &&
|
||||
hmdOverlayWindow.webContents.isPainting();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'app:updateVr',
|
||||
(event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => {
|
||||
if (!active) {
|
||||
disposeOverlay();
|
||||
return;
|
||||
}
|
||||
isOverlayActive = true;
|
||||
|
||||
if (!hmdOverlay) {
|
||||
destroyHmdOverlayWindow();
|
||||
} else if (active && !hmdOverlayWindow) {
|
||||
createHmdOverlayWindowOffscreen();
|
||||
}
|
||||
|
||||
if (!wristOverlay) {
|
||||
destroyWristOverlayWindow();
|
||||
} else if (active && !wristOverlayWindow) {
|
||||
createWristOverlayWindowOffscreen();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function tryRelaunchWithArgs(args) {
|
||||
if (
|
||||
process.platform !== 'linux' ||
|
||||
@@ -188,14 +267,11 @@ function createWindow() {
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
webContents: {
|
||||
userAgent: version
|
||||
}
|
||||
});
|
||||
applyWindowState();
|
||||
const indexPath = path.join(rootDir, 'build/html/index.html');
|
||||
mainWindow.loadFile(indexPath, { userAgent: version });
|
||||
mainWindow.loadFile(indexPath);
|
||||
|
||||
// add proxy config, doesn't work, thanks electron
|
||||
// const proxy = VRCXStorage.Get('VRCX_Proxy');
|
||||
@@ -234,7 +310,7 @@ function createWindow() {
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
|
||||
if (isCloseToTray && !app.isQuitting) {
|
||||
if (isCloseToTray && !appIsQuitting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
@@ -271,8 +347,142 @@ function createWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
let wristOverlayWindow = undefined;
|
||||
|
||||
function createWristOverlayWindowOffscreen() {
|
||||
if (!fs.existsSync(WRIST_SHM_PATH)) {
|
||||
createWristOverlayWindowShm();
|
||||
}
|
||||
|
||||
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
|
||||
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
|
||||
const width = WRIST_FRAME_WIDTH;
|
||||
const height = WRIST_FRAME_HEIGHT;
|
||||
|
||||
wristOverlayWindow = new BrowserWindow({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
icon: path.join(rootDir, 'VRCX.png'),
|
||||
autoHideMenuBar: true,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
offscreen: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
wristOverlayWindow.webContents.setFrameRate(2);
|
||||
|
||||
const indexPath = path.join(rootDir, 'build/html/vr.html');
|
||||
const fileUrl = `file://${indexPath}?1`;
|
||||
wristOverlayWindow.loadURL(fileUrl, { userAgent: version });
|
||||
|
||||
// Use paint event for offscreen rendering
|
||||
wristOverlayWindow.webContents.on('paint', (event, dirty, image) => {
|
||||
const buffer = image.toBitmap();
|
||||
//console.log('Captured wrist frame via paint event, size:', buffer.length);
|
||||
writeWristFrame(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
function writeWristFrame(imageBuffer) {
|
||||
try {
|
||||
const fd = fs.openSync(WRIST_SHM_PATH, 'r+');
|
||||
const buffer = Buffer.alloc(WRIST_FRAME_SIZE + 1);
|
||||
buffer[0] = 0; // not ready
|
||||
imageBuffer.copy(buffer, 1, 0, WRIST_FRAME_SIZE);
|
||||
buffer[0] = 1; // ready
|
||||
fs.writeSync(fd, buffer);
|
||||
fs.closeSync(fd);
|
||||
//console.log('Wrote wrist frame to shared memory');
|
||||
} catch (err) {
|
||||
console.error('Error writing wrist frame to shared memory:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyWristOverlayWindow() {
|
||||
if (wristOverlayWindow && !wristOverlayWindow.isDestroyed()) {
|
||||
wristOverlayWindow.close();
|
||||
}
|
||||
wristOverlayWindow = undefined;
|
||||
}
|
||||
|
||||
let hmdOverlayWindow = undefined;
|
||||
|
||||
function createHmdOverlayWindowOffscreen() {
|
||||
if (!fs.existsSync(HMD_SHM_PATH)) {
|
||||
createHmdOverlayWindowShm();
|
||||
}
|
||||
|
||||
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
|
||||
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
|
||||
const width = HMD_FRAME_WIDTH;
|
||||
const height = HMD_FRAME_HEIGHT;
|
||||
|
||||
hmdOverlayWindow = new BrowserWindow({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
icon: path.join(rootDir, 'VRCX.png'),
|
||||
autoHideMenuBar: true,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
offscreen: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
hmdOverlayWindow.webContents.setFrameRate(48);
|
||||
|
||||
const indexPath = path.join(rootDir, 'build/html/vr.html');
|
||||
const fileUrl = `file://${indexPath}?2`;
|
||||
hmdOverlayWindow.loadURL(fileUrl, { userAgent: version });
|
||||
|
||||
// Use paint event for offscreen rendering
|
||||
hmdOverlayWindow.webContents.on('paint', (event, dirty, image) => {
|
||||
const buffer = image.toBitmap();
|
||||
//console.log('Captured HMD frame via paint event, size:', buffer.length);
|
||||
writeHmdFrame(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
function writeHmdFrame(imageBuffer) {
|
||||
try {
|
||||
const fd = fs.openSync(HMD_SHM_PATH, 'r+');
|
||||
const buffer = Buffer.alloc(HMD_FRAME_SIZE + 1);
|
||||
buffer[0] = 0; // not ready
|
||||
imageBuffer.copy(buffer, 1, 0, HMD_FRAME_SIZE);
|
||||
buffer[0] = 1; // ready
|
||||
fs.writeSync(fd, buffer);
|
||||
fs.closeSync(fd);
|
||||
//console.log('Wrote HMD frame to shared memory');
|
||||
} catch (err) {
|
||||
console.error('Error writing HMD frame to shared memory:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyHmdOverlayWindow() {
|
||||
if (hmdOverlayWindow && !hmdOverlayWindow.isDestroyed()) {
|
||||
hmdOverlayWindow.close();
|
||||
}
|
||||
hmdOverlayWindow = undefined;
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
const tray = new Tray(path.join(rootDir, 'images/tray.png'));
|
||||
let tray = null;
|
||||
if (process.platform === 'darwin') {
|
||||
const image = nativeImage.createFromPath(
|
||||
path.join(rootDir, 'images/tray.png')
|
||||
);
|
||||
tray = new Tray(image.resize({ width: 16, height: 16 }));
|
||||
} else {
|
||||
tray = new Tray(path.join(rootDir, 'images/tray.png'));
|
||||
}
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Open',
|
||||
@@ -296,7 +506,7 @@ function createTray() {
|
||||
label: 'Quit VRCX',
|
||||
type: 'normal',
|
||||
click: function () {
|
||||
app.isQuitting = true;
|
||||
appIsQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
@@ -507,18 +717,19 @@ function getHomePath() {
|
||||
const absoluteHomePath = fs.realpathSync(relativeHomePath);
|
||||
return absoluteHomePath;
|
||||
} catch (err) {
|
||||
console.error('Error resolving absolute home path:', err);
|
||||
return relativeHomePath;
|
||||
}
|
||||
}
|
||||
|
||||
function getVersion() {
|
||||
try {
|
||||
var versionFile = fs
|
||||
const versionFile = fs
|
||||
.readFileSync(path.join(rootDir, 'Version'), 'utf8')
|
||||
.trim();
|
||||
|
||||
// look for trailing git hash "-22bcd96" to indicate nightly build
|
||||
var version = versionFile.split('-');
|
||||
const version = versionFile.split('-');
|
||||
console.log('Version:', versionFile);
|
||||
if (version.length > 0 && version[version.length - 1].length == 7) {
|
||||
return `VRCX (Linux) Nightly ${versionFile}`;
|
||||
@@ -532,19 +743,15 @@ function getVersion() {
|
||||
}
|
||||
|
||||
function isDotNetInstalled() {
|
||||
if (process.platform === 'darwin') {
|
||||
// Assume .NET is already installed on macOS
|
||||
return true;
|
||||
let dotnetPath = path.join(process.env.DOTNET_ROOT, 'dotnet');
|
||||
if (!process.env.DOTNET_ROOT || !fs.existsSync(dotnetPath)) {
|
||||
// fallback to command
|
||||
dotnetPath = 'dotnet';
|
||||
}
|
||||
|
||||
// Check for bundled .NET runtime
|
||||
if (fs.existsSync(bundledDotnet)) {
|
||||
console.log('Using bundled .NET runtime at:', bundledDotNetPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Checking for .NET installation at:', dotnetPath);
|
||||
|
||||
// Fallback to system .NET runtime
|
||||
const result = spawnSync('dotnet', ['--list-runtimes'], {
|
||||
const result = spawnSync(dotnetPath, ['--list-runtimes'], {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return result.stdout?.includes('.NETCore.App 9.0');
|
||||
@@ -611,20 +818,54 @@ function applyWindowState() {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
createTray();
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
createWristOverlayWindowOffscreen();
|
||||
createHmdOverlayWindowOffscreen();
|
||||
}
|
||||
|
||||
installVRCX();
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// app.on('before-quit', function () {
|
||||
// mainWindow.webContents.send('windowClosed');
|
||||
// });
|
||||
function disposeOverlay() {
|
||||
if (!isOverlayActive) {
|
||||
return;
|
||||
}
|
||||
isOverlayActive = false;
|
||||
if (wristOverlayWindow) {
|
||||
wristOverlayWindow.close();
|
||||
wristOverlayWindow = undefined;
|
||||
}
|
||||
if (hmdOverlayWindow) {
|
||||
hmdOverlayWindow.close();
|
||||
hmdOverlayWindow = undefined;
|
||||
}
|
||||
|
||||
if (fs.existsSync(WRIST_SHM_PATH)) {
|
||||
fs.unlinkSync(WRIST_SHM_PATH);
|
||||
}
|
||||
if (fs.existsSync(HMD_SHM_PATH)) {
|
||||
fs.unlinkSync(HMD_SHM_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
app.on('before-quit', function () {
|
||||
disposeOverlay();
|
||||
|
||||
mainWindow.webContents.send('windowClosed');
|
||||
});
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
disposeOverlay();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
5
src-electron/offscreen-preload.js
Normal file
5
src-electron/offscreen-preload.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateImage: (callback) => ipcRenderer.on('update-image', (event, base64) => callback(base64))
|
||||
});
|
||||
29
src-electron/offscreen.html
Normal file
29
src-electron/offscreen.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Offscreen Mirror</title>
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
}
|
||||
#mirror {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="mirror" src="" alt="Main Window Mirror">
|
||||
<script>
|
||||
window.electronAPI.onUpdateImage((base64) => {
|
||||
document.getElementById('mirror').src = `data:image/png;base64,${base64}`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,11 +31,17 @@ managedHostPath = managedHostPath.indexOf('app.asar.unpacked') < 0 ?
|
||||
}
|
||||
|
||||
// Paths to patch
|
||||
let platformName = 'linux';
|
||||
let platformName = '';
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
platformName = 'win';
|
||||
break;
|
||||
case 'darwin':
|
||||
platformName = 'mac';
|
||||
break;
|
||||
case 'linux':
|
||||
platformName = 'linux';
|
||||
break;
|
||||
}
|
||||
const postBuildPath = path.join(
|
||||
__dirname,
|
||||
|
||||
@@ -30,5 +30,9 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer.on('setWindowState', callback),
|
||||
desktopNotification: (title, body, icon) =>
|
||||
ipcRenderer.invoke('notification:showNotification', title, body, icon),
|
||||
restartApp: () => ipcRenderer.invoke('app:restart')
|
||||
});
|
||||
restartApp: () => ipcRenderer.invoke('app:restart'),
|
||||
getWristOverlayWindow: () => ipcRenderer.invoke('app:getWristOverlayWindow'),
|
||||
getHmdOverlayWindow: () => ipcRenderer.invoke('app:getHmdOverlayWindow'),
|
||||
updateVr: (active, hmdOverlay, wristOverlay, menuButton, overlayHand) =>
|
||||
ipcRenderer.invoke('app:updateVr', active, hmdOverlay, wristOverlay, menuButton, overlayHand)
|
||||
});
|
||||
@@ -17,17 +17,34 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
|
||||
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(oldAppImage)) {
|
||||
fs.renameSync(oldAppImage, newAppImage);
|
||||
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
|
||||
} else {
|
||||
console.log(`File not found: ${oldAppImage}`);
|
||||
if (process.platform === 'linux') {
|
||||
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
|
||||
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
|
||||
try {
|
||||
if (fs.existsSync(oldAppImage)) {
|
||||
fs.renameSync(oldAppImage, newAppImage);
|
||||
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
|
||||
} else {
|
||||
console.log(`File not found: ${oldAppImage}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error renaming files:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error renaming files:', err);
|
||||
process.exit(1);
|
||||
} else if (process.platform === 'darwin') {
|
||||
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
|
||||
const newDmg = path.join(buildDir, `VRCX_${version}.dmg`);
|
||||
try {
|
||||
if (fs.existsSync(oldDmg)) {
|
||||
fs.renameSync(oldDmg, newDmg);
|
||||
console.log(`Renamed: ${oldDmg} -> ${newDmg}`);
|
||||
} else {
|
||||
console.log(`File not found: ${oldDmg}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error renaming files:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log('No renaming needed for this platform.');
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ if (WINDOWS) {
|
||||
window.LogWatcher = InteropApi.LogWatcher;
|
||||
window.Discord = InteropApi.Discord;
|
||||
window.AssetBundleManager = InteropApi.AssetBundleManager;
|
||||
}
|
||||
window.AppApiVrElectron = InteropApi.AppApiVrElectron;
|
||||
}
|
||||
@@ -151,35 +151,35 @@ function updateTrustColorClasses(trustColor) {
|
||||
document.getElementsByTagName('head')[0].appendChild(style);
|
||||
}
|
||||
|
||||
function refreshCustomCss() {
|
||||
async function refreshCustomCss() {
|
||||
if (document.contains(document.getElementById('app-custom-style'))) {
|
||||
document.getElementById('app-custom-style').remove();
|
||||
}
|
||||
AppApi.CustomCssPath().then((customCss) => {
|
||||
const customCss = await AppApi.CustomCss();
|
||||
if (customCss) {
|
||||
const head = document.head;
|
||||
if (customCss) {
|
||||
const $appCustomStyle = document.createElement('link');
|
||||
$appCustomStyle.setAttribute('id', 'app-custom-style');
|
||||
$appCustomStyle.rel = 'stylesheet';
|
||||
$appCustomStyle.href = `file://${customCss}?_=${Date.now()}`;
|
||||
head.appendChild($appCustomStyle);
|
||||
}
|
||||
});
|
||||
const $appCustomStyle = document.createElement('link');
|
||||
$appCustomStyle.setAttribute('id', 'app-custom-style');
|
||||
$appCustomStyle.rel = 'stylesheet';
|
||||
$appCustomStyle.type = 'text/css';
|
||||
$appCustomStyle.textContent = customCss;
|
||||
head.appendChild($appCustomStyle);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCustomScript() {
|
||||
async function refreshCustomScript() {
|
||||
if (document.contains(document.getElementById('app-custom-script'))) {
|
||||
document.getElementById('app-custom-script').remove();
|
||||
}
|
||||
AppApi.CustomScriptPath().then((customScript) => {
|
||||
const customScript = await AppApi.CustomScript();
|
||||
if (customScript) {
|
||||
const head = document.head;
|
||||
if (customScript) {
|
||||
const $appCustomScript = document.createElement('script');
|
||||
$appCustomScript.setAttribute('id', 'app-custom-script');
|
||||
$appCustomScript.src = `file://${customScript}?_=${Date.now()}`;
|
||||
head.appendChild($appCustomScript);
|
||||
}
|
||||
});
|
||||
const $appCustomScript = document.createElement('script');
|
||||
$appCustomScript.setAttribute('id', 'app-custom-script');
|
||||
$appCustomScript.type = 'text/javascript';
|
||||
$appCustomScript.textContent = customScript;
|
||||
head.appendChild($appCustomScript);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,7 @@ function parseLocation(tag) {
|
||||
ctx.isPrivate = true;
|
||||
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
|
||||
ctx.isTraveling = true;
|
||||
} else if (!_tag.startsWith('local')) {
|
||||
} else if (tag && !_tag.startsWith('local')) {
|
||||
ctx.isRealInstance = true;
|
||||
const sep = _tag.indexOf(':');
|
||||
// technically not part of instance id, but might be there when coping id from url so why not support it
|
||||
|
||||
@@ -385,31 +385,35 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* aka: `$app.methods.addAvatarToHistory`
|
||||
* @param {string} avatarId
|
||||
*/
|
||||
function addAvatarToHistory(avatarId) {
|
||||
avatarRequest.getAvatar({ avatarId }).then((args) => {
|
||||
const ref = applyAvatar(args.json);
|
||||
avatarRequest
|
||||
.getAvatar({ avatarId })
|
||||
.then((args) => {
|
||||
const ref = applyAvatar(args.json);
|
||||
|
||||
database.addAvatarToCache(ref);
|
||||
database.addAvatarToHistory(ref.id);
|
||||
database.addAvatarToCache(ref);
|
||||
database.addAvatarToHistory(ref.id);
|
||||
|
||||
if (ref.authorId === userStore.currentUser.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const historyArray = state.avatarHistoryArray;
|
||||
for (let i = 0; i < historyArray.length; ++i) {
|
||||
if (historyArray[i].id === ref.id) {
|
||||
historyArray.splice(i, 1);
|
||||
if (ref.authorId === userStore.currentUser.id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.avatarHistoryArray.unshift(ref);
|
||||
state.avatarHistory.delete(ref.id);
|
||||
state.avatarHistory.add(ref.id);
|
||||
});
|
||||
const historyArray = state.avatarHistoryArray;
|
||||
for (let i = 0; i < historyArray.length; ++i) {
|
||||
if (historyArray[i].id === ref.id) {
|
||||
historyArray.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
state.avatarHistoryArray.unshift(ref);
|
||||
state.avatarHistory.delete(ref.id);
|
||||
state.avatarHistory.add(ref.id);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to add avatar to history:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function clearAvatarHistory() {
|
||||
|
||||
@@ -531,10 +531,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
|
||||
state.folderSelectorDialogVisible = true;
|
||||
let newFolder = '';
|
||||
if (LINUX) {
|
||||
newFolder = await window.electron.openDirectoryDialog();
|
||||
if (WINDOWS) {
|
||||
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
|
||||
} else {
|
||||
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
|
||||
newFolder = await window.electron.openDirectoryDialog();
|
||||
}
|
||||
|
||||
state.folderSelectorDialogVisible = false;
|
||||
|
||||
@@ -362,10 +362,10 @@ export const useNotificationsSettingsStore = defineStore(
|
||||
|
||||
function getTTSVoiceName() {
|
||||
let voices;
|
||||
if (LINUX) {
|
||||
voices = state.TTSvoices;
|
||||
} else {
|
||||
if (WINDOWS) {
|
||||
voices = speechSynthesis.getVoices();
|
||||
} else {
|
||||
voices = state.TTSvoices;
|
||||
}
|
||||
if (voices.length === 0) {
|
||||
return '';
|
||||
@@ -379,10 +379,10 @@ export const useNotificationsSettingsStore = defineStore(
|
||||
async function changeTTSVoice(index) {
|
||||
setNotificationTTSVoice(index);
|
||||
let voices;
|
||||
if (LINUX) {
|
||||
voices = state.TTSvoices;
|
||||
} else {
|
||||
if (WINDOWS) {
|
||||
voices = speechSynthesis.getVoices();
|
||||
} else {
|
||||
voices = state.TTSvoices;
|
||||
}
|
||||
if (voices.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -133,16 +133,16 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
|
||||
(vrcxStore.isRunningUnderWine || LINUX) &&
|
||||
--state.nextGameRunningCheck <= 0
|
||||
) {
|
||||
if (LINUX) {
|
||||
if (WINDOWS) {
|
||||
state.nextGameRunningCheck = 3;
|
||||
AppApi.CheckGameRunning();
|
||||
} else {
|
||||
state.nextGameRunningCheck = 1;
|
||||
gameStore.updateIsGameRunning(
|
||||
await AppApi.IsGameRunning(),
|
||||
await AppApi.IsSteamVRRunning(),
|
||||
false
|
||||
);
|
||||
} else {
|
||||
state.nextGameRunningCheck = 3;
|
||||
AppApi.CheckGameRunning();
|
||||
}
|
||||
}
|
||||
if (--state.nextDatabaseOptimize <= 0) {
|
||||
|
||||
@@ -851,12 +851,7 @@ export const useUserStore = defineStore('User', () => {
|
||||
}
|
||||
applyUserDialogLocation(true);
|
||||
|
||||
if (
|
||||
args.cache &&
|
||||
args.ref.$lastFetch < Date.now() - 10000 // 10 seconds
|
||||
) {
|
||||
userRequest.getUser(args.params);
|
||||
}
|
||||
userRequest.getUser(args.params);
|
||||
let inCurrentWorld = false;
|
||||
if (
|
||||
locationStore.lastLocation.playerList.has(D.ref.id)
|
||||
|
||||
@@ -27,7 +27,9 @@ export const useVrStore = defineStore('Vr', () => {
|
||||
const userStore = useUserStore();
|
||||
const sharedFeedStore = useSharedFeedStore();
|
||||
|
||||
const state = reactive({});
|
||||
const state = reactive({
|
||||
overlayActive: false
|
||||
});
|
||||
|
||||
watch(
|
||||
() => watchState.isFriendsLoaded,
|
||||
@@ -48,6 +50,8 @@ export const useVrStore = defineStore('Vr', () => {
|
||||
sharedFeedStore.updateSharedFeed(true);
|
||||
friendStore.onlineFriendCount = 0; // force an update
|
||||
friendStore.updateOnlineFriendCoutner();
|
||||
|
||||
state.overlayActive = true;
|
||||
}
|
||||
|
||||
async function saveOpenVROption() {
|
||||
@@ -74,7 +78,7 @@ export const useVrStore = defineStore('Vr', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
let onlineFor = '';
|
||||
let onlineFor = null;
|
||||
if (!wristOverlaySettingsStore.hideUptimeFromFeed) {
|
||||
onlineFor = userStore.currentUser.$online_for;
|
||||
}
|
||||
@@ -125,6 +129,13 @@ export const useVrStore = defineStore('Vr', () => {
|
||||
}
|
||||
|
||||
function updateOpenVR() {
|
||||
let newState = {
|
||||
active: false,
|
||||
hmdOverlay: false,
|
||||
wristOverlay: false,
|
||||
menuButton: false,
|
||||
overlayHand: 0
|
||||
};
|
||||
if (
|
||||
notificationsSettingsStore.openVR &&
|
||||
gameStore.isSteamVRRunning &&
|
||||
@@ -140,16 +151,42 @@ export const useVrStore = defineStore('Vr', () => {
|
||||
) {
|
||||
hmdOverlay = true;
|
||||
}
|
||||
// active, hmdOverlay, wristOverlay, menuButton, overlayHand
|
||||
AppApi.SetVR(
|
||||
true,
|
||||
newState = {
|
||||
active: true,
|
||||
hmdOverlay,
|
||||
wristOverlaySettingsStore.overlayWrist,
|
||||
wristOverlaySettingsStore.overlaybutton,
|
||||
wristOverlaySettingsStore.overlayHand
|
||||
wristOverlay: wristOverlaySettingsStore.overlayWrist,
|
||||
menuButton: wristOverlaySettingsStore.overlaybutton,
|
||||
overlayHand: wristOverlaySettingsStore.overlayHand
|
||||
};
|
||||
}
|
||||
|
||||
AppApi.SetVR(
|
||||
newState.active,
|
||||
newState.hmdOverlay,
|
||||
newState.wristOverlay,
|
||||
newState.menuButton,
|
||||
newState.overlayHand
|
||||
);
|
||||
|
||||
if (LINUX) {
|
||||
window.electron.updateVr(
|
||||
newState.active,
|
||||
newState.hmdOverlay,
|
||||
newState.wristOverlay,
|
||||
newState.menuButton,
|
||||
newState.overlayHand
|
||||
);
|
||||
} else {
|
||||
AppApi.SetVR(false, false, false, false, 0);
|
||||
|
||||
if (state.overlayActive !== newState.active) {
|
||||
if (
|
||||
window.electron.getWristOverlayWindow() ||
|
||||
window.electron.getHmdOverlayWindow()
|
||||
) {
|
||||
vrInit();
|
||||
state.overlayActive = newState.active;
|
||||
}
|
||||
setTimeout(() => vrInit(), 1000); // give the overlay time to load
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -652,11 +652,11 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
|
||||
async function backupVrcRegistry(name) {
|
||||
let regJson;
|
||||
if (LINUX) {
|
||||
if (WINDOWS) {
|
||||
regJson = await AppApi.GetVRChatRegistry();
|
||||
} else {
|
||||
regJson = await AppApi.GetVRChatRegistryJson();
|
||||
regJson = JSON.parse(regJson);
|
||||
} else {
|
||||
regJson = await AppApi.GetVRChatRegistry();
|
||||
}
|
||||
const newBackup = {
|
||||
name,
|
||||
|
||||
55
src/types/globals.d.ts
vendored
55
src/types/globals.d.ts
vendored
@@ -7,6 +7,7 @@ declare global {
|
||||
interface Window {
|
||||
$app: any;
|
||||
AppApi: AppApi;
|
||||
AppApiVr: AppApiVr;
|
||||
WebApi: WebApi;
|
||||
VRCXStorage: VRCXStorage;
|
||||
SQLite: SQLite;
|
||||
@@ -51,6 +52,15 @@ declare global {
|
||||
Function: (event: any, state: { windowState: any }) => void
|
||||
) => void;
|
||||
restartApp: () => Promise<void>;
|
||||
getWristOverlayWindow: () => Promise<boolean>;
|
||||
getHmdOverlayWindow: () => Promise<boolean>;
|
||||
updateVr: (
|
||||
active: bool,
|
||||
hmdOverlay: bool,
|
||||
wristOverlay: bool,
|
||||
menuButton: bool,
|
||||
overlayHand: int
|
||||
) => Promise<void>;
|
||||
};
|
||||
__APP_GLOBALS__: {
|
||||
debug: boolean;
|
||||
@@ -141,7 +151,10 @@ declare global {
|
||||
};
|
||||
|
||||
const Discord: {
|
||||
SetTimestamps(startTimestamp: number, endTimestamp: number): void;
|
||||
SetTimestamps(
|
||||
startTimestamp: number,
|
||||
endTimestamp: number
|
||||
): Promise<void>;
|
||||
SetAssets(
|
||||
bigIcon: string,
|
||||
bigIconText: string,
|
||||
@@ -154,8 +167,8 @@ declare global {
|
||||
buttonUrl: string,
|
||||
appId: string,
|
||||
activityType: number
|
||||
): void;
|
||||
SetText(details: string, state: string): void;
|
||||
): Promise<void>;
|
||||
SetText(details: string, state: string): Promise<void>;
|
||||
SetActive(active: boolean): Promise<boolean>;
|
||||
};
|
||||
|
||||
@@ -202,8 +215,8 @@ declare global {
|
||||
GetLaunchCommand(): Promise<string>;
|
||||
IPCAnnounceStart(): Promise<void>;
|
||||
SendIpc(type: string, data: string): Promise<void>;
|
||||
CustomCssPath(): Promise<string>;
|
||||
CustomScriptPath(): Promise<string>;
|
||||
CustomCss(): Promise<string>;
|
||||
CustomScript(): Promise<string>;
|
||||
CurrentCulture(): Promise<string>;
|
||||
CurrentLanguage(): Promise<string>;
|
||||
GetVersion(): Promise<string>;
|
||||
@@ -355,21 +368,23 @@ declare global {
|
||||
};
|
||||
|
||||
const AppApiVr: {
|
||||
Init(): void;
|
||||
VrInit(): void;
|
||||
ToggleSystemMonitor(enabled: boolean): void;
|
||||
CpuUsage(): number;
|
||||
GetVRDevices(): string[][];
|
||||
GetUptime(): number;
|
||||
CurrentCulture(): string;
|
||||
CustomVrScriptPath(): string;
|
||||
IsRunningUnderWine(): boolean;
|
||||
Init(): Promise<void>;
|
||||
VrInit(): Promise<void>;
|
||||
ToggleSystemMonitor(enabled: boolean): Promise<void>;
|
||||
CpuUsage(): Promise<number>;
|
||||
GetVRDevices(): Promise<string[][]>;
|
||||
GetUptime(): Promise<number>;
|
||||
CurrentCulture(): Promise<string>;
|
||||
CustomVrScript(): Promise<string>;
|
||||
IsRunningUnderWine(): Promise<boolean>;
|
||||
GetExecuteVrFeedFunctionQueue(): Promise<Map<string, string>>;
|
||||
GetExecuteVrOverlayFunctionQueue(): Promise<Map<string, string>>;
|
||||
};
|
||||
|
||||
const WebApi: {
|
||||
ClearCookies(): void;
|
||||
GetCookies(): string;
|
||||
SetCookies(cookie: string): void;
|
||||
ClearCookies(): Promise<void>;
|
||||
GetCookies(): Promise<string>;
|
||||
SetCookies(cookie: string): Promise<void>;
|
||||
Execute(options: any): Promise<{ Item1: number; Item2: string }>;
|
||||
ExecuteJson(requestJson: string): Promise<string>;
|
||||
};
|
||||
@@ -399,9 +414,9 @@ declare global {
|
||||
};
|
||||
|
||||
const webApiService: {
|
||||
clearCookies(): void;
|
||||
getCookies(): string;
|
||||
setCookies(cookie: string): void;
|
||||
clearCookies(): Promise<void>;
|
||||
getCookies(): Promise<string>;
|
||||
setCookies(cookie: string): Promise<void>;
|
||||
execute(options: {
|
||||
url: string;
|
||||
method: string;
|
||||
|
||||
2
src/types/user.d.ts
vendored
2
src/types/user.d.ts
vendored
@@ -128,7 +128,7 @@ interface getCurrentUserResponse extends getUserResponse {
|
||||
oculusId: string;
|
||||
offlineFriends: string[];
|
||||
onlineFriends: string[];
|
||||
pastDisplayNames: { displayName: string; dateChanged: string }[];
|
||||
pastDisplayNames: { displayName: string; updated_at: string }[];
|
||||
picoId: string;
|
||||
presence?: {
|
||||
avatarThumbnail: string;
|
||||
|
||||
@@ -595,6 +595,7 @@
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
instance.inputValue = instance.inputValue.trim();
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
@@ -622,6 +623,7 @@
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
instance.inputValue = instance.inputValue.trim();
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
if (!directAccessWorld(instance.inputValue)) {
|
||||
$message({
|
||||
@@ -641,6 +643,7 @@
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
instance.inputValue = instance.inputValue.trim();
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
|
||||
@@ -77,32 +77,31 @@
|
||||
<!--//- General | Application-->
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.general.application.header') }}</span>
|
||||
<template v-if="!isLinux">
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.startup')"
|
||||
:value="isStartAtWindowsStartup"
|
||||
@change="setIsStartAtWindowsStartup" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.minimized')"
|
||||
:value="isStartAsMinimizedState"
|
||||
@change="setIsStartAsMinimizedState" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.tray')"
|
||||
:value="isCloseToTray"
|
||||
@change="setIsCloseToTray" />
|
||||
</template>
|
||||
<template v-if="!isLinux">
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.disable_gpu_acceleration')"
|
||||
:value="disableGpuAcceleration"
|
||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||
@change="setDisableGpuAcceleration" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
|
||||
:value="disableVrOverlayGpuAcceleration"
|
||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||
@change="setDisableVrOverlayGpuAcceleration" />
|
||||
</template>
|
||||
<simple-switch
|
||||
v-if="!isLinux"
|
||||
:label="t('view.settings.general.application.startup')"
|
||||
:value="isStartAtWindowsStartup"
|
||||
@change="setIsStartAtWindowsStartup" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.minimized')"
|
||||
:value="isStartAsMinimizedState"
|
||||
@change="setIsStartAsMinimizedState" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.general.application.tray')"
|
||||
:value="isCloseToTray"
|
||||
@change="setIsCloseToTray" />
|
||||
<simple-switch
|
||||
v-if="!isLinux"
|
||||
:label="t('view.settings.general.application.disable_gpu_acceleration')"
|
||||
:value="disableGpuAcceleration"
|
||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||
@change="setDisableGpuAcceleration" />
|
||||
<simple-switch
|
||||
v-if="!isLinux"
|
||||
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
|
||||
:value="disableVrOverlayGpuAcceleration"
|
||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||
@change="setDisableVrOverlayGpuAcceleration" />
|
||||
<div class="options-container-item">
|
||||
<el-button size="small" icon="el-icon-connection" @click="promptProxySettings">{{
|
||||
t('view.settings.general.application.proxy')
|
||||
@@ -796,7 +795,7 @@
|
||||
}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<template v-if="!isLinux">
|
||||
<template>
|
||||
<simple-switch
|
||||
:label="
|
||||
t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')
|
||||
@@ -831,6 +830,43 @@
|
||||
}}</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="options-container-item">
|
||||
<span class="name" style="vertical-align: top; padding-top: 10px">{{
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.notification_opacity'
|
||||
)
|
||||
}}</span>
|
||||
<el-slider
|
||||
:value="notificationOpacity"
|
||||
@input="setNotificationOpacity"
|
||||
:show-tooltip="false"
|
||||
:min="0"
|
||||
:max="100"
|
||||
show-input
|
||||
style="display: inline-block; width: 300px" />
|
||||
</div>
|
||||
<div class="options-container-item">
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-time"
|
||||
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
|
||||
@click="promptNotificationTimeout"
|
||||
>{{
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.notification_timeout'
|
||||
)
|
||||
}}</el-button
|
||||
>
|
||||
</div>
|
||||
<simple-switch
|
||||
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')"
|
||||
:value="imageNotifications"
|
||||
@change="
|
||||
setImageNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
</template>
|
||||
<template v-if="!isLinux">
|
||||
<simple-switch
|
||||
:label="
|
||||
t(
|
||||
@@ -856,61 +892,30 @@
|
||||
saveOpenVROption();
|
||||
" />
|
||||
</template>
|
||||
<simple-switch
|
||||
:label="
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
|
||||
)
|
||||
"
|
||||
:value="ovrtHudNotifications"
|
||||
@change="
|
||||
setOvrtHudNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
<simple-switch
|
||||
:label="
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
|
||||
)
|
||||
"
|
||||
:value="ovrtWristNotifications"
|
||||
@change="
|
||||
setOvrtWristNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')"
|
||||
:value="imageNotifications"
|
||||
@change="
|
||||
setImageNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
<div class="options-container-item">
|
||||
<span class="name" style="vertical-align: top; padding-top: 10px">{{
|
||||
t('view.settings.notifications.notifications.steamvr_notifications.notification_opacity')
|
||||
}}</span>
|
||||
<el-slider
|
||||
:value="notificationOpacity"
|
||||
@input="setNotificationOpacity"
|
||||
:show-tooltip="false"
|
||||
:min="0"
|
||||
:max="100"
|
||||
show-input
|
||||
style="display: inline-block; width: 300px" />
|
||||
</div>
|
||||
<div class="options-container-item">
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-time"
|
||||
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
|
||||
@click="promptNotificationTimeout"
|
||||
>{{
|
||||
<template v-if="!isLinux">
|
||||
<simple-switch
|
||||
:label="
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.notification_timeout'
|
||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
|
||||
)
|
||||
}}</el-button
|
||||
>
|
||||
</div>
|
||||
"
|
||||
:value="ovrtHudNotifications"
|
||||
@change="
|
||||
setOvrtHudNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
<simple-switch
|
||||
:label="
|
||||
t(
|
||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
|
||||
)
|
||||
"
|
||||
:value="ovrtWristNotifications"
|
||||
@change="
|
||||
setOvrtWristNotifications();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
</template>
|
||||
</div>
|
||||
<!--//- Notifications | Notifications | Desktop Notifications-->
|
||||
<div class="options-container">
|
||||
@@ -1047,7 +1052,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!--//- Wrist Overlay Tab-->
|
||||
<el-tab-pane v-if="!isLinux" lazy :label="t('view.settings.category.wrist_overlay')">
|
||||
<el-tab-pane lazy :label="t('view.settings.category.wrist_overlay')">
|
||||
<!--//- Wrist Overlay | SteamVR Wrist Overlay-->
|
||||
<div class="options-container" style="margin-top: 0">
|
||||
<span class="header">{{ t('view.settings.wrist_overlay.steamvr_wrist_overlay.header') }}</span>
|
||||
@@ -1436,19 +1441,15 @@
|
||||
saveOpenVROption();
|
||||
"></simple-switch>
|
||||
<!--//- Advanced | VRChat Quit Fix-->
|
||||
<template v-if="!isLinux">
|
||||
<span class="sub-header">{{
|
||||
t('view.settings.advanced.advanced.vrchat_quit_fix.header')
|
||||
}}</span>
|
||||
<simple-switch
|
||||
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
|
||||
:value="vrcQuitFix"
|
||||
:long-label="true"
|
||||
@change="
|
||||
setVrcQuitFix();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
</template>
|
||||
<span class="sub-header">{{ t('view.settings.advanced.advanced.vrchat_quit_fix.header') }}</span>
|
||||
<simple-switch
|
||||
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
|
||||
:value="vrcQuitFix"
|
||||
:long-label="true"
|
||||
@change="
|
||||
setVrcQuitFix();
|
||||
saveOpenVROption();
|
||||
" />
|
||||
<!--//- Advanced | Auto Cache Management-->
|
||||
<span class="sub-header">{{
|
||||
t('view.settings.advanced.advanced.auto_cache_management.header')
|
||||
@@ -1528,7 +1529,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!--//- Advanced | Video Progress Pie-->
|
||||
<div v-if="!isLinux" class="options-container">
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.advanced.advanced.video_progress_pie.header') }}</span>
|
||||
<simple-switch
|
||||
:label="t('view.settings.advanced.advanced.video_progress_pie.enable')"
|
||||
|
||||
293
src/vr.js
293
src/vr.js
@@ -23,13 +23,19 @@ import { removeFromArray } from './shared/utils/base/array';
|
||||
import { timeToText } from './shared/utils/base/format';
|
||||
|
||||
import pugTemplate from './vr.pug';
|
||||
import InteropApi from './ipc-electron/interopApi.js';
|
||||
|
||||
Vue.component('marquee-text', MarqueeText);
|
||||
|
||||
(async function () {
|
||||
let $app = {};
|
||||
|
||||
await CefSharp.BindObjectAsync('AppApiVr');
|
||||
if (WINDOWS) {
|
||||
await CefSharp.BindObjectAsync('AppApiVr');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.AppApiVr = InteropApi.AppApiVrElectron;
|
||||
}
|
||||
|
||||
Noty.overrideDefaults({
|
||||
animation: {
|
||||
@@ -82,7 +88,7 @@ Vue.component('marquee-text', MarqueeText);
|
||||
methods: {
|
||||
parse() {
|
||||
this.text = this.location;
|
||||
var L = parseLocation(this.location);
|
||||
const L = parseLocation(this.location);
|
||||
if (L.isOffline) {
|
||||
this.text = 'Offline';
|
||||
} else if (L.isPrivate) {
|
||||
@@ -176,9 +182,12 @@ Vue.component('marquee-text', MarqueeText);
|
||||
watch: {},
|
||||
el: '#root',
|
||||
async mounted() {
|
||||
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
|
||||
await this.applyWineEmojis();
|
||||
workerTimers.setTimeout(() => AppApiVr.VrInit(), 5000);
|
||||
if (WINDOWS) {
|
||||
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
|
||||
await this.applyWineEmojis();
|
||||
} else {
|
||||
this.updateVrElectronLoop();
|
||||
}
|
||||
if (this.appType === '1') {
|
||||
this.refreshCustomScript();
|
||||
this.updateStatsLoop();
|
||||
@@ -189,97 +198,97 @@ Vue.component('marquee-text', MarqueeText);
|
||||
Object.assign($app, app);
|
||||
|
||||
$app.methods.configUpdate = function (json) {
|
||||
this.config = JSON.parse(json);
|
||||
this.hudFeed = [];
|
||||
this.hudTimeout = [];
|
||||
this.setDatetimeFormat();
|
||||
this.setAppLanguage(this.config.appLanguage);
|
||||
this.updateFeedLength();
|
||||
$app.config = JSON.parse(json);
|
||||
$app.hudFeed = [];
|
||||
$app.hudTimeout = [];
|
||||
$app.setDatetimeFormat();
|
||||
$app.setAppLanguage($app.config.appLanguage);
|
||||
$app.updateFeedLength();
|
||||
if (
|
||||
this.config.vrOverlayCpuUsage !== this.cpuUsageEnabled ||
|
||||
this.config.pcUptimeOnFeed !== this.pcUptimeEnabled
|
||||
$app.config.vrOverlayCpuUsage !== $app.cpuUsageEnabled ||
|
||||
$app.config.pcUptimeOnFeed !== $app.pcUptimeEnabled
|
||||
) {
|
||||
this.cpuUsageEnabled = this.config.vrOverlayCpuUsage;
|
||||
this.pcUptimeEnabled = this.config.pcUptimeOnFeed;
|
||||
$app.cpuUsageEnabled = $app.config.vrOverlayCpuUsage;
|
||||
$app.pcUptimeEnabled = $app.config.pcUptimeOnFeed;
|
||||
AppApiVr.ToggleSystemMonitor(
|
||||
this.cpuUsageEnabled || this.pcUptimeEnabled
|
||||
$app.cpuUsageEnabled || $app.pcUptimeEnabled
|
||||
);
|
||||
}
|
||||
if (this.config.notificationOpacity !== this.notificationOpacity) {
|
||||
this.notificationOpacity = this.config.notificationOpacity;
|
||||
this.setNotyOpacity(this.notificationOpacity);
|
||||
if ($app.config.notificationOpacity !== $app.notificationOpacity) {
|
||||
$app.notificationOpacity = $app.config.notificationOpacity;
|
||||
$app.setNotyOpacity($app.notificationOpacity);
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.updateOnlineFriendCount = function (count) {
|
||||
this.onlineFriendCount = parseInt(count, 10);
|
||||
$app.onlineFriendCount = parseInt(count, 10);
|
||||
};
|
||||
|
||||
$app.methods.nowPlayingUpdate = function (json) {
|
||||
this.nowPlaying = JSON.parse(json);
|
||||
if (this.appType === '2') {
|
||||
var circle = document.querySelector('.np-progress-circle-stroke');
|
||||
$app.nowPlaying = JSON.parse(json);
|
||||
if ($app.appType === '2') {
|
||||
const circle = document.querySelector('.np-progress-circle-stroke');
|
||||
if (
|
||||
this.lastLocation.progressPie &&
|
||||
this.nowPlaying.percentage !== 0
|
||||
$app.lastLocation.progressPie &&
|
||||
$app.nowPlaying.percentage !== 0
|
||||
) {
|
||||
circle.style.opacity = 0.5;
|
||||
var circumference = circle.getTotalLength();
|
||||
const circumference = circle.getTotalLength();
|
||||
circle.style.strokeDashoffset =
|
||||
circumference -
|
||||
(this.nowPlaying.percentage / 100) * circumference;
|
||||
($app.nowPlaying.percentage / 100) * circumference;
|
||||
} else {
|
||||
circle.style.opacity = 0;
|
||||
}
|
||||
}
|
||||
this.updateFeedLength();
|
||||
$app.updateFeedLength();
|
||||
};
|
||||
|
||||
$app.methods.lastLocationUpdate = function (json) {
|
||||
this.lastLocation = JSON.parse(json);
|
||||
$app.lastLocation = JSON.parse(json);
|
||||
};
|
||||
|
||||
$app.methods.wristFeedUpdate = function (json) {
|
||||
this.wristFeed = JSON.parse(json);
|
||||
this.updateFeedLength();
|
||||
$app.wristFeed = JSON.parse(json);
|
||||
$app.updateFeedLength();
|
||||
};
|
||||
|
||||
$app.methods.updateFeedLength = function () {
|
||||
if (this.appType === '2' || this.wristFeed.length === 0) {
|
||||
if ($app.appType === '2' || $app.wristFeed.length === 0) {
|
||||
return;
|
||||
}
|
||||
var length = 16;
|
||||
if (!this.config.hideDevicesFromFeed) {
|
||||
let length = 16;
|
||||
if (!$app.config.hideDevicesFromFeed) {
|
||||
length -= 2;
|
||||
if (this.deviceCount > 8) {
|
||||
if ($app.deviceCount > 8) {
|
||||
length -= 1;
|
||||
}
|
||||
}
|
||||
if (this.nowPlaying.playing) {
|
||||
if ($app.nowPlaying.playing) {
|
||||
length -= 1;
|
||||
}
|
||||
if (length < this.wristFeed.length) {
|
||||
this.wristFeed.length = length;
|
||||
if (length < $app.wristFeed.length) {
|
||||
$app.wristFeed.length = length;
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.refreshCustomScript = function () {
|
||||
$app.methods.refreshCustomScript = async function () {
|
||||
if (document.contains(document.getElementById('vr-custom-script'))) {
|
||||
document.getElementById('vr-custom-script').remove();
|
||||
}
|
||||
AppApiVr.CustomVrScriptPath().then((customScript) => {
|
||||
var head = document.head;
|
||||
if (customScript) {
|
||||
var $vrCustomScript = document.createElement('script');
|
||||
$vrCustomScript.setAttribute('id', 'vr-custom-script');
|
||||
$vrCustomScript.src = `file://${customScript}?_=${Date.now()}`;
|
||||
head.appendChild($vrCustomScript);
|
||||
}
|
||||
});
|
||||
const customScript = await AppApiVr.CustomVrScript();
|
||||
if (customScript) {
|
||||
const head = document.head;
|
||||
const $vrCustomScript = document.createElement('script');
|
||||
$vrCustomScript.setAttribute('id', 'vr-custom-script');
|
||||
$vrCustomScript.type = 'text/javascript';
|
||||
$vrCustomScript.textContent = customScript;
|
||||
head.appendChild($vrCustomScript);
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.setNotyOpacity = function (value) {
|
||||
var opacity = parseFloat(value / 100).toFixed(2);
|
||||
const opacity = parseFloat(value / 100).toFixed(2);
|
||||
let element = document.getElementById('noty-opacity');
|
||||
if (!element) {
|
||||
document.body.insertAdjacentHTML(
|
||||
@@ -293,43 +302,43 @@ Vue.component('marquee-text', MarqueeText);
|
||||
|
||||
$app.methods.updateStatsLoop = async function () {
|
||||
try {
|
||||
this.currentTime = new Date()
|
||||
.toLocaleDateString(this.currentCulture, {
|
||||
$app.currentTime = new Date()
|
||||
.toLocaleDateString($app.currentCulture, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hourCycle: this.config.dtHour12 ? 'h12' : 'h23'
|
||||
hourCycle: $app.config.dtHour12 ? 'h12' : 'h23'
|
||||
})
|
||||
.replace(' AM', ' am')
|
||||
.replace(' PM', ' pm')
|
||||
.replace(',', '');
|
||||
|
||||
if (this.cpuUsageEnabled) {
|
||||
var cpuUsage = await AppApiVr.CpuUsage();
|
||||
this.cpuUsage = cpuUsage.toFixed(0);
|
||||
if ($app.cpuUsageEnabled) {
|
||||
const cpuUsage = await AppApiVr.CpuUsage();
|
||||
$app.cpuUsage = cpuUsage.toFixed(0);
|
||||
}
|
||||
if (this.lastLocation.date !== 0) {
|
||||
this.lastLocationTimer = timeToText(
|
||||
Date.now() - this.lastLocation.date
|
||||
if ($app.lastLocation.date !== 0) {
|
||||
$app.lastLocationTimer = timeToText(
|
||||
Date.now() - $app.lastLocation.date
|
||||
);
|
||||
} else {
|
||||
this.lastLocationTimer = '';
|
||||
$app.lastLocationTimer = '';
|
||||
}
|
||||
if (this.lastLocation.onlineFor) {
|
||||
this.onlineForTimer = timeToText(
|
||||
Date.now() - this.lastLocation.onlineFor
|
||||
if ($app.lastLocation.onlineFor) {
|
||||
$app.onlineForTimer = timeToText(
|
||||
Date.now() - $app.lastLocation.onlineFor
|
||||
);
|
||||
} else {
|
||||
this.onlineForTimer = '';
|
||||
$app.onlineForTimer = '';
|
||||
}
|
||||
|
||||
if (!this.config.hideDevicesFromFeed) {
|
||||
if (!$app.config.hideDevicesFromFeed) {
|
||||
AppApiVr.GetVRDevices().then((devices) => {
|
||||
var deviceList = [];
|
||||
var baseStations = 0;
|
||||
let deviceList = [];
|
||||
let baseStations = 0;
|
||||
devices.forEach((device) => {
|
||||
device[3] = parseInt(device[3], 10);
|
||||
if (device[0] === 'base' && device[1] === 'connected') {
|
||||
@@ -338,7 +347,7 @@ Vue.component('marquee-text', MarqueeText);
|
||||
deviceList.push(device);
|
||||
}
|
||||
});
|
||||
this.deviceCount = deviceList.length;
|
||||
$app.deviceCount = deviceList.length;
|
||||
const deviceValue = (dev) => {
|
||||
if (dev[0] === 'headset') return 0;
|
||||
if (dev[0] === 'leftController') return 1;
|
||||
@@ -368,38 +377,89 @@ Vue.component('marquee-text', MarqueeText);
|
||||
'',
|
||||
baseStations
|
||||
]);
|
||||
this.deviceCount += 1;
|
||||
$app.deviceCount += 1;
|
||||
}
|
||||
this.devices = deviceList;
|
||||
$app.devices = deviceList;
|
||||
});
|
||||
} else {
|
||||
this.devices = [];
|
||||
$app.devices = [];
|
||||
}
|
||||
if (this.config.pcUptimeOnFeed) {
|
||||
if ($app.config.pcUptimeOnFeed) {
|
||||
AppApiVr.GetUptime().then((uptime) => {
|
||||
if (uptime) {
|
||||
this.pcUptime = timeToText(uptime);
|
||||
$app.pcUptime = timeToText(uptime);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.pcUptime = '';
|
||||
$app.pcUptime = '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
workerTimers.setTimeout(() => this.updateStatsLoop(), 500);
|
||||
workerTimers.setTimeout(() => $app.updateStatsLoop(), 500);
|
||||
};
|
||||
|
||||
$app.methods.updateVrElectronLoop = async function () {
|
||||
try {
|
||||
if ($app.appType === '1') {
|
||||
const wristOverlayQueue =
|
||||
await AppApiVr.GetExecuteVrFeedFunctionQueue();
|
||||
if (wristOverlayQueue) {
|
||||
wristOverlayQueue.forEach((item) => {
|
||||
// item[0] is the function name, item[1] is already an object
|
||||
const fullFunctionName = item[0];
|
||||
const jsonArg = item[1];
|
||||
|
||||
if (
|
||||
typeof window.$app === 'object' &&
|
||||
typeof window.$app[fullFunctionName] === 'function'
|
||||
) {
|
||||
window.$app[fullFunctionName](jsonArg);
|
||||
} else {
|
||||
console.error(
|
||||
`$app.${fullFunctionName} is not defined or is not a function`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const hmdOverlayQueue =
|
||||
await AppApiVr.GetExecuteVrOverlayFunctionQueue();
|
||||
if (hmdOverlayQueue) {
|
||||
hmdOverlayQueue.forEach((item) => {
|
||||
// item[0] is the function name, item[1] is already an object
|
||||
const fullFunctionName = item[0];
|
||||
const jsonArg = item[1];
|
||||
|
||||
if (
|
||||
typeof window.$app === 'object' &&
|
||||
typeof window.$app[fullFunctionName] === 'function'
|
||||
) {
|
||||
window.$app[fullFunctionName](jsonArg);
|
||||
} else {
|
||||
console.error(
|
||||
`$app.${fullFunctionName} is not defined or is not a function`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
workerTimers.setTimeout(() => $app.updateVrElectronLoop(), 500);
|
||||
};
|
||||
|
||||
$app.methods.playNoty = function (json) {
|
||||
var { noty, message, image } = JSON.parse(json);
|
||||
let { noty, message, image } = JSON.parse(json);
|
||||
if (typeof noty === 'undefined') {
|
||||
console.error('noty is undefined');
|
||||
return;
|
||||
}
|
||||
var noty = escapeTagRecursive(noty);
|
||||
var message = escapeTag(message) || '';
|
||||
var text = '';
|
||||
var img = '';
|
||||
noty = escapeTagRecursive(noty);
|
||||
message = escapeTag(message) || '';
|
||||
let text = '';
|
||||
let img = '';
|
||||
if (image) {
|
||||
img = `<img class="noty-img" src="${image}"></img>`;
|
||||
}
|
||||
@@ -423,7 +483,7 @@ Vue.component('marquee-text', MarqueeText);
|
||||
)}`;
|
||||
break;
|
||||
case 'Online':
|
||||
var locationName = '';
|
||||
let locationName = '';
|
||||
if (noty.worldName) {
|
||||
locationName = ` to ${displayLocation(
|
||||
noty.location,
|
||||
@@ -444,7 +504,8 @@ Vue.component('marquee-text', MarqueeText);
|
||||
noty.senderUsername
|
||||
}</strong> has invited you to ${displayLocation(
|
||||
noty.details.worldId,
|
||||
noty.details.worldName
|
||||
noty.details.worldName,
|
||||
''
|
||||
)}${message}`;
|
||||
break;
|
||||
case 'requestInvite':
|
||||
@@ -556,16 +617,16 @@ Vue.component('marquee-text', MarqueeText);
|
||||
if (text) {
|
||||
new Noty({
|
||||
type: 'alert',
|
||||
theme: this.config.notificationTheme,
|
||||
timeout: this.config.notificationTimeout,
|
||||
layout: this.config.notificationPosition,
|
||||
theme: $app.config.notificationTheme,
|
||||
timeout: $app.config.notificationTimeout,
|
||||
layout: $app.config.notificationPosition,
|
||||
text: `${img}<div class="noty-text">${text}</div>`
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.statusClass = function (status) {
|
||||
var style = {};
|
||||
let style = {};
|
||||
if (typeof status === 'undefined') {
|
||||
return style;
|
||||
}
|
||||
@@ -593,56 +654,56 @@ Vue.component('marquee-text', MarqueeText);
|
||||
$app.data.cleanHudFeedLoopStatus = false;
|
||||
|
||||
$app.methods.cleanHudFeedLoop = function () {
|
||||
if (!this.cleanHudFeedLoopStatus) {
|
||||
if (!$app.cleanHudFeedLoopStatus) {
|
||||
return;
|
||||
}
|
||||
this.cleanHudFeed();
|
||||
if (this.hudFeed.length === 0) {
|
||||
this.cleanHudFeedLoopStatus = false;
|
||||
$app.cleanHudFeed();
|
||||
if ($app.hudFeed.length === 0) {
|
||||
$app.cleanHudFeedLoopStatus = false;
|
||||
return;
|
||||
}
|
||||
workerTimers.setTimeout(() => this.cleanHudFeedLoop(), 500);
|
||||
workerTimers.setTimeout(() => $app.cleanHudFeedLoop(), 500);
|
||||
};
|
||||
|
||||
$app.methods.cleanHudFeed = function () {
|
||||
var dt = Date.now();
|
||||
this.hudFeed.forEach((item) => {
|
||||
if (item.time + this.config.photonOverlayMessageTimeout < dt) {
|
||||
removeFromArray(this.hudFeed, item);
|
||||
const dt = Date.now();
|
||||
$app.hudFeed.forEach((item) => {
|
||||
if (item.time + $app.config.photonOverlayMessageTimeout < dt) {
|
||||
removeFromArray($app.hudFeed, item);
|
||||
}
|
||||
});
|
||||
if (this.hudFeed.length > 10) {
|
||||
this.hudFeed.length = 10;
|
||||
if ($app.hudFeed.length > 10) {
|
||||
$app.hudFeed.length = 10;
|
||||
}
|
||||
if (!this.cleanHudFeedLoopStatus) {
|
||||
this.cleanHudFeedLoopStatus = true;
|
||||
this.cleanHudFeedLoop();
|
||||
if (!$app.cleanHudFeedLoopStatus) {
|
||||
$app.cleanHudFeedLoopStatus = true;
|
||||
$app.cleanHudFeedLoop();
|
||||
}
|
||||
};
|
||||
|
||||
$app.methods.addEntryHudFeed = function (json) {
|
||||
var data = JSON.parse(json);
|
||||
var combo = 1;
|
||||
this.hudFeed.forEach((item) => {
|
||||
const data = JSON.parse(json);
|
||||
let combo = 1;
|
||||
$app.hudFeed.forEach((item) => {
|
||||
if (
|
||||
item.displayName === data.displayName &&
|
||||
item.text === data.text
|
||||
) {
|
||||
combo = item.combo + 1;
|
||||
removeFromArray(this.hudFeed, item);
|
||||
removeFromArray($app.hudFeed, item);
|
||||
}
|
||||
});
|
||||
this.hudFeed.unshift({
|
||||
$app.hudFeed.unshift({
|
||||
time: Date.now(),
|
||||
combo,
|
||||
...data
|
||||
});
|
||||
this.cleanHudFeed();
|
||||
$app.cleanHudFeed();
|
||||
};
|
||||
|
||||
$app.methods.updateHudFeedTag = function (json) {
|
||||
var ref = JSON.parse(json);
|
||||
this.hudFeed.forEach((item) => {
|
||||
const ref = JSON.parse(json);
|
||||
$app.hudFeed.forEach((item) => {
|
||||
if (item.userId === ref.userId) {
|
||||
item.colour = ref.colour;
|
||||
}
|
||||
@@ -652,16 +713,16 @@ Vue.component('marquee-text', MarqueeText);
|
||||
$app.data.hudTimeout = [];
|
||||
|
||||
$app.methods.updateHudTimeout = function (json) {
|
||||
this.hudTimeout = JSON.parse(json);
|
||||
$app.hudTimeout = JSON.parse(json);
|
||||
};
|
||||
|
||||
$app.methods.setDatetimeFormat = async function () {
|
||||
this.currentCulture = await AppApiVr.CurrentCulture();
|
||||
var formatDate = function (date) {
|
||||
$app.currentCulture = await AppApiVr.CurrentCulture();
|
||||
const formatDate = function (date) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
var dt = new Date(date);
|
||||
const dt = new Date(date);
|
||||
return dt
|
||||
.toLocaleTimeString($app.currentCulture, {
|
||||
hour: '2-digit',
|
||||
@@ -678,9 +739,9 @@ Vue.component('marquee-text', MarqueeText);
|
||||
if (!appLanguage) {
|
||||
return;
|
||||
}
|
||||
if (appLanguage !== this.appLanguage) {
|
||||
this.appLanguage = appLanguage;
|
||||
i18n.locale = this.appLanguage;
|
||||
if (appLanguage !== $app.appLanguage) {
|
||||
$app.appLanguage = appLanguage;
|
||||
i18n.locale = $app.appLanguage;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -688,8 +749,8 @@ Vue.component('marquee-text', MarqueeText);
|
||||
if (document.contains(document.getElementById('app-emoji-font'))) {
|
||||
document.getElementById('app-emoji-font').remove();
|
||||
}
|
||||
if (this.isRunningUnderWine) {
|
||||
var $appEmojiFont = document.createElement('link');
|
||||
if ($app.isRunningUnderWine) {
|
||||
const $appEmojiFont = document.createElement('link');
|
||||
$appEmojiFont.setAttribute('id', 'app-emoji-font');
|
||||
$appEmojiFont.rel = 'stylesheet';
|
||||
$appEmojiFont.href = 'emoji.font.css';
|
||||
|
||||
17
src/vr.scss
17
src/vr.scss
@@ -8,7 +8,7 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
//
|
||||
|
||||
@use "./assets/scss/flags.scss";
|
||||
@use './assets/scss/flags.scss';
|
||||
|
||||
@import '~animate.css/animate.min.css';
|
||||
@import '~noty/lib/noty.css';
|
||||
@@ -20,6 +20,10 @@
|
||||
손등 18px -> 나나 24
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.noty_body {
|
||||
display: block;
|
||||
}
|
||||
@@ -171,13 +175,20 @@
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ellipsis-font';
|
||||
src: local('Times New Roman');
|
||||
unicode-range: U+2026;
|
||||
}
|
||||
|
||||
body,
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
button {
|
||||
font-family: 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', 'Noto Sans SC',
|
||||
'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
|
||||
font-family:
|
||||
'ellipsis-font', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC',
|
||||
'Noto Sans SC', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
|
||||
line-height: normal;
|
||||
text-shadow:
|
||||
#000 0px 0px 3px,
|
||||
|
||||
Reference in New Issue
Block a user