refactor: app.js (#1291)

* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

* refactor favorite handling to use favorite store methods and clean up event emissions

* refactor moderation handling to use dedicated functions for player moderation events

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

* Fix friend in same instance on orange showing twice in friends list

* Add back in broken friend state repair methods

* add types

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
pa
2025-07-14 12:00:08 +09:00
committed by GitHub
parent 952fd77ed5
commit f4f78bb5ec
323 changed files with 47745 additions and 43326 deletions

View File

@@ -1,60 +0,0 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:vue/recommended", "prettier"],
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true,
"jsx": true
},
"requireConfigFile": false,
"babelOptions": {
"parserOpts": {
"plugins": ["importAssertions"]
}
}
},
"globals": {
"CefSharp": "readonly",
"VRCX": "readonly",
"VRCXStorage": "readonly",
"SQLite": "readonly",
"LogWatcher": "readonly",
"Discord": "readonly",
"AppApi": "readonly",
"AppApiVr": "readonly",
"SharedVariable": "readonly",
"WebApi": "readonly",
"AssetBundleManager": "readonly",
"WINDOWS": "readonly",
"LINUX": "readonly"
},
"rules": {
"no-console": 0,
"no-control-regex": 0,
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"no-var": "warn",
"prefer-const": "warn",
"no-loop-func": 0,
"vars-on-top": 0,
"object-curly-spacing": ["error", "always"],
"require-atomic-updates": 0,
"no-unused-vars": 1,
"vue/require-default-prop": 0,
"vue/no-mutating-props": 1,
"vue/no-v-text-v-html-on-component": 1
}
}

4
.gitignore vendored
View File

@@ -5,4 +5,6 @@ obj1/
.idea/
*.log
*.DotSettings.user
Installer/version_define.nsh
.vs/
Installer/version_define.nsh
bun.lock

View File

@@ -43,7 +43,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog" Version="6.0.1" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

View File

@@ -1,5 +1,4 @@
using NLog;
using NLog.Fluent;
using SQLite;
using System;
using System.Collections.Generic;

View File

@@ -168,10 +168,8 @@ namespace DBMerger
{
FileName = "DBMerger.log",
Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
ArchiveFileName = Path.Combine("DBMerger_Logs", "DBMerger.{#}.log"),
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
ArchiveSuffixFormat = "{1:yyyy-MM-dd.HH-mm-ss}",
ArchiveOldFileOnStartup = true,
ConcurrentWrites = true,
KeepFileOpen = true,
AutoFlush = true,
Encoding = System.Text.Encoding.UTF8

View File

@@ -19,18 +19,18 @@ namespace VRCX
{
// Create Instance before Cef tries to bind it
}
public void VrInit()
{
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.vrInit", "");
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
}
public void ToggleSystemMonitor(bool enabled)
{
SystemMonitor.Instance.Start(enabled);
}
/// <summary>
/// Returns the current CPU usage as a percentage.
/// </summary>
@@ -39,7 +39,7 @@ namespace VRCX
{
return SystemMonitor.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
@@ -49,7 +49,7 @@ namespace VRCX
{
return Program.VRCXVRInstance.GetDevices();
}
/// <summary>
/// Returns the number of milliseconds that the system has been running.
/// </summary>
@@ -58,7 +58,7 @@ namespace VRCX
{
return SystemMonitor.Instance.UpTime;
}
/// <summary>
/// Returns the current language of the operating system.
/// </summary>
@@ -67,7 +67,7 @@ namespace VRCX
{
return CultureInfo.CurrentCulture.ToString();
}
/// <summary>
/// Returns the file path of the custom user js file, if it exists.
/// </summary>
@@ -80,7 +80,7 @@ namespace VRCX
output = filePath;
return output;
}
public bool IsRunningUnderWine()
{
return Wine.GetIfWine();

View File

@@ -47,7 +47,7 @@ namespace VRCX
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.game.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
}
public override bool IsGameRunning()

View File

@@ -24,9 +24,10 @@ namespace VRCX
dragData.Dispose();
return true;
}
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vrcx.dragEnterCef", file);
// forgive me father for i have sinned once again
Program.AppApiInstance.ExecuteAppFunction("dragEnterCef", file);
dragData.Dispose();
return false;
}

View File

@@ -0,0 +1,74 @@
// 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.Security.Cryptography.X509Certificates;
using CefSharp;
using NLog;
namespace VRCX
{
public class CustomRequestHandler : IRequestHandler
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
public bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect)
{
if (Program.LaunchDebug ||
request.Url.StartsWith("file://vrcx/") ||
request.Url.StartsWith("chrome-extension://"))
return false;
_logger.Error("Blocking navigation to: {Url}", request.Url);
return true;
}
public void OnDocumentAvailableInMainFrame(IWebBrowser chromiumWebBrowser, IBrowser browser)
{
}
public bool OnOpenUrlFromTab(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl,
WindowOpenDisposition targetDisposition, bool userGesture)
{
_logger.Debug("Blocking OnOpenUrlFromTab: {TargetUrl}",
targetUrl);
return true;
}
public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame,
IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
{
return null;
}
public bool GetAuthCredentials(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, bool isProxy, string host,
int port, string realm, string scheme, IAuthCallback callback)
{
return false;
}
public bool OnCertificateError(IWebBrowser chromiumWebBrowser, IBrowser browser, CefErrorCode errorCode, string requestUrl,
ISslInfo sslInfo, IRequestCallback callback)
{
return true;
}
public bool OnSelectClientCertificate(IWebBrowser chromiumWebBrowser, IBrowser browser, bool isProxy, string host, int port,
X509Certificate2Collection certificates, ISelectClientCertificateCallback callback)
{
return false;
}
public void OnRenderViewReady(IWebBrowser chromiumWebBrowser, IBrowser browser)
{
}
public void OnRenderProcessTerminated(IWebBrowser chromiumWebBrowser, IBrowser browser, CefTerminationStatus status,
int errorCode, string errorMessage)
{
}
}
}

View File

@@ -25,7 +25,8 @@ namespace VRCX
{
RootCachePath = userDataDir,
CachePath = Path.Join(userDataDir, "cache"),
LogSeverity = LogSeverity.Disable,
LogSeverity = Program.LaunchDebug ? LogSeverity.Verbose : LogSeverity.Error,
LogFile = Path.Join(Program.AppDataDirectory, "logs", "cef.log"),
WindowlessRenderingEnabled = true,
PersistSessionCookies = true,
UserAgent = Program.Version,

View File

@@ -8,7 +8,6 @@ namespace VRCX
{
repository.NameConverter = null;
repository.Register("AppApi", Program.AppApiInstance);
repository.Register("SharedVariable", SharedVariable.Instance);
repository.Register("WebApi", WebApi.Instance);
repository.Register("VRCXStorage", VRCXStorage.Instance);
repository.Register("SQLite", SQLiteLegacy.Instance);

View File

@@ -5,8 +5,8 @@
// For a copy, see <https://opensource.org/licenses/MIT>.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Net;
using System.Reflection;
using System.Windows.Forms;
using CefSharp;
@@ -15,6 +15,7 @@ using NLog;
namespace VRCX
{
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
public partial class MainForm : WinformBase
{
public static MainForm Instance;
@@ -50,30 +51,40 @@ namespace VRCX
logger.Error(ex);
}
Browser = new ChromiumWebBrowser("file://vrcx/index.html")
Browser = new ChromiumWebBrowser(Program.LaunchDebug ? "http://localhost:9000/index.html" : "file://vrcx/index.html")
{
DragHandler = new CustomDragHandler(),
MenuHandler = new CustomMenuHandler(),
DownloadHandler = new CustomDownloadHandler(),
RequestHandler = new CustomRequestHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
Browser.IsBrowserInitializedChanged += (A, B) =>
Browser.IsBrowserInitializedChanged += (_, _) =>
{
if (Program.LaunchDebug)
Browser.ShowDevTools();
};
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
Browser.ConsoleMessage += (_, args) =>
Browser.AddressChanged += (_, addressChangedEventArgs) =>
{
logger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
logger.Debug("Address changed: " + addressChangedEventArgs.Address);
};
Browser.LoadingStateChanged += (_, loadingFailedEventArgs) =>
{
if (loadingFailedEventArgs.IsLoading)
logger.Debug("Loading page");
else
logger.Debug("Loaded page: " + loadingFailedEventArgs.Browser.MainFrame.Url);
};
Browser.ConsoleMessage += (_, consoleMessageEventArgs) =>
{
logger.Debug(consoleMessageEventArgs.Message + " (" + consoleMessageEventArgs.Source + ":" + consoleMessageEventArgs.Line + ")");
};
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
Controls.Add(Browser);
}

View File

@@ -12,7 +12,8 @@ namespace VRCX
{
protected override void OnHandleCreated(EventArgs e)
{
WinformThemer.SetThemeToGlobal(this);
if (!DesignMode)
WinformThemer.SetThemeToGlobal(this);
base.OnHandleCreated(e);
}
}

View File

@@ -4,14 +4,17 @@
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
using System;
using DiscordRPC;
using System.Text;
using System.Threading;
using NLog;
namespace VRCX
{
public class Discord
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
public static readonly Discord Instance;
private readonly ReaderWriterLockSlim m_Lock;
private readonly RichPresence m_Presence;
@@ -54,8 +57,9 @@ namespace VRCX
{
Update();
}
catch
catch (Exception ex)
{
_logger.Error(ex, "Error updating Discord Rich Presence {Error}", ex.Message);
}
}
}
@@ -132,7 +136,7 @@ namespace VRCX
}
}
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId)
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId, int activityType = 0)
{
m_Lock.EnterWriteLock();
try
@@ -153,6 +157,7 @@ namespace VRCX
m_Presence.Party.ID = partyId;
m_Presence.Party.Size = partySize;
m_Presence.Party.Max = partyMax;
m_Presence.Type = (ActivityType)activityType;
Button[] buttons = [];
if (!string.IsNullOrEmpty(buttonUrl))
{
@@ -207,6 +212,10 @@ namespace VRCX
}
}
}
catch (Exception ex)
{
_logger.Error(ex, "Error setting timestamps in Discord Rich Presence {Error}", ex.Message);
}
finally
{
m_Lock.ExitWriteLock();

View File

@@ -90,7 +90,7 @@ namespace VRCX
#if !LINUX
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.ipcEvent", packet);
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vrcx.ipcEvent", packet);
#endif
}

View File

@@ -297,7 +297,7 @@ namespace VRCX
m_LogQueue.Enqueue(logLine);
#else
if (MainForm.Instance != null && MainForm.Instance.Browser != null)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine);
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.gameLog.addGameLogEvent", logLine);
#endif
}
@@ -729,10 +729,9 @@ namespace VRCX
return false;
var data = line.Substring(offset + 13);
#if !LINUX
WorldDBManager.Instance.ProcessLogWorldDataRequest(data);
#endif
// PWI, deprecated
logger.Info("VRCX-World data: {0}", data);
return true;
}

View File

@@ -188,13 +188,13 @@ namespace VRCX
var dashboardHandle = 0UL;
_wristOverlay = new OffScreenBrowser(
"file://vrcx/vr.html?1",
Program.LaunchDebug ? "http://localhost:9000/vr.html?1": "file://vrcx/vr.html?1",
512,
512
);
_hmdOverlay = new OffScreenBrowser(
"file://vrcx/vr.html?2",
Program.LaunchDebug ? "http://localhost:9000/vr.html?2": "file://vrcx/vr.html?2",
1024,
1024
);

View File

@@ -26,6 +26,7 @@ namespace VRCX
)
{
DragHandler = new CefNoopDragHandler(),
RequestHandler = new CustomRequestHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",

View File

@@ -1,750 +0,0 @@
using System.Linq;
using System.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using CefSharp;
using Newtonsoft.Json;
using NLog;
namespace VRCX
{
public class WorldDBManager
{
public static readonly WorldDBManager Instance;
private readonly HttpListener listener;
private readonly WorldDatabase worldDB;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private const string WorldDBServerUrl = "http://127.0.0.1:22500/";
private string lastError = null;
private bool debugWorld = false;
static WorldDBManager()
{
Instance = new WorldDBManager();
}
public WorldDBManager()
{
// http://localhost:22500
listener = new HttpListener();
listener.Prefixes.Add(WorldDBServerUrl);
worldDB = new WorldDatabase(Path.Join(Program.AppDataDirectory, "VRCX-WorldData.db"));
}
public void Init()
{
if (VRCXStorage.Instance.Get("VRCX_DisableWorldDatabase") == "true")
{
logger.Info("World database is disabled. Not starting.");
return;
}
Task.Run(Start);
}
private async Task Start()
{
// typing this in vr gonna kms
try
{
listener.Start();
}
catch (HttpListenerException e)
{
logger.Error(e, "Failed to start HTTP listener. Is VRCX already running?");
return;
}
logger.Info("Listening for requests on {0}", listener.Prefixes.First());
while (true)
{
var context = await listener.GetContextAsync();
var request = context.Request;
var responseData = new WorldDataRequestResponse(false, null, null);
try
{
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
{
logger.Error("Received a request to {0} while VRCX is still initializing the browser window. Responding with error 503.", request.Url);
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
responseData.StatusCode = 503;
responseData.ConnectionKey = null;
SendJsonResponse(context.Response, responseData);
continue;
};
logger.Debug("Received a request to '{0}'", request.Url);
// TODO: Maybe an endpoint for getting a group of arbitrary keys by a group 'name'? eg; /getgroup?name=testgroup1 would return all keys with the column group set to 'testgroup1'
switch (request.Url.LocalPath)
{
case "/vrcx/data/init":
responseData = await HandleInitRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/get":
responseData = await HandleDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/getall":
responseData = await HandleAllDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/lasterror":
responseData.OK = lastError == null;
responseData.StatusCode = 200;
responseData.Data = lastError;
responseData.ConnectionKey = null;
lastError = null;
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/getbulk":
responseData = await HandleBulkDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/settings":
responseData = await HandleSetSettingsRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/status":
// Send a blank 200 response to indicate that the server is running.
context.Response.StatusCode = 200;
context.Response.Close();
break;
default:
responseData.Error = "Invalid VRCX endpoint.";
responseData.StatusCode = 404;
responseData.ConnectionKey = null;
SendJsonResponse(context.Response, responseData);
break;
}
if (context.Response.StatusCode != 200)
{
logger.Warn("Received a request to '{0}' that returned a non-successful response. Error: {1} - {2}", request.Url, responseData.StatusCode, responseData.Error);
}
}
catch (Exception ex)
{
logger.Error(ex, $"Exception while processing a request to endpoint '{request.Url}'.");
responseData.Error = $"VRCX has encountered an exception while processing the url '{request.Url}': {ex.Message}";
responseData.StatusCode = 500;
responseData.ConnectionKey = null;
SendJsonResponse(context.Response, responseData);
}
}
}
private async Task<WorldDataRequestResponse> HandleSetSettingsRequest(HttpListenerContext context)
{
var request = context.Request;
string worldId = await GetCurrentWorldID();
string set = request.QueryString["set"];
string value = request.QueryString["value"];
if (!TryInitializeWorld(worldId, out string connectionKey))
{
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
if (set != null && value != null)
{
switch (set)
{
case "externalReads":
if (request.QueryString["value"] == "true")
{
worldDB.SetWorldAllowExternalRead(worldId, true);
}
else if (request.QueryString["value"] == "false")
{
worldDB.SetWorldAllowExternalRead(worldId, false);
}
else
{
return ConstructErrorResponse(400, "Invalid value for 'externalReads' setting.");
}
break;
default:
return ConstructErrorResponse(400, "Invalid setting name.");
}
}
return ConstructSuccessResponse(null, connectionKey);
}
/// <summary>
/// Handles an HTTP listener request to initialize a connection to the world db manager.
/// </summary>
/// <param name="context">The HTTP listener context object.</param>
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
private async Task<WorldDataRequestResponse> HandleInitRequest(HttpListenerContext context)
{
var request = context.Request;
if (request.QueryString["debug"] == "true")
{
debugWorld = true;
}
else if (request.QueryString["debug"] == "false")
{
debugWorld = false;
}
string worldId = await GetCurrentWorldID();
if (TryInitializeWorld(worldId, out string connectionKey))
{
logger.Info("Initialized a connection to the world database for world ID '{0}' with connection key {1}.", worldId, connectionKey);
return ConstructSuccessResponse(connectionKey, connectionKey);
}
else
{
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
}
/// <summary>
/// Handles an HTTP listener request for data from the world database.
/// </summary>
/// <param name="context">The HTTP listener context object.</param>
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
private async Task<WorldDataRequestResponse> HandleDataRequest(HttpListenerContext context)
{
var request = context.Request;
var key = request.QueryString["key"];
if (key == null)
{
return ConstructErrorResponse(400, "Missing key parameter.");
}
var worldId = await GetCurrentWorldID();
if (!TryInitializeWorld(worldId, out string connectionKey))
{
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
var worldOverride = request.QueryString["world"];
if (worldOverride == "global")
{
TryInitializeWorld(worldOverride, out connectionKey);
}
if (worldOverride != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
if (!allowed)
{
return ConstructSuccessResponse(null, connectionKey);
}
var otherValue = worldDB.GetDataEntry(worldOverride, key);
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' requested by world ID '{2}' with connection key {3}.", key, worldOverride, worldId, connectionKey);
// This value is intended to be null if the key doesn't exist.
return ConstructSuccessResponse(otherValue?.Value, connectionKey);
}
var value = worldDB.GetDataEntry(worldId, key);
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' with connection key {2}.", key, worldId, connectionKey);
// This value is intended to be null if the key doesn't exist.
return ConstructSuccessResponse(value?.Value, connectionKey);
}
/// <summary>
/// Handles an HTTP listener request for all data from the world database for a given world.
/// </summary>
/// <param name="context">The HTTP listener context object.</param>
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
private async Task<WorldDataRequestResponse> HandleAllDataRequest(HttpListenerContext context)
{
var request = context.Request;
var worldId = await GetCurrentWorldID();
if (!TryInitializeWorld(worldId, out string connectionKey))
{
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
var worldOverride = request.QueryString["world"];
if (worldOverride == "global")
{
TryInitializeWorld(worldOverride, out connectionKey);
}
if (worldOverride != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
if (!allowed)
{
return ConstructSuccessResponse(null, connectionKey);
}
var otherEntries = worldDB.GetAllDataEntries(worldOverride);
var otherData = new Dictionary<string, string>();
foreach (var entry in otherEntries)
{
otherData.Add(entry.Key, entry.Value);
}
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
}
var entries = worldDB.GetAllDataEntries(worldId);
var data = new Dictionary<string, string>();
foreach (var entry in entries)
{
data.Add(entry.Key, entry.Value);
}
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' with connection key {2}.", data.Count, worldId, connectionKey);
return ConstructSuccessResponse(JsonConvert.SerializeObject(data), connectionKey);
}
/// <summary>
/// Handles an HTTP listener request for bulk data from the world database.
/// </summary>
/// <param name="context">The HTTP listener context object.</param>
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
private async Task<WorldDataRequestResponse> HandleBulkDataRequest(HttpListenerContext context)
{
var request = context.Request;
var keys = request.QueryString["keys"];
if (keys == null)
{
return ConstructErrorResponse(400, "Missing/invalid keys parameter.");
}
var keyArray = keys.Split(',');
var worldId = await GetCurrentWorldID();
if (!TryInitializeWorld(worldId, out string connectionKey))
{
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
var worldOverride = request.QueryString["world"];
if (worldOverride == "global")
{
TryInitializeWorld(worldOverride, out connectionKey);
}
if (worldOverride != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
if (!allowed)
{
return ConstructSuccessResponse(null, connectionKey);
}
var otherEntries = worldDB.GetAllDataEntries(worldOverride);
var otherData = new Dictionary<string, string>();
foreach (var entry in otherEntries)
{
otherData.Add(entry.Key, entry.Value);
}
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
}
var values = worldDB.GetDataEntries(worldId, keyArray).ToList();
// Build a dictionary of key/value pairs to send back. If a key doesn't exist in the database, the key will be included in the response as requested but with a null value.
var data = new Dictionary<string, string>();
for (int i = 0; i < keyArray.Length; i++)
{
string dataKey = keyArray[i];
string dataValue = values?.Where(x => x.Key == dataKey).FirstOrDefault()?.Value; // Get the value from the list of data entries, if it exists, otherwise null
data.Add(dataKey, dataValue);
}
logger.Debug("Serving a request for bulk data with keys '{0}' from world ID '{1}' with connection key {2}.", keys, worldId, connectionKey);
return ConstructSuccessResponse(JsonConvert.SerializeObject(data), connectionKey);
}
/// <summary>
/// Attempts to initialize a world with the given ID by generating a connection key and adding it to the world database if it does not already exist.
/// </summary>
/// <param name="worldId">The ID of the world to initialize.</param>
/// <param name="connectionKey">The connection key generated for the world.</param>
/// <returns>True if the world was successfully initialized, false otherwise.</returns>
private bool TryInitializeWorld(string worldId, out string connectionKey)
{
if (string.IsNullOrEmpty(worldId))
{
connectionKey = null;
return false;
}
var existsInDB = worldDB.DoesWorldExist(worldId);
if (!existsInDB)
{
connectionKey = GenerateWorldConnectionKey();
worldDB.AddWorld(worldId, connectionKey);
logger.Info("Added new world ID '{0}' with connection key '{1}' to the database.", worldId, connectionKey);
}
else
{
connectionKey = worldDB.GetWorldConnectionKey(worldId);
}
return true;
}
/// <summary>
/// Generates a unique identifier for a world connection request.
/// </summary>
/// <returns>A string representation of a GUID that can be used to identify the world on requests.</returns>
private string GenerateWorldConnectionKey()
{
if (debugWorld) return "12345";
// Ditched the old method of generating a short key, since we're just going with json anyway who cares about a longer identifier
// Since we can rely on this GUID being unique, we can use it to identify the world on requests instead of trying to keep track of the user's current world.
// I uhh, should probably make sure this is actually unique though. Just in case. I'll do that later.
return Guid.NewGuid().ToString();
}
/// <summary>
/// Gets the ID of the current world by evaluating a JavaScript function in the main browser instance.
/// </summary>
/// <returns>The ID of the current world as a string, or null if it could not be retrieved.</returns>
private async Task<string> GetCurrentWorldID()
{
if (debugWorld) return "wrld_12345";
JavascriptResponse funcResult;
try
{
funcResult = await MainForm.Instance.Browser.EvaluateScriptAsync("$app.API.actuallyGetCurrentLocation();", TimeSpan.FromSeconds(5));
}
catch (Exception ex)
{
logger.Error(ex, "Failed to evaluate actuallyGetCurrentLocation JS function to get current world ID.");
return null;
}
string worldId = funcResult?.Result?.ToString();
if (string.IsNullOrEmpty(worldId))
{
// implement
// wait what was i going to do here again
// seriously i forgot, hope it wasn't important
logger.Warn("actuallyGetCurrentLocation returned null or empty.");
return null;
}
return worldId;
}
private WorldDataRequestResponse ConstructSuccessResponse(string data = null, string connectionKey = null)
{
var responseData = new WorldDataRequestResponse(true, null, null);
responseData.StatusCode = 200;
responseData.Error = null;
responseData.OK = true;
responseData.Data = data;
responseData.ConnectionKey = connectionKey;
return responseData;
}
private WorldDataRequestResponse ConstructErrorResponse(int statusCode, string error)
{
var responseData = new WorldDataRequestResponse(true, null, null);
responseData.StatusCode = statusCode;
responseData.Error = error;
responseData.OK = false;
responseData.Data = null;
responseData.ConnectionKey = null;
return responseData;
}
/// <summary>
/// Sends a JSON response to an HTTP listener request with the specified response data and status code.
/// </summary>
/// <param name="response">The HTTP listener response object.</param>
/// <param name="responseData">The response data to be serialized to JSON.</param>
/// <param name="statusCode">The HTTP status code to be returned.</param>
/// <returns>The HTTP listener response object.</returns>
private HttpListenerResponse SendJsonResponse(HttpListenerResponse response, WorldDataRequestResponse responseData)
{
response.ContentType = "application/json";
response.StatusCode = responseData.StatusCode;
response.AddHeader("Cache-Control", "no-cache");
// Use newtonsoft.json to serialize WorldDataRequestResponse to json
var json = JsonConvert.SerializeObject(responseData);
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.Close();
return response;
}
/// <summary>
/// Processes a JSON request containing world data and logs it to the world database.
/// </summary>
/// <param name="json">The JSON request containing the world data.</param>
public void ProcessLogWorldDataRequest(string json)
{
// Current format:
// {
// "requestType": "store",
// "connectionKey": "abc123",
// "key": "example_key",
// "value": "example_value"
// }
// * I could rate limit the processing of this, but I don't think it's necessary.
// * At the amount of data you'd need to be spitting out to lag vrcx, you'd fill up the log file and lag out VRChat far before VRCX would have any issues; at least in my testing.
// As long as malicious worlds can't permanently *store* stupid amounts of unculled data, this is pretty safe with the 10MB cap. If a world wants to just fill up a users HDD with logs, they can do that already anyway.
WorldDataRequest request;
try // try to deserialize the json into a WorldDataRequest object
{
request = JsonConvert.DeserializeObject<WorldDataRequest>(json);
}
catch (JsonReaderException ex)
{
logger.Error(ex, json.ToString());
this.lastError = ex.Message;
// invalid json
return;
}
catch (Exception ex)
{
logger.Error(ex, json.ToString());
this.lastError = ex.Message;
// something else happened lol
return;
}
if (string.IsNullOrEmpty(request.RequestType))
{
logger.Warn("World tried to store data with no request type provided. Request: ", json);
this.lastError = "`requestType` is missing or null";
return;
}
// Make sure the connection key is a valid GUID. No point in doing anything else if it's not.
if (!debugWorld && !Guid.TryParse(request.ConnectionKey, out Guid _))
{
logger.Warn("World tried to store data with an invalid GUID as a connection key '{0}'", request.ConnectionKey);
this.lastError = "Invalid GUID provided as connection key";
// invalid guid
return;
}
// Get the world ID from the connection key
string worldId = worldDB.GetWorldByConnectionKey(request.ConnectionKey);
// Global override
if (request.ConnectionKey == "global")
{
worldId = "global";
}
// World ID is null, which means the connection key is invalid (or someone just deleted a world from the DB while VRCX was running lol).
if (worldId == null)
{
logger.Warn("World tried to store data under {0} with an invalid connection key {1}", request.Key, request.ConnectionKey);
this.lastError = "Invalid connection key";
// invalid connection key
return;
}
try
{
string requestType = request.RequestType.ToLower();
switch (requestType)
{
case "store":
if (String.IsNullOrEmpty(request.Key))
{
logger.Warn("World {0} tried to store data with no key provided", worldId);
this.lastError = "`key` is missing or null";
return;
}
if (request.Key.Length > 255)
{
logger.Warn("World {0} tried to store data with a key that was too long ({1}/256 characters)", worldId, request.Key.Length);
this.lastError = "`key` is too long. Keep it below <256 characters.";
return;
}
if (request.Value == null)
{
logger.Warn("World {0} tried to store data under key {1} with null value", worldId, request.Key);
this.lastError = "`value` is null";
return;
}
StoreWorldData(worldId, request.Key, request.Value);
break;
case "delete":
if (String.IsNullOrEmpty(request.Key))
{
logger.Warn("World {0} tried to delete data with no key provided", worldId);
this.lastError = "`key` is missing or null";
return;
}
DeleteWorldData(worldId, request.Key);
break;
case "delete-all":
logger.Info("World {0} requested to delete all data.", worldId);
worldDB.DeleteAllDataEntriesForWorld(worldId);
worldDB.UpdateWorldDataSize(worldId, 0);
break;
case "set-setting":
if (String.IsNullOrEmpty(request.Key))
{
logger.Warn("World {0} tried to delete data with no key provided", worldId);
this.lastError = "`key` is missing or null";
return;
}
if (String.IsNullOrEmpty(request.Value))
{
logger.Warn("World {0} tried to set settings with no value provided", worldId);
this.lastError = "`value` is missing or null";
return;
}
SetWorldProperty(worldId, request.Key, request.Value);
break;
default:
logger.Warn("World {0} sent an invalid request type '{0}'", worldId, request.RequestType);
this.lastError = "Invalid request type";
// invalid request type
return;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to process world data request for world {0}", worldId);
logger.Error("Failed Request: {0}", json);
this.lastError = ex.Message;
return;
}
}
/// <summary>
/// Sets a property for a given world in the world database.
/// </summary>
/// <param name="worldId">The ID of the world to set the property for.</param>
/// <param name="key">The key of the property to set.</param>
/// <param name="value">The value to set the property to.</param>
public void SetWorldProperty(string worldId, string key, string value)
{
switch (key)
{
case "externalReads":
if (bool.TryParse(value, out bool result))
{
worldDB.SetWorldAllowExternalRead(worldId, result);
}
else
{
logger.Warn("World {0} tried to set externalReads to an invalid value '{1}'", worldId, value);
this.lastError = "Invalid value for externalReads";
return;
}
break;
}
}
/// <summary>
/// Stores a data entry for a given world in the world database.
/// </summary>
/// <param name="worldId">The ID of the world to store the data entry for.</param>
/// <param name="key">The key of the data entry to store.</param>
/// <param name="value">The value of the data entry to store.</param>
public void StoreWorldData(string worldId, string key, string value)
{
// Get/calculate the old and new data sizes for this key/the world
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
int oldDataSize = worldDB.GetDataEntrySize(worldId, key);
int newDataSize = Encoding.UTF8.GetByteCount(value);
int newTotalDataSize = oldTotalDataSize + newDataSize - oldDataSize;
// Make sure we don't exceed 10MB total size for a world
// This works, I tested it. Hopefully this prevents/limits any possible abuse.
if (newTotalDataSize > 1024 * 1024 * 10)
{
logger.Warn("World {0} exceeded 10MB total data size trying to store key {0}. {1}:{2} + {3} = {4}", key, worldId, oldTotalDataSize - oldDataSize, newDataSize, newTotalDataSize);
this.lastError = $"You have hit the 10MB total data cap. The previous data entry was *not* stored. Your request was {newDataSize} bytes, your current shared byte total is {oldTotalDataSize} and you went over the table limit by {newTotalDataSize - (1024 * 1024 * 10)} bytes.";
return;
}
worldDB.AddDataEntry(worldId, key, value, newDataSize);
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
logger.Info("World {0} stored data entry {1} with size {2} bytes", worldId, key, newDataSize);
logger.Debug("{0} : {1}", key, value);
}
/// <summary>
/// Deletes a data entry for a given world from the world database.
/// </summary>
/// <param name="worldId">The ID of the world to delete the data entry from.</param>
/// <param name="key">The key of the data entry to delete.</param>
public void DeleteWorldData(string worldId, string key)
{
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
int oldDataSize = worldDB.GetDataEntrySize(worldId, key);
int newTotalDataSize = oldTotalDataSize - oldDataSize;
worldDB.DeleteDataEntry(worldId, key);
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
logger.Info("World {0} deleted data entry {1} with size {2} bytes.", worldId, key, oldDataSize);
}
public void Stop()
{
try
{
worldDB?.Close();
listener?.Stop();
listener?.Close();
}
catch (ObjectDisposedException)
{
// ignore
}
}
}
}

View File

@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace VRCX
{
public class WorldDataRequestResponse
{
/// <summary>
/// Gets or sets a value indicating whether the request was successful.
/// </summary>
[JsonProperty("ok")]
public bool OK { get; set; }
/// <summary>
/// Gets or sets the error message if the request was not successful.
/// </summary>
[JsonProperty("error")]
public string Error { get; set; }
/// <summary>
/// Gets or sets the data returned by the request.
/// </summary>
[JsonProperty("data")]
public string Data { get; set; }
/// <summary>
/// Gets or sets the response code.
/// </summary>
/// <value></value>
[JsonProperty("statusCode")]
public int StatusCode { get; set; }
[JsonProperty("connectionKey")]
public string ConnectionKey { get; set; }
public WorldDataRequestResponse(bool ok, string error, string data)
{
OK = ok;
Error = error;
Data = data;
}
}
public class WorldDataRequest
{
[JsonProperty("requestType")]
public string RequestType;
[JsonProperty("connectionKey")]
public string ConnectionKey;
[JsonProperty("key")]
public string Key;
[JsonProperty("value")]
public string Value;
}
}

View File

@@ -1,322 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SQLite;
namespace VRCX
{
[Table("data")]
public class WorldData
{
[PrimaryKey, AutoIncrement]
[Column("id")]
public int Id { get; set; }
[Column("world_id"), NotNull]
public string WorldId { get; set; }
[Column("key"), NotNull]
public string Key { get; set; }
[Column("value"), NotNull]
public string Value { get; set; }
[Column("value_size"), NotNull]
public int ValueSize { get; set; }
[Column("last_accessed")]
public DateTimeOffset LastAccessed { get; set; }
[Column("last_modified")]
public DateTimeOffset LastModified { get; set; }
}
[Table("worlds")]
public class World
{
[PrimaryKey, AutoIncrement]
[Column("id")]
public int Id { get; set; }
[Column("world_id"), NotNull]
public string WorldId { get; set; }
[Column("connection_key"), NotNull]
public string ConnectionKey { get; set; }
[Column("total_data_size"), NotNull]
public int TotalDataSize { get; set; }
[Column("allow_external_read")]
public bool AllowExternalRead { get; set; }
}
internal class WorldDatabase
{
private static SQLiteConnection sqlite;
private readonly static string dbInitQuery = @"
CREATE TABLE IF NOT EXISTS worlds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
world_id TEXT NOT NULL UNIQUE,
connection_key TEXT NOT NULL,
total_data_size INTEGER DEFAULT 0,
allow_external_read INTEGER DEFAULT 0
);
\
CREATE TABLE IF NOT EXISTS data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
world_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
value_size INTEGER NOT NULL DEFAULT 0,
last_accessed INTEGER DEFAULT (strftime('%s', 'now')),
last_modified INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (world_id) REFERENCES worlds(world_id) ON DELETE CASCADE,
UNIQUE (world_id, key)
);
\
CREATE TRIGGER IF NOT EXISTS data_update_trigger
AFTER UPDATE ON data
FOR EACH ROW
BEGIN
UPDATE data SET last_modified = (strftime('%s', 'now')) WHERE id = OLD.id;
END;
\
CREATE TRIGGER IF NOT EXISTS data_insert_trigger
AFTER INSERT ON data
FOR EACH ROW
BEGIN
UPDATE data SET last_accessed = (strftime('%s', 'now')), last_modified = (strftime('%s', 'now')) WHERE id = NEW.id;
END;";
public WorldDatabase(string databaseLocation)
{
var options = new SQLiteConnectionString(databaseLocation, true);
sqlite = new SQLiteConnection(options);
sqlite.Execute(dbInitQuery);
// TODO: Split these init queries into their own functions so we can call/update them individually.
var queries = dbInitQuery.Split('\\');
sqlite.BeginTransaction();
foreach (var query in queries)
{
sqlite.Execute(query);
}
sqlite.Commit();
}
/// <summary>
/// Checks if a world with the specified ID exists in the database.
/// </summary>
/// <param name="worldId">The ID of the world to check for.</param>
/// <returns>True if the world exists in the database, false otherwise.</returns>
public bool DoesWorldExist(string worldId)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.WorldId);
return query.Any();
}
/// <summary>
/// Gets the ID of the world with the specified connection key from the database.
/// </summary>
/// <param name="connectionKey">The connection key of the world to get the ID for.</param>
/// <returns>The ID of the world with the specified connection key, or null if no such world exists in the database.</returns>
public string GetWorldByConnectionKey(string connectionKey)
{
var query = sqlite.Table<World>().Where(w => w.ConnectionKey == connectionKey).Select(w => w.WorldId);
return query.FirstOrDefault();
}
/// <summary>
/// Gets the connection key for a world from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the connection key for.</param>
/// <returns>The connection key for the specified world, or null if the world does not exist in the database.</returns>
public string GetWorldConnectionKey(string worldId)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
return query.FirstOrDefault();
}
/// <summary>
/// Sets the connection key for a world in the database. If the world already exists in the database, the connection key is updated. Otherwise, a new world is added to the database with the specified connection key.
/// </summary>
/// <param name="worldId">The ID of the world to set the connection key for.</param>
/// <param name="connectionKey">The connection key to set for the world.</param>
/// <returns>The connection key that was set.</returns>
public string SetWorldConnectionKey(string worldId, string connectionKey)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
if (query.Any())
{
sqlite.Execute("UPDATE worlds SET connection_key = ? WHERE world_id = ?", connectionKey, worldId);
}
else
{
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
}
return connectionKey;
}
/// <summary>
/// Sets the value of the allow_external_read field for the world with the specified ID in the database.
/// </summary>
/// <param name="worldId">The ID of the world to set the allow_external_read field for.</param>
/// <param name="allowExternalRead">The value to set for the allow_external_read field.</param>
public void SetWorldAllowExternalRead(string worldId, bool allowExternalRead)
{
sqlite.Execute("UPDATE worlds SET allow_external_read = ? WHERE world_id = ?", allowExternalRead, worldId);
}
/// <summary>
/// Gets the value of the allow_external_read field for the world with the specified ID from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the allow_external_read field for.</param>
/// <returns>The value of the allow_external_read field for the specified world.</returns>
public bool GetWorldAllowExternalRead(string worldId)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.AllowExternalRead);
return query.FirstOrDefault();
}
/// <summary>
/// Adds a new world to the database.
/// </summary>
/// <param name="worldId">The ID of the world to add.</param>
/// <param name="connectionKey">The connection key of the world to add.</param>
/// <exception cref="SQLiteException">Thrown if a world with the specified ID already exists in the database.</exception>
public void AddWorld(string worldId, string connectionKey)
{
// * This will throw an error if the world already exists.. so don't do that
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
}
/// <summary>
/// Gets the world with the specified ID from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get.</param>
/// <returns>The world with the specified ID, or null if no such world exists in the database.</returns>
public World GetWorld(string worldId)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId);
return query.FirstOrDefault();
}
/// <summary>
/// Gets the total data size shared across all rows, in bytes, for the world with the specified ID from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the total data size for.</param>
/// <returns>The total data size for the world, in bytes.</returns>
public int GetWorldDataSize(string worldId)
{
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.TotalDataSize);
return query.FirstOrDefault();
}
/// <summary>
/// Updates the total data size, in bytes for the world with the specified ID in the database.
/// </summary>
/// <param name="worldId">The ID of the world to update the total data size for.</param>
/// <param name="size">The new total data size for the world, in bytes.</param>
public void UpdateWorldDataSize(string worldId, int size)
{
sqlite.Execute("UPDATE worlds SET total_data_size = ? WHERE world_id = ?", size, worldId);
}
/// <summary>
/// Adds or updates a data entry in the database with the specified world ID, key, and value.
/// </summary>
/// <param name="worldId">The ID of the world to add the data entry for.</param>
/// <param name="key">The key of the data entry to add or replace.</param>
/// <param name="value">The value of the data entry to add or replace.</param>
/// <param name="dataSize">The size of the data entry to add or replace, in bytes. If null, the size is calculated from the value automatically.</param>
public void AddDataEntry(string worldId, string key, string value, int? dataSize = null)
{
int byteSize = dataSize ?? Encoding.UTF8.GetByteCount(value);
// check if entry already exists;
// INSERT OR REPLACE(InsertOrReplace method) deletes the old row and creates a new one, incrementing the id, which I don't want
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key);
if (query.Any())
{
sqlite.Execute("UPDATE data SET value = ?, value_size = ? WHERE world_id = ? AND key = ?", value, byteSize, worldId, key);
}
else
{
sqlite.Insert(new WorldData() { WorldId = worldId, Key = key, Value = value, ValueSize = byteSize });
}
}
/// <summary>
/// Gets the data entry with the specified world ID and key from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the data entry for.</param>
/// <param name="key">The key of the data entry to get.</param>
/// <returns>The data entry with the specified world ID and key, or null if no such data entry exists in the database.</returns>
public WorldData GetDataEntry(string worldId, string key)
{
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key);
return query.FirstOrDefault();
}
/// <summary>
/// Gets the data entries with the specified world ID and keys from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the data entries for.</param>
/// <param name="keys">The keys of the data entries to get.</param>
/// <returns>An enumerable collection of the data entries with the specified world ID and keys.</returns>
public IEnumerable<WorldData> GetDataEntries(string worldId, string[] keys)
{
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && keys.Contains(w.Key));
return query.ToList();
}
/// <summary>
/// Gets all data entries for the world with the specified ID from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the data entries for.</param>
/// <returns>An enumerable collection of all data entries for the world with the specified ID.</returns>
public IEnumerable<WorldData> GetAllDataEntries(string worldId)
{
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId).Take(10000);
return query.ToList();
}
/// <summary>
/// Gets the size of the data entry, in bytes, with the specified world ID and key from the database.
/// </summary>
/// <param name="worldId">The ID of the world to get the data entry size for.</param>
/// <param name="key">The key of the data entry to get the size for.</param>
/// <returns>The size of the data entry with the specified world ID and key, or 0 if no such data entry exists in the database.</returns>
public int GetDataEntrySize(string worldId, string key)
{
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key).Select(w => w.ValueSize);
return query.FirstOrDefault();
}
/// <summary>
/// Deletes the data entry with the specified world ID and key from the database.
/// </summary>
/// <param name="worldId">The ID of the world to delete the data entry from.</param>
/// <param name="key">The key of the data entry to delete.</param>
public void DeleteDataEntry(string worldId, string key)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ? AND key = ?", worldId, key);
}
/// <summary>
/// Deletes all data entries for the world with the specified ID from the database.
/// </summary>
/// <param name="worldId">The ID of the world to delete all data entries for.</param>
public void DeleteAllDataEntriesForWorld(string worldId)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ?", worldId);
}
public void Close()
{
sqlite.Close();
}
}
}

View File

@@ -8,6 +8,7 @@ using NLog;
using NLog.Targets;
using System;
using System.Data.SQLite;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
using System.Threading;
@@ -99,14 +100,12 @@ namespace VRCX
// Layout with padding between the level/logger and message so that the message always starts at the same column
Layout =
"${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
ArchiveFileName = Path.Join(AppDataDirectory, "logs", "VRCX.{#}.log"),
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
ArchiveSuffixFormat = "{0:000}",
ArchiveEvery = FileArchivePeriod.Day,
MaxArchiveFiles = 4,
MaxArchiveDays = 7,
ArchiveAboveSize = 10000000,
ArchiveOldFileOnStartup = true,
ConcurrentWrites = true,
KeepFileOpen = true,
AutoFlush = true,
Encoding = System.Text.Encoding.UTF8
@@ -118,12 +117,13 @@ namespace VRCX
Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
DetectConsoleAvailable = true
};
builder.ForLogger("VRCX").FilterMinLevel(LogLevel.Info).WriteTo(consoleTarget);
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(consoleTarget);
});
}
#if !LINUX
[STAThread]
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
private static void Main()
{
if (Wine.GetIfWine())
@@ -150,7 +150,7 @@ namespace VRCX
{
case DialogResult.Yes:
logger.Fatal("Handled Exception, user selected auto install of vc_redist.");
Update.DownloadInstallRedist();
Update.DownloadInstallRedist().GetAwaiter().GetResult();
MessageBox.Show(
"vc_redist has finished installing, if the issue persists upon next restart, please reinstall VRCX From GitHub,\nVRCX Will now restart.",
"vc_redist installation complete", MessageBoxButtons.OK);
@@ -210,6 +210,7 @@ namespace VRCX
}
}
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
private static void Run()
{
var args = Environment.GetCommandLineArgs();
@@ -236,7 +237,6 @@ namespace VRCX
AppApiVr.Instance.Init();
ProcessMonitor.Instance.Init();
Discord.Instance.Init();
WorldDBManager.Instance.Init();
WebApi.Instance.Init();
LogWatcher.Instance.Init();
AutoAppLaunchManager.Instance.Init();
@@ -258,7 +258,6 @@ namespace VRCX
AutoAppLaunchManager.Instance.Exit();
LogWatcher.Instance.Exit();
WebApi.Instance.Exit();
WorldDBManager.Instance.Stop();
Discord.Instance.Exit();
SystemMonitor.Instance.Exit();

View File

@@ -1,86 +0,0 @@
// 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.Collections.Generic;
using System.Threading;
namespace VRCX
{
public class SharedVariable
{
public static readonly SharedVariable Instance;
private readonly ReaderWriterLockSlim m_MapLock;
private readonly Dictionary<string, string> m_Map;
static SharedVariable()
{
Instance = new SharedVariable();
}
public SharedVariable()
{
m_MapLock = new ReaderWriterLockSlim();
m_Map = new Dictionary<string, string>();
}
public void Clear()
{
m_MapLock.EnterWriteLock();
try
{
m_Map.Clear();
}
finally
{
m_MapLock.ExitWriteLock();
}
}
public string Get(string key)
{
m_MapLock.EnterReadLock();
try
{
if (m_Map.TryGetValue(key, out string value) == true)
{
return value;
}
}
finally
{
m_MapLock.ExitReadLock();
}
return null;
}
public void Set(string key, string value)
{
m_MapLock.EnterWriteLock();
try
{
m_Map[key] = value;
}
finally
{
m_MapLock.ExitWriteLock();
}
}
public bool Remove(string key)
{
m_MapLock.EnterWriteLock();
try
{
return m_Map.Remove(key);
}
finally
{
m_MapLock.ExitWriteLock();
}
}
}
}

View File

@@ -91,12 +91,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="137.0.100" />
<PackageReference Include="CefSharp.WinForms.NETCore" Version="137.0.100" />
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="138.0.170" />
<PackageReference Include="CefSharp.WinForms.NETCore" Version="138.0.170" />
<PackageReference Include="DiscordRichPresence" Version="1.3.0.28" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog" Version="6.0.1" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
@@ -104,10 +104,10 @@
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Management" Version="9.0.6" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.Management" Version="9.0.7" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.Json" Version="9.0.6" />
<PackageReference Include="System.Text.Json" Version="9.0.7" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Websocket.Client" Version="5.2.0" />
</ItemGroup>

View File

@@ -93,12 +93,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.11" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.11" />
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.12" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.12" />
<PackageReference Include="DiscordRichPresence" Version="1.3.0.28" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog" Version="6.0.1" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
@@ -106,10 +106,10 @@
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Management" Version="9.0.6" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.Management" Version="9.0.7" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.Json" Version="9.0.6" />
<PackageReference Include="System.Text.Json" Version="9.0.7" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Websocket.Client" Version="5.2.0" />
</ItemGroup>
@@ -121,8 +121,6 @@
<Compile Remove="Cef\**" />
<Content Remove="AppApi\Cef\**" />
<Compile Remove="AppApi\Cef\**" />
<Content Remove="PWI\**" />
<Compile Remove="PWI\**" />
<Content Remove="Overlay\**" />
<Compile Remove="Overlay\**" />
<Content Remove="DBMerger\**" />

View File

@@ -33,10 +33,6 @@ Versión beta/nocturna disponible [aquí](https://vrcx.app/github/nightly) o en
- :electric_plug: Inicia automáticamente aplicaciones cuando inicias VRChat
- Puedes configurar VRCX para iniciar otras aplicaciones cuando inicias VRChat.
- Por ejemplo, podrías hacer que VRCX inicie una aplicación OSC o una aplicación de cambio de voz cuando se abra VRChat.
- :floppy_disk: Persistencia de mundos
- Para los mundos que soportan la función, VRCX puede guardar configuraciones de mundos, estados guardados, inventarios y otros datos.
- **Nota**: Para usar esta función, debes tener "Permitir URLs no confiables" habilitado en la configuración de VRChat.
- Para desarrolladores: [Página Wiki - Persistencia de Mundos (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Busca avatares, usuarios, mundos y grupos
- :earth_americas: Crea una lista de favoritos de mundos local y sin restricciones
- :camera: Almacena datos de mundos en las fotos que tomas en el juego, para que puedas recordar ese mundo donde tomaste esas fotos geniales hace como... 6 meses.

View File

@@ -31,10 +31,6 @@ Téléchargez et exécutez le dernier programme d'installation (`VRCX_Setup.exe`
- :electric_plug: Lancement automatique d'applications lorsque vous démarrez VRChat
- Vous pouvez configurer VRCX pour lancer d'autres applications lorsque vous démarrez VRChat.
- Par exemple, vous pourriez avoir VRCX qui lance une application OSC ou une application de modification de voix lorsque VRChat s'ouvre.
- :floppy_disk: Persistance de Monde
- Pour les mondes qui prennent en charge cette fonctionnalité, VRCX peut sauvegarder les paramètres de monde, les états sauvegardés, les inventaires et d'autres données !
- **Note** : Pour utiliser cette fonctionnalité, vous devez avoir activé "Allow Untrusted URLs" dans vos paramètres VRChat.
- Pour les Développeurs : [Page Wiki - World Persistence (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Recherche d'avatars, d'utilisateurs, de mondes et de groupes
- :earth_americas: Créez une liste de favoris de mondes locale et sans restrictions
- :camera: Stockez les données du monde dans les images que vous prenez en jeu, afin de vous souvenir de ce monde où vous avez pris ces superbes photos il y a... 6 mois !

View File

@@ -31,10 +31,6 @@ Scarica e installa l'ultimo installer (`VRCX_Setup.exe`) da [qui](https://github
- :electric_plug: Avvio automatico delle applicazioni all'avvio di VRChat
- È possibile configurare VRCX per avviare altre applicazioni quando si avvia VRChat.
- Ad esempio, si può fare in modo che VRCX lanci un'applicazione OSC o un cambiavoce all'apertura di VRChat.
- :floppy_disk: Persistenza del mondo
- Per i mondi che supportano questa funzione, VRCX può salvare le impostazioni del mondo, gli stati, gli inventari e altri dati!
- **Nota**: Per utilizzare questa funzione, è necessario che nelle impostazioni di VRChat sia abilitato "Consenti URL non attendibili".
- Per gli sviluppatori: [Pagina Wiki - World Persistence (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Ricerca di avatar, utenti, mondi e gruppi
- :earth_americas: Costruire un elenco di preferiti locali e non limitati del mondo
- :camera: Memorizza i dati del mondo nelle foto scattate nel gioco, in modo da poter ricordare quel mondo in cui hai scattato quelle belle foto come... 6 mesi fa!

View File

@@ -31,10 +31,6 @@ VRCX は VRChat クライアント (デスクトップ & VR) や Web サイト
- :electric_plug: VRChat 起動時に一緒にアプリを起動
- VRChat の起動時に他のアプリを同時起動できるよう設定できます。
- 例えば、VRChat を起動したら同時に OSC アプリやボイスチェンジャーを起動するようにできます。
- :floppy_disk: ワールドの永続化
- ワールドが対応している場合、設定やセーブデータ、インベントリなどのデータを保存することができます。
- **注意**: この機能を使うには、VRChat の設定で「信頼されていない URL を許可」を有効化しておく必要があります。
- 開発者向け: [Wiki Page - World Persistence (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: アバター、ユーザー、ワールド、グループの検索
- :earth_americas: 無制限!ローカル保存のワールドお気に入りリスト
- :camera: ゲーム内で撮った写真にワールドデータを保存することで、半年前に撮影した綺麗なワールドをいつでも振り返ることができます。

View File

@@ -5,7 +5,6 @@
[![GitHub release](https://img.shields.io/github/release/vrcx-team/VRCX.svg)](https://github.com/vrcx-team/VRCX/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/vrcx-team/VRCX/total?color=6451f1)](https://github.com/vrcx-team/VRCX/releases/latest)
[![GitHub Workflow Status](https://github.com/vrcx-team/VRCX/actions/workflows/github_actions.yml/badge.svg)](https://github.com/vrcx-team/VRCX/actions/workflows/github_actions.yml)
[![Crowdin](https://badges.crowdin.net/vrcx/localized.svg)](https://crowdin.com/project/vrcx)
[![VRCX Discord Invite](https://img.shields.io/discord/854071236363550763?color=%237289DA&logo=discord&logoColor=white&label=discord)](https://vrcx.app/discord)
| **English** | [Français](./README.fr.md) | [日本語](./README.jp.md) | [简体中文](./README.zh_CN.md) | [Italiano](./README.it.md) | [Русский](./README.ru_RU.md) | [Español](./README.es.md) | [Polski](./README.pl.md)
@@ -34,10 +33,6 @@ Beta/nightly build available [here](https://vrcx.app/github/nightly) or in-app `
- :electric_plug: Automatically launch apps when you start VRChat
- You can configure VRCX to launch other apps when you start VRChat.
- For example, you could have VRCX launch an OSC app or a voice changer app when VRChat opens up.
- :floppy_disk: World Persistence
- For worlds that support the feature, VRCX can save world settings, save states, inventories, and other data!
- **Note**: To use this feature, you must have "Allow Untrusted URLs" enabled in your VRChat settings.
- For Developers: [Wiki Page - World Persistence (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Search for avatars, users, worlds, and groups
- :earth_americas: Build a local, unrestricted world favorites list
- :camera: Store world data in the pictures you take in-game, so you can remember that one world you took those cool pictures in like... 6 months ago!
@@ -47,7 +42,7 @@ Beta/nightly build available [here](https://vrcx.app/github/nightly) or in-app `
- :tv: See the links to videos and that are playing in the world you're in, as well as various other logged data.
- :bar_chart: Improved Discord Rich Presence
- You can optionally display more information about your current instance in Discord.
- World integration for popular worlds like PyPyDance, LSMedia, Movies&Chill and VRDancing.
- World integration for popular worlds like Popcorn Palace, PyPyDance, VRDancing and LSMedia.
- This includes the world thumbnail, name, instance ID, and player count, depending on your settings and whether the lobby is private. You can also add a join button for public lobbies!
- :crystal_ball: VR Overlay with configurable live feed of all supported events/notifications
- :outbox_tray: Upload avatar/world images without Unity

View File

@@ -31,10 +31,6 @@ Pobierz i uruchom najnowszy instalator (`VRCX_Setup.exe`) [stąd](https://github
- :electric_plug: Automatycznie uruchamiaj aplikacje podczas startu VRChat
- Ustaw, aby VRCX uruchamiał inne aplikacje, gdy włączysz VRChat.
- Na przykład, możesz ustawić, aby VRCX uruchamiał aplikację OSC lub modulacji głosu, gdy włączysz VRChat.
- :floppy_disk: Persystencja światów
- VRCX może zapisywać ustawienia, stany, ekwipunek czy inne dane światów, jeżeli wspierają one tę funkcję.
- **Notatka**: Aby używać tej funkcji, musisz zezwolić na niezaufane linki URL w ustawieniach VRChatu.
- Dla programistów: [Strona wiki o persystencji światów](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Wyszukuj awatary, użytkwoników, światy i grupy
- :earth_americas: Twórz lokalną i nieograniczoną listę ulubionych światów
- :camera: Przechowuj dane światów w zdjęciach robionych w grze, aby pamiętać o światach odwiedzanych dawno temu!

View File

@@ -33,10 +33,6 @@ VRCX является ассистентом/компаньоном прилож
- :electric_plug: Автоматически запускает приложения при запуске VRChat
- Вы можете настроить VRCX на запуск других приложений при запуске VRChat.
- Например, при открытии VRChat вы можете запустить приложение OSC или программу для изменения голоса.
- :floppy_disk: Сохранение мира
- В мирах, поддерживающих эту функцию, VRCX может сохранять настройки мира, сохранять состояния, инвентарь и другие данные!
- **Примечание**: Для использования этой функции в настройках VRChat должна быть включена опция "Разрешить недоверенные URL'ы" ("Allow Untrusted URLs").
- Для разработчиков: [Вики страница - Сохранение мира (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: Поиск аватаров, пользователей, миров и групп
- :earth_americas: Создает локальный, неограниченный список избранных миров
- :camera: Храните данные о мире в фотографиях, которые вы делаете в игре, чтобы вы могли вспомнить тот мир, в котором вы сделали те классные фотографии, например... 6 месяцев назад!

View File

@@ -33,10 +33,6 @@ VRCX 是一款用于 VRChat 的外部辅助小工具,可以比 VRChat 游戏
- :electric_plug: 当你启动 VRChat 时自动启动其他程序
- 你可以配置 VRCX让其在启动 VRChat 时自动启动你指定的程序。
- 例如,你可以在启动时让 VRCX 打开一个 OSC 应用或变声器。
- :floppy_disk: 世界数据持久化保存 (World Persistence)
- 对于支持此功能的世界VRCX 将能够保存世界设置、存档、各种清单以及其他可以保存的数据!
- **注意**:要使用此功能,必须在 VRChat 设置中启用 “Allow Untrusted URLs”。
- 给世界开发者的指南: [Wiki Page - World Persistence (PWI)](<https://github.com/vrcx-team/VRCX/wiki/World-Persistence-(PWI)>)
- :mag: 以更加方便的形式搜索模型、房间、世界以及群组。
- :earth_americas: 创建本地的、没有任何限制的世界收藏夹
- :camera: 将世界数据存储在你在游戏内拍摄的照片中,这样即使几个月后也能知道当时是在什么世界拍的照片

View File

@@ -1,5 +1,4 @@
@echo off
cd ..
cd ../../
dotnet build Dotnet\VRCX-Cef.csproj -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
mklink /J "%~dp0\..\build\Cef\html" "%~dp0\..\build\html"
pause

View File

@@ -1,6 +1,12 @@
@echo off
setlocal enabledelayedexpansion
if not exist "%~dp0\..\build\Cef" (
mkdir "%~dp0\..\build\Cef"
)
if not exist "%~dp0\..\build\html" (
mkdir "%~dp0\..\build\html"
)
if not exist "%~dp0\..\build\Cef\html" (
mklink /J "%~dp0\..\build\Cef\html" "%~dp0\..\build\html"
)

View File

@@ -0,0 +1,3 @@
cd ../../
call npm run dev
pause

View File

@@ -1,4 +1,3 @@
cd ../
call npm ci
cd ../../
call npm run prod
pause

View File

@@ -0,0 +1,3 @@
cd ../../
call npm ci
pause

60
eslint.config.mjs Normal file
View File

@@ -0,0 +1,60 @@
import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs,vue}'],
plugins: { js },
extends: ['js/recommended']
},
{
files: ['**/*.{js,mjs,cjs,vue}'],
languageOptions: {
globals: {
...globals.browser,
CefSharp: 'readonly',
VRCX: 'readonly',
VRCXStorage: 'readonly',
SQLite: 'readonly',
LogWatcher: 'readonly',
Discord: 'readonly',
AppApi: 'readonly',
AppApiVr: 'readonly',
WebApi: 'readonly',
AssetBundleManager: 'readonly',
WINDOWS: 'readonly',
LINUX: 'readonly',
webApiService: 'readonly',
process: 'readonly'
}
}
},
{
files: [
'**/webpack.*.js',
'src-electron/*.js',
'src/localization/*.js'
],
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node
}
}
},
pluginVue.configs['flat/vue2-essential'],
{
rules: {
'no-unused-vars': 'warn',
'no-case-declarations': 'off',
'no-control-regex': 'warn',
'vue/no-mutating-props': 'warn',
'vue/multi-word-component-names': 'off',
'vue/no-v-text-v-html-on-component': 'off',
'vue/no-use-v-if-with-v-for': 'warn'
}
}
]);

17
jsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"allowJs": true,
"checkJs": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"forceConsistentCasingInFileNames": true,
"lib": ["esnext", "dom", "dom.iterable"]
},
"include": ["src/**/*", "src-electron/**/*"],
"exclude": ["node_modules", "build"]
}

6439
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"private": true,
"main": "src-electron/main.js",
"scripts": {
"dev": "cross-env PLATFORM=windows webpack serve --config webpack.config.js --mode development",
"watch": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode development --watch",
"watch-linux": "cross-env PLATFORM=linux webpack --config webpack.config.js --mode development --watch",
"localization": "node ./src/localization/localizationHelperCLI.js",
@@ -27,50 +28,52 @@
},
"homepage": "https://github.com/vrcx-team/VRCX#readme",
"devDependencies": {
"@babel/eslint-parser": "^7.27.5",
"@electron/rebuild": "^4.0.1",
"@fontsource/noto-sans-jp": "^5.2.5",
"@fontsource/noto-sans-kr": "^5.2.5",
"@eslint/js": "^9.31.0",
"@fontsource/noto-sans-jp": "^5.2.6",
"@fontsource/noto-sans-kr": "^5.2.6",
"@fontsource/noto-sans-sc": "^5.2.6",
"@fontsource/noto-sans-tc": "^5.2.6",
"@infolektuell/noto-color-emoji": "^0.2.0",
"@prettier/plugin-pug": "^3.4.0",
"@types/node": "^24.0.13",
"animate.css": "^4.1.1",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"dayjs": "^1.11.13",
"default-passive-events": "^4.0.0",
"echarts": "^5.6.0",
"electron": "^36.4.0",
"electron": "^37.2.1",
"electron-builder": "^26.0.12",
"element-ui": "^2.15.14",
"esbuild-loader": "^4.3.0",
"eslint": "^9.28.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^9.32.0",
"eslint-plugin-vue": "^9.33.0",
"globals": "^16.3.0",
"html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta-deprecated",
"prettier": "^3.5.3",
"pinia": "^2.3.1",
"prettier": "^3.6.2",
"pug": "^3.0.3",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
"sass": "^1.89.1",
"sass": "^1.89.2",
"sass-loader": "^16.0.5",
"vue": "^2.7.16",
"vue-data-tables": "^3.4.5",
"vue-demi": "^0.14.10",
"vue-i18n": "^8.28.2",
"vue-i18n-bridge": "^9.14.1",
"vue-lazyload": "^1.3.4",
"vue-lazyload": "^1.3.5",
"vue-loader": "^15.11.1",
"vue-markdown": "^2.2.4",
"vue-marquee-text-component": "^1.2.0",
"webpack": "^5.99.9",
"webpack": "^5.100.1",
"webpack-cli": "^6.0.1",
"worker-timers": "^8.0.21",
"webpack-dev-server": "^5.2.2",
"worker-timers": "^8.0.23",
"yargs": "^18.0.0"
},
"build": {
@@ -135,6 +138,6 @@
},
"dependencies": {
"hazardous": "^0.3.0",
"node-api-dotnet": "^0.9.11"
"node-api-dotnet": "^0.9.12"
}
}

109
src/App.vue Normal file
View File

@@ -0,0 +1,109 @@
<script>
import Vue, { onMounted } from 'vue';
import template from './app.pug';
import { createGlobalStores } from './stores';
import { watchState } from './service/watchState';
import Login from './views/Login/Login.vue';
import NavMenu from './components/NavMenu.vue';
import Sidebar from './views/Sidebar/Sidebar.vue';
import Feed from './views/Feed/Feed.vue';
import GameLog from './views/GameLog/GameLog.vue';
import PlayerList from './views/PlayerList/PlayerList.vue';
import Search from './views/Search/Search.vue';
import Favorites from './views/Favorites/Favorites.vue';
import FriendLog from './views/FriendLog/FriendLog.vue';
import Moderation from './views/Moderation/Moderation.vue';
import Notification from './views/Notifications/Notification.vue';
import FriendList from './views/FriendList/FriendList.vue';
import Charts from './views/Charts/Charts.vue';
import Profile from './views/Profile/Profile.vue';
import Settings from './views/Settings/Settings.vue';
import UserDialog from './components/dialogs/UserDialog/UserDialog.vue';
import WorldDialog from './components/dialogs/WorldDialog/WorldDialog.vue';
import AvatarDialog from './components/dialogs/AvatarDialog/AvatarDialog.vue';
import GroupDialog from './components/dialogs/GroupDialog/GroupDialog.vue';
import GalleryDialog from './components/dialogs/GalleryDialog.vue';
import FullscreenImageDialog from './components/dialogs/FullscreenImageDialog.vue';
import PreviousInstancesInfoDialog from './components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue';
import LaunchDialog from './components/dialogs/LaunchDialog.vue';
import LaunchOptionsDialog from './views/Settings/dialogs/LaunchOptionsDialog.vue';
import FriendImportDialog from './views/Favorites/dialogs/FriendImportDialog.vue';
import WorldImportDialog from './views/Favorites/dialogs/WorldImportDialog.vue';
import AvatarImportDialog from './views/Favorites/dialogs/AvatarImportDialog.vue';
import ChooseFavoriteGroupDialog from './components/dialogs/ChooseFavoriteGroupDialog.vue';
import EditInviteMessageDialog from './views/Profile/dialogs/EditInviteMessageDialog.vue';
import VRCXUpdateDialog from './components/dialogs/VRCXUpdateDialog.vue';
import VRChatConfigDialog from './views/Settings/dialogs/VRChatConfigDialog.vue';
import PrimaryPasswordDialog from './views/Settings/dialogs/PrimaryPasswordDialog.vue';
import { utils } from './shared/utils/_utils';
export default {
template,
components: {
Login,
NavMenu,
Sidebar,
Feed,
GameLog,
PlayerList,
Search,
Favorites,
FriendLog,
Moderation,
Notification,
FriendList,
Charts,
Profile,
Settings,
UserDialog,
WorldDialog,
AvatarDialog,
GroupDialog,
GalleryDialog,
FullscreenImageDialog,
PreviousInstancesInfoDialog,
LaunchDialog,
LaunchOptionsDialog,
FriendImportDialog,
WorldImportDialog,
AvatarImportDialog,
ChooseFavoriteGroupDialog,
EditInviteMessageDialog,
VRCXUpdateDialog,
VRChatConfigDialog,
PrimaryPasswordDialog
},
setup() {
const store = createGlobalStores();
Vue.prototype.store = store;
Vue.prototype.utils = utils;
store.updateLoop.updateLoop();
onMounted(async () => {
store.gameLog.getGameLogTable();
await store.auth.migrateStoredUsers();
store.auth.autoLoginAfterMounted();
store.vrcx.checkAutoBackupRestoreVrcRegistry();
store.game.checkVRChatDebugLogging();
try {
if (await AppApi.IsRunningUnderWine()) {
store.vrcx.isRunningUnderWine = true;
store.vrcx.applyWineEmojis();
}
} catch (err) {
console.error(err, 'Failed to check if running under Wine');
}
});
return {
store,
watchState
};
}
};
</script>

69
src/api/auth.js Normal file
View File

@@ -0,0 +1,69 @@
import { request } from '../service/request';
import { useUserStore } from '../stores';
const loginReq = {
/**
* @param {{ code: string }} params One-time password
* @returns {Promise<{json: any, params}>}
*/
verifyOTP(params) {
return request('auth/twofactorauth/otp/verify', {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
/**
* @param {{ code: string }} params One-time token
* @returns {Promise<{json: any, params}>}
*/
verifyTOTP(params) {
return request('auth/twofactorauth/totp/verify', {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
/**
* @param {{ code: string }} params One-time token
* @returns {Promise<{json: any, params}>}
*/
verifyEmailOTP(params) {
return request('auth/twofactorauth/emailotp/verify', {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
getConfig() {
return request('config', {
method: 'GET'
}).then((json) => {
const args = {
json
};
useUserStore().handleConfig(args);
return args;
});
}
};
export default loginReq;

View File

@@ -1,19 +1,18 @@
// #region | API: Avatar
import { request } from '../service/request';
import { useUserStore } from '../stores';
const avatarReq = {
/**
* @param {{ avatarId: string }} params
* @returns {Promise<{json: any, params}>}
* @type {import('../types/avatar').getAvatar}
*/
getAvatar(params) {
return window.API.call(`avatars/${params.avatarId}`, {
return request(`avatars/${params.avatarId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('AVATAR', args);
return args;
});
},
@@ -37,7 +36,7 @@ const avatarReq = {
* @returns {Promise<{json: any, params}>}
*/
getAvatars(params) {
return window.API.call('avatars', {
return request('avatars', {
method: 'GET',
params
}).then((json) => {
@@ -45,16 +44,16 @@ const avatarReq = {
json,
params
};
window.API.$emit('AVATAR:LIST', args);
return args;
});
},
/**
* @param {{ id: string, releaseStatus: 'public' | 'private' }} params
* @param {{ id: string, releaseStatus?: 'public' | 'private', name?: string, description?: string }} params
* @returns {Promise<{json: any, params}>}
*/
saveAvatar(params) {
return window.API.call(`avatars/${params.id}`, {
return request(`avatars/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
@@ -62,7 +61,6 @@ const avatarReq = {
json,
params
};
window.API.$emit('AVATAR:SAVE', args);
return args;
});
},
@@ -72,7 +70,8 @@ const avatarReq = {
* @returns {Promise<{json: any, params}>}
*/
selectAvatar(params) {
return window.API.call(`avatars/${params.avatarId}/select`, {
const userStore = useUserStore();
return request(`avatars/${params.avatarId}/select`, {
method: 'PUT',
params
}).then((json) => {
@@ -80,7 +79,7 @@ const avatarReq = {
json,
params
};
window.API.$emit('AVATAR:SELECT', args);
userStore.applyCurrentUser(json);
return args;
});
},
@@ -90,7 +89,8 @@ const avatarReq = {
* @return { Promise<{json: any, params}> }
*/
selectFallbackAvatar(params) {
return window.API.call(`avatars/${params.avatarId}/selectfallback`, {
const userStore = useUserStore();
return request(`avatars/${params.avatarId}/selectfallback`, {
method: 'PUT',
params
}).then((json) => {
@@ -98,7 +98,7 @@ const avatarReq = {
json,
params
};
window.API.$emit('AVATAR:SELECT', args);
userStore.applyCurrentUser(json);
return args;
});
},
@@ -108,14 +108,13 @@ const avatarReq = {
* @return { Promise<{json: any, params}> }
*/
deleteAvatar(params) {
return window.API.call(`avatars/${params.avatarId}`, {
return request(`avatars/${params.avatarId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('AVATAR:DELETE', args);
return args;
});
},
@@ -125,14 +124,13 @@ const avatarReq = {
* @returns {Promise<{json: any, params}>}
*/
createImposter(params) {
return window.API.call(`avatars/${params.avatarId}/impostor/enqueue`, {
return request(`avatars/${params.avatarId}/impostor/enqueue`, {
method: 'POST'
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('AVATAR:IMPOSTER:CREATE', args);
return args;
});
},
@@ -142,35 +140,33 @@ const avatarReq = {
* @returns {Promise<{json: T, params}>}
*/
deleteImposter(params) {
return window.API.call(`avatars/${params.avatarId}/impostor`, {
return request(`avatars/${params.avatarId}/impostor`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('AVATAR:IMPOSTER:DELETE', args);
return args;
});
},
/**
* @returns {Promise<{json: any, params}>}
* @returns {Promise<{json: any}>}
*/
getAvailableAvatarStyles() {
return window.API.call('avatarStyles', {
return request('avatarStyles', {
method: 'GET'
}).then((json) => {
const args = {
json
};
// window.API.$emit('AVATAR:STYLES', args);
return args;
});
},
/**
* @param {{ avatarId: string }} params
* @param {{ avatarId: string }} avatarId
* @returns {Promise<{json: any, params}>}
*/
getAvatarGallery(avatarId) {
@@ -180,7 +176,7 @@ const avatarReq = {
n: 100,
offset: 0
};
return window.API.call(`files`, {
return request(`files`, {
params,
method: 'GET'
}).then((json) => {
@@ -188,7 +184,6 @@ const avatarReq = {
json,
params
};
// window.API.$emit('AVATAR:GALLERY', args);
return args;
});
},
@@ -202,7 +197,7 @@ const avatarReq = {
tag: 'avatargallery',
galleryId: avatarId
};
return window.API.call('file/image', {
return request('file/image', {
uploadImage: true,
matchingDimensions: false,
postData: JSON.stringify(params),
@@ -212,7 +207,6 @@ const avatarReq = {
json,
params
};
// window.API.$emit('AVATARGALLERYIMAGE:ADD', args);
return args;
});
},
@@ -225,7 +219,7 @@ const avatarReq = {
const params = {
ids: order
};
return window.API.call('files/order', {
return request('files/order', {
method: 'PUT',
params
}).then((json) => {
@@ -233,11 +227,9 @@ const avatarReq = {
json,
params
};
// window.API.$emit('AVATARGALLERYIMAGE:ORDER', args);
return args;
});
}
};
// #endregion
export default avatarReq;

View File

@@ -1,14 +1,13 @@
// #region | API: AvatarModeration
import { request } from '../service/request';
const avatarModerationReq = {
getAvatarModerations() {
return window.API.call('auth/user/avatarmoderations', {
return request('auth/user/avatarmoderations', {
method: 'GET'
}).then((json) => {
const args = {
json
};
// window.API.$emit('AVATAR-MODERATION:LIST', args);
return args;
});
},
@@ -18,7 +17,7 @@ const avatarModerationReq = {
* @return { Promise<{json: any, params}> }
*/
sendAvatarModeration(params) {
return window.API.call('auth/user/avatarmoderations', {
return request('auth/user/avatarmoderations', {
method: 'POST',
params
}).then((json) => {
@@ -26,7 +25,6 @@ const avatarModerationReq = {
json,
params
};
// window.API.$emit('AVATAR-MODERATION', args);
return args;
});
},
@@ -36,7 +34,7 @@ const avatarModerationReq = {
* @return { Promise<{json: any, params}> }
*/
deleteAvatarModeration(params) {
return window.API.call(
return request(
`auth/user/avatarmoderations?targetAvatarId=${encodeURIComponent(
params.targetAvatarId
)}&avatarModerationType=${encodeURIComponent(
@@ -50,11 +48,9 @@ const avatarModerationReq = {
json,
params
};
window.API.$emit('AVATAR-MODERATION:DELETE', args);
return args;
});
}
};
// #endregion
export default avatarModerationReq;

View File

@@ -1,29 +1,27 @@
// #region | API: Favorite
import { request } from '../service/request';
import { useFavoriteStore, useUserStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const favoriteReq = {
getFavoriteLimits() {
return window.API.call('auth/user/favoritelimits', {
return request('auth/user/favoritelimits', {
method: 'GET'
}).then((json) => {
const args = {
json
};
window.API.$emit('FAVORITE:LIMITS', args);
return args;
});
},
/**
* @param {{
* n: number,
* offset: number,
* type: string,
* tag: string
* }} params
* @return { Promise<{json: any, params}> }
* @type {import('../types/favorite').getFavorites}
*/
getFavorites(params) {
return window.API.call('favorites', {
return request('favorites', {
method: 'GET',
params
}).then((json) => {
@@ -31,7 +29,6 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:LIST', args);
return args;
});
},
@@ -45,7 +42,7 @@ const favoriteReq = {
* @return { Promise<{json: any, params}> }
*/
addFavorite(params) {
return window.API.call('favorites', {
return request('favorites', {
method: 'POST',
params
}).then((json) => {
@@ -53,7 +50,7 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:ADD', args);
useFavoriteStore().handleFavoriteAdd(args);
return args;
});
},
@@ -63,14 +60,14 @@ const favoriteReq = {
* @return { Promise<{json: any, params}> }
*/
deleteFavorite(params) {
return window.API.call(`favorites/${params.objectId}`, {
return request(`favorites/${params.objectId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('FAVORITE:DELETE', args);
useFavoriteStore().handleFavoriteDelete(args);
return args;
});
},
@@ -80,7 +77,7 @@ const favoriteReq = {
* @return { Promise<{json: any, params}> }
*/
getFavoriteGroups(params) {
return window.API.call('favorite/groups', {
return request('favorite/groups', {
method: 'GET',
params
}).then((json) => {
@@ -88,7 +85,6 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:GROUP:LIST', args);
return args;
});
},
@@ -99,8 +95,8 @@ const favoriteReq = {
* @return { Promise<{json: any, params}> }
*/
saveFavoriteGroup(params) {
return window.API.call(
`favorite/group/${params.type}/${params.group}/${window.API.currentUser.id}`,
return request(
`favorite/group/${params.type}/${params.group}/${getCurrentUserId()}`,
{
method: 'PUT',
params
@@ -110,7 +106,6 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:GROUP:SAVE', args);
return args;
});
},
@@ -123,8 +118,8 @@ const favoriteReq = {
* @return { Promise<{json: any, params}> }
*/
clearFavoriteGroup(params) {
return window.API.call(
`favorite/group/${params.type}/${params.group}/${window.API.currentUser.id}`,
return request(
`favorite/group/${params.type}/${params.group}/${getCurrentUserId()}`,
{
method: 'DELETE',
params
@@ -134,20 +129,16 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:GROUP:CLEAR', args);
useFavoriteStore().handleFavoriteGroupClear(args);
return args;
});
},
/**
* @param {{
* n: number,
* offset: number
* }} params
* @return { Promise<{json: any, params}> }
* @type {import('../types/favorite').getFavoriteWorlds}
*/
getFavoriteWorlds(params) {
return window.API.call('worlds/favorites', {
return request('worlds/favorites', {
method: 'GET',
params
}).then((json) => {
@@ -155,20 +146,15 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:WORLD:LIST', args);
return args;
});
},
/**
* @param {{
* n: number,
* offset: number
* }} params
* @return { Promise<{json: any, params}> }
* @type {import('../types/favorite').getFavoriteAvatars}
*/
getFavoriteAvatars(params) {
return window.API.call('avatars/favorites', {
return request('avatars/favorites', {
method: 'GET',
params
}).then((json) => {
@@ -176,12 +162,9 @@ const favoriteReq = {
json,
params
};
window.API.$emit('FAVORITE:AVATAR:LIST', args);
return args;
});
}
};
// #endregion
export default favoriteReq;

View File

@@ -1,13 +1,14 @@
// #region | API: Friend
import { request } from '../service/request';
import { useUserStore } from '../stores/user';
const friendReq = {
/**
* Fetch friends of current user.
* @param {{ n: number, offset: number, offline: boolean }} params
* @returns {Promise<{json: any, params}>}
* @type {import('../types/friend').getFriends}
*/
getFriends(params) {
return window.API.call('auth/user/friends', {
const userStore = useUserStore();
return request('auth/user/friends', {
method: 'GET',
params
}).then((json) => {
@@ -15,7 +16,13 @@ const friendReq = {
json,
params
};
window.API.$emit('FRIEND:LIST', args);
for (const user of args.json) {
if (!user.displayName) {
console.error('/friends gave us garbage', user);
continue;
}
userStore.applyUser(user);
}
return args;
});
},
@@ -25,14 +32,13 @@ const friendReq = {
* @returns {Promise<{json: T, params}>}
*/
sendFriendRequest(params) {
return window.API.call(`user/${params.userId}/friendRequest`, {
return request(`user/${params.userId}/friendRequest`, {
method: 'POST'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('FRIEND:REQUEST', args);
return args;
});
},
@@ -42,14 +48,13 @@ const friendReq = {
* @returns {Promise<{json: any, params}>}
*/
cancelFriendRequest(params) {
return window.API.call(`user/${params.userId}/friendRequest`, {
return request(`user/${params.userId}/friendRequest`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('FRIEND:REQUEST:CANCEL', args);
return args;
});
},
@@ -59,14 +64,13 @@ const friendReq = {
* @returns {Promise<{json: any, params}>}
*/
deleteFriend(params) {
return window.API.call(`auth/user/friends/${params.userId}`, {
return request(`auth/user/friends/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('FRIEND:DELETE', args);
return args;
});
},
@@ -76,7 +80,7 @@ const friendReq = {
* @returns {Promise<{json: any, params}>}
*/
getFriendStatus(params) {
return window.API.call(`user/${params.userId}/friendStatus`, {
return request(`user/${params.userId}/friendStatus`, {
method: 'GET'
}).then((json) => {
console.log('getFriendStatus', json);
@@ -84,15 +88,12 @@ const friendReq = {
json,
params
};
window.API.$emit('FRIEND:STATUS', args);
return args;
});
},
// ------------------- need to test -------------------
deleteHiddenFriendRequest(params, userId) {
return window.API.call(`user/${userId}/friendRequest`, {
return request(`user/${userId}/friendRequest`, {
method: 'DELETE',
params
}).then((json) => {
@@ -101,11 +102,9 @@ const friendReq = {
params,
userId
};
window.API.$emit('NOTIFICATION:HIDE', args);
return args;
});
}
};
// #endregion
export default friendReq;

View File

@@ -1,3 +1,9 @@
import { request } from '../service/request';
import { useUserStore, useGroupStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const groupReq = {
/**
* @param {string} groupId
@@ -5,7 +11,7 @@ const groupReq = {
* @returns
*/
setGroupRepresentation(groupId, params) {
return window.API.call(`groups/${groupId}/representation`, {
return request(`groups/${groupId}/representation`, {
method: 'PUT',
params
}).then((json) => {
@@ -23,7 +29,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
cancelGroupRequest(params) {
return window.API.call(`groups/${params.groupId}/requests`, {
return request(`groups/${params.groupId}/requests`, {
method: 'DELETE'
}).then((json) => {
const args = {
@@ -39,12 +45,9 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
deleteGroupPost(params) {
return window.API.call(
`groups/${params.groupId}/posts/${params.postId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/posts/${params.postId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
@@ -53,10 +56,10 @@ const groupReq = {
});
},
/**
* @param {{ groupId: string }} params
* @type {import('../types/group').getGroup}
*/
getGroup(params) {
return window.API.call(`groups/${params.groupId}`, {
return request(`groups/${params.groupId}`, {
method: 'GET',
params: {
includeRoles: params.includeRoles || false
@@ -66,23 +69,48 @@ const groupReq = {
json,
params
};
window.API.$emit('GROUP', args);
return args;
});
},
/**
*
* @param {{ groupId: string }} params
* @return { Promise<{json: any, params}> }
*/
getCachedGroup(params) {
const groupStore = useGroupStore();
return new Promise((resolve, reject) => {
const ref = groupStore.cachedGroups.get(params.groupId);
if (typeof ref === 'undefined') {
groupReq
.getGroup(params)
.catch(reject)
.then((args) => {
args.ref = groupStore.applyGroup(args.json);
resolve(args);
});
} else {
resolve({
cache: true,
json: ref,
params,
ref
});
}
});
},
/**
* @param {{ userId: string }} params
* @return { Promise<{json: any, params}> }
*/
getRepresentedGroup(params) {
return window.API.call(`users/${params.userId}/groups/represented`, {
return request(`users/${params.userId}/groups/represented`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('GROUP:REPRESENTED', args);
return args;
});
},
@@ -91,14 +119,13 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroups(params) {
return window.API.call(`users/${params.userId}/groups`, {
return request(`users/${params.userId}/groups`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('GROUP:LIST', args);
return args;
});
},
@@ -107,14 +134,13 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
joinGroup(params) {
return window.API.call(`groups/${params.groupId}/join`, {
return request(`groups/${params.groupId}/join`, {
method: 'POST'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('GROUP:JOIN', args);
return args;
});
},
@@ -123,7 +149,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
leaveGroup(params) {
return window.API.call(`groups/${params.groupId}/leave`, {
return request(`groups/${params.groupId}/leave`, {
method: 'POST'
}).then((json) => {
const args = {
@@ -138,7 +164,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
groupStrictsearch(params) {
return window.API.call(`groups/strictsearch`, {
return request(`groups/strictsearch`, {
method: 'GET',
params
}).then((json) => {
@@ -159,7 +185,7 @@ const groupReq = {
}
*/
setGroupMemberProps(userId, groupId, params) {
return window.API.call(`groups/${groupId}/members/${userId}`, {
return request(`groups/${groupId}/members/${userId}`, {
method: 'PUT',
params
}).then((json) => {
@@ -169,7 +195,6 @@ const groupReq = {
groupId,
params
};
window.API.$emit('GROUP:MEMBER:PROPS', args);
return args;
});
},
@@ -182,7 +207,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
addGroupMemberRole(params) {
return window.API.call(
return request(
`groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`,
{
method: 'PUT'
@@ -192,7 +217,6 @@ const groupReq = {
json,
params
};
// window.API.$emit('GROUP:MEMBER:ROLE:CHANGE', args);
return args;
});
},
@@ -205,7 +229,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
removeGroupMemberRole(params) {
return window.API.call(
return request(
`groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`,
{
method: 'DELETE'
@@ -215,19 +239,17 @@ const groupReq = {
json,
params
};
// window.API.$emit('GROUP:MEMBER:ROLE:CHANGE', args);
return args;
});
},
getGroupPermissions(params) {
return window.API.call(`users/${params.userId}/groups/permissions`, {
return request(`users/${params.userId}/groups/permissions`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('GROUP:PERMISSIONS', args);
return args;
});
},
@@ -240,7 +262,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupPosts(params) {
return window.API.call(`groups/${params.groupId}/posts`, {
return request(`groups/${params.groupId}/posts`, {
method: 'GET',
params
}).then((json) => {
@@ -248,28 +270,23 @@ const groupReq = {
json,
params
};
window.API.$emit('GROUP:POSTS', args);
return args;
});
},
editGroupPost(params) {
return window.API.call(
`groups/${params.groupId}/posts/${params.postId}`,
{
method: 'PUT',
params
}
).then((json) => {
return request(`groups/${params.groupId}/posts/${params.postId}`, {
method: 'PUT',
params
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('GROUP:POST', args);
return args;
});
},
createGroupPost(params) {
return window.API.call(`groups/${params.groupId}/posts`, {
return request(`groups/${params.groupId}/posts`, {
method: 'POST',
params
}).then((json) => {
@@ -277,7 +294,6 @@ const groupReq = {
json,
params
};
window.API.$emit('GROUP:POST', args);
return args;
});
},
@@ -289,17 +305,13 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupMember(params) {
return window.API.call(
`groups/${params.groupId}/members/${params.userId}`,
{
method: 'GET'
}
).then((json) => {
return request(`groups/${params.groupId}/members/${params.userId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('GROUP:MEMBER', args);
return args;
});
},
@@ -312,7 +324,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupMembers(params) {
return window.API.call(`groups/${params.groupId}/members`, {
return request(`groups/${params.groupId}/members`, {
method: 'GET',
params
}).then((json) => {
@@ -320,7 +332,6 @@ const groupReq = {
json,
params
};
window.API.$emit('GROUP:MEMBERS', args);
return args;
});
},
@@ -334,7 +345,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupMembersSearch(params) {
return window.API.call(`groups/${params.groupId}/members/search`, {
return request(`groups/${params.groupId}/members/search`, {
method: 'GET',
params
}).then((json) => {
@@ -352,7 +363,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
blockGroup(params) {
return window.API.call(`groups/${params.groupId}/block`, {
return request(`groups/${params.groupId}/block`, {
method: 'POST'
}).then((json) => {
const args = {
@@ -370,12 +381,9 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
unblockGroup(params) {
return window.API.call(
`groups/${params.groupId}/members/${params.userId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/members/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
@@ -391,7 +399,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
sendGroupInvite(params) {
return window.API.call(`groups/${params.groupId}/invites`, {
return request(`groups/${params.groupId}/invites`, {
method: 'POST',
params: {
userId: params.userId
@@ -401,7 +409,6 @@ const groupReq = {
json,
params
};
window.API.$emit('GROUP:INVITE', args);
return args;
});
},
@@ -413,18 +420,13 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
kickGroupMember(params) {
return window.API.call(
`groups/${params.groupId}/members/${params.userId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/members/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:MEMBER:KICK', args);
return args;
});
},
@@ -433,7 +435,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
banGroupMember(params) {
return window.API.call(`groups/${params.groupId}/bans`, {
return request(`groups/${params.groupId}/bans`, {
method: 'POST',
params: {
userId: params.userId
@@ -443,24 +445,17 @@ const groupReq = {
json,
params
};
// useless code
// window.API.$emit('GROUP:MEMBER:BAN', args);
return args;
});
},
unbanGroupMember(params) {
return window.API.call(
`groups/${params.groupId}/bans/${params.userId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/bans/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:MEMBER:UNBAN', args);
return args;
});
},
@@ -469,97 +464,73 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
deleteSentGroupInvite(params) {
return window.API.call(
`groups/${params.groupId}/invites/${params.userId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/invites/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:INVITE:DELETE', args);
return args;
});
},
deleteBlockedGroupRequest(params) {
return window.API.call(
`groups/${params.groupId}/members/${params.userId}`,
{
method: 'DELETE'
}
).then((json) => {
return request(`groups/${params.groupId}/members/${params.userId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:BLOCKED:DELETE', args);
return args;
});
},
acceptGroupInviteRequest(params) {
return window.API.call(
`groups/${params.groupId}/requests/${params.userId}`,
{
method: 'PUT',
params: {
action: 'accept'
}
return request(`groups/${params.groupId}/requests/${params.userId}`, {
method: 'PUT',
params: {
action: 'accept'
}
).then((json) => {
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:INVITE:ACCEPT', args);
return args;
});
},
rejectGroupInviteRequest(params) {
return window.API.call(
`groups/${params.groupId}/requests/${params.userId}`,
{
method: 'PUT',
params: {
action: 'reject'
}
return request(`groups/${params.groupId}/requests/${params.userId}`, {
method: 'PUT',
params: {
action: 'reject'
}
).then((json) => {
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:INVITE:REJECT', args);
return args;
});
},
blockGroupInviteRequest(params) {
return window.API.call(
`groups/${params.groupId}/requests/${params.userId}`,
{
method: 'PUT',
params: {
action: 'reject',
block: true
}
return request(`groups/${params.groupId}/requests/${params.userId}`, {
method: 'PUT',
params: {
action: 'reject',
block: true
}
).then((json) => {
}).then((json) => {
const args = {
json,
params
};
// useless code
// window.API.$emit('GROUP:INVITE:BLOCK', args);
return args;
});
},
getGroupBans(params) {
return window.API.call(`groups/${params.groupId}/bans`, {
return request(`groups/${params.groupId}/bans`, {
method: 'GET',
params
}).then((json) => {
@@ -575,7 +546,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupAuditLogTypes(params) {
return window.API.call(`groups/${params.groupId}/auditLogTypes`, {
return request(`groups/${params.groupId}/auditLogTypes`, {
method: 'GET'
}).then((json) => {
const args = {
@@ -590,7 +561,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupLogs(params) {
return window.API.call(`groups/${params.groupId}/auditLogs`, {
return request(`groups/${params.groupId}/auditLogs`, {
method: 'GET',
params
}).then((json) => {
@@ -606,7 +577,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupInvites(params) {
return window.API.call(`groups/${params.groupId}/invites`, {
return request(`groups/${params.groupId}/invites`, {
method: 'GET',
params
}).then((json) => {
@@ -622,7 +593,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupJoinRequests(params) {
return window.API.call(`groups/${params.groupId}/requests`, {
return request(`groups/${params.groupId}/requests`, {
method: 'GET',
params
}).then((json) => {
@@ -630,7 +601,6 @@ const groupReq = {
json,
params
};
// window.API.$emit('GROUP:JOINREQUESTS', args);
return args;
});
},
@@ -639,8 +609,8 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupInstances(params) {
return window.API.call(
`users/${window.API.currentUser.id}/instances/groups/${params.groupId}`,
return request(
`users/${getCurrentUserId()}/instances/groups/${params.groupId}`,
{
method: 'GET'
}
@@ -657,7 +627,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupRoles(params) {
return window.API.call(`groups/${params.groupId}/roles`, {
return request(`groups/${params.groupId}/roles`, {
method: 'GET',
params
}).then((json) => {
@@ -665,22 +635,16 @@ const groupReq = {
json,
params
};
// useless code
// this.$emit('GROUP:ROLES', args);
return args;
});
},
getUsersGroupInstances() {
return window.API.call(
`users/${window.API.currentUser.id}/instances/groups`,
{
method: 'GET'
}
).then((json) => {
return request(`users/${getCurrentUserId()}/instances/groups`, {
method: 'GET'
}).then((json) => {
const args = {
json
};
window.API.$emit('GROUP:USER:INSTANCES', args);
return args;
});
},
@@ -696,7 +660,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
groupSearch(params) {
return window.API.call(`groups`, {
return request(`groups`, {
method: 'GET',
params
}).then((json) => {
@@ -717,7 +681,7 @@ const groupReq = {
* @return { Promise<{json: any, params}> }
*/
getGroupGallery(params) {
return window.API.call(
return request(
`groups/${params.groupId}/galleries/${params.galleryId}`,
{
method: 'GET',
@@ -735,10 +699,9 @@ const groupReq = {
});
}
// no place to use this
// getRequestedGroups() {
// return window.API.call(
// `users/${window.API.currentUser.id}/groups/requested`,
// return request(
// `users/${API.currentUser.id}/groups/requested`,
// {
// method: 'GET'
// }
@@ -746,18 +709,17 @@ const groupReq = {
// const args = {
// json
// };
// window.API.$emit('GROUP:REQUESTED', args);
// API.$emit('GROUP:REQUESTED', args);
// return args;
// });
// }
// ----------------- left over code -----------------
// /**
// * @param {{ groupId: string }} params
// * @return { Promise<{json: any, params}> }
// */
// API.getGroupAnnouncement = function (params) {
// return this.call(`groups/${params.groupId}/announcement`, {
// return request(`groups/${params.groupId}/announcement`, {
// method: 'GET'
// }).then((json) => {
// var args = {

View File

@@ -1,25 +1,26 @@
import { request } from '../service/request';
import { useAvatarStore, useWorldStore } from '../stores';
const imageReq = {
// use in uploadAvatarImage
// need to test
async uploadAvatarFailCleanup(id) {
const json = await window.API.call(`file/${id}`, {
const avatarStore = useAvatarStore();
const json = await request(`file/${id}`, {
method: 'GET'
});
const fileId = json.id;
const fileVersion = json.versions[json.versions.length - 1].version;
window.API.call(`file/${fileId}/${fileVersion}/signature/finish`, {
request(`file/${fileId}/${fileVersion}/signature/finish`, {
method: 'PUT'
});
window.API.call(`file/${fileId}/${fileVersion}/file/finish`, {
request(`file/${fileId}/${fileVersion}/file/finish`, {
method: 'PUT'
});
window.$app.avatarDialog.loading = false;
// window.$app.changeAvatarImageDialogLoading = false;
avatarStore.avatarDialog.loading = false;
},
async uploadAvatarImage(params, fileId) {
try {
return await window.API.call(`file/${fileId}`, {
return await request(`file/${fileId}`, {
method: 'POST',
params
}).then((json) => {
@@ -28,7 +29,6 @@ const imageReq = {
params,
fileId
};
// window.API.$emit('AVATARIMAGE:INIT', args);
return args;
});
} catch (err) {
@@ -36,12 +36,11 @@ const imageReq = {
imageReq.uploadAvatarFailCleanup(fileId);
throw err;
}
// return void 0;
},
async uploadAvatarImageFileStart(params) {
try {
return await window.API.call(
return await request(
`file/${params.fileId}/${params.fileVersion}/file/start`,
{
method: 'PUT'
@@ -51,18 +50,16 @@ const imageReq = {
json,
params
};
// window.API.$emit('AVATARIMAGE:FILESTART', args);
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
return void 0;
},
uploadAvatarImageFileFinish(params) {
return window.API.call(
return request(
`file/${params.fileId}/${params.fileVersion}/file/finish`,
{
method: 'PUT',
@@ -76,14 +73,13 @@ const imageReq = {
json,
params
};
// window.API.$emit('AVATARIMAGE:FILEFINISH', args);
return args;
});
},
async uploadAvatarImageSigStart(params) {
try {
return await window.API.call(
return await request(
`file/${params.fileId}/${params.fileVersion}/signature/start`,
{
method: 'PUT'
@@ -93,18 +89,16 @@ const imageReq = {
json,
params
};
// window.API.$emit('AVATARIMAGE:SIGSTART', args);
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
return void 0;
},
uploadAvatarImageSigFinish(params) {
return window.API.call(
return request(
`file/${params.fileId}/${params.fileVersion}/signature/finish`,
{
method: 'PUT',
@@ -118,13 +112,12 @@ const imageReq = {
json,
params
};
// window.API.$emit('AVATARIMAGE:SIGFINISH', args);
return args;
});
},
setAvatarImage(params) {
return window.API.call(`avatars/${params.id}`, {
return request(`avatars/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
@@ -132,33 +125,29 @@ const imageReq = {
json,
params
};
// window.API.$emit('AVATARIMAGE:SET', args);
window.API.$emit('AVATAR', args);
return args;
});
},
// use in uploadWorldImage
// need to test
async uploadWorldFailCleanup(id) {
const json = await window.API.call(`file/${id}`, {
const worldStore = useWorldStore();
const json = await request(`file/${id}`, {
method: 'GET'
});
const fileId = json.id;
const fileVersion = json.versions[json.versions.length - 1].version;
window.API.call(`file/${fileId}/${fileVersion}/signature/finish`, {
request(`file/${fileId}/${fileVersion}/signature/finish`, {
method: 'PUT'
});
window.API.call(`file/${fileId}/${fileVersion}/file/finish`, {
request(`file/${fileId}/${fileVersion}/file/finish`, {
method: 'PUT'
});
window.$app.worldDialog.loading = false;
// window.$app.changeWorldImageDialogLoading = false;
worldStore.worldDialog.loading = false;
},
async uploadWorldImage(params, fileId) {
try {
return await window.API.call(`file/${fileId}`, {
return await request(`file/${fileId}`, {
method: 'POST',
params
}).then((json) => {
@@ -167,7 +156,6 @@ const imageReq = {
params,
fileId
};
// window.API.$emit('WORLDIMAGE:INIT', args);
return args;
});
} catch (err) {
@@ -179,7 +167,7 @@ const imageReq = {
async uploadWorldImageFileStart(params) {
try {
return await window.API.call(
return await request(
`file/${params.fileId}/${params.fileVersion}/file/start`,
{
method: 'PUT'
@@ -189,7 +177,6 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:FILESTART', args);
return args;
});
} catch (err) {
@@ -200,7 +187,7 @@ const imageReq = {
},
uploadWorldImageFileFinish(params) {
return window.API.call(
return request(
`file/${params.fileId}/${params.fileVersion}/file/finish`,
{
method: 'PUT',
@@ -214,14 +201,13 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:FILEFINISH', args);
return args;
});
},
async uploadWorldImageSigStart(params) {
try {
return await window.API.call(
return await request(
`file/${params.fileId}/${params.fileVersion}/signature/start`,
{
method: 'PUT'
@@ -231,7 +217,6 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:SIGSTART', args);
return args;
});
} catch (err) {
@@ -242,7 +227,7 @@ const imageReq = {
},
uploadWorldImageSigFinish(params) {
return window.API.call(
return request(
`file/${params.fileId}/${params.fileVersion}/signature/finish`,
{
method: 'PUT',
@@ -256,13 +241,13 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:SIGFINISH', args);
return args;
});
},
setWorldImage(params) {
return window.API.call(`worlds/${params.id}`, {
const worldStore = useWorldStore();
return request(`worlds/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
@@ -270,27 +255,25 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:SET', args);
window.API.$emit('WORLD', args);
args.ref = worldStore.applyWorld(json);
return args;
});
},
getAvatarImages(params) {
return window.API.call(`file/${params.fileId}`, {
return request(`file/${params.fileId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('AVATARIMAGE:GET', args);
return args;
});
},
getWorldImages(params) {
return window.API.call(`file/${params.fileId}`, {
return request(`file/${params.fileId}`, {
method: 'GET',
params
}).then((json) => {
@@ -298,7 +281,6 @@ const imageReq = {
json,
params
};
// window.API.$emit('WORLDIMAGE:GET', args);
return args;
});
}

View File

@@ -1,9 +1,6 @@
/**
* API requests
* Export all API requests from here
*
* "window.API" is used as app.js is a large IIFE, preventing direct API export. No current issues
* Refactoring may be required
*/
import userRequest from './user';
@@ -21,6 +18,7 @@ import inviteMessagesRequest from './inviteMessages';
import imageRequest from './image';
import miscRequest from './misc';
import groupRequest from './group';
import authRequest from './auth';
import inventoryRequest from './inventory';
import propRequest from './prop';
@@ -39,6 +37,7 @@ window.request = {
inviteMessagesRequest,
imageRequest,
miscRequest,
authRequest,
groupRequest,
inventoryRequest,
propRequest
@@ -59,6 +58,7 @@ export {
inviteMessagesRequest,
imageRequest,
miscRequest,
authRequest,
groupRequest,
inventoryRequest,
propRequest

View File

@@ -1,22 +1,22 @@
// #region | API: Instance
import { $app } from '../app';
import { t } from '../plugin';
import { request } from '../service/request';
import { useInstanceStore } from '../stores';
const instanceReq = {
/**
* @param {{worldId: string, instanceId: string}} params
* @returns {Promise<{json: any, params}>}
* @type {import('../types/instance').getInstance}
*/
getInstance(params) {
return window.API.call(
`instances/${params.worldId}:${params.instanceId}`,
{
method: 'GET'
}
).then((json) => {
const instanceStore = useInstanceStore();
return request(`instances/${params.worldId}:${params.instanceId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('INSTANCE', args);
args.ref = instanceStore.applyInstance(json);
return args;
});
},
@@ -37,7 +37,8 @@ const instanceReq = {
* @returns {Promise<{json: any, params}>}
*/
createInstance(params) {
return window.API.call('instances', {
const instanceStore = useInstanceStore();
return request('instances', {
method: 'POST',
params
}).then((json) => {
@@ -45,7 +46,7 @@ const instanceReq = {
json,
params
};
window.API.$emit('INSTANCE', args);
args.ref = instanceStore.applyInstance(json);
return args;
});
},
@@ -59,7 +60,7 @@ const instanceReq = {
if (instance.shortName) {
params.shortName = instance.shortName;
}
return window.API.call(
return request(
`instances/${instance.worldId}:${instance.instanceId}/shortName`,
{
method: 'GET',
@@ -80,14 +81,15 @@ const instanceReq = {
* @returns {Promise<{json: any, params}>}
*/
getInstanceFromShortName(params) {
return window.API.call(`instances/s/${params.shortName}`, {
const instanceStore = useInstanceStore();
return request(`instances/s/${params.shortName}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('INSTANCE', args);
args.ref = instanceStore.applyInstance(json);
return args;
});
},
@@ -105,7 +107,7 @@ const instanceReq = {
if (instance.shortName) {
params.shortName = instance.shortName;
}
return window.API.call(
return request(
`invite/myself/to/${instance.worldId}:${instance.instanceId}`,
{
method: 'POST',
@@ -121,20 +123,19 @@ const instanceReq = {
})
.catch((err) => {
if (err?.error?.message) {
window.$app.$message({
$app.$message({
message: err.error.message,
type: 'error'
});
throw err;
}
window.$app.$message({
message: window.$t('message.instance.not_allowed'),
$app.$message({
message: t('message.instance.not_allowed'),
type: 'error'
});
throw err;
});
}
};
// #endregion
export default instanceReq;

View File

@@ -1,10 +1,12 @@
import { request } from '../service/request';
const inventoryReq = {
/**
* @param {{ inventoryId: string, userId: string }} params
* @returns {Promise<{json: any, params}>}
*/
getUserInventoryItem(params) {
return window.API.call(
return request(
`user/${params.userId}/inventory/${params.inventoryId}`,
{
method: 'GET'
@@ -23,7 +25,7 @@ const inventoryReq = {
* @returns {Promise<{json: any, params}>}
*/
getInventoryItem(params) {
return window.API.call(`inventory/${params.inventoryId}`, {
return request(`inventory/${params.inventoryId}`, {
method: 'GET',
params
}).then((json) => {
@@ -40,7 +42,7 @@ const inventoryReq = {
* @returns {Promise<{json: any, params}>}
*/
getInventoryItems(params) {
return window.API.call('inventory', {
return request('inventory', {
method: 'GET',
params
}).then((json) => {
@@ -57,7 +59,7 @@ const inventoryReq = {
* @returns {Promise<{json: any, params}>}
*/
consumeInventoryBundle(params) {
return window.API.call(`inventory/${params.inventoryId}/consume`, {
return request(`inventory/${params.inventoryId}/consume`, {
method: 'PUT',
params
}).then((json) => {
@@ -74,13 +76,10 @@ const inventoryReq = {
* @returns {Promise<{json: any, params}>}
*/
getInventoryTemplate(params) {
return window.API.call(
`inventory/template/${params.inventoryTemplateId}`,
{
method: 'GET',
params
}
).then((json) => {
return request(`inventory/template/${params.inventoryTemplateId}`, {
method: 'GET',
params
}).then((json) => {
const args = {
json,
params

View File

@@ -1,30 +1,28 @@
// #region | App: Invite Messages
import { request } from '../service/request';
import { useUserStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const inviteMessagesReq = {
refreshInviteMessageTableData(messageType) {
return window.API.call(
`message/${window.API.currentUser.id}/${messageType}`,
{
method: 'GET'
}
).then((json) => {
return request(`message/${getCurrentUserId()}/${messageType}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
messageType
};
window.API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
return args;
});
},
editInviteMessage(params, messageType, slot) {
return window.API.call(
`message/${window.API.currentUser.id}/${messageType}/${slot}`,
{
method: 'PUT',
params
}
).then((json) => {
return request(`message/${getCurrentUserId()}/${messageType}/${slot}`, {
method: 'PUT',
params
}).then((json) => {
const args = {
json,
params,
@@ -36,6 +34,4 @@ const inviteMessagesReq = {
}
};
// #endregion
export default inviteMessagesReq;

View File

@@ -1,6 +1,13 @@
import { request } from '../service/request';
import { useUserStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const miscReq = {
getBundles(fileId) {
return window.API.call(`file/${fileId}`, {
return request(`file/${fileId}`, {
method: 'GET'
}).then((json) => {
const args = {
@@ -11,7 +18,7 @@ const miscReq = {
},
saveNote(params) {
return window.API.call('userNotes', {
return request('userNotes', {
method: 'POST',
params
}).then((json) => {
@@ -19,7 +26,6 @@ const miscReq = {
json,
params
};
// window.API.$emit('NOTE', args);
return args;
});
},
@@ -34,7 +40,7 @@ const miscReq = {
* @return { Promise<{json: any, params}> }
*/
reportUser(params) {
return window.API.call(`feedback/${params.userId}/user`, {
return request(`feedback/${params.userId}/user`, {
method: 'POST',
params: {
contentType: params.contentType,
@@ -46,7 +52,6 @@ const miscReq = {
json,
params
};
// window.API.$emit('FEEDBACK:REPORT:USER', args);
return args;
});
},
@@ -59,7 +64,7 @@ const miscReq = {
* @return { Promise<{json: any, params}> }
*/
getFileAnalysis(params) {
return window.API.call(
return request(
`analysis/${params.fileId}/${params.version}/${params.variant}`,
{
method: 'GET'
@@ -69,19 +74,17 @@ const miscReq = {
json,
params
};
// window.API.$emit('FILE:ANALYSIS', args);
return args;
});
},
getVRChatCredits() {
return window.API.call(`user/${window.API.currentUser.id}/balance`, {
return request(`user/${getCurrentUserId()}/balance`, {
method: 'GET'
}).then((json) => {
const args = {
json
};
// window.API.$emit('VRCCREDITS', args);
return args;
});
},
@@ -94,7 +97,7 @@ const miscReq = {
* @returns {Promise<{json: any, params}>}
*/
closeInstance(params) {
return window.API.call(`instances/${params.location}`, {
return request(`instances/${params.location}`, {
method: 'DELETE',
params: {
hardClose: params.hardClose ?? false
@@ -104,7 +107,6 @@ const miscReq = {
json,
params
};
window.API.$emit('INSTANCE:CLOSE', args);
return args;
});
},
@@ -116,8 +118,8 @@ const miscReq = {
* @returns {Promise<{json: any, params}>}
*/
deleteWorldPersistData(params) {
return window.API.call(
`users/${window.API.currentUser.id}/${params.worldId}/persist`,
return request(
`users/${getCurrentUserId()}/${params.worldId}/persist`,
{
method: 'DELETE'
}
@@ -126,7 +128,6 @@ const miscReq = {
json,
params
};
window.API.$emit('WORLD:PERSIST:DELETE', args);
return args;
});
},
@@ -138,8 +139,8 @@ const miscReq = {
* @returns {Promise<{json: any, params}>}
*/
hasWorldPersistData(params) {
return window.API.call(
`users/${window.API.currentUser.id}/${params.worldId}/persist/exists`,
return request(
`users/${getCurrentUserId()}/${params.worldId}/persist/exists`,
{
method: 'GET'
}
@@ -148,47 +149,41 @@ const miscReq = {
json,
params
};
window.API.$emit('WORLD:PERSIST:HAS', args);
return args;
});
},
updateBadge(params) {
return window.API.call(
`users/${window.API.currentUser.id}/badges/${params.badgeId}`,
{
method: 'PUT',
params: {
userId: window.API.currentUser.id,
badgeId: params.badgeId,
hidden: params.hidden,
showcased: params.showcased
}
return request(`users/${getCurrentUserId()}/badges/${params.badgeId}`, {
method: 'PUT',
params: {
userId: getCurrentUserId(),
badgeId: params.badgeId,
hidden: params.hidden,
showcased: params.showcased
}
).then((json) => {
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('BADGE:UPDATE', args);
return args;
});
},
getVisits() {
return window.API.call('visits', {
return request('visits', {
method: 'GET'
}).then((json) => {
const args = {
json
};
// window.API.$emit('VISITS', args);
return args;
});
},
deleteFile(fileId) {
return window.API.call(`file/${fileId}`, {
return request(`file/${fileId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
@@ -207,7 +202,7 @@ const miscReq = {
// * @returns {Promise<{json: any, params}>}
// */
// sendBoop(params) {
// return window.API.call(`users/${params.userId}/boop`, {
// return request(`users/${params.userId}/boop`, {
// method: 'POST',
// params
// }).then((json) => {

View File

@@ -1,4 +1,9 @@
// #region | API: Notification
import { request } from '../service/request';
import { useGroupStore, useNotificationStore } from '../stores';
function getGalleryStore() {
return useGroupStore();
}
const notificationReq = {
/** @typedef {{
@@ -17,7 +22,7 @@ const notificationReq = {
* @returns {Promise<{json: any, params}>}
*/
getNotifications(params) {
return window.API.call('auth/user/notifications', {
return request('auth/user/notifications', {
method: 'GET',
params
}).then((json) => {
@@ -25,13 +30,13 @@ const notificationReq = {
json,
params
};
window.API.$emit('NOTIFICATION:LIST', args);
return args;
});
},
getHiddenFriendRequests(params) {
return window.API.call('auth/user/notifications', {
return request('auth/user/notifications', {
method: 'GET',
params: {
type: 'friendRequest',
@@ -43,13 +48,12 @@ const notificationReq = {
json,
params
};
window.API.$emit('NOTIFICATION:LIST:HIDDEN', args);
return args;
});
},
getNotificationsV2(params) {
return window.API.call('notifications', {
return request('notifications', {
method: 'GET',
params
}).then((json) => {
@@ -57,7 +61,6 @@ const notificationReq = {
json,
params
};
window.API.$emit('NOTIFICATION:V2:LIST', args);
return args;
});
},
@@ -80,7 +83,7 @@ const notificationReq = {
* @return { Promise<{json: any, params}> }
*/
sendInvite(params, receiverUserId) {
return window.API.call(`invite/${receiverUserId}`, {
return request(`invite/${receiverUserId}`, {
method: 'POST',
params
}).then((json) => {
@@ -89,28 +92,26 @@ const notificationReq = {
params,
receiverUserId
};
window.API.$emit('NOTIFICATION:INVITE:SEND', args);
return args;
});
},
sendInvitePhoto(params, receiverUserId) {
return window.API.call(`invite/${receiverUserId}/photo`, {
return request(`invite/${receiverUserId}/photo`, {
uploadImageLegacy: true,
postData: JSON.stringify(params),
imageData: window.$app.uploadImage
imageData: getGalleryStore().uploadImage
}).then((json) => {
const args = {
json,
params,
receiverUserId
};
window.API.$emit('NOTIFICATION:INVITE:PHOTO:SEND', args);
return args;
});
},
sendRequestInvite(params, receiverUserId) {
return window.API.call(`requestInvite/${receiverUserId}`, {
return request(`requestInvite/${receiverUserId}`, {
method: 'POST',
params
}).then((json) => {
@@ -119,29 +120,27 @@ const notificationReq = {
params,
receiverUserId
};
window.API.$emit('NOTIFICATION:REQUESTINVITE:SEND', args);
return args;
});
},
sendRequestInvitePhoto(params, receiverUserId) {
return window.API.call(`requestInvite/${receiverUserId}/photo`, {
return request(`requestInvite/${receiverUserId}/photo`, {
uploadImageLegacy: true,
postData: JSON.stringify(params),
imageData: window.$app.uploadImage
imageData: getGalleryStore().uploadImage
}).then((json) => {
const args = {
json,
params,
receiverUserId
};
window.API.$emit('NOTIFICATION:REQUESTINVITE:PHOTO:SEND', args);
return args;
});
},
sendInviteResponse(params, inviteId) {
return window.API.call(`invite/${inviteId}/response`, {
return request(`invite/${inviteId}/response`, {
method: 'POST',
params,
inviteId
@@ -151,16 +150,15 @@ const notificationReq = {
params,
inviteId
};
window.API.$emit('INVITE:RESPONSE:SEND', args);
return args;
});
},
sendInviteResponsePhoto(params, inviteId) {
return window.API.call(`invite/${inviteId}/response/photo`, {
return request(`invite/${inviteId}/response/photo`, {
uploadImageLegacy: true,
postData: JSON.stringify(params),
imageData: window.$app.uploadImage,
imageData: getGalleryStore().uploadImage,
inviteId
}).then((json) => {
const args = {
@@ -168,7 +166,6 @@ const notificationReq = {
params,
inviteId
};
window.API.$emit('INVITE:RESPONSE:PHOTO:SEND', args);
return args;
});
},
@@ -178,7 +175,7 @@ const notificationReq = {
* @return { Promise<{json: any, params}> }
*/
acceptFriendRequestNotification(params) {
return window.API.call(
return request(
`auth/user/notifications/${params.notificationId}/accept`,
{
method: 'PUT'
@@ -189,13 +186,13 @@ const notificationReq = {
json,
params
};
window.API.$emit('NOTIFICATION:ACCEPT', args);
useNotificationStore().handleNotificationAccept(args);
return args;
})
.catch((err) => {
// if friend request could not be found, delete it
if (err && err.message && err.message.includes('404')) {
window.API.$emit('NOTIFICATION:HIDE', { params });
useNotificationStore().handleNotificationHide({ params });
}
});
},
@@ -205,7 +202,7 @@ const notificationReq = {
* @return { Promise<{json: any, params}> }
*/
hideNotification(params) {
return window.API.call(
return request(
`auth/user/notifications/${params.notificationId}/hide`,
{
method: 'PUT'
@@ -215,13 +212,11 @@ const notificationReq = {
json,
params
};
window.API.$emit('NOTIFICATION:HIDE', args);
useNotificationStore().handleNotificationHide(args);
return args;
});
},
// ------------------- need to test -------------------
/**
* @param {{
* notificationId: string,
@@ -231,32 +226,14 @@ const notificationReq = {
* @return { Promise<{json: any, params}> }
*/
sendNotificationResponse(params) {
return window.API.call(
`notifications/${params.notificationId}/respond`,
{
method: 'POST',
params
}
)
.then((json) => {
const args = {
json,
params
};
window.API.$emit('NOTIFICATION:RESPONSE', args);
return args;
})
.catch((err) => {
// TODO: need to test
// something went wrong, lets assume it's already expired
window.API.$emit('NOTIFICATION:HIDE', { params });
notificationReq.hideNotificationV2(params.notificationId);
throw err;
});
return request(`notifications/${params.notificationId}/respond`, {
method: 'POST',
params
});
},
// use in sendNotificationResponse
hideNotificationV2(notificationId) {
return window.API.call(`notifications/${notificationId}`, {
return request(`notifications/${notificationId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
@@ -265,16 +242,12 @@ const notificationReq = {
notificationId
}
};
// useless
// window.API.$emit('NOTIFICATION:V2:HIDE', args);
return args;
});
}
// ------------------ look like no place use these requests ------------------
// sendInviteGalleryPhoto(params, receiverUserId) {
// return window.API.call(`invite/${receiverUserId}/photo`, {
// return request(`invite/${receiverUserId}/photo`, {
// method: 'POST',
// params
// }).then((json) => {
@@ -283,13 +256,13 @@ const notificationReq = {
// params,
// receiverUserId
// };
// window.API.$emit('NOTIFICATION:INVITE:GALLERYPHOTO:SEND', args);
// API.$emit('NOTIFICATION:INVITE:GALLERYPHOTO:SEND', args);
// return args;
// });
// },
// API.clearNotifications = function () {
// return this.call('auth/user/notifications/clear', {
// return request('auth/user/notifications/clear', {
// method: 'PUT'
// }).then((json) => {
// var args = {

View File

@@ -1,14 +1,13 @@
// #region | API: PlayerModeration
import { request } from '../service/request';
const playerModerationReq = {
getPlayerModerations() {
return window.API.call('auth/user/playermoderations', {
return request('auth/user/playermoderations', {
method: 'GET'
}).then((json) => {
const args = {
json
};
window.API.$emit('PLAYER-MODERATION:LIST', args);
return args;
});
},
@@ -19,7 +18,7 @@ const playerModerationReq = {
*/
// old-way: POST auth/user/blocks {blocked:userId}
sendPlayerModeration(params) {
return window.API.call('auth/user/playermoderations', {
return request('auth/user/playermoderations', {
method: 'POST',
params
}).then((json) => {
@@ -27,7 +26,6 @@ const playerModerationReq = {
json,
params
};
// window.API.$emit('PLAYER-MODERATION:SEND', args);
return args;
});
},
@@ -38,7 +36,7 @@ const playerModerationReq = {
*/
// old-way: PUT auth/user/unblocks {blocked:userId}
deletePlayerModeration(params) {
return window.API.call('auth/user/unplayermoderate', {
return request('auth/user/unplayermoderate', {
method: 'PUT',
params
}).then((json) => {
@@ -46,11 +44,9 @@ const playerModerationReq = {
json,
params
};
window.API.$emit('PLAYER-MODERATION:DELETE', args);
return args;
});
}
};
// #endregion
export default playerModerationReq;

View File

@@ -1,10 +1,12 @@
import { request } from '../service/request';
const propReq = {
/**
* @param {{ propId: string }} params
* @returns {Promise<{json: any, params}>}
*/
getProp(params) {
return window.API.call(`props/${params.propId}`, {
return request(`props/${params.propId}`, {
method: 'GET',
params
}).then((json) => {

View File

@@ -1,13 +1,19 @@
// #region | API: User
import { request } from '../service/request';
import { useUserStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const userReq = {
/**
* Fetch user from API.
* @param {{ userId: string }} params identifier of registered user
* @returns {Promise<{json: any, params}>}
* identifier of registered user
* @type {import('../types/user').getUser}
*/
getUser(params) {
return window.API.call(`users/${params.userId}`, {
const userStore = useUserStore();
return request(`users/${params.userId}`, {
method: 'GET'
}).then((json) => {
if (!json) {
@@ -19,7 +25,7 @@ const userReq = {
json,
params
};
window.API.$emit('USER', args);
args.ref = userStore.applyUser(json);
return args;
});
},
@@ -30,10 +36,17 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
getCachedUser(params) {
const userStore = useUserStore();
return new Promise((resolve, reject) => {
const ref = window.API.cachedUsers.get(params.userId);
const ref = userStore.cachedUsers.get(params.userId);
if (typeof ref === 'undefined') {
userReq.getUser(params).catch(reject).then(resolve);
userReq
.getUser(params)
.catch(reject)
.then((args) => {
args.ref = userStore.applyUser(args.json);
resolve(args);
});
} else {
resolve({
cache: true,
@@ -59,7 +72,7 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
getUsers(params) {
return window.API.call('users', {
return request('users', {
method: 'GET',
params
}).then((json) => {
@@ -67,7 +80,6 @@ const userReq = {
json,
params
};
window.API.$emit('USER:LIST', args);
return args;
});
},
@@ -77,7 +89,8 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
addUserTags(params) {
return window.API.call(`users/${window.API.currentUser.id}/addTags`, {
const userStore = useUserStore();
return request(`users/${getCurrentUserId()}/addTags`, {
method: 'POST',
params
}).then((json) => {
@@ -85,7 +98,7 @@ const userReq = {
json,
params
};
window.API.$emit('USER:CURRENT:SAVE', args);
userStore.applyCurrentUser(json);
return args;
});
},
@@ -95,18 +108,16 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
removeUserTags(params) {
return window.API.call(
`users/${window.API.currentUser.id}/removeTags`,
{
method: 'POST',
params
}
).then((json) => {
const userStore = useUserStore();
return request(`users/${getCurrentUserId()}/removeTags`, {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('USER:CURRENT:SAVE', args);
userStore.applyCurrentUser(json);
return args;
});
},
@@ -116,7 +127,7 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
getUserFeedback(params) {
return window.API.call(`users/${params.userId}/feedback`, {
return request(`users/${params.userId}/feedback`, {
method: 'GET',
params: {
n: 100
@@ -126,7 +137,6 @@ const userReq = {
json,
params
};
// window.API.$emit('USER:FEEDBACK', args);
return args;
});
},
@@ -144,15 +154,16 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
saveCurrentUser(params) {
return window.API.call(`users/${window.API.currentUser.id}`, {
const userStore = useUserStore();
return request(`users/${getCurrentUserId()}`, {
method: 'PUT',
params
}).then((json) => {
var args = {
const args = {
json,
params
};
window.API.$emit('USER:CURRENT:SAVE', args);
userStore.applyCurrentUser(json);
return args;
});
},
@@ -162,7 +173,7 @@ const userReq = {
* @returns {Promise<{json: any, params}>}
*/
getUserNotes(params) {
return window.API.call(`userNotes`, {
return request(`userNotes`, {
method: 'GET',
params
}).then((json) => {
@@ -170,11 +181,9 @@ const userReq = {
json,
params
};
// window.API.$emit('USER:NOTES', args);
return args;
});
}
};
// #endregion
export default userReq;

View File

@@ -1,8 +1,8 @@
// #region | App: VRCPlus Icons
import { request } from '../service/request';
const VRCPlusIconsReq = {
getFileList(params) {
return window.API.call('files', {
return request('files', {
method: 'GET',
params
}).then((json) => {
@@ -10,7 +10,18 @@ const VRCPlusIconsReq = {
json,
params
};
window.API.$emit('FILES:LIST', args);
return args;
});
},
deleteFile(fileId) {
return request(`file/${fileId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
fileId
};
return args;
});
},
@@ -19,7 +30,7 @@ const VRCPlusIconsReq = {
const params = {
tag: 'icon'
};
return window.API.call('file/image', {
return request('file/image', {
uploadImage: true,
matchingDimensions: true,
postData: JSON.stringify(params),
@@ -29,15 +40,12 @@ const VRCPlusIconsReq = {
json,
params
};
window.API.$emit('VRCPLUSICON:ADD', args);
return args;
});
}
// unused
// images.pug line 63
// deleteFileVersion(params) {
// return window.API.call(`file/${params.fileId}/${params.version}`, {
// return request(`file/${params.fileId}/${params.version}`, {
// method: 'DELETE'
// }).then((json) => {
// const args = {
@@ -49,6 +57,4 @@ const VRCPlusIconsReq = {
// }
};
// #endregion
export default VRCPlusIconsReq;

View File

@@ -1,9 +1,15 @@
import { request } from '../service/request';
import { useUserStore } from '../stores';
function getCurrentUserId() {
return useUserStore().currentUser.id;
}
const vrcPlusImageReq = {
uploadGalleryImage(imageData) {
const params = {
tag: 'gallery'
};
return window.API.call('file/image', {
return request('file/image', {
uploadImage: true,
matchingDimensions: false,
postData: JSON.stringify(params),
@@ -13,13 +19,12 @@ const vrcPlusImageReq = {
json,
params
};
window.API.$emit('GALLERYIMAGE:ADD', args);
return args;
});
},
uploadSticker(imageData, params) {
return window.API.call('file/image', {
return request('file/image', {
uploadImage: true,
matchingDimensions: true,
postData: JSON.stringify(params),
@@ -29,13 +34,12 @@ const vrcPlusImageReq = {
json,
params
};
window.API.$emit('STICKER:ADD', args);
return args;
});
},
getPrints(params) {
return window.API.call(`prints/user/${window.API.currentUser.id}`, {
return request(`prints/user/${getCurrentUserId()}`, {
method: 'GET',
params
}).then((json) => {
@@ -48,20 +52,19 @@ const vrcPlusImageReq = {
},
deletePrint(printId) {
return window.API.call(`prints/${printId}`, {
return request(`prints/${printId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
printId
};
// window.API.$emit('PRINT:DELETE', args);
return args;
});
},
uploadPrint(imageData, cropWhiteBorder, params) {
return window.API.call('prints', {
return request('prints', {
uploadImagePrint: true,
cropWhiteBorder,
postData: JSON.stringify(params),
@@ -71,26 +74,24 @@ const vrcPlusImageReq = {
json,
params
};
window.API.$emit('PRINT:ADD', args);
return args;
});
},
getPrint(params) {
return window.API.call(`prints/${params.printId}`, {
return request(`prints/${params.printId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('PRINT', args);
return args;
});
},
uploadEmoji(imageData, params) {
return window.API.call('file/image', {
return request('file/image', {
uploadImage: true,
matchingDimensions: true,
postData: JSON.stringify(params),
@@ -100,15 +101,12 @@ const vrcPlusImageReq = {
json,
params
};
window.API.$emit('EMOJI:ADD', args);
return args;
});
}
// ----------- no place uses this function ------------
// editPrint(params) {
// return window.API.call(`prints/${params.printId}`, {
// return request(`prints/${params.printId}`, {
// method: 'POST',
// params
// }).then((json) => {
@@ -116,7 +114,7 @@ const vrcPlusImageReq = {
// json,
// params
// };
// window.API.$emit('PRINT:EDIT', args);
// API.$emit('PRINT:EDIT', args);
// return args;
// });
// },

View File

@@ -1,4 +1,5 @@
// #region | API: World
import { request } from '../service/request';
import { useWorldStore } from '../stores';
const worldReq = {
/**
@@ -6,14 +7,15 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
getWorld(params) {
return window.API.call(`worlds/${params.worldId}`, {
const worldStore = useWorldStore();
return request(`worlds/${params.worldId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('WORLD', args);
args.ref = worldStore.applyWorld(json);
return args;
});
},
@@ -23,10 +25,17 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
getCachedWorld(params) {
const worldStore = useWorldStore();
return new Promise((resolve, reject) => {
const ref = window.API.cachedWorlds.get(params.worldId);
const ref = worldStore.cachedWorlds.get(params.worldId);
if (typeof ref === 'undefined') {
worldReq.getWorld(params).catch(reject).then(resolve);
worldReq
.getWorld(params)
.catch(reject)
.then((args) => {
args.ref = worldStore.applyWorld(args.json);
resolve(args);
});
} else {
resolve({
cache: true,
@@ -57,11 +66,12 @@ const worldReq = {
* @returns {Promise<{json: any, params, option}>}
*/
getWorlds(params, option) {
const worldStore = useWorldStore();
let endpoint = 'worlds';
if (typeof option !== 'undefined') {
endpoint = `worlds/${option}`;
}
return window.API.call(endpoint, {
return request(endpoint, {
method: 'GET',
params
}).then((json) => {
@@ -70,7 +80,9 @@ const worldReq = {
params,
option
};
window.API.$emit('WORLD:LIST', args);
for (const json of args.json) {
worldStore.applyWorld(json);
}
return args;
});
},
@@ -79,14 +91,13 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
deleteWorld(params) {
return window.API.call(`worlds/${params.worldId}`, {
return request(`worlds/${params.worldId}`, {
method: 'DELETE'
}).then((json) => {
const args = {
json,
params
};
window.API.$emit('WORLD:DELETE', args);
return args;
});
},
@@ -96,7 +107,8 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
saveWorld(params) {
return window.API.call(`worlds/${params.id}`, {
const worldStore = useWorldStore();
return request(`worlds/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
@@ -104,7 +116,7 @@ const worldReq = {
json,
params
};
window.API.$emit('WORLD:SAVE', args);
args.ref = worldStore.applyWorld(json);
return args;
});
},
@@ -114,7 +126,8 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
publishWorld(params) {
return window.API.call(`worlds/${params.worldId}/publish`, {
const worldStore = useWorldStore();
return request(`worlds/${params.worldId}/publish`, {
method: 'PUT',
params
}).then((json) => {
@@ -122,7 +135,7 @@ const worldReq = {
json,
params
};
window.API.$emit('WORLD:SAVE', args);
args.ref = worldStore.applyWorld(json);
return args;
});
},
@@ -132,7 +145,8 @@ const worldReq = {
* @returns {Promise<{json: any, params}>}
*/
unpublishWorld(params) {
return window.API.call(`worlds/${params.worldId}/publish`, {
const worldStore = useWorldStore();
return request(`worlds/${params.worldId}/publish`, {
method: 'DELETE',
params
}).then((json) => {
@@ -140,12 +154,10 @@ const worldReq = {
json,
params
};
window.API.$emit('WORLD:SAVE', args);
args.ref = worldStore.applyWorld(json);
return args;
});
}
};
// #endregion
export default worldReq;

14159
src/app.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,71 @@
doctype html
#x-app.x-app(@dragenter.prevent @dragover.prevent @drop.prevent)
LoginPage(v-if="!API.isLoggedIn" v-bind="loginPageBind" v-on="loginPageEvent")
//- ### Login ###
Login(v-if='!watchState.isLoggedIn')
VRCXUpdateDialog(v-bind="vrcxUpdateDialogBind" v-on="vrcxUpdateDialogEvent")
VRCXUpdateDialog
//- menu
.x-menu-container
//- download progress, update pending
.pending-update(v-if='updateInProgress' @click='showVRCXUpdateDialog')
el-progress(
type='circle'
width='50'
stroke-width='3'
:percentage='updateProgress'
:format='updateProgressText')
.pending-update(v-else-if='pendingVRCXUpdate || pendingVRCXInstall')
el-button(
type='default'
@click='showVRCXUpdateDialog'
size='mini'
icon='el-icon-download'
circle
style='font-size: 14px; height: 50px; width: 50px')
template(v-if='watchState.isLoggedIn')
//- ### Menu ###
NavMenu
NavMenu(ref='menu' @select='selectMenu' :menu-active-index='menuActiveIndex')
//- ### Sidebar ###
Sidebar
//- ### Tabs ###
template(v-if='API.isLoggedIn')
FeedTab(v-bind='feedTabBind' v-on='feedTabEvent')
//- ### Tabs ###
Feed
GameLogTab(v-bind='gameLogTabBind' v-on='gameLogTabEvent')
GameLog
PlayerListTab(v-bind='playerListTabBind' v-on='playerListTabEvent')
PlayerList
SearchTab(v-bind='searchTabBind' v-on='searchTabEvent')
Search
FavoritesTab(v-bind='favoritesTabBind' v-on='favoritesTabEvent')
Favorites
FriendLogTab(v-bind='friendLogTabBind')
FriendLog
ModerationTab(v-bind='moderationTabBind')
Moderation
NotificationTab(v-bind='notificationTabBind' v-on='notificationTabEvent')
Notification
ProfileTab(v-bind='profileTabBind' v-on='profileTabEvent')
FriendList
FriendListTab(v-bind='friendsListTabBind' v-on='friendsListTabEvent')
Charts
KeepAlive
ChartsTab(v-if='menuActiveIndex === "charts"' v-bind='chartsTabBind' v-on='chartsTabEvent')
Profile
//- settings
include ./mixins/tabs/settings.pug
+settingsTab
SideBar(v-bind='sideBarTabBind' v-on='sideBarTabEvent')
Settings
//- ## Dialogs ## -\\
include ./mixins/dialogs/dialogs.pug
+dialogs
UserDialog
//- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="templateDialog" :visible.sync="templateDialog.visible" :title="$t('dialog.template_dialog.header')" width="450px")
WorldDialog
AvatarDialog
GroupDialog
GalleryDialog
FullscreenImageDialog
PreviousInstancesInfoDialog
LaunchDialog
LaunchOptionsDialog
FriendImportDialog
WorldImportDialog
AvatarImportDialog
ChooseFavoriteGroupDialog
EditInviteMessageDialog
VRChatConfigDialog
PrimaryPasswordDialog

View File

@@ -8,6 +8,9 @@
// For a copy, see <https://opensource.org/licenses/MIT>.
//
@use "./assets/scss/flags.scss";
@use "./assets/scss/animated-emoji.scss";
@import '~animate.css/animate.min.css';
@import '~noty/lib/noty.css';
@import '~element-ui/lib/theme-chalk/index.css';
@@ -299,6 +302,7 @@ hr.x-vertical-divider {
flex-direction: column;
background: #f8f8f8;
padding: 5px;
order: 99;
}
.el-popper.x-quick-search {

View File

@@ -2,7 +2,7 @@
* VRCX Dark-Vanilla theme by MintLily
* https://github.com/MintLily/Dark-Vanilla
*/
@import 'theme.dark';
@use 'theme.dark';
:root {
--ThemeName: 'Dark Vanilla';
--ThemeVersion: 'v1.7';

View File

@@ -2,7 +2,7 @@
* VRCX Material-You-like theme by Kamiya
* https://github.com/kamiya10/VRCX-theme
*/
@import 'theme.dark';
@use 'theme.dark';
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600&family=Noto+Sans+TC:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&family=Noto+Sans+JP:wght@300;400;500&family=Roboto&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
@@ -653,6 +653,8 @@ input[type='number'],
border-left: 1px solid rgb(var(--md-sys-color-surface-variant));
*/
padding: 8px 10px;
display: flex;
align-items: center;
}
/* ---------- Switch ---------- */
@@ -1083,8 +1085,8 @@ input[type='number'],
border-radius: 8px;
height: 28px;
padding: 0 12px;
margin-top: 8px !important;
margin-right: 8px !important;
// margin-top: 8px !important;
// margin-right: 8px !important;
color: rgb(var(--md-sys-color-on-surface-variant));
font-family: var(--md-sys-typescale-label-large-font);
line-height: 28px;
@@ -1380,7 +1382,7 @@ img.x-link.el-popover__reference {
*:not(.x-user-dialog, .x-world-dialog, .x-avatar-dialog, .x-group-dialog)
> .el-dialog:not([aria-label*='Notification Position']):not(
[aria-label*='Launch']
):not([aria-label*='VRCX Updater']) {
):not([aria-label*='VRCX Updater']):not([aria-label*='Social Status']) {
max-width: 1125px !important;
width: 1125px !important;
}
@@ -2008,6 +2010,7 @@ i.x-user-status {
.el-collapse-item .el-tag--mini {
background-color: transparent;
border: transparent;
padding-top: 6px;
}
.simple-switch {
justify-content: space-between;
@@ -2071,4 +2074,12 @@ i.x-user-status {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.el-dialog[aria-label='Launch'] .el-form .el-form-item__content {
display: flex;
align-items: center;
}
.el-dialog[aria-label='Launch'] .el-form > .el-form-item:nth-child(2) .el-form-item__label {
display: flex;
align-items: center;
}

View File

@@ -2,7 +2,7 @@
* VRCX Pink theme by Kamiya
* https://github.com/kamiya10/VRCX-theme
*/
@import 'theme.dark';
@use 'theme.dark';
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
:root {
--theme: #dfa2a2;
@@ -70,7 +70,7 @@ textarea {
.el-textarea .el-input__count,
.el-textarea__inner,
.x-menu-container {
background-color: var(--lighter-bg);
background-color: var(--lighter-bg) !important;
}
.el-color-picker__panel {
border-color: var(--lighter-bg);

76
src/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,76 @@
import '@fontsource/noto-sans-kr';
import '@fontsource/noto-sans-jp';
import '@fontsource/noto-sans-sc';
import '@fontsource/noto-sans-tc';
import '@infolektuell/noto-color-emoji';
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { DataTables } from 'vue-data-tables';
import VueLazyload from 'vue-lazyload';
import configRepository from './service/config';
import vrcxJsonStorage from './service/jsonStorage';
import {
changeAppDarkStyle,
changeAppThemeStyle,
refreshCustomCss,
refreshCustomScript,
systemIsDarkMode
} from './shared/utils';
import { i18n } from './plugin';
configRepository.init();
i18n.locale = await configRepository.getString('VRCX_appLanguage', 'en');
AppApi.SetUserAgent();
const initThemeMode = await configRepository.getString(
'VRCX_ThemeMode',
'system'
);
let isDarkMode;
if (initThemeMode === 'light') {
isDarkMode = false;
} else if (initThemeMode === 'system') {
isDarkMode = systemIsDarkMode();
} else {
isDarkMode = true;
}
changeAppDarkStyle(isDarkMode);
changeAppThemeStyle(initThemeMode);
refreshCustomCss();
refreshCustomScript();
Vue.use(PiniaVuePlugin);
Vue.use(DataTables);
Vue.use(VueLazyload, {
preLoad: 1,
observer: true,
observerOptions: {
rootMargin: '0px',
threshold: 0
},
attempt: 3
});
new vrcxJsonStorage(VRCXStorage);
// some workaround for failing to get voice list first run
speechSynthesis.getVoices();
if (process.env.NODE_ENV !== 'production') {
Vue.config.errorHandler = function (err, vm, info) {
console.error('Vue Error', err);
console.error('Component', vm);
console.error('Error Info', info);
};
Vue.config.warnHandler = function (msg, vm, trace) {
console.warn('Vue Warning', msg);
console.warn('Component', vm);
console.warn('Trace', trace);
};
}

View File

@@ -1,46 +0,0 @@
import { baseClass, $app, API, $t, $utils } from '../baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.getConfig = function () {
return this.call('config', {
method: 'GET'
}).then((json) => {
var args = {
json
};
this.$emit('CONFIG', args);
return args;
});
};
API.$on('CONFIG', function (args) {
args.ref = this.applyConfig(args.json);
});
API.$on('CONFIG', function (args) {
if (typeof args.ref?.whiteListedAssetUrls !== 'object') {
console.error('Invalid config whiteListedAssetUrls');
}
AppApi.PopulateImageHosts(
JSON.stringify(args.ref.whiteListedAssetUrls)
);
});
API.applyConfig = function (json) {
var ref = {
...json
};
this.cachedConfig = ref;
return ref;
};
}
_data = {};
_methods = {};
}

View File

@@ -1,16 +0,0 @@
import * as workerTimers from 'worker-timers';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {}
_data = {};
_methods = {};
}

View File

@@ -1,53 +0,0 @@
import { baseClass, $app, API, $t } from './baseClass.js';
export default class extends baseClass {
constructor(_app) {
super(_app);
}
eventHandlers = new Map();
$emit = function (name, ...args) {
if ($app.debug) {
console.log(name, ...args);
}
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
return;
}
try {
for (var handler of handlers) {
handler.apply(this, args);
}
} catch (err) {
console.error(err);
}
};
$on = function (name, handler) {
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
handlers = [];
this.eventHandlers.set(name, handlers);
}
handlers.push(handler);
};
$off = function (name, handler) {
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
return;
}
var { length } = handlers;
for (var i = 0; i < length; ++i) {
if (handlers[i] === handler) {
if (length > 1) {
handlers.splice(i, 1);
} else {
this.eventHandlers.delete(name);
}
break;
}
}
};
}

View File

@@ -1,421 +0,0 @@
import Noty from 'noty';
import security from '../service/security.js';
import configRepository from '../service/config.js';
import { baseClass, $app, API, $t } from './baseClass.js';
/* eslint-disable no-unused-vars */
let webApiService = {};
/* eslint-enable no-unused-vars */
export default class extends baseClass {
constructor(_app, _API, _t, _webApiService) {
super(_app, _API, _t);
webApiService = _webApiService;
}
async init() {
API.isLoggedIn = false;
API.attemptingAutoLogin = false;
API.autoLoginAttempts = new Set();
/**
* @param {{ username: string, password: string }} params credential to login
* @returns {Promise<{origin: boolean, json: any, params}>}
*/
API.login = function (params) {
var { username, password, saveCredentials, cipher } = params;
username = encodeURIComponent(username);
password = encodeURIComponent(password);
var auth = btoa(`${username}:${password}`);
if (saveCredentials) {
delete params.saveCredentials;
if (cipher) {
params.password = cipher;
delete params.cipher;
}
$app.saveCredentials = params;
}
return this.call('auth/user', {
method: 'GET',
headers: {
Authorization: `Basic ${auth}`
}
}).then((json) => {
var args = {
json,
params,
origin: true
};
if (
json.requiresTwoFactorAuth &&
json.requiresTwoFactorAuth.includes('emailOtp')
) {
this.$emit('USER:EMAILOTP', args);
} else if (json.requiresTwoFactorAuth) {
this.$emit('USER:2FA', args);
} else {
this.$emit('USER:CURRENT', args);
}
return args;
});
};
/**
* @param {{ code: string }} params One-time password
* @returns {Promise<{json: any, params}>}
*/
API.verifyOTP = function (params) {
return this.call('auth/twofactorauth/otp/verify', {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('OTP', args);
return args;
});
};
/**
* @param {{ code: string }} params One-time token
* @returns {Promise<{json: any, params}>}
*/
API.verifyTOTP = function (params) {
return this.call('auth/twofactorauth/totp/verify', {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('TOTP', args);
return args;
});
};
/**
* @param {{ code: string }} params One-time token
* @returns {Promise<{json: any, params}>}
*/
API.verifyEmailOTP = function (params) {
return this.call('auth/twofactorauth/emailotp/verify', {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('EMAILOTP', args);
return args;
});
};
API.$on('AUTOLOGIN', function () {
if (this.attemptingAutoLogin) {
return;
}
this.attemptingAutoLogin = true;
var user =
$app.loginForm.savedCredentials[
$app.loginForm.lastUserLoggedIn
];
if (typeof user === 'undefined') {
this.attemptingAutoLogin = false;
return;
}
if ($app.enablePrimaryPassword) {
console.error(
'Primary password is enabled, this disables auto login.'
);
this.attemptingAutoLogin = false;
this.logout();
return;
}
var attemptsInLastHour = Array.from(this.autoLoginAttempts).filter(
(timestamp) => timestamp > new Date().getTime() - 3600000
).length;
if (attemptsInLastHour >= 3) {
console.error(
'More than 3 auto login attempts within the past hour, logging out instead of attempting auto login.'
);
this.attemptingAutoLogin = false;
this.logout();
return;
}
this.autoLoginAttempts.add(new Date().getTime());
$app.relogin(user)
.then(() => {
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'success',
text: 'Automatically logged in.'
}).show();
console.log('Automatically logged in.');
})
.catch((err) => {
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'error',
text: 'Failed to login automatically.'
}).show();
console.error('Failed to login automatically.', err);
})
.finally(() => {
if (!navigator.onLine) {
this.errorNoty = new Noty({
type: 'error',
text: `You're offline.`
}).show();
console.error(`You're offline.`);
}
});
});
API.$on('USER:CURRENT', function () {
this.attemptingAutoLogin = false;
});
API.$on('LOGOUT', function () {
this.attemptingAutoLogin = false;
this.autoLoginAttempts.clear();
});
API.logout = function () {
this.$emit('LOGOUT');
// return this.call('logout', {
// method: 'PUT'
// }).finally(() => {
// this.$emit('LOGOUT');
// });
};
}
_data = {
loginForm: {
loading: true,
username: '',
password: '',
endpoint: '',
websocket: '',
saveCredentials: false,
savedCredentials: {},
lastUserLoggedIn: '',
rules: {
username: [
{
required: true,
trigger: 'blur'
}
],
password: [
{
required: true,
trigger: 'blur'
}
]
}
}
};
_methods = {
async relogin(user) {
var { loginParmas } = user;
if (user.cookies) {
await webApiService.setCookies(user.cookies);
}
this.loginForm.lastUserLoggedIn = user.user.id; // for resend email 2fa
if (loginParmas.endpoint) {
API.endpointDomain = loginParmas.endpoint;
API.websocketDomain = loginParmas.websocket;
} else {
API.endpointDomain = API.endpointDomainVrchat;
API.websocketDomain = API.websocketDomainVrchat;
}
return new Promise((resolve, reject) => {
this.loginForm.loading = true;
if (this.enablePrimaryPassword) {
this.checkPrimaryPassword(loginParmas)
.then((pwd) => {
return API.getConfig()
.catch((err) => {
reject(err);
})
.then(() => {
API.login({
username: loginParmas.username,
password: pwd,
cipher: loginParmas.password,
endpoint: loginParmas.endpoint,
websocket: loginParmas.websocket
})
.catch((err2) => {
// API.logout();
reject(err2);
})
.then(() => {
resolve();
});
});
})
.catch((_) => {
this.$message({
message: 'Incorrect primary password',
type: 'error'
});
reject(_);
});
} else {
API.getConfig()
.catch((err) => {
reject(err);
})
.then(() => {
API.login({
username: loginParmas.username,
password: loginParmas.password,
endpoint: loginParmas.endpoint,
websocket: loginParmas.websocket
})
.catch((err2) => {
API.logout();
reject(err2);
})
.then(() => {
resolve();
});
});
}
}).finally(() => (this.loginForm.loading = false));
},
async deleteSavedLogin(userId) {
var savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
delete savedCredentials[userId];
// Disable primary password when no account is available.
if (Object.keys(savedCredentials).length === 0) {
this.enablePrimaryPassword = false;
await configRepository.setBool('enablePrimaryPassword', false);
}
this.loginForm.savedCredentials = savedCredentials;
var jsonCredentials = JSON.stringify(savedCredentials);
await configRepository.setString(
'savedCredentials',
jsonCredentials
);
new Noty({
type: 'success',
text: 'Account removed.'
}).show();
},
async login() {
await webApiService.clearCookies();
if (!this.loginForm.loading) {
this.loginForm.loading = true;
if (this.loginForm.endpoint) {
API.endpointDomain = this.loginForm.endpoint;
API.websocketDomain = this.loginForm.websocket;
} else {
API.endpointDomain = API.endpointDomainVrchat;
API.websocketDomain = API.websocketDomainVrchat;
}
API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
throw err;
})
.then((args) => {
if (
this.loginForm.saveCredentials &&
this.enablePrimaryPassword
) {
$app.$prompt(
$t('prompt.primary_password.description'),
$t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
let saveCredential =
this.loginForm.savedCredentials[
Object.keys(
this.loginForm.savedCredentials
)[0]
];
security
.decrypt(
saveCredential.loginParmas.password,
value
)
.then(() => {
security
.encrypt(
this.loginForm.password,
value
)
.then((pwd) => {
API.login({
username:
this.loginForm
.username,
password:
this.loginForm
.password,
endpoint:
this.loginForm
.endpoint,
websocket:
this.loginForm
.websocket,
saveCredentials:
this.loginForm
.saveCredentials,
cipher: pwd
});
});
});
})
.finally(() => {
this.loginForm.loading = false;
});
return args;
}
API.login({
username: this.loginForm.username,
password: this.loginForm.password,
endpoint: this.loginForm.endpoint,
websocket: this.loginForm.websocket,
saveCredentials: this.loginForm.saveCredentials
}).finally(() => {
this.loginForm.loading = false;
});
return args;
});
}
},
logout() {
this.$confirm('Continue? Logout', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
API.logout();
}
}
});
}
};
}

View File

@@ -1,402 +0,0 @@
import Noty from 'noty';
import { baseClass, $app, API, $t } from './baseClass.js';
/* eslint-disable no-unused-vars */
let webApiService = {};
/* eslint-enable no-unused-vars */
export default class extends baseClass {
constructor(_app, _API, _t, _webApiService) {
super(_app, _API, _t);
webApiService = _webApiService;
}
init() {
API.cachedConfig = {};
API.pendingGetRequests = new Map();
API.failedGetRequests = new Map();
API.endpointDomainVrchat = 'https://api.vrchat.cloud/api/1';
API.websocketDomainVrchat = 'wss://pipeline.vrchat.cloud';
API.endpointDomain = 'https://api.vrchat.cloud/api/1';
API.websocketDomain = 'wss://pipeline.vrchat.cloud';
API.call = function (endpoint, options) {
var init = {
url: `${API.endpointDomain}/${endpoint}`,
method: 'GET',
...options
};
var { params } = init;
if (init.method === 'GET') {
// don't retry recent 404/403
if (this.failedGetRequests.has(endpoint)) {
var lastRun = this.failedGetRequests.get(endpoint);
if (lastRun >= Date.now() - 900000) {
// 15mins
throw new Error(
`${$t('api.error.message.403_404_bailing_request')}, ${endpoint}`
);
}
this.failedGetRequests.delete(endpoint);
}
// transform body to url
if (params === Object(params)) {
var url = new URL(init.url);
var { searchParams } = url;
for (var key in params) {
searchParams.set(key, params[key]);
}
init.url = url.toString();
}
// merge requests
var req = this.pendingGetRequests.get(init.url);
if (typeof req !== 'undefined') {
if (req.time >= Date.now() - 10000) {
// 10s
return req.req;
}
this.pendingGetRequests.delete(init.url);
}
} else if (
init.uploadImage ||
init.uploadFilePUT ||
init.uploadImageLegacy
) {
// nothing
} else {
init.headers = {
'Content-Type': 'application/json;charset=utf-8',
...init.headers
};
init.body =
params === Object(params) ? JSON.stringify(params) : '{}';
}
var req = webApiService
.execute(init)
.catch((err) => {
this.$throw(0, err, endpoint);
})
.then((response) => {
if (!response.data) {
if ($app.debugWebRequests) {
console.log(init, response);
}
return response;
}
try {
response.data = JSON.parse(response.data);
if ($app.debugWebRequests) {
console.log(init, response.data);
}
return response;
} catch (e) {}
if (response.status === 200) {
this.$throw(
0,
$t('api.error.message.invalid_json_response'),
endpoint
);
}
if (
response.status === 429 &&
init.url.endsWith('/instances/groups')
) {
$app.nextGroupInstanceRefresh = 120; // 1min
throw new Error(
`${response.status}: rate limited ${endpoint}`
);
}
if (response.status === 504 || response.status === 502) {
// ignore expected API errors
throw new Error(
`${response.status}: ${response.data} ${endpoint}`
);
}
this.$throw(response.status, endpoint);
return {};
})
.then(({ data, status }) => {
if (status === 200) {
if (!data) {
return data;
}
var text = '';
if (data.success === Object(data.success)) {
text = data.success.message;
} else if (data.OK === String(data.OK)) {
text = data.OK;
}
if (text) {
new Noty({
type: 'success',
text: $app.escapeTag(text)
}).show();
}
return data;
}
if (
status === 401 &&
data.error.message === '"Missing Credentials"'
) {
this.$emit('AUTOLOGIN');
throw new Error(
`401 ${$t('api.error.message.missing_credentials')}`
);
}
if (
status === 401 &&
data.error.message === '"Unauthorized"' &&
endpoint !== 'auth/user'
) {
// trigger 2FA dialog
if (!$app.twoFactorAuthDialogVisible) {
$app.API.getCurrentUser();
}
throw new Error(`401 ${$t('api.status_code.401')}`);
}
if (status === 403 && endpoint === 'config') {
$app.$alert(
$t('api.error.message.vpn_in_use'),
`403 ${$t('api.error.message.login_error')}`
);
this.logout();
throw new Error(`403 ${endpoint}`);
}
if (
init.method === 'GET' &&
status === 404 &&
endpoint.startsWith('avatars/')
) {
$app.$message({
message: $t(
'message.api_handler.avatar_private_or_deleted'
),
type: 'error'
});
$app.avatarDialog.visible = false;
throw new Error(
`404: ${data.error.message} ${endpoint}`
);
}
if (
status === 404 &&
endpoint.endsWith('/persist/exists')
) {
return false;
}
if (
init.method === 'GET' &&
(status === 404 || status === 403) &&
!endpoint.startsWith('auth/user')
) {
this.failedGetRequests.set(endpoint, Date.now());
}
if (
init.method === 'GET' &&
status === 404 &&
endpoint.startsWith('users/') &&
endpoint.split('/').length - 1 === 1
) {
throw new Error(
`404: ${data.error.message} ${endpoint}`
);
}
if (
status === 404 &&
endpoint.startsWith('invite/') &&
init.inviteId
) {
this.expireNotification(init.inviteId);
}
if (
status === 403 &&
endpoint.startsWith('invite/myself/to/')
) {
throw new Error(
`403: ${data.error.message} ${endpoint}`
);
}
if (data && data.error === Object(data.error)) {
this.$throw(
data.error.status_code || status,
data.error.message,
endpoint
);
} else if (data && typeof data.error === 'string') {
this.$throw(
data.status_code || status,
data.error,
endpoint
);
}
this.$throw(status, data, endpoint);
return data;
});
if (init.method === 'GET') {
req.finally(() => {
this.pendingGetRequests.delete(init.url);
});
this.pendingGetRequests.set(init.url, {
req,
time: Date.now()
});
}
return req;
};
// FIXME : extra를 없애줘
API.$throw = function (code, error, endpoint) {
var text = [];
if (code > 0) {
const status = this.statusCodes[code];
if (typeof status === 'undefined') {
text.push(`${code}`);
} else {
const codeText = $t(`api.status_code.${code}`);
text.push(`${code} ${codeText}`);
}
}
if (typeof error !== 'undefined') {
text.push(
`${$t('api.error.message.error_message')}: ${typeof error === 'string' ? error : JSON.stringify(error)}`
);
}
if (typeof endpoint !== 'undefined') {
text.push(
`${$t('api.error.message.endpoint')}: "${typeof endpoint === 'string' ? endpoint : JSON.stringify(endpoint)}"`
);
}
text = text.map((s) => $app.escapeTag(s)).join('<br>');
if (text.length) {
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'error',
text
}).show();
}
throw new Error(text);
};
API.$bulk = function (options, args) {
if ('handle' in options) {
options.handle.call(this, args, options);
}
if (
args.json.length > 0 &&
((options.params.offset += args.json.length),
// eslint-disable-next-line no-nested-ternary
options.N > 0
? options.N > options.params.offset
: options.N < 0
? args.json.length
: options.params.n === args.json.length)
) {
this.bulk(options);
} else if ('done' in options) {
options.done.call(this, true, options);
}
return args;
};
API.bulk = function (options) {
// it's stupid, but I won't waste time on the 'this' context
// works, that's enough.
if (typeof options.fn === 'function') {
options
.fn(options.params)
.catch((err) => {
if ('done' in options) {
options.done.call(this, false, options);
}
throw err;
})
.then((args) => this.$bulk(options, args));
} else {
this[options.fn](options.params)
.catch((err) => {
if ('done' in options) {
options.done.call(this, false, options);
}
throw err;
})
.then((args) => this.$bulk(options, args));
}
};
API.statusCodes = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: 'Switch Proxy',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: "I'm a teapot",
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
// CloudFlare Error
520: 'Web server returns an unknown error',
521: 'Web server is down',
522: 'Connection timed out',
523: 'Origin is unreachable',
524: 'A timeout occurred',
525: 'SSL handshake failed',
526: 'Invalid SSL certificate',
527: 'Railgun Listener to origin error'
};
}
}

View File

@@ -1,28 +0,0 @@
import $utils from './utils';
/* eslint-disable no-unused-vars */
let $app = {};
let API = {};
let $t = {};
/* eslint-enable no-unused-vars */
class baseClass {
constructor(_app, _API, _t) {
$app = _app;
API = _API;
$t = _t;
this.init();
}
updateRef(_app) {
$app = _app;
}
init() {}
_data = {};
_methods = {};
}
export { baseClass, $app, API, $t, $utils };

View File

@@ -1,325 +0,0 @@
import { isRealInstance, parseLocation } from '../composables/instance/utils';
import { $app, API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.currentUser = {
$userColour: ''
};
API.getCurrentUser = function () {
return this.call('auth/user', {
method: 'GET'
}).then((json) => {
var args = {
json,
fromGetCurrentUser: true
};
if (
json.requiresTwoFactorAuth &&
json.requiresTwoFactorAuth.includes('emailOtp')
) {
this.$emit('USER:EMAILOTP', args);
} else if (json.requiresTwoFactorAuth) {
this.$emit('USER:2FA', args);
} else {
if ($app.debugCurrentUserDiff) {
var ref = args.json;
var $ref = this.currentUser;
var props = {};
for (var prop in $ref) {
if ($ref[prop] !== Object($ref[prop])) {
props[prop] = true;
}
}
for (var prop in ref) {
if (
Array.isArray(ref[prop]) &&
Array.isArray($ref[prop])
) {
if (!$app.arraysMatch(ref[prop], $ref[prop])) {
props[prop] = true;
}
} else if (ref[prop] !== Object(ref[prop])) {
props[prop] = true;
}
}
var has = false;
for (var prop in props) {
var asis = $ref[prop];
var tobe = ref[prop];
if (asis === tobe) {
delete props[prop];
} else {
if (
prop.startsWith('$') ||
prop === 'offlineFriends' ||
prop === 'onlineFriends' ||
prop === 'activeFriends'
) {
delete props[prop];
continue;
}
props[prop] = [tobe, asis];
has = true;
}
}
if (has) {
console.log('API.getCurrentUser diff', props);
}
}
$app.nextCurrentUserRefresh = 420; // 7mins
this.$emit('USER:CURRENT', args);
}
return args;
});
};
API.$on('USER:CURRENT', function (args) {
var { json } = args;
args.ref = this.applyCurrentUser(json);
// when isGameRunning use gameLog instead of API
var $location = parseLocation($app.lastLocation.location);
var $travelingLocation = parseLocation(
$app.lastLocationDestination
);
var location = $app.lastLocation.location;
var instanceId = $location.instanceId;
var worldId = $location.worldId;
var travelingToLocation = $app.lastLocationDestination;
var travelingToWorld = $travelingLocation.worldId;
var travelingToInstance = $travelingLocation.instanceId;
if (!$app.isGameRunning && json.presence) {
if (isRealInstance(json.presence.world)) {
location = `${json.presence.world}:${json.presence.instance}`;
} else {
location = json.presence.world;
}
if (isRealInstance(json.presence.travelingToWorld)) {
travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`;
} else {
travelingToLocation = json.presence.travelingToWorld;
}
instanceId = json.presence.instance;
worldId = json.presence.world;
travelingToInstance = json.presence.travelingToInstance;
travelingToWorld = json.presence.travelingToWorld;
}
this.applyUser({
allowAvatarCopying: json.allowAvatarCopying,
badges: json.badges,
bio: json.bio,
bioLinks: json.bioLinks,
currentAvatarImageUrl: json.currentAvatarImageUrl,
currentAvatarTags: json.currentAvatarTags,
currentAvatarThumbnailImageUrl:
json.currentAvatarThumbnailImageUrl,
date_joined: json.date_joined,
developerType: json.developerType,
displayName: json.displayName,
friendKey: json.friendKey,
// json.friendRequestStatus - missing from currentUser
id: json.id,
// instanceId - missing from currentUser
isFriend: json.isFriend,
last_activity: json.last_activity,
last_login: json.last_login,
last_mobile: json.last_mobile,
last_platform: json.last_platform,
// location - missing from currentUser
// platform - missing from currentUser
// note - missing from currentUser
profilePicOverride: json.profilePicOverride,
// profilePicOverrideThumbnail - missing from currentUser
pronouns: json.pronouns,
state: json.state,
status: json.status,
statusDescription: json.statusDescription,
tags: json.tags,
// travelingToInstance - missing from currentUser
// travelingToLocation - missing from currentUser
// travelingToWorld - missing from currentUser
userIcon: json.userIcon,
// worldId - missing from currentUser
fallbackAvatar: json.fallbackAvatar,
// Location from gameLog/presence
location,
instanceId,
worldId,
travelingToLocation,
travelingToInstance,
travelingToWorld,
// set VRCX online/offline timers
$online_for: this.currentUser.$online_for,
$offline_for: this.currentUser.$offline_for,
$location_at: this.currentUser.$location_at,
$travelingToTime: this.currentUser.$travelingToTime
});
});
API.applyCurrentUser = function (json) {
var ref = this.currentUser;
if (this.isLoggedIn) {
if (json.currentAvatar !== ref.currentAvatar) {
$app.addAvatarToHistory(json.currentAvatar);
if ($app.isGameRunning) {
$app.addAvatarWearTime(ref.currentAvatar);
ref.$previousAvatarSwapTime = Date.now();
}
}
Object.assign(ref, json);
if (ref.homeLocation !== ref.$homeLocation.tag) {
ref.$homeLocation = parseLocation(ref.homeLocation);
// apply home location name to user dialog
if (
$app.userDialog.visible &&
$app.userDialog.id === ref.id
) {
$app.getWorldName(API.currentUser.homeLocation).then(
(worldName) => {
$app.userDialog.$homeLocationName = worldName;
}
);
}
}
ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref);
this.applyPresenceLocation(ref);
this.applyQueuedInstance(ref.queuedInstance);
this.applyPresenceGroups(ref);
} else {
ref = {
acceptedPrivacyVersion: 0,
acceptedTOSVersion: 0,
accountDeletionDate: null,
accountDeletionLog: null,
activeFriends: [],
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatar: '',
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
displayName: '',
emailVerified: false,
fallbackAvatar: '',
friendGroupNames: [],
friendKey: '',
friends: [],
googleId: '',
hasBirthday: false,
hasEmail: false,
hasLoggedInFromClient: false,
hasPendingEmail: false,
hideContentFilterSettings: false,
homeLocation: '',
id: '',
isAdult: true,
isBoopingEnabled: false,
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
obfuscatedEmail: '',
obfuscatedPendingEmail: '',
oculusId: '',
offlineFriends: [],
onlineFriends: [],
pastDisplayNames: [],
picoId: '',
presence: {
avatarThumbnail: '',
currentAvatarTags: '',
displayName: '',
groups: [],
id: '',
instance: '',
instanceType: '',
platform: '',
profilePicOverride: '',
status: '',
travelingToInstance: '',
travelingToWorld: '',
userIcon: '',
world: '',
...json.presence
},
profilePicOverride: '',
pronouns: '',
queuedInstance: '',
state: '',
status: '',
statusDescription: '',
statusFirstTime: false,
statusHistory: [],
steamDetails: {},
steamId: '',
tags: [],
twoFactorAuthEnabled: false,
twoFactorAuthEnabledDate: null,
unsubscribe: false,
updated_at: '',
userIcon: '',
userLanguage: '',
userLanguageCode: '',
username: '',
viveId: '',
// VRCX
$online_for: Date.now(),
$offline_for: '',
$location_at: Date.now(),
$travelingToTime: Date.now(),
$previousAvatarSwapTime: '',
$homeLocation: {},
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$locationTag: '',
$travelingToLocation: '',
...json
};
if ($app.isGameRunning) {
ref.$previousAvatarSwapTime = Date.now();
}
ref.$homeLocation = parseLocation(ref.homeLocation);
ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref);
this.applyPresenceLocation(ref);
this.applyPresenceGroups(ref);
this.currentUser = ref;
this.isLoggedIn = true;
this.$emit('LOGIN', {
json,
ref
});
}
return ref;
};
}
_data = {};
_methods = {};
}

View File

@@ -1,291 +0,0 @@
import { worldRequest } from '../api';
import { parseLocation } from '../composables/instance/utils';
import { getLaunchURL } from '../composables/shared/utils';
import configRepository from '../service/config.js';
import { API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
_data = {
isDiscordActive: false,
discordActive: false,
discordInstance: true,
discordJoinButton: false,
discordHideInvite: true,
discordHideImage: false
};
_methods = {
updateDiscord() {
var currentLocation = this.lastLocation.location;
var timeStamp = this.lastLocation.date;
if (this.lastLocation.location === 'traveling') {
currentLocation = this.lastLocationDestination;
timeStamp = this.lastLocationDestinationTime;
}
if (
!this.discordActive ||
(!this.isGameRunning && !this.gameLogDisabled) ||
(!currentLocation && !this.lastLocation$.tag)
) {
this.setDiscordActive(false);
return;
}
this.setDiscordActive(true);
var L = this.lastLocation$;
if (currentLocation !== this.lastLocation$.tag) {
Discord.SetTimestamps(timeStamp, 0);
L = parseLocation(currentLocation);
L.worldName = '';
L.thumbnailImageUrl = '';
L.worldCapacity = 0;
L.joinUrl = '';
L.accessName = '';
if (L.worldId) {
var ref = API.cachedWorlds.get(L.worldId);
if (ref) {
L.worldName = ref.name;
L.thumbnailImageUrl = ref.thumbnailImageUrl;
L.worldCapacity = ref.capacity;
} else {
worldRequest
.getWorld({
worldId: L.worldId
})
.then((args) => {
L.worldName = args.ref.name;
L.thumbnailImageUrl =
args.ref.thumbnailImageUrl;
L.worldCapacity = args.ref.capacity;
return args;
});
}
if (this.isGameNoVR) {
var platform = 'Desktop';
} else {
var platform = 'VR';
}
var groupAccessType = '';
if (L.groupAccessType) {
if (L.groupAccessType === 'public') {
groupAccessType = 'Public';
} else if (L.groupAccessType === 'plus') {
groupAccessType = 'Plus';
}
}
switch (L.accessType) {
case 'public':
L.joinUrl = getLaunchURL(L);
L.accessName = `Public #${L.instanceName} (${platform})`;
break;
case 'invite+':
L.accessName = `Invite+ #${L.instanceName} (${platform})`;
break;
case 'invite':
L.accessName = `Invite #${L.instanceName} (${platform})`;
break;
case 'friends':
L.accessName = `Friends #${L.instanceName} (${platform})`;
break;
case 'friends+':
L.accessName = `Friends+ #${L.instanceName} (${platform})`;
break;
case 'group':
L.accessName = `Group #${L.instanceName} (${platform})`;
this.getGroupName(L.groupId).then((groupName) => {
if (groupName) {
L.accessName = `Group${groupAccessType}(${groupName}) #${L.instanceName} (${platform})`;
}
});
break;
}
}
this.lastLocation$ = L;
}
var hidePrivate = false;
if (
this.discordHideInvite &&
(L.accessType === 'invite' ||
L.accessType === 'invite+' ||
L.groupAccessType === 'members')
) {
hidePrivate = true;
}
switch (API.currentUser.status) {
case 'active':
L.statusName = 'Online';
L.statusImage = 'active';
break;
case 'join me':
L.statusName = 'Join Me';
L.statusImage = 'joinme';
break;
case 'ask me':
L.statusName = 'Ask Me';
L.statusImage = 'askme';
if (this.discordHideInvite) {
hidePrivate = true;
}
break;
case 'busy':
L.statusName = 'Do Not Disturb';
L.statusImage = 'busy';
hidePrivate = true;
break;
}
var appId = '883308884863901717';
var bigIcon = 'vrchat';
var partyId = `${L.worldId}:${L.instanceName}`;
var partySize = this.lastLocation.playerList.size;
var partyMaxSize = L.worldCapacity;
if (partySize > partyMaxSize) {
partyMaxSize = partySize;
}
var buttonText = 'Join';
var buttonUrl = L.joinUrl;
if (!this.discordJoinButton) {
buttonText = '';
buttonUrl = '';
}
if (!this.discordInstance) {
partySize = 0;
partyMaxSize = 0;
}
if (hidePrivate) {
partyId = '';
partySize = 0;
partyMaxSize = 0;
buttonText = '';
buttonUrl = '';
} else if (this.isRpcWorld(L.tag)) {
// custom world rpc
if (
L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' ||
L.worldId === 'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' ||
L.worldId === 'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
) {
appId = '784094509008551956';
bigIcon = 'pypy';
} else if (
L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' ||
L.worldId === 'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c'
) {
appId = '846232616054030376';
bigIcon = 'vr_dancing';
} else if (
L.worldId === 'wrld_52bdcdab-11cd-4325-9655-0fb120846945' ||
L.worldId === 'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd'
) {
appId = '939473404808007731';
bigIcon = 'zuwa_zuwa_dance';
} else if (
L.worldId === 'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' ||
L.worldId === 'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' ||
L.worldId === 'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' ||
L.worldId === 'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
) {
appId = '968292722391785512';
bigIcon = 'ls_media';
} else if (
L.worldId === 'wrld_266523e8-9161-40da-acd0-6bd82e075833'
) {
appId = '1095440531821170820';
bigIcon = 'movie_and_chill';
}
if (this.nowPlaying.name) {
L.worldName = this.nowPlaying.name;
}
if (this.nowPlaying.playing) {
Discord.SetTimestamps(
Date.now(),
(this.nowPlaying.startTime -
this.nowPlaying.offset +
this.nowPlaying.length) *
1000
);
}
} else if (!this.discordHideImage && L.thumbnailImageUrl) {
bigIcon = L.thumbnailImageUrl;
}
Discord.SetAssets(
bigIcon, // big icon
'Powered by VRCX', // big icon hover text
L.statusImage, // small icon
L.statusName, // small icon hover text
partyId, // party id
partySize, // party size
partyMaxSize, // party max size
buttonText, // button text
buttonUrl, // button url
appId // app id
);
// NOTE
// 글자 수가 짧으면 업데이트가 안된다..
if (L.worldName.length < 2) {
L.worldName += '\uFFA0'.repeat(2 - L.worldName.length);
}
if (hidePrivate) {
Discord.SetText('Private', '');
Discord.SetTimestamps(0, 0);
} else if (this.discordInstance) {
Discord.SetText(L.worldName, L.accessName);
} else {
Discord.SetText(L.worldName, '');
}
},
async setDiscordActive(active) {
if (active !== this.isDiscordActive) {
this.isDiscordActive = await Discord.SetActive(active);
}
},
async saveDiscordOption(configLabel = '') {
if (configLabel === 'discordActive') {
this.discordActive = !this.discordActive;
await configRepository.setBool(
'discordActive',
this.discordActive
);
}
if (configLabel === 'discordInstance') {
this.discordInstance = !this.discordInstance;
await configRepository.setBool(
'discordInstance',
this.discordInstance
);
}
if (configLabel === 'discordJoinButton') {
this.discordJoinButton = !this.discordJoinButton;
await configRepository.setBool(
'discordJoinButton',
this.discordJoinButton
);
}
if (configLabel === 'discordHideInvite') {
this.discordHideInvite = !this.discordHideInvite;
await configRepository.setBool(
'discordHideInvite',
this.discordHideInvite
);
}
if (configLabel === 'discordHideImage') {
this.discordHideImage = !this.discordHideImage;
await configRepository.setBool(
'discordHideImage',
this.discordHideImage
);
}
this.lastLocation$.tag = '';
this.nextDiscordUpdate = 3;
this.updateDiscord();
}
};
}

View File

@@ -1,179 +0,0 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import configRepository from '../service/config.js';
import database from '../service/database.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
_data = {
feedTable: {
data: [],
search: '',
vip: false,
loading: false,
filter: [],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
},
feedSessionTable: []
};
_methods = {
feedSearch(row) {
var value = this.feedTable.search.toUpperCase();
if (!value) {
return true;
}
if (
(value.startsWith('wrld_') || value.startsWith('grp_')) &&
String(row.location).toUpperCase().includes(value)
) {
return true;
}
switch (row.type) {
case 'GPS':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Online':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Offline':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Status':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.status).toUpperCase().includes(value)) {
return true;
}
if (
String(row.statusDescription)
.toUpperCase()
.includes(value)
) {
return true;
}
return false;
case 'Avatar':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.avatarName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Bio':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.bio).toUpperCase().includes(value)) {
return true;
}
if (String(row.previousBio).toUpperCase().includes(value)) {
return true;
}
return false;
}
return true;
},
async feedTableLookup() {
await configRepository.setString(
'VRCX_feedTableFilters',
JSON.stringify(this.feedTable.filter)
);
await configRepository.setBool(
'VRCX_feedTableVIPFilter',
this.feedTable.vip
);
this.feedTable.loading = true;
var vipList = [];
if (this.feedTable.vip) {
vipList = Array.from(this.localFavoriteFriends.values());
}
this.feedTable.data = await database.lookupFeedDatabase(
this.feedTable.search,
this.feedTable.filter,
vipList
);
this.feedTable.loading = false;
},
addFeed(feed) {
this.queueFeedNoty(feed);
this.feedSessionTable.push(feed);
this.updateSharedFeed(false);
if (
this.feedTable.filter.length > 0 &&
!this.feedTable.filter.includes(feed.type)
) {
return;
}
if (
this.feedTable.vip &&
!this.localFavoriteFriends.has(feed.userId)
) {
return;
}
if (!this.feedSearch(feed)) {
return;
}
this.feedTable.data.push(feed);
this.sweepFeed();
this.notifyMenu('feed');
},
sweepFeed() {
var { data } = this.feedTable;
var j = data.length;
if (j > this.maxTableSize) {
data.splice(0, j - this.maxTableSize);
}
var date = new Date();
date.setDate(date.getDate() - 1); // 24 hour limit
var limit = date.toJSON();
var i = 0;
var k = this.feedSessionTable.length;
while (i < k && this.feedSessionTable[i].created_at < limit) {
++i;
}
if (i === k) {
this.feedSessionTable = [];
} else if (i) {
this.feedSessionTable.splice(0, i);
}
}
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
import * as workerTimers from 'worker-timers';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { inventoryRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.currentUserInventory = new Map();
API.$on('LOGIN', function () {
API.currentUserInventory.clear();
});
}
_data = {
inventoryTable: []
};
_methods = {
async getInventory() {
this.inventoryTable = [];
API.currentUserInventory.clear();
var params = {
n: 100,
offset: 0,
order: 'newest'
};
this.galleryDialogInventoryLoading = true;
try {
for (let i = 0; i < 100; i++) {
params.offset = i * params.n;
const args =
await inventoryRequest.getInventoryItems(params);
for (const item of args.json.data) {
API.currentUserInventory.set(item.id, item);
if (!item.flags.includes('ugc')) {
this.inventoryTable.push(item);
}
}
if (args.json.data.length === 0) {
break;
}
}
} catch (error) {
console.error('Error fetching inventory items:', error);
} finally {
this.galleryDialogInventoryLoading = false;
}
}
};
}

View File

@@ -1,41 +0,0 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { userRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.$on('CONFIG', function (args) {
var languages =
args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS;
if (!languages) {
return;
}
$app.subsetOfLanguages = languages;
var data = [];
for (var key in languages) {
var value = languages[key];
data.push({
key,
value
});
}
$app.languageDialog.languages = data;
});
}
_data = {
subsetOfLanguages: [],
languageDialog: {
visible: false,
loading: false,
languageChoice: false,
languages: []
}
};
_methods = {};
}

View File

@@ -1,106 +0,0 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import database from '../service/database.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {}
_data = {
hideUserMemos: false
};
_methods = {
async migrateMemos() {
var json = JSON.parse(await VRCXStorage.GetAll());
for (var line in json) {
if (line.substring(0, 8) === 'memo_usr') {
var userId = line.substring(5);
var memo = json[line];
if (memo) {
await this.saveUserMemo(userId, memo);
VRCXStorage.Remove(`memo_${userId}`);
}
}
}
},
async getUserMemo(userId) {
try {
return await database.getUserMemo(userId);
} catch (err) {
console.error(err);
return {
userId: '',
editedAt: '',
memo: ''
};
}
},
async saveUserMemo(id, memo) {
if (memo) {
await database.setUserMemo({
userId: id,
editedAt: new Date().toJSON(),
memo
});
} else {
await database.deleteUserMemo(id);
}
var ref = this.friends.get(id);
if (ref) {
ref.memo = String(memo || '');
if (memo) {
var array = memo.split('\n');
ref.$nickName = array[0];
} else {
ref.$nickName = '';
}
}
},
async getAllUserMemos() {
var memos = await database.getAllUserMemos();
memos.forEach((memo) => {
var ref = $app.friends.get(memo.userId);
if (typeof ref !== 'undefined') {
ref.memo = memo.memo;
ref.$nickName = '';
if (memo.memo) {
var array = memo.memo.split('\n');
ref.$nickName = array[0];
}
}
});
},
async getWorldMemo(worldId) {
try {
return await database.getWorldMemo(worldId);
} catch (err) {
console.error(err);
return {
worldId: '',
editedAt: '',
memo: ''
};
}
},
async getAvatarMemo(avatarId) {
try {
return await database.getAvatarMemoDB(avatarId);
} catch (err) {
console.error(err);
return {
avatarId: '',
editedAt: '',
memo: ''
};
}
}
};
}

View File

@@ -1,574 +0,0 @@
import * as workerTimers from 'worker-timers';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { avatarRequest, favoriteRequest, worldRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
_methods = {
promptTOTP() {
if (this.twoFactorAuthDialogVisible) {
return;
}
AppApi.FlashWindow();
this.twoFactorAuthDialogVisible = true;
this.$prompt(
$t('prompt.totp.description'),
$t('prompt.totp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: $t('prompt.totp.use_otp'),
confirmButtonText: $t('prompt.totp.verify'),
inputPlaceholder: $t('prompt.totp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: $t('prompt.totp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
API.verifyTOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
$app.clearCookiesTryLogin();
throw err;
})
.then((args) => {
API.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
this.promptOTP();
}
},
beforeClose: (action, instance, done) => {
this.twoFactorAuthDialogVisible = false;
done();
}
}
);
},
promptOTP() {
if (this.twoFactorAuthDialogVisible) {
return;
}
this.twoFactorAuthDialogVisible = true;
this.$prompt(
$t('prompt.otp.description'),
$t('prompt.otp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: $t('prompt.otp.use_totp'),
confirmButtonText: $t('prompt.otp.verify'),
inputPlaceholder: $t('prompt.otp.input_placeholder'),
inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/,
inputErrorMessage: $t('prompt.otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
API.verifyOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
$app.clearCookiesTryLogin();
throw err;
})
.then((args) => {
API.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
this.promptTOTP();
}
},
beforeClose: (action, instance, done) => {
this.twoFactorAuthDialogVisible = false;
done();
}
}
);
},
promptEmailOTP() {
if (this.twoFactorAuthDialogVisible) {
return;
}
AppApi.FlashWindow();
this.twoFactorAuthDialogVisible = true;
this.$prompt(
$t('prompt.email_otp.description'),
$t('prompt.email_otp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: $t('prompt.email_otp.resend'),
confirmButtonText: $t('prompt.email_otp.verify'),
inputPlaceholder: $t('prompt.email_otp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: $t('prompt.email_otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
API.verifyEmailOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
this.promptEmailOTP();
throw err;
})
.then((args) => {
API.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
this.resendEmail2fa();
}
},
beforeClose: (action, instance, done) => {
this.twoFactorAuthDialogVisible = false;
done();
}
}
);
},
promptOmniDirectDialog() {
this.$prompt(
$t('prompt.direct_access_omni.description'),
$t('prompt.direct_access_omni.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.direct_access_omni.ok'),
cancelButtonText: $t('prompt.direct_access_omni.cancel'),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.direct_access_omni.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
var input = instance.inputValue.trim();
if (!this.directAccessParse(input)) {
this.$message({
message: $t(
'prompt.direct_access_omni.message.error'
),
type: 'error'
});
}
}
}
}
);
},
promptNotificationTimeout() {
this.$prompt(
$t('prompt.notification_timeout.description'),
$t('prompt.notification_timeout.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.notification_timeout.ok'),
cancelButtonText: $t('prompt.notification_timeout.cancel'),
inputValue: this.notificationTimeout / 1000,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.notification_timeout.input_error'
),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
this.notificationTimeout = Math.trunc(
Number(instance.inputValue) * 1000
);
await configRepository.setString(
'VRCX_notificationTimeout',
this.notificationTimeout
);
this.updateVRConfigVars();
}
}
}
);
},
promptPhotonOverlayMessageTimeout() {
this.$prompt(
$t('prompt.overlay_message_timeout.description'),
$t('prompt.overlay_message_timeout.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.overlay_message_timeout.ok'),
cancelButtonText: $t(
'prompt.overlay_message_timeout.cancel'
),
inputValue: this.photonOverlayMessageTimeout / 1000,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.overlay_message_timeout.input_error'
),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
this.photonOverlayMessageTimeout = Math.trunc(
Number(instance.inputValue) * 1000
);
await configRepository.setString(
'VRCX_photonOverlayMessageTimeout',
this.photonOverlayMessageTimeout
);
this.updateVRConfigVars();
}
}
}
);
},
promptRenameWorld(world) {
this.$prompt(
$t('prompt.rename_world.description'),
$t('prompt.rename_world.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.rename_world.ok'),
cancelButtonText: $t('prompt.rename_world.cancel'),
inputValue: world.ref.name,
inputErrorMessage: $t('prompt.rename_world.input_error'),
callback: (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue !== world.ref.name
) {
worldRequest
.saveWorld({
id: world.id,
name: instance.inputValue
})
.then((args) => {
this.$message({
message: $t(
'prompt.rename_world.message.success'
),
type: 'success'
});
return args;
});
}
}
}
);
},
promptChangeWorldDescription(world) {
this.$prompt(
$t('prompt.change_world_description.description'),
$t('prompt.change_world_description.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.change_world_description.ok'),
cancelButtonText: $t(
'prompt.change_world_description.cancel'
),
inputValue: world.ref.description,
inputErrorMessage: $t(
'prompt.change_world_description.input_error'
),
callback: (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue !== world.ref.description
) {
worldRequest
.saveWorld({
id: world.id,
description: instance.inputValue
})
.then((args) => {
this.$message({
message: $t(
'prompt.change_world_description.message.success'
),
type: 'success'
});
return args;
});
}
}
}
);
},
promptChangeWorldCapacity(world) {
this.$prompt(
$t('prompt.change_world_capacity.description'),
$t('prompt.change_world_capacity.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.change_world_capacity.ok'),
cancelButtonText: $t('prompt.change_world_capacity.cancel'),
inputValue: world.ref.capacity,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.change_world_capacity.input_error'
),
callback: (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue !== world.ref.capacity
) {
worldRequest
.saveWorld({
id: world.id,
capacity: instance.inputValue
})
.then((args) => {
this.$message({
message: $t(
'prompt.change_world_capacity.message.success'
),
type: 'success'
});
return args;
});
}
}
}
);
},
promptChangeWorldRecommendedCapacity(world) {
this.$prompt(
$t('prompt.change_world_recommended_capacity.description'),
$t('prompt.change_world_recommended_capacity.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.change_world_capacity.ok'),
cancelButtonText: $t('prompt.change_world_capacity.cancel'),
inputValue: world.ref.recommendedCapacity,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.change_world_recommended_capacity.input_error'
),
callback: (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue !==
world.ref.recommendedCapacity
) {
worldRequest
.saveWorld({
id: world.id,
recommendedCapacity: instance.inputValue
})
.then((args) => {
this.$message({
message: $t(
'prompt.change_world_recommended_capacity.message.success'
),
type: 'success'
});
return args;
});
}
}
}
);
},
promptChangeWorldYouTubePreview(world) {
this.$prompt(
$t('prompt.change_world_preview.description'),
$t('prompt.change_world_preview.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.change_world_preview.ok'),
cancelButtonText: $t('prompt.change_world_preview.cancel'),
inputValue: world.ref.previewYoutubeId,
inputErrorMessage: $t(
'prompt.change_world_preview.input_error'
),
callback: (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue !== world.ref.previewYoutubeId
) {
if (instance.inputValue.length > 11) {
try {
var url = new URL(instance.inputValue);
var id1 = url.pathname;
var id2 = url.searchParams.get('v');
if (id1 && id1.length === 12) {
instance.inputValue = id1.substring(
1,
12
);
}
if (id2 && id2.length === 11) {
instance.inputValue = id2;
}
} catch {
this.$message({
message: $t(
'prompt.change_world_preview.message.error'
),
type: 'error'
});
return;
}
}
if (
instance.inputValue !==
world.ref.previewYoutubeId
) {
worldRequest
.saveWorld({
id: world.id,
previewYoutubeId: instance.inputValue
})
.then((args) => {
this.$message({
message: $t(
'prompt.change_world_preview.message.success'
),
type: 'success'
});
return args;
});
}
}
}
}
);
},
promptMaxTableSizeDialog() {
this.$prompt(
$t('prompt.change_table_size.description'),
$t('prompt.change_table_size.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.change_table_size.save'),
cancelButtonText: $t('prompt.change_table_size.cancel'),
inputValue: this.maxTableSize,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.change_table_size.input_error'
),
callback: async (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
if (instance.inputValue > 10000) {
instance.inputValue = 10000;
}
this.maxTableSize = instance.inputValue;
await configRepository.setString(
'VRCX_maxTableSize',
this.maxTableSize
);
database.setmaxTableSize(this.maxTableSize);
this.feedTableLookup();
this.gameLogTableLookup();
}
}
}
);
},
promptProxySettings() {
this.$prompt(
$t('prompt.proxy_settings.description'),
$t('prompt.proxy_settings.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.proxy_settings.restart'),
cancelButtonText: $t('prompt.proxy_settings.close'),
inputValue: this.proxyServer,
inputPlaceholder: $t('prompt.proxy_settings.placeholder'),
callback: async (action, instance) => {
this.proxyServer = instance.inputValue;
await VRCXStorage.Set(
'VRCX_ProxyServer',
this.proxyServer
);
await VRCXStorage.Flush();
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 100);
});
if (action === 'confirm') {
var isUpgrade = false;
this.restartVRCX(isUpgrade);
}
}
}
);
},
promptPhotonLobbyTimeoutThreshold() {
this.$prompt(
$t('prompt.photon_lobby_timeout.description'),
$t('prompt.photon_lobby_timeout.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.photon_lobby_timeout.ok'),
cancelButtonText: $t('prompt.photon_lobby_timeout.cancel'),
inputValue: this.photonLobbyTimeoutThreshold / 1000,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.photon_lobby_timeout.input_error'
),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
this.photonLobbyTimeoutThreshold = Math.trunc(
Number(instance.inputValue) * 1000
);
await configRepository.setString(
'VRCX_photonLobbyTimeoutThreshold',
this.photonLobbyTimeoutThreshold
);
}
}
}
);
},
promptAutoClearVRCXCacheFrequency() {
this.$prompt(
$t('prompt.auto_clear_cache.description'),
$t('prompt.auto_clear_cache.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.auto_clear_cache.ok'),
cancelButtonText: $t('prompt.auto_clear_cache.cancel'),
inputValue: this.clearVRCXCacheFrequency / 3600 / 2,
inputPattern: /\d+$/,
inputErrorMessage: $t(
'prompt.auto_clear_cache.input_error'
),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
this.clearVRCXCacheFrequency = Math.trunc(
Number(instance.inputValue) * 3600 * 2
);
await configRepository.setString(
'VRCX_clearVRCXCacheFrequency',
this.clearVRCXCacheFrequency
);
}
}
}
);
}
};
}

View File

@@ -1,284 +0,0 @@
import * as workerTimers from 'worker-timers';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {}
_data = {};
_methods = {
async tryRestoreFriendNumber() {
var lastUpdate = await configRepository.getString(
`VRCX_lastStoreTime_${API.currentUser.id}`
);
if (lastUpdate == -4) {
// this means the backup was already applied
return;
}
var status = false;
this.friendNumber = 0;
for (var ref of this.friendLog.values()) {
ref.friendNumber = 0;
}
try {
if (lastUpdate) {
// backup ready to try apply
status = await this.restoreFriendNumber();
}
// needs to be in reverse because we don't know the starting number
this.applyFriendLogFriendOrderInReverse();
} catch (err) {
console.error(err);
}
// if (status) {
// this.$message({
// message: 'Friend order restored from backup',
// type: 'success',
// duration: 0,
// showClose: true
// });
// } else if (this.friendLogTable.data.length > 0) {
// this.$message({
// message:
// 'No backup found, friend order partially restored from friendLog',
// type: 'success',
// duration: 0,
// showClose: true
// });
// }
await configRepository.setString(
`VRCX_lastStoreTime_${API.currentUser.id}`,
-4
);
},
async restoreFriendNumber() {
var storedData = null;
try {
var data = await configRepository.getString(
`VRCX_friendOrder_${API.currentUser.id}`
);
if (data) {
var storedData = JSON.parse(data);
}
} catch (err) {
console.error(err);
}
if (!storedData || storedData.length === 0) {
var message = 'whomp whomp, no friend order backup found';
console.error(message);
return false;
}
var friendLogTable = this.getFriendLogFriendOrder();
// for storedData
var machList = [];
for (var i = 0; i < Object.keys(storedData).length; i++) {
var key = Object.keys(storedData)[i];
var value = storedData[key];
var item = this.parseFriendOrderBackup(
friendLogTable,
key,
value
);
machList.push(item);
}
machList.sort((a, b) => b.matches - a.matches);
console.log(
`friendLog: ${friendLogTable.length} friendOrderBackups:`,
machList
);
var bestBackup = machList[0];
if (!bestBackup?.isValid) {
var message = 'whomp whomp, no valid backup found';
console.error(message);
return false;
}
this.applyFriendOrderBackup(bestBackup.table);
this.applyFriendLogFriendOrder();
await configRepository.setInt(
`VRCX_friendNumber_${API.currentUser.id}`,
this.friendNumber
);
return true;
},
getFriendLogFriendOrder() {
var friendLogTable = [];
for (var i = 0; i < this.friendLogTable.data.length; i++) {
var ref = this.friendLogTable.data[i];
if (ref.type !== 'Friend') {
continue;
}
if (
friendLogTable.findIndex((x) => x.id === ref.userId) !== -1
) {
// console.log(
// 'ignoring duplicate friend',
// ref.displayName,
// ref.created_at
// );
continue;
}
friendLogTable.push({
id: ref.userId,
displayName: ref.displayName,
created_at: ref.created_at
});
}
var compareByCreatedAt = function (a, b) {
var A = a.created_at;
var B = b.created_at;
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
};
friendLogTable.sort(compareByCreatedAt);
return friendLogTable;
},
applyFriendLogFriendOrder() {
var friendLogTable = this.getFriendLogFriendOrder();
if (this.friendNumber === 0) {
console.log(
'No backup applied, applying friend log in reverse'
);
// this means no FriendOrderBackup was applied
// will need to apply in reverse order instead
return;
}
for (var friendLog of friendLogTable) {
var ref = this.friendLog.get(friendLog.id);
if (!ref || ref.friendNumber) {
continue;
}
ref.friendNumber = ++this.friendNumber;
this.friendLog.set(ref.userId, ref);
database.setFriendLogCurrent(ref);
var friendRef = this.friends.get(friendLog.id);
if (friendRef?.ref) {
friendRef.ref.$friendNumber = ref.friendNumber;
}
}
},
applyFriendLogFriendOrderInReverse() {
this.friendNumber = this.friends.size + 1;
var friendLogTable = this.getFriendLogFriendOrder();
for (var i = friendLogTable.length - 1; i > -1; i--) {
var friendLog = friendLogTable[i];
var ref = this.friendLog.get(friendLog.id);
if (!ref) {
continue;
}
if (ref.friendNumber) {
break;
}
ref.friendNumber = --this.friendNumber;
this.friendLog.set(ref.userId, ref);
database.setFriendLogCurrent(ref);
var friendRef = this.friends.get(friendLog.id);
if (friendRef?.ref) {
friendRef.ref.$friendNumber = ref.friendNumber;
}
}
this.friendNumber = this.friends.size;
console.log('Applied friend order from friendLog');
},
parseFriendOrderBackup(friendLogTable, created_at, backupUserIds) {
var backupTable = [];
for (var i = 0; i < backupUserIds.length; i++) {
var userId = backupUserIds[i];
var ctx = this.friends.get(userId);
if (ctx) {
backupTable.push({
id: ctx.id,
displayName: ctx.name
});
}
}
// var compareTable = [];
// compare 2 tables, find max amount of id's in same order
var maxMatches = 0;
var currentMatches = 0;
var backupIndex = 0;
for (var i = 0; i < friendLogTable.length; i++) {
var isMatch = false;
var ref = friendLogTable[i];
if (backupIndex <= 0) {
backupIndex = backupTable.findIndex((x) => x.id === ref.id);
if (backupIndex !== -1) {
currentMatches = 1;
}
} else if (backupTable[backupIndex].id === ref.id) {
currentMatches++;
isMatch = true;
} else {
var backupIndex = backupTable.findIndex(
(x) => x.id === ref.id
);
if (backupIndex !== -1) {
currentMatches = 1;
}
}
if (backupIndex === backupTable.length - 1) {
backupIndex = 0;
} else {
backupIndex++;
}
if (currentMatches > maxMatches) {
maxMatches = currentMatches;
}
// compareTable.push({
// id: ref.id,
// displayName: ref.displayName,
// match: isMatch
// });
}
var lerp = (a, b, alpha) => {
return a + alpha * (b - a);
};
return {
matches: parseFloat(`${maxMatches}.${created_at}`),
table: backupUserIds,
isValid: maxMatches > lerp(4, 10, backupTable.length / 1000) // pls no collisions
};
},
applyFriendOrderBackup(userIdOrder) {
for (var i = 0; i < userIdOrder.length; i++) {
var userId = userIdOrder[i];
var ctx = this.friends.get(userId);
var ref = ctx?.ref;
if (!ref || ref.$friendNumber) {
continue;
}
var friendLogCurrent = {
userId,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: i + 1
};
this.friendLog.set(userId, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
this.friendNumber = i + 1;
}
}
};
}

View File

@@ -1,577 +0,0 @@
import * as workerTimers from 'worker-timers';
import { baseClass, $app, API } from './baseClass.js';
import { worldRequest, groupRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
_data = {
sharedFeed: {
gameLog: {
wrist: [],
lastEntryDate: ''
},
feedTable: {
wrist: [],
lastEntryDate: ''
},
notificationTable: {
wrist: [],
lastEntryDate: ''
},
friendLogTable: {
wrist: [],
lastEntryDate: ''
},
moderationAgainstTable: {
wrist: [],
lastEntryDate: ''
},
pendingUpdate: false
},
updateSharedFeedTimer: null,
updateSharedFeedPending: false,
updateSharedFeedPendingForceUpdate: false
};
_methods = {
updateSharedFeed(forceUpdate) {
if (!this.friendLogInitStatus) {
return;
}
if (this.updateSharedFeedTimer) {
if (forceUpdate) {
this.updateSharedFeedPendingForceUpdate = true;
}
this.updateSharedFeedPending = true;
} else {
this.updateSharedExecute(forceUpdate);
this.updateSharedFeedTimer = setTimeout(() => {
if (this.updateSharedFeedPending) {
this.updateSharedExecute(
this.updateSharedFeedPendingForceUpdate
);
}
this.updateSharedFeedTimer = null;
}, 150);
}
},
updateSharedExecute(forceUpdate) {
try {
this.updateSharedFeedDebounce(forceUpdate);
} catch (err) {
console.error(err);
}
this.updateSharedFeedTimer = null;
this.updateSharedFeedPending = false;
this.updateSharedFeedPendingForceUpdate = false;
},
updateSharedFeedDebounce(forceUpdate) {
this.updateSharedFeedGameLog(forceUpdate);
this.updateSharedFeedFeedTable(forceUpdate);
this.updateSharedFeedNotificationTable(forceUpdate);
this.updateSharedFeedFriendLogTable(forceUpdate);
this.updateSharedFeedModerationAgainstTable(forceUpdate);
var feeds = this.sharedFeed;
if (!feeds.pendingUpdate) {
return;
}
var wristFeed = [];
wristFeed = wristFeed.concat(
feeds.gameLog.wrist,
feeds.feedTable.wrist,
feeds.notificationTable.wrist,
feeds.friendLogTable.wrist,
feeds.moderationAgainstTable.wrist
);
// OnPlayerJoining/Traveling
API.currentTravelers.forEach((ref) => {
var isFavorite = this.localFavoriteFriends.has(ref.id);
if (
(this.sharedFeedFilters.wrist.OnPlayerJoining ===
'Friends' ||
(this.sharedFeedFilters.wrist.OnPlayerJoining ===
'VIP' &&
isFavorite)) &&
!$app.lastLocation.playerList.has(ref.id)
) {
if (ref.$location.tag === $app.lastLocation.location) {
var feedEntry = {
...ref,
isFavorite,
isFriend: true,
type: 'OnPlayerJoining'
};
wristFeed.unshift(feedEntry);
} else {
var worldRef = API.cachedWorlds.get(
ref.$location.worldId
);
var groupName = '';
if (ref.$location.groupId) {
var groupRef = API.cachedGroups.get(
ref.$location.groupId
);
if (typeof groupRef !== 'undefined') {
groupName = groupRef.name;
} else {
// no group cache, fetch group and try again
groupRequest
.getGroup({
groupId: ref.$location.groupId
})
.then((args) => {
workerTimers.setTimeout(() => {
// delay to allow for group cache to update
$app.sharedFeed.pendingUpdate =
true;
$app.updateSharedFeed(false);
}, 100);
return args;
})
.catch((err) => {
console.error(err);
});
}
}
if (typeof worldRef !== 'undefined') {
var feedEntry = {
created_at: ref.created_at,
type: 'GPS',
userId: ref.id,
displayName: ref.displayName,
location: ref.$location.tag,
worldName: worldRef.name,
groupName,
previousLocation: '',
isFavorite,
time: 0,
isFriend: true,
isTraveling: true
};
wristFeed.unshift(feedEntry);
} else {
// no world cache, fetch world and try again
worldRequest
.getWorld({
worldId: ref.$location.worldId
})
.then((args) => {
workerTimers.setTimeout(() => {
// delay to allow for world cache to update
$app.sharedFeed.pendingUpdate = true;
$app.updateSharedFeed(false);
}, 100);
return args;
})
.catch((err) => {
console.error(err);
});
}
}
}
});
wristFeed.sort(function (a, b) {
if (a.created_at < b.created_at) {
return 1;
}
if (a.created_at > b.created_at) {
return -1;
}
return 0;
});
wristFeed.splice(16);
AppApi.ExecuteVrFeedFunction(
'wristFeedUpdate',
JSON.stringify(wristFeed)
);
this.applyUserDialogLocation();
this.applyWorldDialogInstances();
this.applyGroupDialogInstances();
feeds.pendingUpdate = false;
},
updateSharedFeedGameLog(forceUpdate) {
// Location, OnPlayerJoined, OnPlayerLeft
var sessionTable = this.gameLogSessionTable;
var i = sessionTable.length;
if (i > 0) {
if (
sessionTable[i - 1].created_at ===
this.sharedFeed.gameLog.lastEntryDate &&
forceUpdate === false
) {
return;
}
this.sharedFeed.gameLog.lastEntryDate =
sessionTable[i - 1].created_at;
} else {
return;
}
var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
var wristArr = [];
var w = 0;
var wristFilter = this.sharedFeedFilters.wrist;
var currentUserLeaveTime = 0;
var locationJoinTime = 0;
for (var i = sessionTable.length - 1; i > -1; i--) {
var ctx = sessionTable[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'Notification') {
continue;
}
// on Location change remove OnPlayerLeft
if (ctx.type === 'LocationDestination') {
currentUserLeaveTime = Date.parse(ctx.created_at);
var currentUserLeaveTimeOffset =
currentUserLeaveTime + 5 * 1000;
for (var k = w - 1; k > -1; k--) {
var feedItem = wristArr[k];
if (
(feedItem.type === 'OnPlayerLeft' ||
feedItem.type === 'BlockedOnPlayerLeft' ||
feedItem.type === 'MutedOnPlayerLeft') &&
Date.parse(feedItem.created_at) >=
currentUserLeaveTime &&
Date.parse(feedItem.created_at) <=
currentUserLeaveTimeOffset
) {
wristArr.splice(k, 1);
w--;
}
}
}
// on Location change remove OnPlayerJoined
if (ctx.type === 'Location') {
locationJoinTime = Date.parse(ctx.created_at);
var locationJoinTimeOffset = locationJoinTime + 20 * 1000;
for (var k = w - 1; k > -1; k--) {
var feedItem = wristArr[k];
if (
(feedItem.type === 'OnPlayerJoined' ||
feedItem.type === 'BlockedOnPlayerJoined' ||
feedItem.type === 'MutedOnPlayerJoined') &&
Date.parse(feedItem.created_at) >=
locationJoinTime &&
Date.parse(feedItem.created_at) <=
locationJoinTimeOffset
) {
wristArr.splice(k, 1);
w--;
}
}
}
// remove current user
if (
(ctx.type === 'OnPlayerJoined' ||
ctx.type === 'OnPlayerLeft' ||
ctx.type === 'PortalSpawn') &&
ctx.displayName === API.currentUser.displayName
) {
continue;
}
var isFriend = false;
var isFavorite = false;
if (ctx.userId) {
isFriend = this.friends.has(ctx.userId);
isFavorite = this.localFavoriteFriends.has(ctx.userId);
} else if (ctx.displayName) {
for (var ref of API.cachedUsers.values()) {
if (ref.displayName === ctx.displayName) {
isFriend = this.friends.has(ref.id);
isFavorite = this.localFavoriteFriends.has(ref.id);
break;
}
}
}
// add tag colour
var tagColour = '';
if (ctx.userId) {
var tagRef = this.customUserTags.get(ctx.userId);
if (typeof tagRef !== 'undefined') {
tagColour = tagRef.colour;
}
}
// BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft
if (
ctx.type === 'OnPlayerJoined' ||
ctx.type === 'OnPlayerLeft'
) {
for (var ref of API.cachedPlayerModerations.values()) {
if (
ref.targetDisplayName !== ctx.displayName &&
ref.sourceUserId !== ctx.userId
) {
continue;
}
if (ref.type === 'block') {
var type = `Blocked${ctx.type}`;
} else if (ref.type === 'mute') {
var type = `Muted${ctx.type}`;
} else {
continue;
}
var entry = {
created_at: ctx.created_at,
type,
displayName: ref.targetDisplayName,
userId: ref.targetUserId,
isFriend,
isFavorite
};
if (
wristFilter[type] &&
(wristFilter[type] === 'Everyone' ||
(wristFilter[type] === 'Friends' && isFriend) ||
(wristFilter[type] === 'VIP' && isFavorite))
) {
wristArr.unshift(entry);
}
this.queueGameLogNoty(entry);
}
}
// when too many user joins happen at once when switching instances
// the "w" counter maxes out and wont add any more entries
// until the onJoins are cleared by "Location"
// e.g. if a "VideoPlay" occurs between "OnPlayerJoined" and "Location" it wont be added
if (
w < 50 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Everyone' ||
(wristFilter[ctx.type] === 'Friends' && isFriend) ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
tagColour,
isFriend,
isFavorite
});
++w;
}
}
this.sharedFeed.gameLog.wrist = wristArr;
this.sharedFeed.pendingUpdate = true;
},
updateSharedFeedFeedTable(forceUpdate) {
// GPS, Online, Offline, Status, Avatar
var feedSession = this.feedSessionTable;
var i = feedSession.length;
if (i > 0) {
if (
feedSession[i - 1].created_at ===
this.sharedFeed.feedTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
this.sharedFeed.feedTable.lastEntryDate =
feedSession[i - 1].created_at;
} else {
return;
}
var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
var wristArr = [];
var w = 0;
var wristFilter = this.sharedFeedFilters.wrist;
for (var i = feedSession.length - 1; i > -1; i--) {
var ctx = feedSession[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'Avatar') {
continue;
}
// hide private worlds from feed
if (
this.hidePrivateFromFeed &&
ctx.type === 'GPS' &&
ctx.location === 'private'
) {
continue;
}
var isFriend = this.friends.has(ctx.userId);
var isFavorite = this.localFavoriteFriends.has(ctx.userId);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
this.sharedFeed.feedTable.wrist = wristArr;
this.sharedFeed.pendingUpdate = true;
},
updateSharedFeedNotificationTable(forceUpdate) {
// invite, requestInvite, requestInviteResponse, inviteResponse, friendRequest
var notificationTable = this.notificationTable.data;
var i = notificationTable.length;
if (i > 0) {
if (
notificationTable[i - 1].created_at ===
this.sharedFeed.notificationTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
this.sharedFeed.notificationTable.lastEntryDate =
notificationTable[i - 1].created_at;
} else {
return;
}
var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
var wristArr = [];
var w = 0;
var wristFilter = this.sharedFeedFilters.wrist;
for (var i = notificationTable.length - 1; i > -1; i--) {
var ctx = notificationTable[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.senderUserId === API.currentUser.id) {
continue;
}
var isFriend = this.friends.has(ctx.senderUserId);
var isFavorite = this.localFavoriteFriends.has(
ctx.senderUserId
);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
this.sharedFeed.notificationTable.wrist = wristArr;
this.sharedFeed.pendingUpdate = true;
},
updateSharedFeedFriendLogTable(forceUpdate) {
// TrustLevel, Friend, FriendRequest, Unfriend, DisplayName
var friendLog = this.friendLogTable.data;
var i = friendLog.length;
if (i > 0) {
if (
friendLog[i - 1].created_at ===
this.sharedFeed.friendLogTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
this.sharedFeed.friendLogTable.lastEntryDate =
friendLog[i - 1].created_at;
} else {
return;
}
var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
var wristArr = [];
var w = 0;
var wristFilter = this.sharedFeedFilters.wrist;
for (var i = friendLog.length - 1; i > -1; i--) {
var ctx = friendLog[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'FriendRequest') {
continue;
}
var isFriend = this.friends.has(ctx.userId);
var isFavorite = this.localFavoriteFriends.has(ctx.userId);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
this.sharedFeed.friendLogTable.wrist = wristArr;
this.sharedFeed.pendingUpdate = true;
},
updateSharedFeedModerationAgainstTable(forceUpdate) {
// Unblocked, Blocked, Muted, Unmuted
var moderationAgainst = this.moderationAgainstTable;
var i = moderationAgainst.length;
if (i > 0) {
if (
moderationAgainst[i - 1].created_at ===
this.sharedFeed.moderationAgainstTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
this.sharedFeed.moderationAgainstTable.lastEntryDate =
moderationAgainst[i - 1].created_at;
} else {
return;
}
var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
var wristArr = [];
var w = 0;
var wristFilter = this.sharedFeedFilters.wrist;
for (var i = moderationAgainst.length - 1; i > -1; i--) {
var ctx = moderationAgainst[i];
if (ctx.created_at < bias) {
break;
}
var isFriend = this.friends.has(ctx.userId);
var isFavorite = this.localFavoriteFriends.has(ctx.userId);
// add tag colour
var tagColour = '';
var tagRef = this.customUserTags.get(ctx.userId);
if (typeof tagRef !== 'undefined') {
tagColour = tagRef.colour;
}
if (
w < 20 &&
wristFilter[ctx.type] &&
wristFilter[ctx.type] === 'On'
) {
wristArr.push({
...ctx,
isFriend,
isFavorite,
tagColour
});
++w;
}
}
this.sharedFeed.moderationAgainstTable.wrist = wristArr;
this.sharedFeed.pendingUpdate = true;
}
};
}

View File

@@ -1,512 +0,0 @@
import Vue from 'vue';
import VueMarkdown from 'vue-markdown';
import { instanceRequest, userRequest } from '../api';
import { hasGroupPermission } from '../composables/group/utils';
import { parseLocation } from '../composables/instance/utils';
import { $app, $t, API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
Vue.component('vue-markdown', VueMarkdown);
Vue.component('launch', {
template:
'<el-tooltip placement="top" :content="$t(`dialog.user.info.launch_invite_tooltip`)" :disabled="hideTooltips"><el-button @click="confirm" size="mini" icon="el-icon-switch-button" circle></el-button></el-tooltip>',
props: {
location: String,
hideTooltips: Boolean
},
methods: {
parse() {
this.$el.style.display = $app.checkCanInviteSelf(
this.location
)
? ''
: 'none';
},
confirm() {
this.$emit('show-launch-dialog', this.location);
}
},
watch: {
location() {
this.parse();
}
},
mounted() {
this.parse();
}
});
Vue.component('invite-yourself', {
template:
'<el-button @click="confirm" size="mini" icon="el-icon-message" circle></el-button>',
props: {
location: String,
shortname: String
},
methods: {
parse() {
this.$el.style.display = $app.checkCanInviteSelf(
this.location
)
? ''
: 'none';
},
confirm() {
this.selfInvite(this.location, this.shortname);
},
selfInvite(location, shortName) {
const L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId,
shortName
})
.then((args) => {
this.$message({
message: 'Self invite sent',
type: 'success'
});
return args;
});
}
},
watch: {
location() {
this.parse();
}
},
mounted() {
this.parse();
}
});
Vue.component('location-world', {
template:
'<span><span @click="showLaunchDialog" class="x-link">' +
'<i v-if="isUnlocked" class="el-icon el-icon-unlock" style="display:inline-block;margin-right:5px"></i>' +
'<span>#{{ instanceName }} {{ accessTypeName }}</span></span>' +
'<span v-if="groupName" @click="showGroupDialog" class="x-link">({{ groupName }})</span>' +
'<span class="flags" :class="region" style="display:inline-block;margin-left:5px"></span>' +
'<i v-if="strict" class="el-icon el-icon-lock" style="display:inline-block;margin-left:5px"></i></span>',
props: {
locationobject: Object,
currentuserid: String,
worlddialogshortname: String,
grouphint: {
type: String,
default: ''
}
},
data() {
return {
location: this.location,
instanceName: this.instanceName,
accessTypeName: this.accessTypeName,
region: this.region,
shortName: this.shortName,
isUnlocked: this.isUnlocked,
strict: this.strict,
groupName: this.groupName
};
},
methods: {
parse() {
this.location = this.locationobject.tag;
this.instanceName = this.locationobject.instanceName;
this.accessTypeName = this.locationobject.accessTypeName;
this.strict = this.locationobject.strict;
this.shortName = this.locationobject.shortName;
this.isUnlocked = false;
if (
(this.worlddialogshortname &&
this.locationobject.shortName &&
this.worlddialogshortname ===
this.locationobject.shortName) ||
this.currentuserid === this.locationobject.userId
) {
this.isUnlocked = true;
}
this.region = this.locationobject.region;
if (!this.region) {
this.region = 'us';
}
this.groupName = '';
if (this.grouphint) {
this.groupName = this.grouphint;
} else if (this.locationobject.groupId) {
this.groupName = this.locationobject.groupId;
$app.getGroupName(this.locationobject.groupId).then(
(groupName) => {
this.groupName = groupName;
}
);
}
},
showLaunchDialog() {
this.$emit(
'show-launch-dialog',
this.location,
this.shortName
);
},
showGroupDialog() {
if (!this.location) {
return;
}
var L = parseLocation(this.location);
if (!L.groupId) {
return;
}
$app.showGroupDialog(L.groupId);
}
},
watch: {
locationobject() {
this.parse();
}
},
created() {
this.parse();
}
});
Vue.component('last-join', {
template:
'<span v-if="lastJoin">' +
'<el-tooltip placement="top" style="margin-left:5px" >' +
'<div slot="content">' +
'<span>{{ $t("dialog.user.info.last_join") }} <timer :epoch="lastJoin"></timer></span>' +
'</div>' +
'<i class="el-icon el-icon-location-outline" style="display:inline-block"></i>' +
'</el-tooltip>' +
'</span>',
props: {
location: String,
currentlocation: String
},
data() {
return {
lastJoin: this.lastJoin
};
},
methods: {
parse() {
this.lastJoin = $app.instanceJoinHistory.get(this.location);
}
},
watch: {
location() {
this.parse();
},
currentlocation() {
this.parse();
}
},
created() {
this.parse();
}
});
Vue.component('instance-info', {
template:
'<div style="display:inline-block;margin-left:5px">' +
'<el-tooltip v-if="isValidInstance" placement="bottom">' +
'<div slot="content">' +
'<template v-if="isClosed"><span>Closed At: {{ closedAt | formatDate(\'long\') }}</span></br></template>' +
'<template v-if="canCloseInstance"><el-button :disabled="isClosed" size="mini" type="primary" @click="$root.closeInstance(location)">{{ $t("dialog.user.info.close_instance") }}</el-button></br></br></template>' +
'<span><span style="color:#409eff">PC: </span>{{ platforms.standalonewindows }}</span></br>' +
'<span><span style="color:#67c23a">Android: </span>{{ platforms.android }}</span></br>' +
'<span>{{ $t("dialog.user.info.instance_game_version") }} {{ gameServerVersion }}</span></br>' +
'<span v-if="queueEnabled">{{ $t("dialog.user.info.instance_queuing_enabled") }}</br></span>' +
'<span v-if="disabledContentSettings">{{ $t("dialog.user.info.instance_disabled_content") }} {{ disabledContentSettings }}</br></span>' +
'<span v-if="userList.length">{{ $t("dialog.user.info.instance_users") }}</br></span>' +
'<template v-for="user in userList"><span style="cursor:pointer;margin-right:5px" @click="showUserDialog(user.id)" v-text="user.displayName"></span></template>' +
'</div>' +
'<i class="el-icon-caret-bottom"></i>' +
'</el-tooltip>' +
'<span v-if="occupants" style="margin-left:5px">{{ occupants }}/{{ capacity }}</span>' +
'<span v-if="friendcount" style="margin-left:5px">({{ friendcount }})</span>' +
'<span v-if="isFull" style="margin-left:5px;color:lightcoral">{{ $t("dialog.user.info.instance_full") }}</span>' +
'<span v-if="isHardClosed" style="margin-left:5px;color:lightcoral">{{ $t("dialog.user.info.instance_hard_closed") }}</span>' +
'<span v-else-if="isClosed" style="margin-left:5px;color:lightcoral">{{ $t("dialog.user.info.instance_closed") }}</span>' +
'<span v-if="queueSize" style="margin-left:5px">{{ $t("dialog.user.info.instance_queue") }} {{ queueSize }}</span>' +
'<span v-if="isAgeGated" style="margin-left:5px;color:lightcoral">{{ $t("dialog.user.info.instance_age_gated") }}</span>' +
'</div>',
props: {
location: String,
instance: Object,
friendcount: Number,
updateelement: Number
},
data() {
return {
isValidInstance: this.isValidInstance,
isFull: this.isFull,
isClosed: this.isClosed,
isHardClosed: this.isHardClosed,
closedAt: this.closedAt,
occupants: this.occupants,
capacity: this.capacity,
queueSize: this.queueSize,
queueEnabled: this.queueEnabled,
platforms: this.platforms,
userList: this.userList,
gameServerVersion: this.gameServerVersion,
canCloseInstance: this.canCloseInstance
};
},
methods: {
parse() {
this.isValidInstance = false;
this.isFull = false;
this.isClosed = false;
this.isHardClosed = false;
this.closedAt = '';
this.occupants = 0;
this.capacity = 0;
this.queueSize = 0;
this.queueEnabled = false;
this.platforms = [];
this.userList = [];
this.gameServerVersion = '';
this.canCloseInstance = false;
this.isAgeGated = false;
this.disabledContentSettings = '';
if (
!this.location ||
!this.instance ||
Object.keys(this.instance).length === 0
) {
return;
}
this.isValidInstance = true;
this.isFull =
typeof this.instance.hasCapacityForYou !==
'undefined' && !this.instance.hasCapacityForYou;
if (this.instance.closedAt) {
this.isClosed = true;
this.closedAt = this.instance.closedAt;
}
this.isHardClosed = this.instance.hardClose === true;
this.occupants = this.instance.userCount;
if (this.location === $app.lastLocation.location) {
// use gameLog for occupants when in same location
this.occupants = $app.lastLocation.playerList.size;
}
this.capacity = this.instance.capacity;
this.gameServerVersion = this.instance.gameServerVersion;
this.queueSize = this.instance.queueSize;
if (this.instance.platforms) {
this.platforms = this.instance.platforms;
}
if (this.instance.users) {
this.userList = this.instance.users;
}
if (this.instance.ownerId === API.currentUser.id) {
this.canCloseInstance = true;
} else if (this.instance?.ownerId?.startsWith('grp_')) {
// check group perms
var groupId = this.instance.ownerId;
var group = API.cachedGroups.get(groupId);
this.canCloseInstance = hasGroupPermission(
group,
'group-instance-moderate'
);
}
this.isAgeGated = this.instance.ageGate === true;
if (this.location && this.location.includes('~ageGate')) {
// dumb workaround for API not returning `ageGate`
this.isAgeGated = true;
}
if (
this.instance.$disabledContentSettings &&
this.instance.$disabledContentSettings.length
) {
this.disabledContentSettings =
this.instance.$disabledContentSettings.join(', ');
}
},
showUserDialog(userId) {
this.showUserDialog(userId);
}
},
watch: {
updateelement() {
this.parse();
},
location() {
this.parse();
},
friendcount() {
this.parse();
}
},
created() {
this.parse();
}
});
Vue.component('avatar-info', {
template:
'<div @click="confirm" class="avatar-info">' +
'<span style="margin-right:5px">{{ avatarName }}</span>' +
'<span v-if="avatarType" style="margin-right:5px" :class="color">{{ avatarType }}</span>' +
'<span v-if="avatarTags" style="color:#909399;font-family:monospace;font-size:12px;">{{ avatarTags }}</span>' +
'</div>',
props: {
imageurl: String,
userid: String,
hintownerid: String,
hintavatarname: String,
avatartags: Array
},
data() {
return {
avatarName: this.avatarName,
avatarType: this.avatarType,
avatarTags: this.avatarTags,
color: this.color
};
},
methods: {
async parse() {
this.ownerId = '';
this.avatarName = '';
this.avatarType = '';
this.color = '';
this.avatarTags = '';
if (!this.imageurl) {
this.avatarName = '-';
} else if (this.hintownerid) {
this.avatarName = this.hintavatarname;
this.ownerId = this.hintownerid;
} else {
try {
var avatarInfo = await $app.getAvatarName(
this.imageurl
);
this.avatarName = avatarInfo.avatarName;
this.ownerId = avatarInfo.ownerId;
} catch (err) {}
}
if (typeof this.userid === 'undefined' || !this.ownerId) {
this.color = '';
this.avatarType = '';
} else if (this.ownerId === this.userid) {
this.color = 'avatar-info-own';
this.avatarType = '(own)';
} else {
this.color = 'avatar-info-public';
this.avatarType = '(public)';
}
if (typeof this.avatartags === 'object') {
var tagString = '';
for (var i = 0; i < this.avatartags.length; i++) {
var tagName = this.avatartags[i].replace(
'content_',
''
);
tagString += tagName;
if (i < this.avatartags.length - 1) {
tagString += ', ';
}
}
this.avatarTags = tagString;
}
},
confirm() {
if (!this.imageurl) {
return;
}
$app.showAvatarAuthorDialog(
this.userid,
this.ownerId,
this.imageurl
);
}
},
watch: {
imageurl() {
this.parse();
},
userid() {
this.parse();
},
avatartags() {
this.parse();
}
},
mounted() {
this.parse();
}
});
Vue.component('display-name', {
template:
'<span @click="showUserDialog" class="x-link">{{ username }}</span>',
props: {
userid: String,
location: String,
forceUpdateKey: Number,
hint: {
type: String,
default: ''
}
},
data() {
return {
username: this.username
};
},
methods: {
async parse() {
this.username = this.userid;
if (this.hint) {
this.username = this.hint;
} else if (this.userid) {
var args = await userRequest.getCachedUser({
userId: this.userid
});
}
if (
typeof args !== 'undefined' &&
typeof args.json !== 'undefined' &&
typeof args.json.displayName !== 'undefined'
) {
this.username = args.json.displayName;
}
},
showUserDialog() {
$app.showUserDialog(this.userid);
}
},
watch: {
location() {
this.parse();
},
forceUpdateKey() {
this.parse();
},
userid() {
this.parse();
}
},
mounted() {
this.parse();
}
});
}
}

View File

@@ -1,122 +0,0 @@
import * as workerTimers from 'worker-timers';
import { baseClass, $app, API } from './baseClass.js';
import { groupRequest } from '../api/index.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.$on('LOGIN', function () {
$app.nextCurrentUserRefresh = 300;
$app.nextFriendsRefresh = 3600;
$app.nextGroupInstanceRefresh = 0;
});
}
_data = {
nextCurrentUserRefresh: 300,
nextFriendsRefresh: 3600,
nextGroupInstanceRefresh: 0,
nextAppUpdateCheck: 3600,
ipcTimeout: 0,
nextClearVRCXCacheCheck: 0,
nextDiscordUpdate: 0,
nextAutoStateChange: 0,
nextGetLogCheck: 0,
nextGameRunningCheck: 0,
nextDatabaseOptimize: 3600
};
_methods = {
async updateLoop() {
try {
if (API.isLoggedIn === true) {
if (--this.nextCurrentUserRefresh <= 0) {
this.nextCurrentUserRefresh = 300; // 5min
API.getCurrentUser();
}
if (--this.nextFriendsRefresh <= 0) {
this.nextFriendsRefresh = 3600; // 1hour
this.refreshFriendsList();
this.updateStoredUser(API.currentUser);
if (this.isGameRunning) {
API.refreshPlayerModerations();
}
}
if (--this.nextGroupInstanceRefresh <= 0) {
if (this.friendLogInitStatus) {
this.nextGroupInstanceRefresh = 300; // 5min
groupRequest.getUsersGroupInstances();
}
AppApi.CheckGameRunning();
}
if (--this.nextAppUpdateCheck <= 0) {
this.nextAppUpdateCheck = 3600; // 1hour
if (this.autoUpdateVRCX !== 'Off') {
this.checkForVRCXUpdate();
}
}
if (--this.ipcTimeout <= 0) {
this.ipcEnabled = false;
}
if (
--this.nextClearVRCXCacheCheck <= 0 &&
this.clearVRCXCacheFrequency > 0
) {
this.nextClearVRCXCacheCheck =
this.clearVRCXCacheFrequency / 2;
this.clearVRCXCache();
}
if (--this.nextDiscordUpdate <= 0) {
this.nextDiscordUpdate = 3;
if (this.discordActive) {
this.updateDiscord();
}
}
if (--this.nextAutoStateChange <= 0) {
this.nextAutoStateChange = 3;
this.updateAutoStateChange();
}
if (
(this.isRunningUnderWine || LINUX) &&
--this.nextGetLogCheck <= 0
) {
this.nextGetLogCheck = 0.5;
const logLines = await LogWatcher.GetLogLines();
if (logLines) {
logLines.forEach((logLine) => {
$app.addGameLogEvent(logLine);
});
}
}
if (
(this.isRunningUnderWine || LINUX) &&
--this.nextGameRunningCheck <= 0
) {
if (LINUX) {
this.nextGameRunningCheck = 1;
$app.updateIsGameRunning(
await AppApi.IsGameRunning(),
await AppApi.IsSteamVRRunning(),
false
);
} else {
this.nextGameRunningCheck = 3;
AppApi.CheckGameRunning();
}
}
if (--this.nextDatabaseOptimize <= 0) {
this.nextDatabaseOptimize = 86400; // 1 day
database.optimize();
}
}
} catch (err) {
API.isRefreshFriendsLoading = false;
console.error(err);
}
workerTimers.setTimeout(() => this.updateLoop(), 1000);
}
};
}

View File

@@ -1,118 +0,0 @@
import { userRequest } from '../api';
import database from '../service/database.js';
import utils from '../classes/utils';
import * as workerTimers from 'worker-timers';
const userNotes = {
lastNoteCheck: null,
lastDbNoteDate: null,
notes: new Map(),
async init() {
this.lastNoteCheck = new Date();
this.lastDbNoteDate = null;
this.notes.clear();
try {
// todo: get users from store
const users = window.API.cachedUsers;
const dbNotes = await database.getAllUserNotes();
for (const note of dbNotes) {
this.notes.set(note.userId, note.note);
const user = users.get(note.userId);
if (user) {
user.note = note.note;
}
if (
!this.lastDbNoteDate ||
this.lastDbNoteDate < note.createdAt
) {
this.lastDbNoteDate = note.createdAt;
}
}
await this.getLatestUserNotes();
} catch (error) {
console.error('Error initializing user notes:', error);
}
},
async getLatestUserNotes() {
this.lastNoteCheck = new Date();
const params = {
offset: 0,
n: 10 // start light
};
const newNotes = new Map();
let done = false;
try {
for (let i = 0; i < 100; i++) {
params.offset = i * params.n;
const args = await userRequest.getUserNotes(params);
for (const note of args.json) {
if (
this.lastDbNoteDate &&
this.lastDbNoteDate > note.createdAt
) {
done = true;
}
if (
!this.lastDbNoteDate ||
this.lastDbNoteDate < note.createdAt
) {
this.lastDbNoteDate = note.createdAt;
}
note.note = utils.replaceBioSymbols(note.note);
newNotes.set(note.targetUserId, note);
}
if (done || args.json.length === 0) {
break;
}
params.n = 100; // crank it after first run
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 1000);
});
}
} catch (error) {
console.error('Error fetching user notes:', error);
}
// todo: get users from store
const users = window.API.cachedUsers;
for (const note of newNotes.values()) {
const newNote = {
userId: note.targetUserId,
displayName: note.targetUser?.displayName || note.targetUserId,
note: note.note,
createdAt: note.createdAt
};
await database.addUserNote(newNote);
this.notes.set(note.targetUserId, note.note);
const user = users.get(note.targetUserId);
if (user) {
user.note = note.note;
}
}
},
async checkNote(userId, newNote) {
// last check was more than than 5 minutes ago
if (
!this.lastNoteCheck ||
this.lastNoteCheck.getTime() + 5 * 60 * 1000 > Date.now()
) {
return;
}
const existingNote = this.notes.get(userId);
if (typeof existingNote !== 'undefined' && !newNote) {
console.log('deleting note', userId);
this.notes.delete(userId);
await database.deleteUserNote(userId);
return;
}
if (typeof existingNote === 'undefined' || existingNote !== newNote) {
console.log('detected note change', userId, newNote);
await this.getLatestUserNotes();
}
}
};
export { userNotes };

View File

@@ -1,302 +0,0 @@
let echarts = null;
const _utils = {
removeFromArray(array, item) {
var { length } = array;
for (var i = 0; i < length; ++i) {
if (array[i] === item) {
array.splice(i, 1);
return true;
}
}
return false;
},
arraysMatch(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) {
return false;
}
return (
a.length === b.length &&
a.every(
(element, index) =>
JSON.stringify(element) === JSON.stringify(b[index])
)
);
},
moveArrayItem(array, fromIndex, toIndex) {
if (!Array.isArray(array) || fromIndex === toIndex) {
return;
}
if (fromIndex < 0 || fromIndex >= array.length) {
return;
}
if (toIndex < 0 || toIndex >= array.length) {
return;
}
const item = array[fromIndex];
array.splice(fromIndex, 1);
array.splice(toIndex, 0, item);
},
escapeTag(tag) {
var s = String(tag);
return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
},
escapeTagRecursive(obj) {
if (typeof obj === 'string') {
return this.escapeTag(obj);
}
if (typeof obj === 'object') {
for (var key in obj) {
obj[key] = this.escapeTagRecursive(obj[key]);
}
}
return obj;
},
timeToText(sec, isNeedSeconds = false) {
let n = Number(sec);
if (isNaN(n)) {
return this.escapeTag(sec);
}
n = Math.floor(n / 1000);
const arr = [];
if (n < 0) {
n = -n;
}
if (n >= 86400) {
arr.push(`${Math.floor(n / 86400)}d`);
n %= 86400;
}
if (n >= 3600) {
arr.push(`${Math.floor(n / 3600)}h`);
n %= 3600;
}
if (n >= 60) {
arr.push(`${Math.floor(n / 60)}m`);
n %= 60;
}
if (isNeedSeconds || (arr.length === 0 && n < 60)) {
arr.push(`${n}s`);
}
return arr.join(' ');
},
textToHex(text) {
var s = String(text);
return s
.split('')
.map((c) => c.charCodeAt(0).toString(16))
.join(' ');
},
commaNumber(num) {
if (!num) {
return '0';
}
var s = String(Number(num));
return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
},
buildTreeData(json) {
var node = [];
for (var key in json) {
if (key[0] === '$') {
continue;
}
var value = json[key];
if (Array.isArray(value) && value.length === 0) {
node.push({
key,
value: '[]'
});
} else if (
value === Object(value) &&
Object.keys(value).length === 0
) {
node.push({
key,
value: '{}'
});
} else if (Array.isArray(value)) {
node.push({
children: value.map((val, idx) => {
if (val === Object(val)) {
return {
children: this.buildTreeData(val),
key: idx
};
}
return {
key: idx,
value: val
};
}),
key
});
} else if (value === Object(value)) {
node.push({
children: this.buildTreeData(value),
key
});
} else {
node.push({
key,
value: String(value)
});
}
}
node.sort(function (a, b) {
var A = String(a.key).toUpperCase();
var B = String(b.key).toUpperCase();
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
});
return node;
},
// descending
compareByCreatedAt(a, b) {
if (
typeof a.created_at !== 'string' ||
typeof b.created_at !== 'string'
) {
return 0;
}
var A = a.created_at.toUpperCase();
var B = b.created_at.toUpperCase();
if (A < B) {
return 1;
}
if (A > B) {
return -1;
}
return 0;
},
// lazy load echarts
loadEcharts() {
if (echarts) {
return Promise.resolve(echarts);
}
return import('echarts').then((module) => {
echarts = module;
return echarts;
});
},
// CJK character in Japanese, Korean, Chinese are different
// so change font-family order when users change language to display CJK character correctly
changeCJKorder(lang) {
const otherFonts = window
.getComputedStyle(document.body)
.fontFamily.split(',')
.filter((item) => !item.includes('Noto Sans'))
.join(', ');
const notoSans = 'Noto Sans';
const fontFamilies = {
ja_JP: ['JP', 'KR', 'TC', 'SC'],
ko: ['KR', 'JP', 'TC', 'SC'],
zh_TW: ['TC', 'JP', 'KR', 'SC'],
zh_CN: ['SC', 'JP', 'KR', 'TC']
};
if (fontFamilies[lang]) {
const CJKFamily = fontFamilies[lang]
.map((item) => `${notoSans} ${item}`)
.join(', ');
document.body.style.fontFamily = `${CJKFamily}, ${otherFonts}`;
}
},
localeIncludes(str, search, comparer) {
// These checks are stolen from https://stackoverflow.com/a/69623589/11030436
if (search === '') {
return true;
} else if (!str || !search) {
return false;
}
const strObj = String(str);
const searchObj = String(search);
if (strObj.length === 0) {
return false;
}
if (searchObj.length > strObj.length) {
return false;
}
// Now simply loop through each substring and compare them
for (let i = 0; i < str.length - searchObj.length + 1; i++) {
const substr = strObj.substring(i, i + searchObj.length);
if (comparer.compare(substr, searchObj) === 0) {
return true;
}
}
return false;
},
compareByName(a, b) {
if (typeof a.name !== 'string' || typeof b.name !== 'string') {
return 0;
}
return a.name.localeCompare(b.name);
},
replaceBioSymbols(text) {
if (!text) {
return '';
}
var symbolList = {
'@': '',
'#': '',
$: '',
'%': '',
'&': '',
'=': '',
'+': '',
'/': '',
'\\': '',
';': ';',
':': '˸',
',': '',
'?': '',
'!': 'ǃ',
'"': '',
'<': '≺',
'>': '≻',
'.': '',
'^': '',
'{': '',
'}': '',
'[': '',
']': '',
'(': '',
')': '',
'|': '',
'*': ''
};
var newText = text;
for (var key in symbolList) {
var regex = new RegExp(symbolList[key], 'g');
newText = newText.replace(regex, key);
}
return newText.replace(/ {1,}/g, ' ').trimRight();
},
// descending
compareByUpdatedAt(a, b) {
if (
typeof a.updated_at !== 'string' ||
typeof b.updated_at !== 'string'
) {
return 0;
}
var A = a.updated_at.toUpperCase();
var B = b.updated_at.toUpperCase();
if (A < B) {
return 1;
}
if (A > B) {
return -1;
}
return 0;
}
};
export default _utils;

View File

@@ -1,121 +0,0 @@
import configRepository from '../service/config.js';
import { baseClass, $t, $utils } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {}
_data = {};
_methods = {
async backupVrcRegistry(name) {
var regJson;
if (LINUX) {
regJson = await AppApi.GetVRChatRegistryJson();
regJson = JSON.parse(regJson);
} else {
regJson = await AppApi.GetVRChatRegistry();
}
var newBackup = {
name,
date: new Date().toJSON(),
data: regJson
};
var backupsJson = await configRepository.getString(
'VRCX_VRChatRegistryBackups'
);
if (!backupsJson) {
backupsJson = JSON.stringify([]);
}
var backups = JSON.parse(backupsJson);
backups.push(newBackup);
await configRepository.setString(
'VRCX_VRChatRegistryBackups',
JSON.stringify(backups)
);
// await this.updateRegistryBackupDialog();
},
// Because it is a startup func, it is not integrated into RegistryBackupDialog.vue now
// func backupVrcRegistry is also split up
async checkAutoBackupRestoreVrcRegistry() {
if (!this.vrcRegistryAutoBackup) {
return;
}
// check for auto restore
var hasVRChatRegistryFolder =
await AppApi.HasVRChatRegistryFolder();
if (!hasVRChatRegistryFolder) {
var lastBackupDate = await configRepository.getString(
'VRCX_VRChatRegistryLastBackupDate'
);
var lastRestoreCheck = await configRepository.getString(
'VRCX_VRChatRegistryLastRestoreCheck'
);
if (
!lastBackupDate ||
(lastRestoreCheck &&
lastBackupDate &&
lastRestoreCheck === lastBackupDate)
) {
// only ask to restore once and when backup is present
return;
}
// popup message about auto restore
this.$alert(
$t('dialog.registry_backup.restore_prompt'),
$t('dialog.registry_backup.header')
);
this.showRegistryBackupDialog();
await AppApi.FocusWindow();
await configRepository.setString(
'VRCX_VRChatRegistryLastRestoreCheck',
lastBackupDate
);
} else {
await this.autoBackupVrcRegistry();
}
},
async autoBackupVrcRegistry() {
var date = new Date();
var lastBackupDate = await configRepository.getString(
'VRCX_VRChatRegistryLastBackupDate'
);
if (lastBackupDate) {
var lastBackup = new Date(lastBackupDate);
var diff = date.getTime() - lastBackup.getTime();
var diffDays = Math.floor(diff / (1000 * 60 * 60 * 24));
if (diffDays < 7) {
return;
}
}
var backupsJson = await configRepository.getString(
'VRCX_VRChatRegistryBackups'
);
if (!backupsJson) {
backupsJson = JSON.stringify([]);
}
var backups = JSON.parse(backupsJson);
backups.forEach((backup) => {
if (backup.name === 'Auto Backup') {
// remove old auto backup
$utils.removeFromArray(backups, backup);
}
});
await configRepository.setString(
'VRCX_VRChatRegistryBackups',
JSON.stringify(backups)
);
this.backupVrcRegistry('Auto Backup');
await configRepository.setString(
'VRCX_VRChatRegistryLastBackupDate',
date.toJSON()
);
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,351 +0,0 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import * as workerTimers from 'worker-timers';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
_data = {
VRCXUpdateDialog: {
visible: false,
updatePending: false,
updatePendingIsLatest: false,
release: '',
releases: [],
json: {}
},
branch: 'Stable',
autoUpdateVRCX: 'Auto Download',
checkingForVRCXUpdate: false,
pendingVRCXInstall: '',
pendingVRCXUpdate: false,
branches: {
Stable: {
name: 'Stable',
urlReleases: 'https://api0.vrcx.app/releases/stable',
urlLatest: 'https://api0.vrcx.app/releases/stable/latest'
},
Nightly: {
name: 'Nightly',
urlReleases: 'https://api0.vrcx.app/releases/nightly',
urlLatest: 'https://api0.vrcx.app/releases/nightly/latest'
}
// LinuxTest: {
// name: 'LinuxTest',
// urlReleases: 'https://api.github.com/repos/rs189/VRCX/releases',
// urlLatest:
// 'https://api.github.com/repos/rs189/VRCX/releases/latest'
// }
},
updateProgress: 0,
updateInProgress: false
};
_methods = {
async showVRCXUpdateDialog() {
var D = this.VRCXUpdateDialog;
D.visible = true;
D.updatePendingIsLatest = false;
D.updatePending = await AppApi.CheckForUpdateExe();
this.loadBranchVersions();
},
async downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
) {
if (this.updateInProgress) {
return;
}
try {
this.updateInProgress = true;
this.downloadFileProgress();
await AppApi.DownloadUpdate(
downloadUrl,
downloadName,
hashUrl,
size
);
this.pendingVRCXInstall = releaseName;
} catch (err) {
console.error(err);
this.$message({
message: `${$t('message.vrcx_updater.failed_install')} ${err}`,
type: 'error'
});
} finally {
this.updateInProgress = false;
this.updateProgress = 0;
}
},
async cancelUpdate() {
await AppApi.CancelUpdate();
this.updateInProgress = false;
this.updateProgress = 0;
},
async downloadFileProgress() {
this.updateProgress = await AppApi.CheckUpdateProgress();
if (this.updateInProgress) {
workerTimers.setTimeout(() => this.downloadFileProgress(), 150);
}
},
updateProgressText() {
if (this.updateProgress === 100) {
return $t('message.vrcx_updater.checking_hash');
}
return `${this.updateProgress}%`;
},
installVRCXUpdate() {
for (var release of this.VRCXUpdateDialog.releases) {
if (release.name !== this.VRCXUpdateDialog.release) {
continue;
}
var downloadUrl = '';
var downloadName = '';
var hashUrl = '';
var size = 0;
for (var asset of release.assets) {
if (asset.state !== 'uploaded') {
continue;
}
if (
WINDOWS &&
(asset.content_type === 'application/x-msdownload' ||
asset.content_type ===
'application/x-msdos-program')
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
LINUX &&
asset.content_type === 'application/octet-stream'
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
}
if (!downloadUrl) {
return;
}
var releaseName = release.name;
var type = 'Manual';
this.downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
);
break;
}
},
async loadBranchVersions() {
var D = this.VRCXUpdateDialog;
var url = this.branches[this.branch].urlReleases;
this.checkingForVRCXUpdate = true;
try {
var response = await webApiService.execute({
url,
method: 'GET',
headers: {
'VRCX-ID': this.vrcxId
}
});
} finally {
this.checkingForVRCXUpdate = false;
}
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
}
var releases = [];
if (typeof json !== 'object' || json.message) {
$app.$message({
message: $t('message.vrcx_updater.failed', {
message: json.message
}),
type: 'error'
});
return;
}
for (var release of json) {
for (var asset of release.assets) {
if (
(asset.content_type === 'application/x-msdownload' ||
asset.content_type ===
'application/x-msdos-program') &&
asset.state === 'uploaded'
) {
releases.push(release);
}
}
}
D.releases = releases;
D.release = json[0].name;
this.VRCXUpdateDialog.updatePendingIsLatest = false;
if (D.release === this.pendingVRCXInstall) {
// update already downloaded and latest version
this.VRCXUpdateDialog.updatePendingIsLatest = true;
}
if (
(await configRepository.getString('VRCX_branch')) !==
this.branch
) {
await configRepository.setString('VRCX_branch', this.branch);
}
},
async checkForVRCXUpdate() {
var currentVersion = this.appVersion.replace(' (Linux)', '');
if (
!currentVersion ||
currentVersion === 'VRCX Nightly Build' ||
currentVersion === 'VRCX Build'
) {
// ignore custom builds
return;
}
if (this.branch === 'Beta') {
// move Beta users to stable
this.branch = 'Stable';
await configRepository.setString('VRCX_branch', this.branch);
}
if (typeof this.branches[this.branch] === 'undefined') {
// handle invalid branch
this.branch = 'Stable';
await configRepository.setString('VRCX_branch', this.branch);
}
var url = this.branches[this.branch].urlLatest;
this.checkingForVRCXUpdate = true;
try {
var response = await webApiService.execute({
url,
method: 'GET',
headers: {
'VRCX-ID': this.vrcxId
}
});
} finally {
this.checkingForVRCXUpdate = false;
}
this.pendingVRCXUpdate = false;
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
}
if (json === Object(json) && json.name && json.published_at) {
this.VRCXUpdateDialog.updateJson = json;
this.changeLogDialog.buildName = json.name;
this.changeLogDialog.changeLog = this.changeLogRemoveLinks(
json.body
);
var releaseName = json.name;
this.latestAppVersion = releaseName;
this.VRCXUpdateDialog.updatePendingIsLatest = false;
if (releaseName === this.pendingVRCXInstall) {
// update already downloaded
this.VRCXUpdateDialog.updatePendingIsLatest = true;
} else if (releaseName > currentVersion) {
var downloadUrl = '';
var downloadName = '';
var hashUrl = '';
var size = 0;
for (var asset of json.assets) {
if (asset.state !== 'uploaded') {
continue;
}
if (
!LINUX &&
(asset.content_type ===
'application/x-msdownload' ||
asset.content_type ===
'application/x-msdos-program')
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
LINUX &&
asset.content_type === 'application/octet-stream'
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
}
if (!downloadUrl) {
return;
}
this.pendingVRCXUpdate = true;
this.notifyMenu('settings');
var type = 'Auto';
if (!API.isLoggedIn) {
this.showVRCXUpdateDialog();
} else if (this.autoUpdateVRCX === 'Notify') {
// this.showVRCXUpdateDialog();
} else if (this.autoUpdateVRCX === 'Auto Download') {
this.downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
);
}
}
}
},
restartVRCX(isUpgrade) {
if (!LINUX) {
AppApi.RestartApplication(isUpgrade);
} else {
window.electron.restartApp();
}
},
async saveAutoUpdateVRCX() {
if (this.autoUpdateVRCX === 'Off') {
this.pendingVRCXUpdate = false;
}
await configRepository.setString(
'VRCX_autoUpdateVRCX',
this.autoUpdateVRCX
);
}
};
}

View File

@@ -1,620 +0,0 @@
import * as workerTimers from 'worker-timers';
import Noty from 'noty';
import { parseLocation } from '../composables/instance/utils';
import { baseClass, $app, API, $utils } from './baseClass.js';
import { groupRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
API.webSocket = null;
API.lastWebSocketMessage = '';
API.$on('USER:CURRENT', function () {
if ($app.friendLogInitStatus && this.webSocket === null) {
this.getAuth();
}
});
API.getAuth = function () {
return this.call('auth', {
method: 'GET'
}).then((json) => {
var args = {
json
};
this.$emit('AUTH', args);
return args;
});
};
API.$on('AUTH', function (args) {
if (args.json.ok) {
this.connectWebSocket(args.json.token);
}
});
API.connectWebSocket = function (token) {
if (this.webSocket !== null) {
return;
}
var socket = new WebSocket(`${API.websocketDomain}/?auth=${token}`);
socket.onopen = () => {
if ($app.debugWebSocket) {
console.log('WebSocket connected');
}
};
socket.onclose = () => {
if (this.webSocket === socket) {
this.webSocket = null;
}
try {
socket.close();
} catch (err) {}
if ($app.debugWebSocket) {
console.log('WebSocket closed');
}
workerTimers.setTimeout(() => {
if (
this.isLoggedIn &&
$app.friendLogInitStatus &&
this.webSocket === null
) {
this.getAuth();
}
}, 5000);
};
socket.onerror = () => {
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'error',
text: 'WebSocket Error'
}).show();
socket.onclose();
};
socket.onmessage = ({ data }) => {
try {
if (this.lastWebSocketMessage === data) {
// pls no spam
return;
}
this.lastWebSocketMessage = data;
var json = JSON.parse(data);
try {
json.content = JSON.parse(json.content);
} catch (err) {}
this.$emit('PIPELINE', {
json
});
if ($app.debugWebSocket && json.content) {
var displayName = '';
var user = this.cachedUsers.get(json.content.userId);
if (user) {
displayName = user.displayName;
}
console.log(
'WebSocket',
json.type,
displayName,
json.content
);
}
} catch (err) {
console.error(err);
}
};
this.webSocket = socket;
};
API.$on('LOGOUT', function () {
this.closeWebSocket();
});
API.closeWebSocket = function () {
var socket = this.webSocket;
if (socket === null) {
return;
}
this.webSocket = null;
try {
socket.close();
} catch (err) {}
};
API.reconnectWebSocket = function () {
if (!this.isLoggedIn || !$app.friendLogInitStatus) {
return;
}
this.closeWebSocket();
this.getAuth();
};
API.$on('PIPELINE', function (args) {
var { type, content, err } = args.json;
if (typeof err !== 'undefined') {
console.error('PIPELINE: error', args);
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'error',
text: $app.escapeTag(`WebSocket Error: ${err}`)
}).show();
return;
}
if (typeof content === 'undefined') {
console.error('PIPELINE: missing content', args);
return;
}
if (typeof content.user !== 'undefined') {
// I forgot about this...
delete content.user.state;
}
switch (type) {
case 'notification':
this.$emit('NOTIFICATION', {
json: content,
params: {
notificationId: content.id
}
});
this.$emit('PIPELINE:NOTIFICATION', {
json: content,
params: {
notificationId: content.id
}
});
break;
case 'notification-v2':
console.log('notification-v2', content);
this.$emit('NOTIFICATION:V2', {
json: content,
params: {
notificationId: content.id
}
});
break;
case 'notification-v2-delete':
console.log('notification-v2-delete', content);
for (var id of content.ids) {
this.$emit('NOTIFICATION:HIDE', {
params: {
notificationId: id
}
});
this.$emit('NOTIFICATION:SEE', {
params: {
notificationId: id
}
});
}
break;
case 'notification-v2-update':
console.log('notification-v2-update', content);
this.$emit('NOTIFICATION:V2:UPDATE', {
json: content.updates,
params: {
notificationId: content.id
}
});
break;
case 'see-notification':
this.$emit('NOTIFICATION:SEE', {
params: {
notificationId: content
}
});
break;
case 'hide-notification':
this.$emit('NOTIFICATION:HIDE', {
params: {
notificationId: content
}
});
this.$emit('NOTIFICATION:SEE', {
params: {
notificationId: content
}
});
break;
case 'response-notification':
this.$emit('NOTIFICATION:HIDE', {
params: {
notificationId: content.notificationId
}
});
this.$emit('NOTIFICATION:SEE', {
params: {
notificationId: content.notificationId
}
});
break;
case 'friend-add':
this.$emit('USER', {
json: content.user,
params: {
userId: content.userId
}
});
this.$emit('FRIEND:ADD', {
params: {
userId: content.userId
}
});
break;
case 'friend-delete':
this.$emit('FRIEND:DELETE', {
params: {
userId: content.userId
}
});
break;
case 'friend-online':
// Where is instanceId, travelingToWorld, travelingToInstance?
// More JANK, what a mess
var $location = parseLocation(content.location);
var $travelingToLocation = parseLocation(
content.travelingToLocation
);
if (content?.user?.id) {
this.$emit('USER', {
json: {
id: content.userId,
platform: content.platform,
state: 'online',
location: content.location,
worldId: content.worldId,
instanceId: $location.instanceId,
travelingToLocation:
content.travelingToLocation,
travelingToWorld: $travelingToLocation.worldId,
travelingToInstance:
$travelingToLocation.instanceId,
...content.user
},
params: {
userId: content.userId
}
});
} else {
this.$emit('FRIEND:STATE', {
json: {
state: 'online'
},
params: {
userId: content.userId
}
});
}
break;
case 'friend-active':
if (content?.user?.id) {
this.$emit('USER', {
json: {
id: content.userId,
platform: content.platform,
state: 'active',
location: 'offline',
worldId: 'offline',
instanceId: 'offline',
travelingToLocation: 'offline',
travelingToWorld: 'offline',
travelingToInstance: 'offline',
...content.user
},
params: {
userId: content.userId
}
});
} else {
this.$emit('FRIEND:STATE', {
json: {
state: 'active'
},
params: {
userId: content.userId
}
});
}
break;
case 'friend-offline':
// more JANK, hell yeah
this.$emit('USER', {
json: {
id: content.userId,
platform: content.platform,
state: 'offline',
location: 'offline',
worldId: 'offline',
instanceId: 'offline',
travelingToLocation: 'offline',
travelingToWorld: 'offline',
travelingToInstance: 'offline'
},
params: {
userId: content.userId
}
});
break;
case 'friend-update':
this.$emit('USER', {
json: content.user,
params: {
userId: content.userId
}
});
break;
case 'friend-location':
var $location = parseLocation(content.location);
var $travelingToLocation = parseLocation(
content.travelingToLocation
);
if (!content?.user?.id) {
var ref = this.cachedUsers.get(content.userId);
if (typeof ref !== 'undefined') {
this.$emit('USER', {
json: {
...ref,
location: content.location,
worldId: content.worldId,
instanceId: $location.instanceId,
travelingToLocation:
content.travelingToLocation,
travelingToWorld:
$travelingToLocation.worldId,
travelingToInstance:
$travelingToLocation.instanceId
},
params: {
userId: content.userId
}
});
}
break;
}
this.$emit('USER', {
json: {
location: content.location,
worldId: content.worldId,
instanceId: $location.instanceId,
travelingToLocation: content.travelingToLocation,
travelingToWorld: $travelingToLocation.worldId,
travelingToInstance:
$travelingToLocation.instanceId,
...content.user,
state: 'online' // JANK
},
params: {
userId: content.userId
}
});
break;
case 'user-update':
this.$emit('USER:CURRENT', {
json: content.user,
params: {
userId: content.userId
}
});
break;
case 'user-location':
// update current user location
if (content.userId !== this.currentUser.id) {
console.error('user-location wrong userId', content);
break;
}
// content.user: {} // we don't trust this
// content.world: {} // this is long gone
// content.worldId // where did worldId go?
// content.instance // without worldId, this is useless
$app.setCurrentUserLocation(
content.location,
content.travelingToLocation
);
break;
case 'group-joined':
// var groupId = content.groupId;
// $app.onGroupJoined(groupId);
break;
case 'group-left':
// var groupId = content.groupId;
// $app.onGroupLeft(groupId);
break;
case 'group-role-updated':
var groupId = content.role.groupId;
groupRequest.getGroup({ groupId, includeRoles: true });
console.log('group-role-updated', content);
// content {
// role: {
// createdAt: string,
// description: string,
// groupId: string,
// id: string,
// isManagementRole: boolean,
// isSelfAssignable: boolean,
// name: string,
// order: number,
// permissions: string[],
// requiresPurchase: boolean,
// requiresTwoFactor: boolean
break;
case 'group-member-updated':
var member = content.member;
if (!member) {
console.error(
'group-member-updated missing member',
content
);
break;
}
var groupId = member.groupId;
if (
$app.groupDialog.visible &&
$app.groupDialog.id === groupId
) {
$app.getGroupDialogGroup(groupId);
}
this.$emit('GROUP:MEMBER', {
json: member,
params: {
groupId
}
});
console.log('group-member-updated', member);
break;
case 'instance-queue-joined':
case 'instance-queue-position':
var instanceId = content.instanceLocation;
var position = content.position ?? 0;
var queueSize = content.queueSize ?? 0;
$app.instanceQueueUpdate(instanceId, position, queueSize);
break;
case 'instance-queue-ready':
var instanceId = content.instanceLocation;
// var expiry = Date.parse(content.expiry);
$app.instanceQueueReady(instanceId);
break;
case 'instance-queue-left':
var instanceId = content.instanceLocation;
$app.removeQueuedInstance(instanceId);
// $app.instanceQueueClear();
break;
case 'content-refresh':
var contentType = content.contentType;
console.log('content-refresh', content);
if (contentType === 'icon') {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogIconsLoading
) {
$app.refreshVRCPlusIconsTable();
}
} else if (contentType === 'gallery') {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogGalleryLoading
) {
$app.refreshGalleryTable();
}
} else if (contentType === 'emoji') {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogEmojisLoading
) {
$app.refreshEmojiTable();
}
} else if (contentType === 'sticker') {
// on sticker upload
} else if (contentType === 'print') {
if (
$app.autoDeleteOldPrints &&
content.actionType === 'created'
) {
$app.tryDeleteOldPrints();
} else if (
$app.galleryDialogVisible &&
!$app.galleryDialogPrintsLoading
) {
$app.refreshPrintTable();
}
} else if (contentType === 'prints') {
// lol
} else if (contentType === 'avatar') {
// hmm, utilizing this might be too spamy and cause UI to move around
} else if (contentType === 'world') {
// hmm
} else if (contentType === 'created') {
// on avatar upload, might be gone now
} else if (contentType === 'avatargallery') {
// on avatar gallery image upload
} else if (contentType === 'invitePhoto') {
// on uploading invite photo
} else if (contentType === 'inventory') {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogInventoryLoading
) {
$app.getInventory();
}
// on consuming a bundle
// {contentType: 'inventory', itemId: 'inv_', itemType: 'prop', actionType: 'add'}
} else if (!contentType) {
console.log(
'content-refresh without contentType',
content
);
} else {
console.log(
'Unknown content-refresh type',
content.contentType
);
}
break;
case 'instance-closed':
// TODO: get worldName, groupName, hardClose
var noty = {
type: 'instance.closed',
location: content.instanceLocation,
message: 'Instance Closed',
created_at: new Date().toJSON()
};
if (
$app.notificationTable.filters[0].value.length === 0 ||
$app.notificationTable.filters[0].value.includes(
noty.type
)
) {
$app.notifyMenu('notification');
}
$app.queueNotificationNoty(noty);
$app.notificationTable.data.push(noty);
$app.updateSharedFeed(true);
break;
default:
console.log('Unknown pipeline type', args.json);
}
});
}
_data = {};
_methods = {};
}

View File

@@ -0,0 +1,73 @@
<template>
<div @click="confirm" class="avatar-info">
<span style="margin-right: 5px">{{ avatarName }}</span>
<span v-if="avatarType" :class="color" style="margin-right: 5px">{{ avatarType }}</span>
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useAvatarStore } from '../stores';
const avatarStore = useAvatarStore();
const props = defineProps({
imageurl: String,
userid: String,
hintownerid: String,
hintavatarname: String,
avatartags: Array
});
const avatarName = ref('');
const avatarType = ref('');
const avatarTags = ref('');
const color = ref('');
let ownerId = '';
const parse = async () => {
ownerId = '';
avatarName.value = '';
avatarType.value = '';
color.value = '';
avatarTags.value = '';
if (!props.imageurl) {
avatarName.value = '-';
} else if (props.hintownerid) {
avatarName.value = props.hintavatarname;
ownerId = props.hintownerid;
} else {
try {
const info = await avatarStore.getAvatarName(props.imageurl);
avatarName.value = info.avatarName;
ownerId = info.ownerId;
} catch {
console.error('Failed to fetch avatar name');
}
}
if (typeof props.userid === 'undefined' || !ownerId) {
color.value = '';
avatarType.value = '';
} else if (ownerId === props.userid) {
color.value = 'avatar-info-own';
avatarType.value = '(own)';
} else {
color.value = 'avatar-info-public';
avatarType.value = '(public)';
}
if (Array.isArray(props.avatartags)) {
avatarTags.value = props.avatartags.map((tag) => tag.replace('content_', '')).join(', ');
}
};
const confirm = () => {
if (!props.imageurl) return;
avatarStore.showAvatarAuthorDialog(props.userid, ownerId, props.imageurl);
};
watch([() => props.imageurl, () => props.userid, () => props.avatartags], parse, { immediate: true });
</script>

View File

@@ -0,0 +1,33 @@
<template>
<span>{{ text }}</span>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import * as workerTimers from 'worker-timers';
import { timeToText } from '../shared/utils';
const props = defineProps({
datetime: { type: String, default: '' },
hours: { type: Number, default: 1 }
});
const text = ref('');
function update() {
const epoch = new Date(props.datetime).getTime() + 1000 * 60 * 60 * props.hours - Date.now();
text.value = epoch >= 0 ? timeToText(epoch) : '-';
}
watch(() => props.datetime, update);
onMounted(() => {
update();
});
const timer = workerTimers.setInterval(update, 5000);
onBeforeUnmount(() => {
workerTimers.clearInterval(timer);
});
</script>

View File

@@ -0,0 +1,41 @@
<template>
<span @click="showUserDialog" class="x-link">{{ username }}</span>
</template>
<script setup>
import { ref, watch } from 'vue';
import { userRequest } from '../api';
import { useUserStore } from '../stores';
const userStore = useUserStore();
const props = defineProps({
userid: String,
location: String,
forceUpdateKey: Number,
hint: {
type: String,
default: ''
}
});
const username = ref(props.userid);
async function parse() {
username.value = props.userid;
if (props.hint) {
username.value = props.hint;
} else if (props.userid) {
const args = await userRequest.getCachedUser({ userId: props.userid });
if (args?.json?.displayName) {
username.value = args.json.displayName;
}
}
}
function showUserDialog() {
userStore.showUserDialog(props.userid);
}
watch([() => props.userid, () => props.location, () => props.forceUpdateKey], parse, { immediate: true });
</script>

View File

@@ -21,14 +21,12 @@
</span>
<template v-else-if="isGroupByInstance">
<i v-if="isFriendTraveling" class="el-icon el-icon-loading"></i>
<timer
<Timer
class="extra"
:epoch="epoch"
:style="
isFriendTraveling ? { display: 'inline-block', overflow: 'unset' } : undefined
"></timer>
:style="isFriendTraveling ? { display: 'inline-block', overflow: 'unset' } : undefined" />
</template>
<location
<Location
v-else
class="extra"
:location="friend.ref.location"
@@ -37,7 +35,7 @@
</template>
</div>
</template>
<template v-else-if="!friend.ref && !API.isRefreshFriendsLoading">
<template v-else-if="!friend.ref && !isRefreshFriendsLoading">
<span>{{ friend.name || friend.id }}</span>
<el-button
ttype="text"
@@ -62,38 +60,25 @@
</div>
</template>
<script>
import Location from './Location.vue';
<script setup>
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { userImage, userStatusClass } from '../shared/utils';
import { useAppearanceSettingsStore, useFriendStore } from '../stores';
export default {
name: 'FriendItem',
components: {
Location
},
inject: ['API', 'userImage', 'userStatusClass'],
props: {
friend: {
type: Object,
required: true
},
hideNicknames: {
type: Boolean,
default: false
},
isGroupByInstance: Boolean
},
computed: {
isFriendTraveling() {
return this.friend.ref.location === 'traveling';
},
isFriendActiveOrOffline() {
return this.friend.state === 'active' || this.friend.state === 'offline';
},
epoch() {
return this.isFriendTraveling ? this.friend.ref.$travelingToTime : this.friend.ref.$location_at;
}
}
};
const props = defineProps({
friend: { type: Object, required: true },
isGroupByInstance: Boolean
});
const { hideNicknames } = storeToRefs(useAppearanceSettingsStore());
const { isRefreshFriendsLoading } = storeToRefs(useFriendStore());
const isFriendTraveling = computed(() => props.friend.ref.location === 'traveling');
const isFriendActiveOrOffline = computed(() => props.friend.state === 'active' || props.friend.state === 'offline');
const epoch = computed(() =>
isFriendTraveling.value ? props.friend.ref.$travelingToTime : props.friend.ref.$location_at
);
</script>
<style scoped>

View File

@@ -0,0 +1,170 @@
<template>
<div style="display: inline-block; margin-left: 5px">
<el-tooltip v-if="state.isValidInstance" placement="bottom">
<template #content>
<div>
<span v-if="state.isClosed">Closed At: {{ formatDateFilter(state.closedAt, 'long') }}<br /></span>
<template v-if="state.canCloseInstance">
<el-button
:disabled="state.isClosed"
size="mini"
type="primary"
@click="closeInstance(props.location)">
{{ t('dialog.user.info.close_instance') }} </el-button
><br /><br />
</template>
<span><span style="color: #409eff">PC: </span>{{ state.platforms.standalonewindows }}</span
><br />
<span><span style="color: #67c23a">Android: </span>{{ state.platforms.android }}</span
><br />
<span>{{ t('dialog.user.info.instance_game_version') }} {{ state.gameServerVersion }}</span
><br />
<span v-if="state.queueEnabled">{{ t('dialog.user.info.instance_queuing_enabled') }}<br /></span>
<span v-if="state.disabledContentSettings"
>{{ t('dialog.user.info.instance_disabled_content') }} {{ state.disabledContentSettings }}<br
/></span>
<span v-if="state.userList.length">{{ t('dialog.user.info.instance_users') }}<br /></span>
<template v-for="user in state.userList">
<span
style="cursor: pointer; margin-right: 5px"
@click="showUserDialog(user.id)"
:key="user.id"
>{{ user.displayName }}</span
>
</template>
</div>
</template>
<i class="el-icon-caret-bottom"></i>
</el-tooltip>
<span v-if="state.occupants" style="margin-left: 5px">{{ state.occupants }}/{{ state.capacity }}</span>
<span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span>
<span v-if="state.isFull" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_full')
}}</span>
<span v-if="state.isHardClosed" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_hard_closed')
}}</span>
<span v-else-if="state.isClosed" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_closed')
}}</span>
<span v-if="state.queueSize" style="margin-left: 5px"
>{{ t('dialog.user.info.instance_queue') }} {{ state.queueSize }}</span
>
<span v-if="state.isAgeGated" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_age_gated')
}}</span>
</div>
</template>
<script setup>
import { getCurrentInstance, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { miscRequest } from '../api';
import { formatDateFilter, hasGroupPermission } from '../shared/utils';
import { useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../stores';
const { t } = useI18n();
const locationStore = useLocationStore();
const userStore = useUserStore();
const groupStore = useGroupStore();
const instanceStore = useInstanceStore();
const props = defineProps({
location: String,
instance: Object,
friendcount: Number
});
const state = reactive({
isValidInstance: false,
isFull: false,
isClosed: false,
isHardClosed: false,
closedAt: '',
occupants: 0,
capacity: 0,
queueSize: 0,
queueEnabled: false,
platforms: [],
userList: [],
gameServerVersion: '',
canCloseInstance: false,
isAgeGated: false,
disabledContentSettings: ''
});
const { proxy } = getCurrentInstance();
function parse() {
Object.assign(state, {
isValidInstance: false,
isFull: false,
isClosed: false,
isHardClosed: false,
closedAt: '',
occupants: 0,
capacity: 0,
queueSize: 0,
queueEnabled: false,
platforms: [],
userList: [],
gameServerVersion: '',
canCloseInstance: false,
isAgeGated: false,
disabledContentSettings: ''
});
if (!props.location || !props.instance || Object.keys(props.instance).length === 0) return;
state.isValidInstance = true;
state.isFull = props.instance.hasCapacityForYou === false;
if (props.instance.closedAt) {
state.isClosed = true;
state.closedAt = props.instance.closedAt;
}
state.isHardClosed = props.instance.hardClose === true;
state.occupants = props.instance.userCount;
if (props.location === locationStore.lastLocation.location) {
state.occupants = locationStore.lastLocation.playerList.size;
}
state.capacity = props.instance.capacity;
state.gameServerVersion = props.instance.gameServerVersion;
state.queueSize = props.instance.queueSize;
if (props.instance.platforms) state.platforms = props.instance.platforms;
if (props.instance.users) state.userList = props.instance.users;
if (props.instance.ownerId === userStore.currentUser.id) {
state.canCloseInstance = true;
} else if (props.instance.ownerId?.startsWith('grp_')) {
const group = groupStore.cachedGroups.get(props.instance.ownerId);
state.canCloseInstance = hasGroupPermission(group, 'group-instance-moderate');
}
state.isAgeGated = props.instance.ageGate === true;
if (props.location?.includes('~ageGate')) state.isAgeGated = true;
if (props.instance.$disabledContentSettings?.length) {
state.disabledContentSettings = props.instance.$disabledContentSettings.join(', ');
}
}
watch([() => props.location, () => props.instance, () => props.friendcount], parse, { immediate: true });
function showUserDialog(userId) {
userStore.showUserDialog(userId);
}
function closeInstance(location) {
proxy.$confirm('Continue? Close Instance, nobody will be able to join', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: async (action) => {
if (action !== 'confirm') return;
const args = await miscRequest.closeInstance({ location, hardClose: false });
if (args.json) {
proxy.$message({ message: t('message.instance.closed'), type: 'success' });
instanceStore.applyInstance(args.json);
}
}
});
}
</script>

Some files were not shown because too many files have changed in this diff Show More