mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 23:03:51 +02:00
Electron support for Linux (#1074)
* init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Fix UI var * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * Test updater * Rebase and handle merge conflicts * Fix Linux updater * Fix Linux app restart * Fix friend order * Handle AppImageInstaller, show an install message on Linux * Updates to the AppImage installer * Fix Linux updater, fix set version, check for .NET, copy wine prefix * Handle random errors * Rotate tall prints * try fix Linux restart bug * Final --------- Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
148
Dotnet/AppApi/Common/AppApiCommon.cs
Normal file
148
Dotnet/AppApi/Common/AppApiCommon.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using librsync.net;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly MD5 _hasher = MD5.Create();
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public string MD5File(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length);
|
||||
using var md5 = MD5.Create();
|
||||
var md5Hash = md5.ComputeHash(fileData);
|
||||
return Convert.ToBase64String(md5Hash);
|
||||
}
|
||||
|
||||
public int GetColourFromUserID(string userId)
|
||||
{
|
||||
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
|
||||
return (hash[3] << 8) | hash[4];
|
||||
}
|
||||
|
||||
public string SignFile(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(blob);
|
||||
using var sig = Librsync.ComputeSignature(new MemoryStream(fileData));
|
||||
using var memoryStream = new MemoryStream();
|
||||
sig.CopyTo(memoryStream);
|
||||
var sigBytes = memoryStream.ToArray();
|
||||
return Convert.ToBase64String(sigBytes);
|
||||
}
|
||||
|
||||
public string FileLength(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(blob);
|
||||
return fileData.Length.ToString();
|
||||
}
|
||||
|
||||
public void OpenLink(string url)
|
||||
{
|
||||
if (url.StartsWith("http://") ||
|
||||
url.StartsWith("https://"))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(url)
|
||||
{
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void IPCAnnounceStart()
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VRCXLaunch",
|
||||
MsgType = "VRCXLaunch"
|
||||
});
|
||||
}
|
||||
|
||||
public void SendIpc(string type, string data)
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VrcxMessage",
|
||||
MsgType = type,
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
|
||||
public string CustomCssPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public string CustomScriptPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
public string CurrentLanguage()
|
||||
{
|
||||
return CultureInfo.InstalledUICulture.Name;
|
||||
}
|
||||
|
||||
public string GetVersion()
|
||||
{
|
||||
return Program.Version;
|
||||
}
|
||||
|
||||
public bool VrcClosedGracefully()
|
||||
{
|
||||
return LogWatcher.Instance.VrcClosedGracefully;
|
||||
}
|
||||
|
||||
public Dictionary<string, int> GetColourBulk(List<object> userIds)
|
||||
{
|
||||
var output = new Dictionary<string, int>();
|
||||
foreach (string userId in userIds)
|
||||
{
|
||||
output.Add(userId, GetColourFromUserID(userId));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public void SetAppLauncherSettings(bool enabled, bool killOnExit)
|
||||
{
|
||||
AutoAppLaunchManager.Instance.Enabled = enabled;
|
||||
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
|
||||
}
|
||||
|
||||
public string GetFileBase64(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return Convert.ToBase64String(File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Dotnet/AppApi/Common/AppApiCommonBase.cs
Normal file
77
Dotnet/AppApi/Common/AppApiCommonBase.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public abstract partial class AppApi
|
||||
{
|
||||
// AppApi
|
||||
public abstract void ShowDevTools();
|
||||
public abstract void DeleteAllCookies();
|
||||
public abstract void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand);
|
||||
public abstract void RefreshVR();
|
||||
public abstract void RestartVR();
|
||||
public abstract void SetZoom(double zoomLevel);
|
||||
public abstract Task<double> GetZoom();
|
||||
public abstract void DesktopNotification(string BoldText, string Text = "", string Image = "");
|
||||
|
||||
public abstract void RestartApplication(bool isUpgrade);
|
||||
public abstract bool CheckForUpdateExe();
|
||||
public abstract void ExecuteAppFunction(string function, string json);
|
||||
public abstract void ExecuteVrFeedFunction(string function, string json);
|
||||
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
||||
public abstract string GetLaunchCommand();
|
||||
public abstract void FocusWindow();
|
||||
public abstract void ChangeTheme(int value);
|
||||
public abstract void DoFunny();
|
||||
public abstract string GetClipboard();
|
||||
public abstract void SetStartup(bool enabled);
|
||||
public abstract void CopyImageToClipboard(string path);
|
||||
public abstract void FlashWindow();
|
||||
public abstract void SetUserAgent();
|
||||
public abstract bool IsRunningUnderWine();
|
||||
|
||||
// Folders
|
||||
public abstract string GetVRChatAppDataLocation();
|
||||
public abstract string GetVRChatPhotosLocation();
|
||||
public abstract string GetUGCPhotoLocation(string path = "");
|
||||
public abstract string GetVRChatScreenshotsLocation();
|
||||
public abstract string GetVRChatCacheLocation();
|
||||
public abstract bool OpenVrcxAppDataFolder();
|
||||
public abstract bool OpenVrcAppDataFolder();
|
||||
public abstract bool OpenVrcPhotosFolder();
|
||||
public abstract bool OpenUGCPhotosFolder(string ugcPath = "");
|
||||
public abstract bool OpenVrcScreenshotsFolder();
|
||||
public abstract bool OpenCrashVrcCrashDumps();
|
||||
public abstract void OpenShortcutFolder();
|
||||
public abstract void OpenFolderAndSelectItem(string path, bool isFolder = false);
|
||||
public abstract Task<string> OpenFolderSelectorDialog(string defaultPath = "");
|
||||
|
||||
public abstract Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "",
|
||||
string defaultFilter = "All files (*.*)|*.*");
|
||||
|
||||
// GameHandler
|
||||
public abstract void OnProcessStateChanged(MonitoredProcess monitoredProcess);
|
||||
public abstract void CheckGameRunning();
|
||||
public abstract bool IsGameRunning();
|
||||
public abstract bool IsSteamVRRunning();
|
||||
public abstract int QuitGame();
|
||||
public abstract bool StartGame(string arguments);
|
||||
public abstract bool StartGameFromPath(string path, string arguments);
|
||||
|
||||
// RegistryPlayerPrefs
|
||||
public abstract object GetVRChatRegistryKey(string key);
|
||||
public abstract string GetVRChatRegistryKeyString(string key);
|
||||
public abstract bool SetVRChatRegistryKey(string key, object value, int typeInt);
|
||||
public abstract void SetVRChatRegistryKey(string key, byte[] value);
|
||||
public abstract Dictionary<string, Dictionary<string, object>> GetVRChatRegistry();
|
||||
public abstract void SetVRChatRegistry(string json);
|
||||
public abstract bool HasVRChatRegistryFolder();
|
||||
public abstract void DeleteVRChatRegistryFolder();
|
||||
public abstract string ReadVrcRegJsonFile(string filepath);
|
||||
|
||||
// Screenshot
|
||||
public abstract string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false);
|
||||
}
|
||||
}
|
||||
237
Dotnet/AppApi/Common/ImageSaving.cs
Normal file
237
Dotnet/AppApi/Common/ImageSaving.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public async Task<string> GetImage(string url, string fileId, string version)
|
||||
{
|
||||
return await ImageCache.GetImage(url, fileId, version);
|
||||
}
|
||||
|
||||
public string ResizeImageToFitLimits(string base64data)
|
||||
{
|
||||
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false));
|
||||
}
|
||||
|
||||
public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000)
|
||||
{
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
// for APNG, check if image is png format and less than maxSize
|
||||
if ((!matchingDimensions || image.Width == image.Height) &&
|
||||
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
|
||||
imageData.Length < maxSize &&
|
||||
image.Width <= maxWidth &&
|
||||
image.Height <= maxHeight)
|
||||
{
|
||||
return imageData;
|
||||
}
|
||||
|
||||
if (image.Width > maxWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)maxWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, maxWidth, newHeight);
|
||||
}
|
||||
if (image.Height > maxHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)maxHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, maxHeight);
|
||||
}
|
||||
if (matchingDimensions && image.Width != image.Height)
|
||||
{
|
||||
var newSize = Math.Max(image.Width, image.Height);
|
||||
var newImage = new Bitmap(newSize, newSize);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.Transparent);
|
||||
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
}
|
||||
|
||||
SaveToFileToUpload();
|
||||
for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
|
||||
{
|
||||
SaveToFileToUpload();
|
||||
if (imageData.Length < maxSize)
|
||||
break;
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
if (image.Width > image.Height)
|
||||
{
|
||||
newWidth = image.Width - 25;
|
||||
newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
newHeight = image.Height - 25;
|
||||
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
|
||||
}
|
||||
image = new Bitmap(image, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (imageData.Length > maxSize)
|
||||
{
|
||||
throw new Exception("Failed to get image into target filesize.");
|
||||
}
|
||||
|
||||
return imageData;
|
||||
|
||||
void SaveToFileToUpload()
|
||||
{
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
imageData = imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ResizePrintImage(byte[] imageData)
|
||||
{
|
||||
const int desiredWidth = 1920;
|
||||
const int desiredHeight = 1080;
|
||||
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
if (image.Height > image.Width)
|
||||
image.RotateFlip(RotateFlipType.Rotate90FlipNone);
|
||||
|
||||
// increase size to 1920x1080
|
||||
if (image.Width < desiredWidth || image.Height < desiredHeight)
|
||||
{
|
||||
var newHeight = image.Height;
|
||||
var newWidth = image.Width;
|
||||
if (image.Width < desiredWidth)
|
||||
{
|
||||
var testHeight = (int)Math.Round(image.Height / (image.Width / (double)desiredWidth));
|
||||
if (testHeight <= desiredHeight)
|
||||
{
|
||||
newWidth = desiredWidth;
|
||||
newHeight = testHeight;
|
||||
}
|
||||
}
|
||||
if (image.Height < desiredHeight)
|
||||
{
|
||||
var testWidth = (int)Math.Round(image.Width / (image.Height / (double)desiredHeight));
|
||||
if (testWidth <= desiredWidth)
|
||||
{
|
||||
newHeight = desiredHeight;
|
||||
newWidth = testWidth;
|
||||
}
|
||||
}
|
||||
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
|
||||
using var graphics1 = Graphics.FromImage(resizedImage);
|
||||
graphics1.Clear(Color.White);
|
||||
var x = (desiredWidth - newWidth) / 2;
|
||||
var y = (desiredHeight - newHeight) / 2;
|
||||
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight));
|
||||
image.Dispose();
|
||||
image = resizedImage;
|
||||
}
|
||||
|
||||
// limit size to 1920x1080
|
||||
if (image.Width > desiredWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)desiredWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, desiredWidth, newHeight);
|
||||
}
|
||||
if (image.Height > desiredHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)desiredHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, desiredHeight);
|
||||
}
|
||||
|
||||
// add white border
|
||||
// wtf are these magic numbers
|
||||
const int xOffset = 64; // 2048 / 32
|
||||
const int yOffset = 69; // 1440 / 20.869
|
||||
var newImage = new Bitmap(2048, 1440);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.White);
|
||||
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
|
||||
var newX = (2048 - image.Width) / 2;
|
||||
var newY = yOffset;
|
||||
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
return imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
|
||||
public async Task CropAllPrints(string ugcFolderPath)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
|
||||
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
await CropPrintImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CropPrintImage(string path)
|
||||
{
|
||||
var tempPath = path + ".temp";
|
||||
var bytes = await File.ReadAllBytesAsync(path);
|
||||
var ms = new MemoryStream(bytes);
|
||||
Bitmap print = new Bitmap(ms);
|
||||
// validation step to ensure image is actually a print
|
||||
if (print.Width != 2048 || print.Height != 1440)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var point = new Point(64, 69);
|
||||
var size = new Size(1920, 1080);
|
||||
var rectangle = new Rectangle(point, size);
|
||||
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
|
||||
cropped.Save(tempPath);
|
||||
if (ScreenshotHelper.HasTXt(path))
|
||||
{
|
||||
var success = ScreenshotHelper.CopyTXt(path, tempPath);
|
||||
if (!success)
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File.Move(tempPath, path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
|
||||
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Dotnet/AppApi/Common/LocalPlayerModerations.cs
Normal file
92
Dotnet/AppApi/Common/LocalPlayerModerations.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
|
||||
{
|
||||
// 004 = hideAvatar
|
||||
// 005 = showAvatar
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var output = new Dictionary<string, short>();
|
||||
using var reader = new StreamReader(filePath);
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
var userId = line.Substring(0, index);
|
||||
var type = short.Parse(line.Substring(line.Length - 3));
|
||||
output.Add(userId, type);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public short GetVRChatUserModeration(string currentUserId, string userId)
|
||||
{
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return 0;
|
||||
|
||||
using var reader = new StreamReader(filePath);
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
if (userId == line.Substring(0, index))
|
||||
{
|
||||
return short.Parse(line.Substring(line.Length - 3));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool SetVRChatUserModeration(string currentUserId, string userId, int type)
|
||||
{
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
var lines = File.ReadAllLines(filePath).ToList();
|
||||
var index = lines.FindIndex(x => x.StartsWith(userId));
|
||||
if (index >= 0)
|
||||
lines.RemoveAt(index);
|
||||
|
||||
if (type != 0)
|
||||
{
|
||||
var sb = new StringBuilder(userId);
|
||||
while (sb.Length < 64)
|
||||
sb.Append(' ');
|
||||
|
||||
sb.Append(type.ToString("000"));
|
||||
lines.Add(sb.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(filePath, lines);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
162
Dotnet/AppApi/Common/OVRToolkit.cs
Normal file
162
Dotnet/AppApi/Common/OVRToolkit.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using Websocket.Client;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
private static readonly Uri _ovrtWebsocketUri = new("ws://127.0.0.1:11450/api");
|
||||
private static readonly byte[] _vrcxIcon = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHaGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1NTE2MWIyMi1hYzgxLTY3NDYtODAyYi1kODIzYWFmN2RjYjciIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3ZjJjNTA2ZS02YTVhLWRhNGEtOTg5Mi02NDZiMzQ0MGQxZTgiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6NmJmOGE5MTgtY2QzZS03OTRjLTk3NzktMzM0YjYwZWJiNTYyPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N2YyYzUwNmUtNmE1YS1kYTRhLTk4OTItNjQ2YjM0NDBkMWU4IiBzdEV2dDp3aGVuPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJhM2ZjODI3LTM0ZjQtYjU0OC05ZGFiLTZhMTZlZmQzZjAxMSIgc3RFdnQ6d2hlbj0iMjAyMS0wNC0wOFQxNTowMTozMSsxMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHN0RXZ0OndoZW49IjIwMjEtMDQtMDhUMTY6MzM6MTArMTI6MDAiIHN0RXZ0OnNv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");
|
||||
private static readonly object _ovrtLock = new();
|
||||
private static WebsocketClient _ovrtWebsocketClient;
|
||||
|
||||
private static void InitializeOvrTk()
|
||||
{
|
||||
lock (_ovrtLock)
|
||||
{
|
||||
if (_ovrtWebsocketClient != null)
|
||||
return;
|
||||
|
||||
var dotnetWebsocketClientFactory = new Func<ClientWebSocket>(() =>
|
||||
{
|
||||
var client = new ClientWebSocket
|
||||
{
|
||||
Options =
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(5),
|
||||
}
|
||||
};
|
||||
client.Options.SetRequestHeader("user-agent", Program.Version);
|
||||
return client;
|
||||
});
|
||||
|
||||
_ovrtWebsocketClient = new WebsocketClient(_ovrtWebsocketUri, dotnetWebsocketClientFactory)
|
||||
{
|
||||
Name = "OVRToolkit Websocket",
|
||||
// Swap ReconnectTimeout when Ping is implemented
|
||||
// ReconnectTimeout = TimeSpan.FromSeconds(20),
|
||||
ReconnectTimeout = null,
|
||||
ErrorReconnectTimeout = TimeSpan.FromSeconds(30),
|
||||
};
|
||||
|
||||
_ovrtWebsocketClient.ReconnectionHappened.Subscribe(info =>
|
||||
{
|
||||
logger.ConditionalDebug("[OVRToolkit Websocket] Reconnection happened, type: {0}", info?.Type.ToString());
|
||||
});
|
||||
_ovrtWebsocketClient.DisconnectionHappened.Subscribe(info =>
|
||||
{
|
||||
logger.ConditionalDebug("[OVRToolkit Websocket] Disconnection happened, type: {0}", info?.Type.ToString());
|
||||
});
|
||||
_ovrtWebsocketClient.MessageReceived.Subscribe(msg =>
|
||||
{
|
||||
logger.ConditionalDebug("[OVRToolkit Websocket] Message received: {0}", msg.Text);
|
||||
});
|
||||
|
||||
_ovrtWebsocketClient.Start().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendMessages(IEnumerable<OvrtMessage> ovrtMessages)
|
||||
{
|
||||
if (ovrtMessages != null && ovrtMessages.Any())
|
||||
{
|
||||
if (_ovrtWebsocketClient == null)
|
||||
InitializeOvrTk();
|
||||
|
||||
if (_ovrtWebsocketClient.IsRunning)
|
||||
{
|
||||
foreach (var message in ovrtMessages)
|
||||
{
|
||||
_ovrtWebsocketClient.Send(JsonSerializer.Serialize(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an OVRToolkit notification with the specified title and body.
|
||||
/// HUD notification - Visible in the lower part of the HMD view and moves with the head.
|
||||
/// Wrist notification - Visible above the wristwatch until cleared by the user via the 'X' icon.
|
||||
/// </summary>
|
||||
/// <param name="hudNotification">Whether or not to display a HUD notification.</param>
|
||||
/// <param name="wristNotification">Whether or not to display a Wrist notification.</param>
|
||||
/// <param name="title">The title of the notification.</param>
|
||||
/// <param name="body">The content of the notification.</param>
|
||||
/// <param name="timeout">[CURRENTLY UNUSED]The timeout of the notification.</param>
|
||||
/// <param name="image">The image of the notification.</param>
|
||||
public void OVRTNotification(bool hudNotification, bool wristNotification, string title, string body, int timeout, string image = "")
|
||||
{
|
||||
List<OvrtMessage> messages = [];
|
||||
|
||||
byte[] imageBytes;
|
||||
if(!string.IsNullOrWhiteSpace(image) && File.Exists(image))
|
||||
{
|
||||
imageBytes = File.ReadAllBytes(image);
|
||||
}
|
||||
else
|
||||
{
|
||||
imageBytes = _vrcxIcon;
|
||||
}
|
||||
|
||||
if (wristNotification)
|
||||
{
|
||||
messages.Add(new OvrtMessage
|
||||
{
|
||||
MessageType = "SendWristNotification",
|
||||
Json = JsonSerializer.Serialize(new OvrtWristNotificationMessage
|
||||
{
|
||||
Body = title + " - " + body
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (hudNotification)
|
||||
{
|
||||
messages.Add(new OvrtMessage
|
||||
{
|
||||
MessageType = "SendNotification",
|
||||
Json = JsonSerializer.Serialize(new OvrtHudNotificationMessage
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
Icon = imageBytes
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
SendMessages(messages);
|
||||
}
|
||||
|
||||
private struct OvrtMessage
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("messageType")]
|
||||
public string MessageType { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("json")]
|
||||
public string Json { get; set; }
|
||||
}
|
||||
|
||||
private struct OvrtHudNotificationMessage
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("icon")]
|
||||
public byte[] Icon { get; set; }
|
||||
}
|
||||
|
||||
private struct OvrtWristNotificationMessage
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
130
Dotnet/AppApi/Common/Screenshot.cs
Normal file
130
Dotnet/AppApi/Common/Screenshot.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
public string GetExtraScreenshotData(string path, bool carouselCache)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
var metadata = new JObject();
|
||||
|
||||
if (!File.Exists(path) || !path.EndsWith(".png"))
|
||||
return null;
|
||||
|
||||
var files = Directory.GetFiles(Path.GetDirectoryName(path), "*.png");
|
||||
|
||||
// Add previous/next file paths to metadata so the screenshot viewer carousel can request metadata for next/previous images in directory
|
||||
if (carouselCache)
|
||||
{
|
||||
var index = Array.IndexOf(files, path);
|
||||
if (index > 0)
|
||||
{
|
||||
metadata.Add("previousFilePath", files[index - 1]);
|
||||
}
|
||||
if (index < files.Length - 1)
|
||||
{
|
||||
metadata.Add("nextFilePath", files[index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
metadata.Add("fileResolution", ScreenshotHelper.ReadPNGResolution(path));
|
||||
|
||||
var creationDate = File.GetCreationTime(path);
|
||||
metadata.Add("creationDate", creationDate.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
var fileSizeBytes = new FileInfo(path).Length;
|
||||
metadata.Add("fileSizeBytes", fileSizeBytes.ToString());
|
||||
metadata.Add("fileName", fileName);
|
||||
metadata.Add("filePath", path);
|
||||
metadata.Add("fileSize", $"{(fileSizeBytes / 1024f / 1024f).ToString("0.00")} MB");
|
||||
|
||||
return metadata.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
public string GetScreenshotMetadata(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
|
||||
var metadata = ScreenshotHelper.GetScreenshotMetadata(path);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
var obj = new JObject
|
||||
{
|
||||
{ "sourceFile", path },
|
||||
{ "error", "Screenshot contains no metadata." }
|
||||
};
|
||||
|
||||
return obj.ToString(Formatting.Indented);
|
||||
};
|
||||
|
||||
if (metadata.Error != null)
|
||||
{
|
||||
var obj = new JObject
|
||||
{
|
||||
{ "sourceFile", path },
|
||||
{ "error", metadata.Error }
|
||||
};
|
||||
|
||||
return obj.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(metadata, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy() // This'll serialize our .net property names to their camelCase equivalents. Ex; "FileName" -> "fileName"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public string FindScreenshotsBySearch(string searchQuery, int searchType = 0)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
var searchPath = GetVRChatPhotosLocation();
|
||||
var screenshots = ScreenshotHelper.FindScreenshots(searchQuery, searchPath, (ScreenshotHelper.ScreenshotSearchType)searchType);
|
||||
|
||||
JArray json = new JArray();
|
||||
|
||||
foreach (var screenshot in screenshots)
|
||||
{
|
||||
json.Add(screenshot.SourceFile);
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
logger.Info($"FindScreenshotsBySearch took {stopwatch.ElapsedMilliseconds}ms to complete.");
|
||||
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
public string GetLastScreenshot()
|
||||
{
|
||||
// Get the last screenshot taken by VRChat
|
||||
var path = GetVRChatPhotosLocation();
|
||||
if (!Directory.Exists(path))
|
||||
return null;
|
||||
|
||||
var lastDirectory = Directory.GetDirectories(path).OrderByDescending(Directory.GetCreationTime).FirstOrDefault();
|
||||
if (lastDirectory == null)
|
||||
return null;
|
||||
|
||||
var lastScreenshot = Directory.GetFiles(lastDirectory, "*.png").OrderByDescending(File.GetCreationTime).FirstOrDefault();
|
||||
if (lastScreenshot == null)
|
||||
return null;
|
||||
|
||||
return lastScreenshot;
|
||||
}
|
||||
}
|
||||
21
Dotnet/AppApi/Common/Update.cs
Normal file
21
Dotnet/AppApi/Common/Update.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
public async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize)
|
||||
{
|
||||
await Update.DownloadUpdate(fileUrl, fileName, hashUrl, downloadSize);
|
||||
}
|
||||
|
||||
public void CancelUpdate()
|
||||
{
|
||||
Update.CancelUpdate();
|
||||
}
|
||||
|
||||
public int CheckUpdateProgress()
|
||||
{
|
||||
return Update.UpdateProgress;
|
||||
}
|
||||
}
|
||||
23
Dotnet/AppApi/Common/Utils.cs
Normal file
23
Dotnet/AppApi/Common/Utils.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidPathChars()))));
|
||||
|
||||
private static readonly Regex _fileRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidFileNameChars()))));
|
||||
|
||||
private static string MakeValidFileName(string name)
|
||||
{
|
||||
name = name.Replace("/", "");
|
||||
name = name.Replace("\\", "");
|
||||
name = _folderRegex.Replace(name, "");
|
||||
name = _fileRegex.Replace(name, "");
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
28
Dotnet/AppApi/Common/VrcConfigFile.cs
Normal file
28
Dotnet/AppApi/Common/VrcConfigFile.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public string ReadConfigFile()
|
||||
{
|
||||
var path = GetVRChatAppDataLocation();
|
||||
var configFile = Path.Combine(path, "config.json");
|
||||
if (!Directory.Exists(path) || !File.Exists(configFile))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(configFile);
|
||||
return json;
|
||||
}
|
||||
|
||||
public void WriteConfigFile(string json)
|
||||
{
|
||||
var path = GetVRChatAppDataLocation();
|
||||
var configFile = Path.Combine(path, "config.json");
|
||||
File.WriteAllText(configFile, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Dotnet/AppApi/Common/XSOverlay.cs
Normal file
70
Dotnet/AppApi/Common/XSOverlay.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays an XSOverlay notification with the specified title, content, and optional image.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the notification.</param>
|
||||
/// <param name="content">The content of the notification.</param>
|
||||
/// <param name="timeout">The duration of the notification in milliseconds.</param>
|
||||
/// <param name="image">The optional image to display in the notification.</param>
|
||||
public void XSNotification(string title, string content, int timeout, string image = "")
|
||||
{
|
||||
var broadcastSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
var endPoint = new IPEndPoint(IPAddress.Loopback, 42069);
|
||||
var useBase64Icon = false;
|
||||
var icon = image;
|
||||
if (string.IsNullOrEmpty(image))
|
||||
{
|
||||
useBase64Icon = true;
|
||||
icon = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHaGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1NTE2MWIyMi1hYzgxLTY3NDYtODAyYi1kODIzYWFmN2RjYjciIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3ZjJjNTA2ZS02YTVhLWRhNGEtOTg5Mi02NDZiMzQ0MGQxZTgiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6NmJmOGE5MTgtY2QzZS03OTRjLTk3NzktMzM0YjYwZWJiNTYyPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N2YyYzUwNmUtNmE1YS1kYTRhLTk4OTItNjQ2YjM0NDBkMWU4IiBzdEV2dDp3aGVuPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJhM2ZjODI3LTM0ZjQtYjU0OC05ZGFiLTZhMTZlZmQzZjAxMSIgc3RFdnQ6d2hlbj0iMjAyMS0wNC0wOFQxNTowMTozMSsxMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHN0RXZ0OndoZW49IjIwMjEtMDQtMDhUMTY6MzM6MTArMTI6MDAiIHN0RXZ0OnNv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";
|
||||
}
|
||||
|
||||
var height = 110f;
|
||||
if (content.Length > 300)
|
||||
height = 250f;
|
||||
else if (content.Length > 200)
|
||||
height = 200f;
|
||||
else if (content.Length > 100)
|
||||
height = 150f;
|
||||
|
||||
var msg = new XSOMessage
|
||||
{
|
||||
messageType = 1,
|
||||
title = title,
|
||||
content = content,
|
||||
height = height,
|
||||
sourceApp = "VRCX",
|
||||
timeout = timeout,
|
||||
audioPath = string.Empty,
|
||||
useBase64Icon = useBase64Icon,
|
||||
icon = icon
|
||||
};
|
||||
|
||||
var byteBuffer = JsonSerializer.SerializeToUtf8Bytes(msg);
|
||||
broadcastSocket.SendTo(byteBuffer, endPoint);
|
||||
broadcastSocket.Close();
|
||||
}
|
||||
|
||||
private struct XSOMessage
|
||||
{
|
||||
public int messageType { get; set; }
|
||||
public int index { get; set; }
|
||||
public float volume { get; set; }
|
||||
public string audioPath { get; set; }
|
||||
public float timeout { get; set; }
|
||||
public string title { get; set; }
|
||||
public string content { get; set; }
|
||||
public string icon { get; set; }
|
||||
public float height { get; set; }
|
||||
public float opacity { get; set; }
|
||||
public bool useBase64Icon { get; set; }
|
||||
public string sourceApp { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user