Upgrade to Vue3 and Element Plus (#1374)

* Update Vue devtools

* upgrade vue pinia element-plus vue-i18n, add vite

* fix: i18n

* global components

* change v-deep

* upgrade vue-lazyload

* data table

* update enlint and safe-dialog

* package.json and vite.config.js

* el-icon

* el-message

* vue 2 -> vue3 migration changes

* $pinia

* dialog

* el-popover slot

* lint

* chore

* slot

* scss

* remote state access

* misc

* jsconfig

* el-button size mini -> small

* :model-value

* ElMessageBox

* datatable

* remove v-lazyload

* template #dropdown

* mini -> small

* css

* byebye hideTooltips

* use sass-embedded

* Update SQLite, remove unneeded libraries

* Fix shift remove local avatar favorites

* Electron arm64

* arm64 support

* bye pug

* f-word vite hah

* misc

* remove safe dialog component

* Add self invite to launch dialog

* Fix errors

* Icons 1

* improve localfavorite loading performance

* improve favorites world item performance

* dialog visibility changes for Element Plus

* clear element plus error

* import performance

* revert App.vue hah

* hah

* Revert "Add self invite to launch dialog"

This reverts commit 4801cfad58.

* Toggle self invite/open in-game

* Self invite on launch dialog

* el-button icon

* el-icon

* fix user dialog tab switching logic

* fix PlayerList

* Formatting changes

* More icons

* Fix friend log table

* loading margin

* fix markdown

* fix world dialog tab switching issue

* Fixes and formatting

* fix: global i18n.t export

* fix favorites world tab not working

* Create instance, displayName

* Remove group members sort by userId

* Fix loading dialog tabs on swtich

* Star

* charts console.warn

* wip: fix charts

* wip: fix charts

* wip: charts composables

* fix favorite item tooltip warning

* Fixes and formatting

* Clean up image dialogs

* Remove unused method

* Fix platform/size border

* Fix platform/size border

* $vr

* fix friendExportDialogVisible binding

* ElMessageBox and Settings

* Login formatting

* Rename VR overlay query

* Fix image popover and userdialog badges

* Formatting

* Big buttons

* Fixes, update Cef

* Fix gameLog table nav buttons jumping around while using nav buttons

* Fix z-index

* vr overlay

* vite input add theme

* defineAsyncComponent

* ISO 639-1

* fix i18n

* clean t

* Formatting, fix calendar, rotate arrows

* Show user status when user is offline

* Fix VR overlay

* fix theme and clean up

* split InstanceActivity

* tweak

* Fix VR overlay formatting

* fix scss var

* AppDebug hahahaha

* Years

* remove reactive

* improve perf

* state hah…

* fix user rendering poblems when user object is not yet loaded

* improve perf

* Update avatar/world image uploader, licenses, remove previous images dialog (old images are now deleted)

* improve perf 1

* Suppress stray errors

* fix traveling location display issue

* Fix empty instance creator

* improve friend list refresh performance

* fix main charts

* fix chart

* Fix darkmode

* Fix avatar dialog tags

---------

Co-authored-by: pa <maplenagisa@gmail.com>
This commit is contained in:
Natsumi
2025-09-12 10:45:24 +12:00
committed by GitHub
parent b233bbc299
commit 3324d0d279
249 changed files with 12948 additions and 19815 deletions

View File

@@ -8,7 +8,6 @@
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"plugins": ["@prettier/plugin-pug"],
"overrides": [
{
"files": "*.js",
@@ -16,14 +15,6 @@
"parser": "meriyah"
}
},
{
"files": "*.pug",
"options": {
"pugPrintWidth": 120,
"pugAttributeSeparator": "none",
"pugBracketSameLine": true
}
},
{
"files": "*.vue",
"options": {

View File

@@ -17,7 +17,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp;
using librsync.net;
using Microsoft.Toolkit.Uwp.Notifications;
using Microsoft.Win32;
using NLog;
@@ -123,12 +122,6 @@ namespace VRCX
return File.Exists(Path.Join(Program.AppDataDirectory, "update.exe"));
}
public override void ExecuteAppFunction(string function, string json)
{
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json);
}
public override void ExecuteVrFeedFunction(string function, string json)
{
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);

View File

@@ -21,7 +21,7 @@ namespace VRCX
public override void VrInit()
{
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
MainForm.Instance.Browser.ExecuteScriptAsync("$pinia.vr.vrInit", "");
}
public override void ToggleSystemMonitor(bool enabled)

View File

@@ -36,7 +36,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.store.game.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
MainForm.Instance.Browser.ExecuteScriptAsync("$pinia.game.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
}
public override bool IsGameRunning()

View File

@@ -6,7 +6,6 @@ using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using librsync.net;
using Newtonsoft.Json;
using NLog;
@@ -20,10 +19,10 @@ namespace VRCX
public void Init()
{
}
public JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
{
Error = delegate(object _, Newtonsoft.Json.Serialization.ErrorEventArgs args)
Error = delegate (object _, Newtonsoft.Json.Serialization.ErrorEventArgs args)
{
args.ErrorContext.Handled = true;
}
@@ -43,22 +42,6 @@ namespace VRCX
return (hash[3] << 8) | hash[4];
}
public string SignFile(string blob)
{
var fileData = Convert.FromBase64String(blob);
using var sig = Librsync.ComputeSignature(new MemoryStream(fileData));
using var memoryStream = new MemoryStream();
sig.CopyTo(memoryStream);
var sigBytes = memoryStream.ToArray();
return Convert.ToBase64String(sigBytes);
}
public string FileLength(string blob)
{
var fileData = Convert.FromBase64String(blob);
return fileData.Length.ToString();
}
public void OpenLink(string url)
{
if (url.StartsWith("http://") ||
@@ -70,7 +53,7 @@ namespace VRCX
});
}
}
public string GetLaunchCommand()
{
var command = StartupArgs.LaunchArguments.LaunchCommand;
@@ -102,7 +85,7 @@ namespace VRCX
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
if (File.Exists(filePath))
return File.ReadAllText(filePath);
return string.Empty;
}
@@ -111,7 +94,7 @@ namespace VRCX
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
if (File.Exists(filePath))
return File.ReadAllText(filePath);
return string.Empty;
}

View File

@@ -17,7 +17,6 @@ namespace VRCX
public abstract void RestartApplication(bool isUpgrade);
public abstract bool CheckForUpdateExe();
public abstract void ExecuteAppFunction(string function, string json);
public abstract void ExecuteVrFeedFunction(string function, string json);
public abstract void ExecuteVrOverlayFunction(string function, string json);
public abstract void FocusWindow();

View File

@@ -51,10 +51,6 @@ namespace VRCX
return false;
}
public override void ExecuteAppFunction(string function, string json)
{
}
public override void ExecuteVrFeedFunction(string function, string json)
{
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);

View File

@@ -26,7 +26,7 @@ namespace VRCX
}
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vrcx.dragEnterCef", file);
MainForm.Instance.Browser.ExecuteScriptAsync("$pinia.vrcx.dragEnterCef", file);
dragData.Dispose();
return false;

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.store.vrcx.ipcEvent", packet);
MainForm.Instance.Browser.ExecuteScriptAsync("$pinia.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.store.gameLog.addGameLogEvent", logLine);
MainForm.Instance.Browser.ExecuteScriptAsync("$pinia.gameLog.addGameLogEvent", logLine);
#endif
}

View File

@@ -190,13 +190,13 @@ namespace VRCX
var dashboardHandle = 0UL;
_wristOverlay = new OffScreenBrowser(
Program.LaunchDebug ? "http://localhost:9000/vr.html?1": "file://vrcx/vr.html?1",
Program.LaunchDebug ? "http://localhost:9000/vr.html?wrist" : "file://vrcx/vr.html?wrist",
512,
512
);
_hmdOverlay = new OffScreenBrowser(
Program.LaunchDebug ? "http://localhost:9000/vr.html?2": "file://vrcx/vr.html?2",
Program.LaunchDebug ? "http://localhost:9000/vr.html?hmd" : "file://vrcx/vr.html?hmd",
1024,
1024
);
@@ -846,7 +846,7 @@ namespace VRCX
if (_wristOverlay == null) return;
// if (_wristOverlay.IsLoading)
// Restart();
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
_wristOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
@@ -859,7 +859,7 @@ namespace VRCX
if (_hmdOverlay == null) return;
// if (_hmdOverlay.IsLoading)
// Restart();
_hmdOverlay.ExecuteScriptAsync($"$app.{function}", json);
_hmdOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
}
}

View File

@@ -141,13 +141,13 @@ namespace VRCX
var overlayHandle2 = 0UL;
_wristOverlay = new OffScreenBrowserLegacy(
"file://vrcx/vr.html?1",
Program.LaunchDebug ? "http://localhost:9000/vr.html?wrist" : "file://vrcx/vr.html?wrist",
512,
512
);
_hmdOverlay = new OffScreenBrowserLegacy(
"file://vrcx/vr.html?2",
Program.LaunchDebug ? "http://localhost:9000/vr.html?hmd" : "file://vrcx/vr.html?hmd",
1024,
1024
);
@@ -763,7 +763,7 @@ namespace VRCX
return err;
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
{
throw new NotImplementedException();
@@ -774,9 +774,9 @@ namespace VRCX
if (_wristOverlay == null) return;
if (_wristOverlay.IsLoading)
Restart();
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
_wristOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
{
throw new NotImplementedException();
@@ -787,7 +787,7 @@ namespace VRCX
if (_hmdOverlay == null) return;
if (_hmdOverlay.IsLoading)
Restart();
_hmdOverlay.ExecuteScriptAsync($"$app.{function}", json);
_hmdOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace VRCX
InitializeComponent();
_browser1 = new ChromiumWebBrowser(
Path.Join(Program.BaseDirectory, "html/vr.html?1")
Path.Join(Program.BaseDirectory, "html/vr.html?wrist")
)
{
DragHandler = new CefNoopDragHandler(),
@@ -35,7 +35,7 @@ namespace VRCX
};
_browser2 = new ChromiumWebBrowser(
Path.Join(Program.BaseDirectory, "html/vr.html?2")
Path.Join(Program.BaseDirectory, "html/vr.html?hmd")
)
{
DragHandler = new CefNoopDragHandler(),

View File

@@ -61,14 +61,6 @@
<EmbeddedResource Remove="DBMerger\**" />
<None Remove="DBMerger\**" />
</ItemGroup>
<ItemGroup>
<Reference Include="Blake2Sharp">
<HintPath>libs\Blake2Sharp.dll</HintPath>
</Reference>
<Reference Include="librsync.net">
<HintPath>libs\librsync.net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="app.manifest" />
<None Include="..\Version">
@@ -88,8 +80,8 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="138.0.340" />
<PackageReference Include="CefSharp.WinForms.NETCore" Version="138.0.340" />
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="139.0.280" />
<PackageReference Include="CefSharp.WinForms.NETCore" Version="139.0.280" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@@ -49,14 +49,6 @@
<ApplicationIcon>..\VRCX.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="Blake2Sharp">
<HintPath>libs\Blake2Sharp.dll</HintPath>
</Reference>
<Reference Include="librsync.net">
<HintPath>libs\librsync.net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="app.manifest" />
<None Include="..\Version">

View File

@@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<BaseIntermediateOutputPath>obj1\</BaseIntermediateOutputPath>
<OutputPath>..\build\Electron\</OutputPath>
@@ -54,14 +54,6 @@
<ApplicationIcon>..\VRCX.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="Blake2Sharp">
<HintPath>libs\Blake2Sharp.dll</HintPath>
</Reference>
<Reference Include="librsync.net">
<HintPath>libs\librsync.net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="app.manifest" />
<None Include="..\Version">

Binary file not shown.

View File

@@ -1,11 +1,3 @@
### librsync.net.dll
- [https://github.com/braddodson/librsync.net](https://github.com/braddodson/librsync.net)
### Blake2Sharp.dll
- [https://github.com/BLAKE2/BLAKE2/tree/master/csharp](https://github.com/BLAKE2/BLAKE2/tree/master/csharp)
### openvr_api.dll
- [https://github.com/ValveSoftware/openvr](https://github.com/ValveSoftware/openvr)
### sqlite3.dll
- [https://www.sqlite.org/index.html](https://www.sqlite.org/index.html)
- [https://github.com/ValveSoftware/openvr](https://github.com/ValveSoftware/openvr)

Binary file not shown.

Binary file not shown.

View File

@@ -57,7 +57,7 @@ export default defineConfig([
}
}
},
pluginVue.configs['flat/vue2-essential'],
pluginVue.configs['flat/essential'],
{
rules: {
'no-unused-vars': 'warn',

View File

@@ -1,10 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="referrer" content="no-referrer">
<meta http-equiv="Cache-Control" content="no-cache">
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<meta name="referrer" content="no-referrer" />
<meta http-equiv="Cache-Control" content="no-cache" />
<title>VRCX</title>
@@ -15,5 +18,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.js"></script>
</body>
</html>

View File

@@ -10,7 +10,8 @@
"skipLibCheck": true,
"moduleResolution": "bundler",
"forceConsistentCasingInFileNames": true,
"lib": ["esnext", "dom", "dom.iterable"]
"lib": ["esnext", "dom", "dom.iterable"],
"types": ["vite/client", "element-plus/global", "node"]
},
"include": ["src/**/*", "src-electron/**/*"],
"exclude": ["node_modules", "build"]

11727
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@
"private": true,
"main": "src-electron/main.js",
"scripts": {
"dev": "cross-env PLATFORM=windows webpack serve --config webpack.config.js --mode development",
"dev": "cross-env PLATFORM=windows vite serve src",
"dev:test": "concurrently \"npm run dev\" \"jest --watchAll\"",
"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",
"test": "jest",
"test:coverage": "jest --coverage",
"prod": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode production",
"prod": "cross-env PLATFORM=windows vite build src",
"prod-linux": "cross-env PLATFORM=linux webpack --config webpack.config.js --mode production",
"build-electron": "node ./src-electron/download-dotnet-runtime.js --arch=x64 && node ./src-electron/patch-package-version.js && electron-builder --x64 --publish never",
"build-electron-arm64": "node ./src-electron/download-dotnet-runtime.js --arch=arm64 && node ./src-electron/patch-package-version.js && electron-builder --arm64 --publish never",
@@ -34,54 +34,39 @@
"homepage": "https://github.com/vrcx-team/VRCX#readme",
"devDependencies": {
"@electron/rebuild": "^4.0.1",
"@eslint/js": "^9.33.0",
"@fontsource/noto-sans-jp": "^5.2.6",
"@fontsource/noto-sans-kr": "^5.2.6",
"@eslint/js": "^9.35.0",
"@fontsource/noto-sans-jp": "^5.2.7",
"@fontsource/noto-sans-kr": "^5.2.7",
"@fontsource/noto-sans-sc": "^5.2.6",
"@fontsource/noto-sans-tc": "^5.2.6",
"@prettier/plugin-pug": "^3.4.2",
"@fontsource/noto-sans-tc": "^5.2.7",
"@types/jest": "^30.0.0",
"@types/node": "^24.3.0",
"@types/node": "^24.3.1",
"@vitejs/plugin-vue": "^6.0.1",
"animate.css": "^4.1.1",
"concurrently": "^9.2.0",
"copy-webpack-plugin": "^13.0.1",
"babel-runtime": "^6.26.0",
"concurrently": "^9.2.1",
"cross-env": "^10.0.0",
"css-loader": "^7.1.2",
"dayjs": "^1.11.13",
"dayjs": "^1.11.18",
"echarts": "^6.0.0",
"electron": "^37.3.0",
"electron": "^37.4.0",
"electron-builder": "^26.0.12",
"element-ui": "^2.15.14",
"esbuild-jest": "^0.5.0",
"esbuild-loader": "^4.3.0",
"eslint": "^9.33.0",
"element-plus": "^2.11.2",
"esbuild-jest": "^0.4.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-vue": "^9.33.0",
"globals": "^16.3.0",
"html-webpack-plugin": "^5.6.4",
"jest": "^30.0.5",
"mini-css-extract-plugin": "^2.9.4",
"jest": "^30.1.3",
"noty": "^3.2.0-beta-deprecated",
"pinia": "^2.3.1",
"pinia": "^3.0.3",
"prettier": "^3.6.2",
"pug": "^3.0.3",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
"remixicon": "^4.6.0",
"sass": "^1.90.0",
"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.5",
"vue-loader": "^15.11.1",
"vue-markdown": "^2.2.4",
"vue-marquee-text-component": "^1.2.0",
"webpack": "^5.101.2",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"sass-embedded": "^1.92.1",
"vite": "^7.1.4",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-marquee-text-component": "^2.0.1",
"vue-showdown": "^4.2.0",
"worker-timers": "^8.0.24",
"yargs": "^18.0.0"
},

View File

@@ -126,6 +126,7 @@ const gotTheLock = app.requestSingleInstanceLock();
const strip_vrcx_prefix_regex = new RegExp('^' + VRCX_URI_PREFIX + '://');
if (!gotTheLock) {
console.log('Another instance is already running. Exiting.');
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
@@ -427,7 +428,7 @@ function createWristOverlayWindowOffscreen() {
wristOverlayWindow.webContents.setFrameRate(2);
const indexPath = path.join(rootDir, 'build/html/vr.html');
const fileUrl = `file://${indexPath}?1`;
const fileUrl = `file://${indexPath}?wrist`;
wristOverlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering
@@ -490,7 +491,7 @@ function createHmdOverlayWindowOffscreen() {
hmdOverlayWindow.webContents.setFrameRate(48);
const indexPath = path.join(rootDir, 'build/html/vr.html');
const fileUrl = `file://${indexPath}?2`;
const fileUrl = `file://${indexPath}?hmd`;
hmdOverlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering

View File

@@ -1,11 +1,99 @@
<script>
// @ts-ignore
import template from './app.pug';
import Vue, { onMounted } from 'vue';
<template>
<!DOCTYPE html>
<el-config-provider :locale="currentLocale">
<div
id="x-app"
class="x-app"
ondragenter="event.preventDefault()"
ondragover="event.preventDefault()"
ondrop="event.preventDefault()">
<!-- ### Login ### -->
<Login v-if="!watchState.isLoggedIn"></Login>
<VRCXUpdateDialog></VRCXUpdateDialog>
<template v-if="watchState.isLoggedIn">
<!-- ### Menu ### -->
<NavMenu></NavMenu>
<!-- ### Sidebar ### -->
<Sidebar></Sidebar>
<!-- ### Tabs ### -->
<Feed></Feed>
<GameLog></GameLog>
<PlayerList></PlayerList>
<Search></Search>
<Favorites></Favorites>
<FriendLog></FriendLog>
<Moderation></Moderation>
<Notification></Notification>
<FriendList></FriendList>
<Charts></Charts>
<Tools></Tools>
<Profile></Profile>
<Settings></Settings>
<!-- ## Dialogs ## -->
<UserDialog></UserDialog>
<WorldDialog></WorldDialog>
<AvatarDialog></AvatarDialog>
<GroupDialog></GroupDialog>
<GroupMemberModerationDialog></GroupMemberModerationDialog>
<FullscreenImageDialog></FullscreenImageDialog>
<PreviousInstancesInfoDialog></PreviousInstancesInfoDialog>
<LaunchDialog></LaunchDialog>
<LaunchOptionsDialog></LaunchOptionsDialog>
<FriendImportDialog></FriendImportDialog>
<WorldImportDialog></WorldImportDialog>
<AvatarImportDialog></AvatarImportDialog>
<ChooseFavoriteGroupDialog></ChooseFavoriteGroupDialog>
<EditInviteMessageDialog></EditInviteMessageDialog>
<VRChatConfigDialog></VRChatConfigDialog>
<PrimaryPasswordDialog></PrimaryPasswordDialog>
</template>
</div>
</el-config-provider>
</template>
<script setup>
import { onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { createGlobalStores } from './stores';
import { watchState } from './service/watchState';
import '@fontsource/noto-sans-kr';
import '@fontsource/noto-sans-jp';
import '@fontsource/noto-sans-sc';
import '@fontsource/noto-sans-tc';
import Login from './views/Login/Login.vue';
import NavMenu from './components/NavMenu.vue';
import Sidebar from './views/Sidebar/Sidebar.vue';
@@ -28,7 +116,6 @@
import AvatarDialog from './components/dialogs/AvatarDialog/AvatarDialog.vue';
import GroupDialog from './components/dialogs/GroupDialog/GroupDialog.vue';
import GroupMemberModerationDialog from './components/dialogs/GroupDialog/GroupMemberModerationDialog.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';
@@ -42,66 +129,57 @@
import VRChatConfigDialog from './views/Settings/dialogs/VRChatConfigDialog.vue';
import PrimaryPasswordDialog from './views/Settings/dialogs/PrimaryPasswordDialog.vue';
import { utils } from './shared/utils/_utils';
import en from 'element-plus/es/locale/lang/en';
import es from 'element-plus/es/locale/lang/es';
import fr from 'element-plus/es/locale/lang/fr';
import hu from 'element-plus/es/locale/lang/hu';
import ja from 'element-plus/es/locale/lang/ja';
import ko from 'element-plus/es/locale/lang/ko';
import pl from 'element-plus/es/locale/lang/pl';
import pt from 'element-plus/es/locale/lang/pt';
import cs from 'element-plus/es/locale/lang/cs';
import ru from 'element-plus/es/locale/lang/ru';
import vi from 'element-plus/es/locale/lang/vi';
import zhCN from 'element-plus/es/locale/lang/zh-CN';
import zhTW from 'element-plus/es/locale/lang/zh-TW';
import th from 'element-plus/es/locale/lang/th';
export default {
template,
components: {
Login,
NavMenu,
Sidebar,
Feed,
GameLog,
PlayerList,
Search,
Favorites,
FriendLog,
Moderation,
Notification,
FriendList,
Charts,
Tools,
Profile,
Settings,
UserDialog,
WorldDialog,
AvatarDialog,
GroupDialog,
GroupMemberModerationDialog,
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();
});
return {
store,
watchState
};
}
const langMap = {
en: en,
es: es,
fr: fr,
hu: hu,
ja: ja,
ko: ko,
pl: pl,
pt: pt,
cs: cs,
ru: ru,
vi: vi,
'zh-CN': zhCN,
'zh-TW': zhTW,
th: th
};
const currentLocale = computed(() => {
return langMap[locale.value] || en;
});
const { locale } = useI18n();
const store = createGlobalStores();
if (typeof window !== 'undefined') {
window.$pinia = store;
}
store.updateLoop.updateLoop();
onMounted(async () => {
store.gameLog.getGameLogTable();
await store.auth.migrateStoredUsers();
store.auth.autoLoginAfterMounted();
store.vrcx.checkAutoBackupRestoreVrcRegistry();
store.game.checkVRChatDebugLogging();
});
</script>

View File

@@ -1,681 +0,0 @@
<script>
import '@fontsource/noto-sans-kr';
import '@fontsource/noto-sans-jp';
import '@fontsource/noto-sans-sc';
import '@fontsource/noto-sans-tc';
// @ts-ignore
import pugTemplate from './vr.pug';
import Vue, { onMounted, reactive, toRefs, nextTick } from 'vue';
import Noty from 'noty';
import * as workerTimers from 'worker-timers';
import MarqueeText from 'vue-marquee-text-component';
import VrLocation from './components/VrLocation.vue';
import { displayLocation } from './shared/utils/location';
import { escapeTag, escapeTagRecursive } from './shared/utils/base/string';
import { removeFromArray } from './shared/utils/base/array';
import { timeToText } from './shared/utils/base/format';
import { i18n, t as $t } from './plugin/i18n.js';
export default {
name: 'vr',
template: pugTemplate,
components: {
'marquee-text': MarqueeText,
location: VrLocation
},
setup() {
const vrState = reactive({
appType: location.href.substr(-1),
appLanguage: 'en',
currentCulture: 'en-nz',
isRunningUnderWine: false,
currentTime: new Date().toJSON(),
cpuUsageEnabled: false,
cpuUsage: '0',
pcUptimeEnabled: false,
pcUptime: '',
customInfo: '',
config: {},
onlineFriendCount: 0,
nowPlaying: {
url: '',
name: '',
length: 0,
startTime: 0,
elapsed: 0,
percentage: 0,
remainingText: '',
playing: false
},
lastLocation: {
date: 0,
location: '',
name: '',
playerList: [],
friendList: [],
progressPie: false,
onlineFor: 0
},
lastLocationTimer: '',
onlineForTimer: '',
wristFeed: [],
devices: [],
deviceCount: 0,
notificationOpacity: 100,
hudFeed: [],
hudTimeout: [],
cleanHudFeedLoopStatus: false
});
onMounted(async () => {
if (LINUX) {
updateVrElectronLoop();
}
if (vrState.appType === '1') {
refreshCustomScript();
updateStatsLoop();
}
setDatetimeFormat();
nextTick(() => {
window.$app.configUpdate = configUpdate;
window.$app.updateOnlineFriendCount = updateOnlineFriendCount;
window.$app.nowPlayingUpdate = nowPlayingUpdate;
window.$app.lastLocationUpdate = lastLocationUpdate;
window.$app.wristFeedUpdate = wristFeedUpdate;
window.$app.refreshCustomScript = refreshCustomScript;
window.$app.playNoty = playNoty;
window.$app.statusClass = statusClass;
window.$app.notyClear = notyClear;
window.$app.addEntryHudFeed = addEntryHudFeed;
window.$app.updateHudFeedTag = updateHudFeedTag;
window.$app.updateHudTimeout = updateHudTimeout;
window.$app.setDatetimeFormat = setDatetimeFormat;
window.$app.setAppLanguage = setAppLanguage;
window.$app.trackingResultToClass = trackingResultToClass;
window.$app.updateFeedLength = updateFeedLength;
window.$app.updateStatsLoop = updateStatsLoop;
window.$app.updateVrElectronLoop = updateVrElectronLoop;
window.$app.cleanHudFeedLoop = cleanHudFeedLoop;
window.$app.cleanHudFeed = cleanHudFeed;
window.$app.vrState = vrState;
AppApiVr.VrInit();
});
});
function configUpdate(json) {
vrState.config = JSON.parse(json);
vrState.hudFeed = [];
vrState.hudTimeout = [];
setDatetimeFormat();
setAppLanguage(vrState.config.appLanguage);
updateFeedLength();
if (
vrState.config.vrOverlayCpuUsage !== vrState.cpuUsageEnabled ||
vrState.config.pcUptimeOnFeed !== vrState.pcUptimeEnabled
) {
vrState.cpuUsageEnabled = vrState.config.vrOverlayCpuUsage;
vrState.pcUptimeEnabled = vrState.config.pcUptimeOnFeed;
AppApiVr.ToggleSystemMonitor(vrState.cpuUsageEnabled || vrState.pcUptimeEnabled);
}
if (vrState.config.notificationOpacity !== vrState.notificationOpacity) {
vrState.notificationOpacity = vrState.config.notificationOpacity;
setNotyOpacity(vrState.notificationOpacity);
}
}
function updateOnlineFriendCount(count) {
vrState.onlineFriendCount = parseInt(count, 10);
}
function nowPlayingUpdate(json) {
vrState.nowPlaying = JSON.parse(json);
if (vrState.appType === '2') {
const circle = /** @type {SVGCircleElement} */ (
document.querySelector('.np-progress-circle-stroke')
);
if (vrState.lastLocation.progressPie && vrState.nowPlaying.percentage !== 0) {
circle.style.opacity = (0.5).toString();
const circumference = circle.getTotalLength();
circle.style.strokeDashoffset = (
circumference -
(vrState.nowPlaying.percentage / 100) * circumference
).toString();
} else {
circle.style.opacity = '0';
}
}
updateFeedLength();
}
function lastLocationUpdate(json) {
vrState.lastLocation = JSON.parse(json);
}
function wristFeedUpdate(json) {
vrState.wristFeed = JSON.parse(json);
updateFeedLength();
}
function updateFeedLength() {
if (vrState.appType === '2' || vrState.wristFeed.length === 0) {
return;
}
let length = 16;
if (!vrState.config.hideDevicesFromFeed) {
length -= 2;
if (vrState.deviceCount > 8) {
length -= 1;
}
}
if (vrState.nowPlaying.playing) {
length -= 1;
}
if (length < vrState.wristFeed.length) {
vrState.wristFeed.length = length;
}
}
async function refreshCustomScript() {
if (document.contains(document.getElementById('vr-custom-script'))) {
document.getElementById('vr-custom-script').remove();
}
const customScript = await AppApiVr.CustomVrScript();
if (customScript) {
const head = document.head;
const $vrCustomScript = document.createElement('script');
$vrCustomScript.setAttribute('id', 'vr-custom-script');
$vrCustomScript.type = 'text/javascript';
$vrCustomScript.textContent = customScript;
head.appendChild($vrCustomScript);
}
}
function setNotyOpacity(value) {
const opacity = (value / 100).toFixed(2);
let element = document.getElementById('noty-opacity');
if (!element) {
document.body.insertAdjacentHTML(
'beforeend',
`<style id="noty-opacity">.noty_layout { opacity: ${opacity}; }</style>`
);
element = document.getElementById('noty-opacity');
}
element.innerHTML = `.noty_layout { opacity: ${opacity}; }`;
}
async function updateStatsLoop() {
try {
vrState.currentTime = new Date()
.toLocaleDateString(vrState.currentCulture, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hourCycle: vrState.config.dtHour12 ? 'h12' : 'h23'
})
.replace(' AM', ' am')
.replace(' PM', ' pm')
.replace(',', '');
if (vrState.cpuUsageEnabled) {
const cpuUsage = await AppApiVr.CpuUsage();
vrState.cpuUsage = cpuUsage.toFixed(0);
}
if (vrState.lastLocation.date !== 0) {
vrState.lastLocationTimer = timeToText(Date.now() - vrState.lastLocation.date);
} else {
vrState.lastLocationTimer = '';
}
if (vrState.lastLocation.onlineFor) {
vrState.onlineForTimer = timeToText(Date.now() - vrState.lastLocation.onlineFor);
} else {
vrState.onlineForTimer = '';
}
if (!vrState.config.hideDevicesFromFeed) {
AppApiVr.GetVRDevices().then((devices) => {
let deviceList = [];
let baseStations = 0;
devices.forEach((device) => {
device[3] = parseInt(device[3], 10).toString();
if (device[0] === 'base' && device[1] === 'connected') {
baseStations++;
} else {
deviceList.push(device);
}
});
vrState.deviceCount = deviceList.length;
const deviceValue = (dev) => {
if (dev[0] === 'headset') return 0;
if (dev[0] === 'leftController') return 1;
if (dev[0] === 'rightController') return 2;
if (dev[0].toLowerCase().includes('controller')) return 3;
if (dev[0] === 'tracker' || dev[0] === 'base') return 4;
return 5;
};
deviceList.sort((a, b) => deviceValue(a) - deviceValue(b));
deviceList.sort((a, b) => {
if (a[1] === b[1]) {
return 0;
}
if (a[1] === 'connected') {
return -1;
}
if (a[1] === 'disconnected') {
return 1;
}
return 0;
});
if (baseStations > 0) {
deviceList.push(['base', 'connected', '', baseStations]);
vrState.deviceCount += 1;
}
vrState.devices = deviceList;
});
} else {
vrState.devices = [];
}
if (vrState.config.pcUptimeOnFeed) {
AppApiVr.GetUptime().then((uptime) => {
if (uptime) {
vrState.pcUptime = timeToText(uptime);
}
});
} else {
vrState.pcUptime = '';
}
} catch (err) {
console.error(err);
}
workerTimers.setTimeout(() => updateStatsLoop(), 500);
}
async function updateVrElectronLoop() {
try {
if (vrState.appType === '1') {
const wristOverlayQueue = await AppApiVr.GetExecuteVrFeedFunctionQueue();
if (wristOverlayQueue) {
wristOverlayQueue.forEach((item) => {
// item[0] is the function name, item[1] is already an object
const fullFunctionName = item[0];
const jsonArg = item[1];
if (
typeof window.$app === 'object' &&
typeof window.$app[fullFunctionName] === 'function'
) {
window.$app[fullFunctionName](jsonArg);
} else {
console.error(`$app.${fullFunctionName} is not defined or is not a function`);
}
});
}
} else {
const hmdOverlayQueue = await AppApiVr.GetExecuteVrOverlayFunctionQueue();
if (hmdOverlayQueue) {
hmdOverlayQueue.forEach((item) => {
// item[0] is the function name, item[1] is already an object
const fullFunctionName = item[0];
const jsonArg = item[1];
if (
typeof window.$app === 'object' &&
typeof window.$app[fullFunctionName] === 'function'
) {
window.$app[fullFunctionName](jsonArg);
} else {
console.error(`$app.${fullFunctionName} is not defined or is not a function`);
}
});
}
}
} catch (err) {
console.error(err);
}
workerTimers.setTimeout(() => updateVrElectronLoop(), 500);
}
function playNoty(json) {
let { noty, message, image } = JSON.parse(json);
if (typeof noty === 'undefined') {
console.error('noty is undefined');
return;
}
noty = escapeTagRecursive(noty);
message = escapeTag(message) || '';
let text = '';
let img = '';
if (image) {
img = `<img class="noty-img" src="${image}"></img>`;
}
switch (noty.type) {
case 'OnPlayerJoined':
text = `<strong>${noty.displayName}</strong> has joined`;
break;
case 'OnPlayerLeft':
text = `<strong>${noty.displayName}</strong> has left`;
break;
case 'OnPlayerJoining':
text = `<strong>${noty.displayName}</strong> is joining`;
break;
case 'GPS':
text = `<strong>${noty.displayName}</strong> is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`;
break;
case 'Online':
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(noty.location, noty.worldName, noty.groupName)}`;
}
text = `<strong>${noty.displayName}</strong> has logged in${locationName}`;
break;
case 'Offline':
text = `<strong>${noty.displayName}</strong> has logged out`;
break;
case 'Status':
text = `<strong>${noty.displayName}</strong> status is now <i>${noty.status}</i> ${noty.statusDescription}`;
break;
case 'invite':
text = `<strong>${noty.senderUsername}</strong> has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName,
''
)}${message}`;
break;
case 'requestInvite':
text = `<strong>${noty.senderUsername}</strong> has requested an invite ${message}`;
break;
case 'inviteResponse':
text = `<strong>${noty.senderUsername}</strong> has responded to your invite ${message}`;
break;
case 'requestInviteResponse':
text = `<strong>${noty.senderUsername}</strong> has responded to your invite request ${message}`;
break;
case 'friendRequest':
text = `<strong>${noty.senderUsername}</strong> has sent you a friend request`;
break;
case 'Friend':
text = `<strong>${noty.displayName}</strong> is now your friend`;
break;
case 'Unfriend':
text = `<strong>${noty.displayName}</strong> is no longer your friend`;
break;
case 'TrustLevel':
text = `<strong>${noty.displayName}</strong> trust level is now ${noty.trustLevel}`;
break;
case 'DisplayName':
text = `<strong>${noty.previousDisplayName}</strong> changed their name to ${noty.displayName}`;
break;
case 'boop':
text = noty.message;
break;
case 'groupChange':
text = `<strong>${noty.senderUsername}</strong> ${noty.message}`;
break;
case 'group.announcement':
text = noty.message;
break;
case 'group.informative':
text = noty.message;
break;
case 'group.invite':
text = noty.message;
break;
case 'group.joinRequest':
text = noty.message;
break;
case 'group.transfer':
text = noty.message;
break;
case 'group.queueReady':
text = noty.message;
break;
case 'instance.closed':
text = noty.message;
break;
case 'PortalSpawn':
if (noty.displayName) {
text = `<strong>${noty.displayName}</strong> has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
)}`;
} else {
text = 'User has spawned a portal';
}
break;
case 'AvatarChange':
text = `<strong>${noty.displayName}</strong> changed into avatar ${noty.name}`;
break;
case 'ChatBoxMessage':
text = `<strong>${noty.displayName}</strong> said ${noty.text}`;
break;
case 'Event':
text = noty.data;
break;
case 'External':
text = noty.message;
break;
case 'VideoPlay':
text = `<strong>Now playing:</strong> ${noty.notyName}`;
break;
case 'BlockedOnPlayerJoined':
text = `Blocked user <strong>${noty.displayName}</strong> has joined`;
break;
case 'BlockedOnPlayerLeft':
text = `Blocked user <strong>${noty.displayName}</strong> has left`;
break;
case 'MutedOnPlayerJoined':
text = `Muted user <strong>${noty.displayName}</strong> has joined`;
break;
case 'MutedOnPlayerLeft':
text = `Muted user <strong>${noty.displayName}</strong> has left`;
break;
case 'Blocked':
text = `<strong>${noty.displayName}</strong> has blocked you`;
break;
case 'Unblocked':
text = `<strong>${noty.displayName}</strong> has unblocked you`;
break;
case 'Muted':
text = `<strong>${noty.displayName}</strong> has muted you`;
break;
case 'Unmuted':
text = `<strong>${noty.displayName}</strong> has unmuted you`;
break;
default:
break;
}
if (text) {
new Noty({
type: 'alert',
theme: vrState.config.notificationTheme,
timeout: vrState.config.notificationTimeout,
layout: vrState.config.notificationPosition,
text: `${img}<div class="noty-text">${text}</div>`
}).show();
}
}
function statusClass(status) {
let style = {};
if (typeof status === 'undefined') {
return style;
}
if (status === 'active') {
// Online
style.online = true;
} else if (status === 'join me') {
// Join Me
style.joinme = true;
} else if (status === 'ask me') {
// Ask Me
style.askme = true;
} else if (status === 'busy') {
// Do Not Disturb
style.busy = true;
}
return style;
}
function notyClear() {
Noty.closeAll();
}
function cleanHudFeedLoop() {
if (!vrState.cleanHudFeedLoopStatus) {
return;
}
cleanHudFeed();
if (vrState.hudFeed.length === 0) {
vrState.cleanHudFeedLoopStatus = false;
return;
}
workerTimers.setTimeout(() => cleanHudFeedLoop(), 500);
}
function cleanHudFeed() {
const dt = Date.now();
vrState.hudFeed.forEach((item) => {
if (item.time + vrState.config.photonOverlayMessageTimeout < dt) {
removeFromArray(vrState.hudFeed, item);
}
});
if (vrState.hudFeed.length > 10) {
vrState.hudFeed.length = 10;
}
if (!vrState.cleanHudFeedLoopStatus) {
vrState.cleanHudFeedLoopStatus = true;
cleanHudFeedLoop();
}
}
function addEntryHudFeed(json) {
const data = JSON.parse(json);
let combo = 1;
vrState.hudFeed.forEach((item) => {
if (item.displayName === data.displayName && item.text === data.text) {
combo = item.combo + 1;
removeFromArray(vrState.hudFeed, item);
}
});
vrState.hudFeed.unshift({
time: Date.now(),
combo,
...data
});
cleanHudFeed();
}
function updateHudFeedTag(json) {
const ref = JSON.parse(json);
vrState.hudFeed.forEach((item) => {
if (item.userId === ref.userId) {
item.colour = ref.colour;
}
});
}
function updateHudTimeout(json) {
vrState.hudTimeout = JSON.parse(json);
}
async function setDatetimeFormat() {
vrState.currentCulture = await AppApiVr.CurrentCulture();
const formatDate = (date) => {
if (!date) {
return '';
}
const dt = new Date(date);
return dt
.toLocaleTimeString(vrState.currentCulture, {
hour: '2-digit',
minute: 'numeric',
hourCycle: vrState.config.dtHour12 ? 'h12' : 'h23'
})
.replace(' am', '')
.replace(' pm', '');
};
Vue.filter('formatDate', formatDate);
}
function setAppLanguage(appLanguage) {
if (!appLanguage) {
return;
}
if (appLanguage !== vrState.appLanguage) {
vrState.appLanguage = appLanguage;
// @ts-ignore
i18n.locale = vrState.appLanguage;
}
}
function trackingResultToClass(deviceStatus) {
switch (deviceStatus) {
case 'Uninitialized':
case 'Calibrating_OutOfRange':
case 'Fallback_RotationOnly':
return 'tracker-error';
case 'Calibrating_InProgress':
case 'Running_OutOfRange':
return 'tracker-warning';
case 'Running_OK':
default:
return '';
}
}
const {
appType,
config,
wristFeed,
devices,
nowPlaying,
lastLocation,
lastLocationTimer,
onlineForTimer,
pcUptime,
currentTime,
cpuUsageEnabled,
cpuUsage,
onlineFriendCount,
customInfo,
hudFeed,
hudTimeout
} = toRefs(vrState);
return {
appType,
config,
wristFeed,
devices,
nowPlaying,
lastLocation,
lastLocationTimer,
onlineForTimer,
pcUptime,
currentTime,
cpuUsageEnabled,
cpuUsage,
onlineFriendCount,
customInfo,
hudFeed,
hudTimeout,
statusClass,
trackingResultToClass,
$t
};
}
};
</script>

View File

@@ -33,9 +33,9 @@ const avatarReq = {
return args;
});
},
/**
* @param {{ id: string, releaseStatus?: 'public' | 'private', name?: string, description?: string,tags?: string[] }} params
* @returns {Promise<{json: any, params}>}
* @type {import('../types/api/avatar').SaveAvatar}
*/
saveAvatar(params) {
return request(`avatars/${params.id}`, {
@@ -173,6 +173,24 @@ const avatarReq = {
});
},
uploadAvatarImage(imageData) {
const params = {
tag: 'avatarimage'
};
return request('file/image', {
uploadImage: true,
matchingDimensions: false,
postData: JSON.stringify(params),
imageData
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
/**
* @param {{ imageData: string, avatarId: string }}
* @returns {Promise<{json: any, params}>}

View File

@@ -1,297 +0,0 @@
import { request } from '../service/request';
import { useAvatarStore, useWorldStore } from '../stores';
const imageReq = {
async uploadAvatarFailCleanup(id) {
const avatarStore = useAvatarStore();
try {
const json = await request(`file/${id}`, {
method: 'GET'
});
const fileId = json.id;
const fileVersion = json.versions[json.versions.length - 1].version;
request(`file/${fileId}/${fileVersion}/signature/finish`, {
method: 'PUT'
}).catch(err => console.error('Failed to finish signature:', err));
request(`file/${fileId}/${fileVersion}/file/finish`, {
method: 'PUT'
}).catch(err => console.error('Failed to finish file:', err));
} catch (error) {
console.error('Failed to cleanup avatar upload:', error);
}
avatarStore.avatarDialog.loading = false;
},
async uploadAvatarImage(params, fileId) {
try {
return await request(`file/${fileId}`, {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params,
fileId
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadAvatarFailCleanup(fileId);
throw err;
}
},
async uploadAvatarImageFileStart(params) {
try {
return await request(
`file/${params.fileId}/${params.fileVersion}/file/start`,
{
method: 'PUT'
}
).then((json) => {
const args = {
json,
params
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
},
uploadAvatarImageFileFinish(params) {
return request(
`file/${params.fileId}/${params.fileVersion}/file/finish`,
{
method: 'PUT',
params: {
maxParts: 0,
nextPartNumber: 0
}
}
).then((json) => {
const args = {
json,
params
};
return args;
});
},
async uploadAvatarImageSigStart(params) {
try {
return await request(
`file/${params.fileId}/${params.fileVersion}/signature/start`,
{
method: 'PUT'
}
).then((json) => {
const args = {
json,
params
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
},
uploadAvatarImageSigFinish(params) {
return request(
`file/${params.fileId}/${params.fileVersion}/signature/finish`,
{
method: 'PUT',
params: {
maxParts: 0,
nextPartNumber: 0
}
}
).then((json) => {
const args = {
json,
params
};
return args;
});
},
setAvatarImage(params) {
return request(`avatars/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
async uploadWorldFailCleanup(id) {
const worldStore = useWorldStore();
try {
const json = await request(`file/${id}`, {
method: 'GET'
});
const fileId = json.id;
const fileVersion = json.versions[json.versions.length - 1].version;
request(`file/${fileId}/${fileVersion}/signature/finish`, {
method: 'PUT'
}).catch(err => console.error('Failed to finish signature:', err));
request(`file/${fileId}/${fileVersion}/file/finish`, {
method: 'PUT'
}).catch(err => console.error('Failed to finish file:', err));
} catch (error) {
console.error('Failed to cleanup world upload:', error);
}
worldStore.worldDialog.loading = false;
},
async uploadWorldImage(params, fileId) {
try {
return await request(`file/${fileId}`, {
method: 'POST',
params
}).then((json) => {
const args = {
json,
params,
fileId
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadWorldFailCleanup(fileId);
}
return void 0;
},
async uploadWorldImageFileStart(params) {
try {
return await request(
`file/${params.fileId}/${params.fileVersion}/file/start`,
{
method: 'PUT'
}
).then((json) => {
const args = {
json,
params
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadWorldFailCleanup(params.fileId);
}
return void 0;
},
uploadWorldImageFileFinish(params) {
return request(
`file/${params.fileId}/${params.fileVersion}/file/finish`,
{
method: 'PUT',
params: {
maxParts: 0,
nextPartNumber: 0
}
}
).then((json) => {
const args = {
json,
params
};
return args;
});
},
async uploadWorldImageSigStart(params) {
try {
return await request(
`file/${params.fileId}/${params.fileVersion}/signature/start`,
{
method: 'PUT'
}
).then((json) => {
const args = {
json,
params
};
return args;
});
} catch (err) {
console.error(err);
imageReq.uploadWorldFailCleanup(params.fileId);
}
return void 0;
},
uploadWorldImageSigFinish(params) {
return request(
`file/${params.fileId}/${params.fileVersion}/signature/finish`,
{
method: 'PUT',
params: {
maxParts: 0,
nextPartNumber: 0
}
}
).then((json) => {
const args = {
json,
params
};
return args;
});
},
setWorldImage(params) {
const worldStore = useWorldStore();
return request(`worlds/${params.id}`, {
method: 'PUT',
params
}).then((json) => {
const args = {
json,
params
};
args.ref = worldStore.applyWorld(json);
return args;
});
},
getAvatarImages(params) {
return request(`file/${params.fileId}`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
getWorldImages(params) {
return request(`file/${params.fileId}`, {
method: 'GET',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
}
};
export default imageReq;

View File

@@ -16,7 +16,6 @@ import favoriteRequest from './favorite';
import vrcPlusIconRequest from './vrcPlusIcon';
import vrcPlusImageRequest from './vrcPlusImage';
import inviteMessagesRequest from './inviteMessages';
import imageRequest from './image';
import miscRequest from './misc';
import groupRequest from './group';
import authRequest from './auth';
@@ -37,7 +36,6 @@ window.request = {
vrcPlusIconRequest,
vrcPlusImageRequest,
inviteMessagesRequest,
imageRequest,
miscRequest,
authRequest,
groupRequest,
@@ -59,7 +57,6 @@ export {
vrcPlusIconRequest,
vrcPlusImageRequest,
inviteMessagesRequest,
imageRequest,
miscRequest,
authRequest,
groupRequest,

View File

@@ -1,5 +1,5 @@
import { $app } from '../app';
import { t } from '../plugin';
import { ElMessage } from 'element-plus';
import { i18n } from '../plugin/i18n';
import { request } from '../service/request';
import { useInstanceStore } from '../stores';
@@ -110,14 +110,14 @@ const instanceReq = {
})
.catch((err) => {
if (err?.error?.message) {
$app.$message({
ElMessage({
message: err.error.message,
type: 'error'
});
throw err;
}
$app.$message({
message: t('message.instance.not_allowed'),
ElMessage({
message: i18n.global.t('message.instance.not_allowed'),
type: 'error'
});
throw err;

View File

@@ -6,12 +6,13 @@ function getCurrentUserId() {
}
const miscReq = {
getBundles(fileId) {
return request(`file/${fileId}`, {
getFile(params) {
return request(`file/${params.fileId}`, {
method: 'GET'
}).then((json) => {
const args = {
json
json,
params
};
return args;
});

View File

@@ -140,6 +140,24 @@ const worldReq = {
args.ref = worldStore.applyWorld(json);
return args;
});
},
uploadWorldImage(imageData) {
const params = {
tag: 'worldimage'
};
return request('file/image', {
uploadImage: true,
matchingDimensions: false,
postData: JSON.stringify(params),
imageData
}).then((json) => {
const args = {
json,
params
};
return args;
});
}
};

View File

@@ -4,20 +4,29 @@
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
import Vue from 'vue';
import { createApp } from 'vue';
import './bootstrap';
import { i18n } from './plugin/i18n';
import App from './App.vue';
import { pinia } from './stores';
import ElementPlus from 'element-plus';
import './app.scss';
import registerComponents from './plugin/components';
console.log(`isLinux: ${LINUX}`);
// #region | Hey look it's most of VRCX!
// prompt: 'Please clean up and refactor the VRCX codebase.'
const $app = new Vue({
pinia,
render: (h) => h(App)
}).$mount('#root');
const $app = createApp(App);
$app.use(pinia);
$app.use(i18n);
$app.use(ElementPlus);
registerComponents($app);
$app.mount('#root');
window.$app = $app;
export { $app };

View File

@@ -1,75 +0,0 @@
doctype html
#x-app.x-app(@dragenter.prevent @dragover.prevent @drop.prevent)
//- ### Login ###
Login(v-if='!watchState.isLoggedIn')
VRCXUpdateDialog
template(v-if='watchState.isLoggedIn')
//- ### Menu ###
NavMenu
//- ### Sidebar ###
Sidebar
//- ### Tabs ###
Feed
GameLog
PlayerList
Search
Favorites
FriendLog
Moderation
Notification
FriendList
Charts
Tools
Profile
Settings
//- ## Dialogs ## -\\
UserDialog
WorldDialog
AvatarDialog
GroupDialog
GroupMemberModerationDialog
GalleryDialog
FullscreenImageDialog
PreviousInstancesInfoDialog
LaunchDialog
LaunchOptionsDialog
FriendImportDialog
WorldImportDialog
AvatarImportDialog
ChooseFavoriteGroupDialog
EditInviteMessageDialog
VRChatConfigDialog
PrimaryPasswordDialog

View File

@@ -10,10 +10,12 @@
@use './assets/scss/flags.scss';
@use './assets/scss/animated-emoji.scss';
@use 'element-plus/theme-chalk/src/index.scss' as *;
@import '~animate.css/animate.min.css';
@import '~noty/lib/noty.css';
@import '~element-ui/lib/theme-chalk/index.css';
@import 'element-plus/theme-chalk/src/dark/css-vars.scss';
@import 'animate.css/animate.min.css';
@import 'noty/lib/noty.css';
@import 'remixicon/fonts/remixicon.css';
.color-palettes {
background: #409eff;
@@ -26,6 +28,10 @@
background: #c0c4cc;
}
.el-icon.is-loading {
animation: rotating 2s linear infinite;
}
.noty_layout {
word-break: break-all;
}
@@ -78,42 +84,46 @@
border-bottom: 1px solid #a0b55c;
}
.el-table + .pagination-bar {
margin-top: 15px;
// .el-table + .pagination-bar {
// margin-top: 15px;
// }
.el-table__expanded-cell[class*='cell'] {
padding: 20px 30px;
}
.el-table--mini .el-table__expanded-cell[class*='cell'] {
padding: 20px 50px;
.el-table--small .cell {
// fix expand arrow position
padding: 0px;
}
.el-table--mini .el-table__cell {
padding: 5px 0;
}
// .el-table--mini .el-table__cell {
// padding: 5px 0;
// }
// expand table cell on hover, TODO: replace with a better solution
.el-table .cell {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
// .el-table .cell {
// display: -webkit-box;
// -webkit-box-orient: vertical;
// -webkit-line-clamp: 1;
// }
.el-table__row:hover .el-table__cell .cell {
display: revert;
-webkit-line-clamp: unset;
}
// .el-table__row:hover .el-table__cell .cell {
// display: revert;
// -webkit-line-clamp: unset;
// }
.el-table th.is-sortable .cell {
display: flex;
align-items: center;
}
// .el-table th.is-sortable .cell {
// display: flex;
// align-items: center;
// }
.el-table .caret-wrapper {
margin-top: 4.5px;
}
// .el-table .caret-wrapper {
// margin-top: 4.5px;
// }
.notification-table .el-table .cell {
-webkit-line-clamp: 2;
}
// .notification-table .el-table .cell {
// -webkit-line-clamp: 2;
// }
.el-dialog__body {
padding: 20px;
@@ -146,6 +156,10 @@
unicode-range: U+2026;
}
html {
overflow: hidden;
}
body {
font-family:
'ellipsis-font', 'Noto Sans KR', 'Noto Sans JP', 'Noto Sans TC',
@@ -313,11 +327,11 @@ hr.x-vertical-divider {
order: 99;
}
.el-popper.x-quick-search {
width: 225px;
min-width: 0 !important;
border: none;
}
// .el-popper.x-quick-search {
// width: 225px;
// min-width: 0 !important;
// border: none;
// }
.el-popper.x-quick-search .el-select-dropdown__item {
width: 100%;
@@ -383,13 +397,13 @@ hr.x-vertical-divider {
border-radius: 2px;
}
.el-select-dropdown__item .x-friend-item:hover {
background: none;
border-radius: 0;
}
// .el-select-dropdown__item .x-friend-item:hover {
// background: none;
// border-radius: 0;
// }
.x-dialog .x-friend-item {
width: 175px;
width: 167px;
}
.x-friend-item > .avatar {
@@ -438,7 +452,7 @@ img.friends-list-avatar {
.x-friend-item > .avatar.joinme.mobile > img,
.x-friend-item > .avatar.askme.mobile > img,
.x-friend-item > .avatar.busy.mobile > img {
mask-image: url(assets/images/masks/usercutoutmobile.svg);
mask-image: url(./assets/images/masks/usercutoutmobile.svg);
}
.x-friend-item > .avatar.online.mobile::after,
@@ -452,7 +466,7 @@ img.friends-list-avatar {
height: 14px;
content: '';
border-radius: 0px;
mask-image: url(assets/images/masks/phone.svg);
mask-image: url(./assets/images/masks/phone.svg);
}
.x-friend-item > .avatar.active > img,
@@ -461,7 +475,7 @@ img.friends-list-avatar {
.x-friend-item > .avatar.askme > img,
.x-friend-item > .avatar.busy > img,
.x-friend-item > .avatar.offline > img {
mask-image: url(assets/images/masks/usercutout.svg);
mask-image: url(./assets/images/masks/usercutout.svg);
}
.x-friend-item > .avatar.active::after,
@@ -490,17 +504,17 @@ img.friends-list-avatar {
.x-friend-item > .avatar.joinme::after {
background: #409eff;
mask-image: url(assets/images/masks/joinme.svg);
mask-image: url(./assets/images/masks/joinme.svg);
}
.x-friend-item > .avatar.askme::after {
background: #ff9500;
mask-image: url(assets/images/masks/askme.svg);
mask-image: url(./assets/images/masks/askme.svg);
}
.x-friend-item > .avatar.busy::after {
background: #ff2c2c;
mask-image: url(assets/images/masks/busy.svg);
mask-image: url(./assets/images/masks/busy.svg);
}
.x-friend-item > .avatar.offline::after {
@@ -565,16 +579,11 @@ img.friends-list-avatar {
border-radius: 2px;
}
.x-change-image-item > img,
.x-change-image-item > .el-popover__reference-wrapper > img {
width: 240px;
height: 180px;
}
.current-image {
border: 2px solid #67c23a;
padding: 2px 2px 0 2px;
}
// .x-change-image-item > img,
// .x-change-image-item > .el-popover__reference-wrapper > img {
// width: 240px;
// height: 180px;
// }
.x-dialog > .el-dialog {
max-width: 100%;
@@ -595,12 +604,12 @@ img.friends-list-avatar {
padding: 20px;
}
.el-popper.hex {
min-width: auto;
padding: 10px;
font-family: monospace;
text-align: center;
}
// .el-popper.hex {
// min-width: auto;
// padding: 10px;
// font-family: monospace;
// text-align: center;
// }
i.x-user-status,
i.x-status-icon {
@@ -621,17 +630,17 @@ i.x-user-status.online {
i.x-user-status.joinme {
background: #409eff;
mask-image: url(assets/images/masks/joinme.svg);
mask-image: url(./assets/images/masks/joinme.svg);
}
i.x-user-status.askme {
background: #ff9500;
mask-image: url(assets/images/masks/askme.svg);
mask-image: url(./assets/images/masks/askme.svg);
}
i.x-user-status.busy {
background: #ff2c2c;
mask-image: url(assets/images/masks/busy.svg);
mask-image: url(./assets/images/masks/busy.svg);
}
i.x-status-icon.green {
@@ -651,67 +660,79 @@ i.x-status-icon.red {
}
.x-tag-friend {
color: rgb(255, 208, 0) !important;
color: rgb(255, 208, 0);
border-color: rgb(255, 208, 0) !important;
}
.x-tag-vrcplus {
color: rgb(255, 208, 0) !important;
color: rgb(255, 208, 0);
border-color: rgb(255, 208, 0) !important;
}
.x-tag-platform-pc {
color: #409eff !important;
color: #409eff;
border-color: #409eff !important;
}
.x-tag-platform-quest {
color: #67c23a !important;
color: #67c23a;
border-color: #67c23a !important;
}
.x-tag-platform-ios {
color: #c7c7ce !important;
color: #c7c7ce;
border-color: #c7c7ce !important;
}
.x-tag-platform-other {
color: #ff4177 !important;
color: #ff4177;
border-color: #ff4177 !important;
}
.x-tag-age-verification {
color: #5c70ec !important;
color: #5c70ec;
border-color: #5c70ec !important;
}
.x-tag-border-left {
border-left: 0.8px solid;
margin-left: 5px;
padding-left: 5px;
padding-bottom: 0.5px;
}
.x-grey {
color: #909399;
}
.el-tree-node {
white-space: normal;
.x-popover-image {
max-width: 100%;
max-height: 100%;
}
.el-tree-node__content {
height: auto;
}
// .el-tree-node {
// white-space: normal;
// }
.el-progress-bar {
padding-right: 80px;
margin-right: -85px;
}
// .el-tree-node__content {
// height: auto;
// }
.el-progress__text {
color: #c8c8c8;
}
// .el-progress-bar {
// padding-right: 80px;
// margin-right: -85px;
// }
.x-user-dialog .el-textarea__inner {
padding: 0;
background: none;
border: 0;
border-radius: 2px;
}
// .el-progress__text {
// color: #c8c8c8;
// }
// .x-user-dialog .el-textarea__inner {
// padding: 0;
// background: none;
// border: 0;
// border-radius: 2px;
// }
.options-container {
margin-top: 30px;
@@ -767,28 +788,28 @@ i.x-status-icon.red {
vertical-align: top;
}
.el-color-picker__trigger {
border: unset;
}
// .el-color-picker__trigger {
// border: unset;
// }
.el-color-picker__color {
border: 0.5px solid #999;
}
// .el-color-picker__color {
// border: 0.5px solid #999;
// }
.el-button--success {
background-color: #67c23a !important;
border-color: #67c23a !important;
}
// .el-button--success {
// background-color: #67c23a !important;
// border-color: #67c23a !important;
// }
.x-dialog .el-button--danger {
background-color: #f56c6c !important;
border-color: #f56c6c !important;
}
// .x-dialog .el-button--danger {
// background-color: #f56c6c !important;
// border-color: #f56c6c !important;
// }
.el-button--warning {
background-color: #e6a23c !important;
border-color: #e6a23c !important;
}
// .el-button--warning {
// background-color: #e6a23c !important;
// border-color: #e6a23c !important;
// }
.avatar-info {
margin-top: 2px;
@@ -812,14 +833,14 @@ i.x-status-icon.red {
color: #f56c6c;
}
.el-form-item {
margin-bottom: 4px;
}
// .el-form-item {
// margin-bottom: 4px;
// }
.photon-event-table .el-table--mini .el-table__cell,
.current-instance-table .el-table--mini .el-table__cell {
padding: 0;
}
// .photon-event-table .el-table--mini .el-table__cell,
// .current-instance-table .el-table--mini .el-table__cell {
// padding: 0;
// }
.photon-event-table {
margin-top: 20px;
@@ -835,14 +856,14 @@ i.x-status-icon.red {
margin-bottom: -1px;
}
.el-pagination .el-select .el-input .el-input__inner,
.el-input--mini .el-input__icon {
height: 22px;
}
// .el-pagination .el-select .el-input .el-input__inner,
// .el-input--mini .el-input__icon {
// height: 22px;
// }
.el-pagination .btn-next {
margin-right: 10px;
}
// .el-pagination .btn-next {
// margin-right: 10px;
// }
.el-dialog,
.el-message-box {
@@ -850,35 +871,31 @@ i.x-status-icon.red {
word-break: break-word;
}
.el-tabs__nav-wrap::after {
background-color: #e4e7ed14;
}
// .el-tabs__nav-wrap::after {
// background-color: #e4e7ed14;
// }
.dialog-title {
font-weight: bold;
}
.group-banner-image {
display: none;
}
.vrc-instance-queue-message {
padding: 3px;
top: 0 !important;
}
.vrc-instance-queue-message .el-message__content {
margin-right: 20px;
}
// .vrc-instance-queue-message .el-message__content {
// margin-right: 20px;
// }
.el-tabs {
display: flex;
flex-direction: column;
}
// .el-tabs {
// display: flex;
// flex-direction: column;
// }
.el-tabs__content {
flex: 1;
max-height: 100%;
// flex: 1;
// max-height: 100%;
overflow-y: auto;
}
@@ -889,60 +906,56 @@ i.x-status-icon.red {
font-size: 13px;
}
.el-tabs__header {
padding: 0 1px;
}
// .el-tabs__header {
// padding: 0 1px;
// }
.zero-margin-tabs .el-tabs__header {
margin-bottom: 0;
}
// .zero-margin-tabs .el-tabs__header {
// margin-bottom: 0;
// }
.x-friend-item .el-checkbox__inner,
.el-table__row .el-checkbox__inner {
width: 28px;
height: 28px;
border-radius: 4px;
}
// .x-friend-item .el-checkbox__inner,
// .el-table__row .el-checkbox__inner {
// width: 28px;
// height: 28px;
// border-radius: 4px;
// }
.x-friend-item .el-checkbox__inner::after,
.el-table__row .el-checkbox__inner::after {
width: 8px;
height: 14px;
left: 8px;
top: 2px;
}
// .x-friend-item .el-checkbox__inner::after,
// .el-table__row .el-checkbox__inner::after {
// width: 8px;
// height: 14px;
// left: 8px;
// top: 2px;
// }
.max-height-el-select .el-select-dropdown__wrap {
max-height: 83vh;
}
// .max-height-el-select .el-select-dropdown__wrap {
// max-height: 83vh;
// }
.el-pagination .el-input .el-input__icon {
line-height: 22px;
}
// .el-pagination .el-input .el-input__icon {
// line-height: 22px;
// }
// User dialog memo: tag line-height
.el-dialog__body .el-tag--mini {
line-height: 17px;
}
// .el-dialog__body .el-tag--mini {
// line-height: 17px;
// }
// feed table detail time tag line-height
.el-table__expanded-cell .el-tag--mini {
line-height: 18px;
}
// .el-table__expanded-cell .el-tag--mini {
// line-height: 18px;
// }
// User dialog memo: input count background color
.x-friend-item:hover .el-input__count {
background: #f0f0f0;
}
// .x-friend-item:hover .el-input__count {
// background: #f0f0f0;
// }
// Align the left page with the right friend bar
.x-app > .x-container {
padding-top: 15px;
}
.el-collapse-item .el-tag--mini {
line-height: 17px;
}
// .el-collapse-item .el-tag--mini {
// line-height: 17px;
// }
.x-text-removed {
text-decoration: line-through;
@@ -959,21 +972,38 @@ i.x-status-icon.red {
border-radius: 4px;
}
.el-carousel__mask {
// looks bad
display: none;
// expand table cell on hover, TODO: replace with a better solution
.el-table .cell {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.el-table__row:hover .el-table__cell .cell {
display: revert;
-webkit-line-clamp: unset;
}
// .el-carousel__mask {
// // looks bad
// display: none;
// }
// FIXME
// Something changed the CSS of element-ui
// The other parts are the same
// It feels like can't fundamentally modify it
// And can only fix it bit by bit by overriding
.el-switch__core:after {
top: 1.5px;
}
// .el-switch__core:after {
// top: 1.5px;
// }
.el-popover {
text-align: left;
word-break: break-word;
// .el-popover {
// text-align: left;
// word-break: break-word;
// }
.el-dropdown-menu__item:not(.is-disabled):focus {
// dropdown item focus blue background looks bad
background-color: unset;
}

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -3,7 +3,7 @@
--offy: calc(var(--offx) / 72 * 52);
}
.flags {
background: url('/images/flags.png') no-repeat;
background: url('../images/flags.png') no-repeat;
background-size: calc(var(--offx) * 6);
width: var(--offx);
height: var(--offy);

View File

@@ -145,7 +145,7 @@ button {
div.x-friend-list
> div:nth-child(1)
> div
> div.el-textarea.el-input--mini
> div.el-textarea.el-input--small
> span.el-input__count {
background-color: $--theme-bg-4;
}
@@ -168,7 +168,7 @@ div.x-friend-list
background-color: $--theme-bg-5;
}
.el-collapse-item .el-tag--mini {
.el-collapse-item .el-tag--small {
background-color: $--theme-bg-5;
border: transparent;
}
@@ -220,11 +220,11 @@ div.x-friend-list
background-color: hsl($--theme-hue, $--theme-saturation, 27%);
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow::after {
border-bottom-color: $--theme-bg-5;
}
.el-popper[x-placement^='bottom'] .popper__arrow {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow {
border-bottom-color: $--theme-border-2;
}
@@ -303,11 +303,11 @@ div.x-friend-list
border-color: hsl($--theme-hue, $--theme-saturation, 37%);
}
.el-popper[x-placement^='right'] .popper__arrow::after {
.el-popper[data-popper-placement^='right'] .el-popper__arrow::after {
border-right-color: hsl($--theme-hue, $--theme-saturation, 37%);
}
.el-popper[x-placement^='right'] .popper__arrow {
.el-popper[data-popper-placement^='right'] .el-popper__arrow {
border-right-color: hsl($--theme-hue, $--theme-saturation, 37%);
}

View File

@@ -34,7 +34,104 @@ $--theme-warning: #e6a23c;
$--theme-danger: #f56c6c;
$--theme-info: #909399;
// Mapping
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--dropdown-menu-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': $--theme-text-1,
'black': $--theme-bg-1,
'primary': (
'base': $--theme-primary
),
'success': (
'base': $--theme-success
),
'warning': (
'base': $--theme-warning
),
'danger': (
'base': $--theme-danger
),
'error': (
'base': $--theme-danger
),
'info': (
'base': $--theme-info
)
),
$text-color: (
'primary': $--theme-text-1,
'regular': $--theme-text-2,
'secondary': $--theme-info,
'placeholder': $--theme-text-4,
'disabled': $--theme-text-4
),
$border-color: (
'': $--theme-border-1,
'light': $--theme-border-2,
'lighter': $--border-color-lighter,
'extra-light': $--border-color-extra-light,
'dark': $--theme-border-1,
'darker': $--theme-border-2
),
$fill-color: (
'': $--background-color-base,
'light': $--theme-bg-4,
'lighter': $--theme-bg-5,
'extra-light': $--border-color-extra-light,
'dark': $--theme-bg-2,
'darker': $--theme-bg-1,
'blank': $--theme-bg-4
),
$bg-color: (
'': $--theme-bg-2,
'page': $--theme-bg-1,
'overlay': $--theme-bg-4
),
$box-shadow: (
'': (
0px 12px 32px 4px rgba(0, 0, 0, 0.36),
0px 8px 20px rgba(0, 0, 0, 0.72)
),
'light': (
0px 0px 12px rgba(0, 0, 0, 0.72)
),
'lighter': (
0px 0px 6px rgba(0, 0, 0, 0.72)
),
'dark': (
0px 16px 48px 16px rgba(0, 0, 0, 0.72),
0px 12px 32px rgba(0, 0, 0, 0.72),
0px 8px 16px -8px rgba(0, 0, 0, 0.96)
)
),
$disabled: (
'bg-color': $--theme-bg-5,
'text-color': $--theme-text-4,
'border-color': $--theme-border-2
)
);
$--color-primary: $--theme-primary;
$--color-success: $--theme-success;
@@ -52,10 +149,6 @@ $--color-text-placeholder: $--theme-text-4;
$--border-color-base: $--theme-border-1;
$--border-color-light: $--theme-bg-5;
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--button-default-background-color: $--theme-bg-5;
$--button-default-border-color: $--theme-border-2;
@@ -64,20 +157,13 @@ $--button-default-font-color: $--theme-text-2;
$--input-background-color: $--theme-bg-5;
$--input-border-color: $--theme-border-1;
$--input-font-color: $--theme-text-1;
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: $--input-focus-border;
$--dialog-background-color: $--theme-bg-4;
$--popover-background-color: $--theme-bg-4;
$--table-border-color: $--theme-border-2;
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--pagination-font-color: $--theme-text-2;
$--pagination-background-color: $--theme-bg-4;
@@ -95,21 +181,11 @@ $--datepicker-header-font-color: $--theme-text-1;
$--datepicker-icon-color: $--theme-text-1;
$--skeleton-color: $--theme-bg-5;
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-dropdown-background: $--theme-bg-4;
$--select-dropdown-border: 1px solid $--theme-border-1;
$--dropdown-menu-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--tree-font-color: $--theme-text-2;
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--menu-item-font-color: $--theme-text-2;
$--menu-background-color: $--theme-bg-2;
@@ -118,10 +194,8 @@ $--collapse-header-background-color: $--theme-bg-5;
$--collapse-content-background-color: $--theme-bg-4;
$--collapse-border-color: $--theme-border-2;
$--collapse-header-font-color: $--theme-text-2;
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-background-color: $--theme-bg-5;
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--message-close-hover-color: $--theme-text-2;
$--message-success-font-color: #52c41a;
$--message-info-font-color: #1890ff;
@@ -130,13 +204,8 @@ $--message-danger-font-color: #ff4d4f;
$--pagination-hover-color: $--theme-text-4;
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
$--card-background-color: $--theme-bg-4;
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';
:root {
--group-calendar-event-bg: #{$--calendar-selected-background-color};
--group-calendar-badge-following: #{darken($--theme-primary, 20%)};
@@ -218,35 +287,35 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
background-color: #{darken($--theme-primary, 20%)} !important;
}
.el-popper[x-placement^='top'] .popper__arrow {
.el-popper[data-popper-placement^='top'] .el-popper__arrow {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='top'] .popper__arrow::after {
.el-popper[data-popper-placement^='top'] .el-popper__arrow::after {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow::after {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow {
.el-popper[data-popper-placement^='left'] .el-popper__arrow {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow::after {
.el-popper[data-popper-placement^='left'] .el-popper__arrow::after {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow {
.el-popper[data-popper-placement^='right'] .el-popper__arrow {
border-right-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow::after {
.el-popper[data-popper-placement^='right'] .el-popper__arrow::after {
border-right-color: $--theme-bg-4 !important;
}

View File

@@ -32,7 +32,105 @@ $--theme-warning: #e6a23c;
$--theme-danger: #f56c6c;
$--theme-info: #909399;
// Mapping
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--dropdown-menu-box-shadow: 0 2px 12px
hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': $--theme-text-1,
'black': $--theme-bg-1,
'primary': (
'base': $--theme-primary
),
'success': (
'base': $--theme-success
),
'warning': (
'base': $--theme-warning
),
'danger': (
'base': $--theme-danger
),
'error': (
'base': $--theme-danger
),
'info': (
'base': $--theme-info
)
),
$text-color: (
'primary': $--theme-text-1,
'regular': $--theme-text-2,
'secondary': $--theme-info,
'placeholder': $--theme-text-4,
'disabled': $--theme-text-4
),
$border-color: (
'': $--theme-border-1,
'light': $--theme-border-2,
'lighter': $--border-color-lighter,
'extra-light': $--border-color-extra-light,
'dark': $--theme-border-1,
'darker': $--theme-border-2
),
$fill-color: (
'': $--background-color-base,
'light': $--theme-bg-4,
'lighter': $--theme-bg-5,
'extra-light': $--border-color-extra-light,
'dark': $--theme-bg-2,
'darker': $--theme-bg-1,
'blank': $--theme-bg-4
),
$bg-color: (
'': $--theme-bg-2,
'page': $--theme-bg-1,
'overlay': $--theme-bg-4
),
$box-shadow: (
'': (
0px 12px 32px 4px rgba(0, 0, 0, 0.36),
0px 8px 20px rgba(0, 0, 0, 0.72)
),
'light': (
0px 0px 12px rgba(0, 0, 0, 0.72)
),
'lighter': (
0px 0px 6px rgba(0, 0, 0, 0.72)
),
'dark': (
0px 16px 48px 16px rgba(0, 0, 0, 0.72),
0px 12px 32px rgba(0, 0, 0, 0.72),
0px 8px 16px -8px rgba(0, 0, 0, 0.96)
)
),
$disabled: (
'bg-color': $--theme-bg-5,
'text-color': $--theme-text-4,
'border-color': $--theme-border-2
)
);
$--color-primary: $--theme-primary;
$--color-success: $--theme-success;
@@ -50,10 +148,6 @@ $--color-text-placeholder: $--theme-text-4;
$--border-color-base: $--theme-border-1;
$--border-color-light: $--theme-bg-5;
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--button-default-background-color: $--theme-bg-5;
$--button-default-border-color: $--theme-border-2;
@@ -62,20 +156,13 @@ $--button-default-font-color: $--theme-text-2;
$--input-background-color: $--theme-bg-5;
$--input-border-color: $--theme-border-1;
$--input-font-color: $--theme-text-1;
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: $--input-focus-border;
$--dialog-background-color: $--theme-bg-4;
$--popover-background-color: $--theme-bg-4;
$--table-border-color: $--theme-border-2;
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--pagination-font-color: $--theme-text-2;
$--pagination-background-color: $--theme-bg-4;
@@ -93,22 +180,11 @@ $--datepicker-header-font-color: $--theme-text-1;
$--datepicker-icon-color: $--theme-text-1;
$--skeleton-color: $--theme-bg-5;
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-dropdown-background: $--theme-bg-4;
$--select-dropdown-border: 1px solid $--theme-border-1;
$--dropdown-menu-box-shadow: 0 2px 12px
hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--tree-font-color: $--theme-text-2;
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--menu-item-font-color: $--theme-text-2;
$--menu-background-color: $--theme-bg-2;
@@ -117,10 +193,8 @@ $--collapse-header-background-color: $--theme-bg-5;
$--collapse-content-background-color: $--theme-bg-4;
$--collapse-border-color: $--theme-border-2;
$--collapse-header-font-color: $--theme-text-2;
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-background-color: $--theme-bg-5;
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--message-close-hover-color: $--theme-text-2;
$--message-success-font-color: #52c41a;
$--message-info-font-color: #1890ff;
@@ -129,13 +203,8 @@ $--message-danger-font-color: #ff4d4f;
$--pagination-hover-color: $--theme-text-4;
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
$--card-background-color: $--theme-bg-4;
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';
:root {
--group-calendar-event-bg: #{$--calendar-selected-background-color};
--group-calendar-badge-following: #{$--color-success};
@@ -170,35 +239,35 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
background-color: $--theme-border-2 !important;
}
.el-popper[x-placement^='top'] .popper__arrow {
.el-popper[data-popper-placement^='top'] .el-popper__arrow {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='top'] .popper__arrow::after {
.el-popper[data-popper-placement^='top'] .el-popper__arrow::after {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow::after {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow {
.el-popper[data-popper-placement^='left'] .el-popper__arrow {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow::after {
.el-popper[data-popper-placement^='left'] .el-popper__arrow::after {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow {
.el-popper[data-popper-placement^='right'] .el-popper__arrow {
border-right-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow::after {
.el-popper[data-popper-placement^='right'] .el-popper__arrow::after {
border-right-color: $--theme-bg-4 !important;
}

View File

@@ -32,7 +32,105 @@ $--theme-warning: #e6a23c;
$--theme-danger: #f56c6c;
$--theme-info: #909399;
// Mapping
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--dropdown-menu-box-shadow: 0 2px 12px
hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': $--theme-text-1,
'black': $--theme-bg-1,
'primary': (
'base': $--theme-primary
),
'success': (
'base': $--theme-success
),
'warning': (
'base': $--theme-warning
),
'danger': (
'base': $--theme-danger
),
'error': (
'base': $--theme-danger
),
'info': (
'base': $--theme-info
)
),
$text-color: (
'primary': $--theme-text-1,
'regular': $--theme-text-2,
'secondary': $--theme-info,
'placeholder': $--theme-text-4,
'disabled': $--theme-text-4
),
$border-color: (
'': $--theme-border-1,
'light': $--theme-border-2,
'lighter': $--border-color-lighter,
'extra-light': $--border-color-extra-light,
'dark': $--theme-border-1,
'darker': $--theme-border-2
),
$fill-color: (
'': $--background-color-base,
'light': $--theme-bg-4,
'lighter': $--theme-bg-5,
'extra-light': $--border-color-extra-light,
'dark': $--theme-bg-2,
'darker': $--theme-bg-1,
'blank': $--theme-bg-4
),
$bg-color: (
'': $--theme-bg-2,
'page': $--theme-bg-1,
'overlay': $--theme-bg-4
),
$box-shadow: (
'': (
0px 12px 32px 4px hsla($--theme-hue, $--theme-saturation, 0%, 0.36),
0px 8px 20px hsla($--theme-hue, $--theme-saturation, 0%, 0.72)
),
'light': (
0px 0px 12px hsla($--theme-hue, $--theme-saturation, 0%, 0.72)
),
'lighter': (
0px 0px 6px hsla($--theme-hue, $--theme-saturation, 0%, 0.72)
),
'dark': (
0px 16px 48px 16px hsla($--theme-hue, $--theme-saturation, 0%, 0.72),
0px 12px 32px hsla($--theme-hue, $--theme-saturation, 0%, 0.72),
0px 8px 16px -8px hsla($--theme-hue, $--theme-saturation, 0%, 0.96)
)
),
$disabled: (
'bg-color': $--theme-bg-5,
'text-color': $--theme-text-4,
'border-color': $--theme-border-2
)
);
$--color-primary: $--theme-primary;
$--color-success: $--theme-success;
@@ -50,10 +148,6 @@ $--color-text-placeholder: $--theme-text-4;
$--border-color-base: $--theme-border-1;
$--border-color-light: $--theme-bg-5;
$--border-color-lighter: hsl($--theme-hue, $--theme-saturation, 16%);
$--border-color-extra-light: hsl($--theme-hue, $--theme-saturation, 15%);
$--background-color-base: hsl($--theme-hue, $--theme-saturation, 16%);
$--button-default-background-color: $--theme-bg-5;
$--button-default-border-color: $--theme-border-2;
@@ -62,20 +156,13 @@ $--button-default-font-color: $--theme-text-2;
$--input-background-color: $--theme-bg-5;
$--input-border-color: $--theme-border-1;
$--input-font-color: $--theme-text-1;
$--input-focus-border: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-input-focus-border-color: $--input-focus-border;
$--dialog-background-color: $--theme-bg-4;
$--popover-background-color: $--theme-bg-4;
$--table-border-color: $--theme-border-2;
$--table-header-background-color: hsl($--theme-hue, $--theme-saturation, 15%);
$--table-row-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
18%
);
$--pagination-font-color: $--theme-text-2;
$--pagination-background-color: $--theme-bg-4;
@@ -93,22 +180,11 @@ $--datepicker-header-font-color: $--theme-text-1;
$--datepicker-icon-color: $--theme-text-1;
$--skeleton-color: $--theme-bg-5;
$--skeleton-to-color: hsl($--theme-hue, $--theme-saturation, 33%);
$--select-dropdown-background: $--theme-bg-4;
$--select-dropdown-border: 1px solid $--theme-border-1;
$--dropdown-menu-box-shadow: 0 2px 12px
hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--box-shadow-base: 0 1px 2px hsla($--theme-hue, $--theme-saturation, 0%, 0.1);
$--box-shadow-dark: 0 1px 3px hsla($--theme-hue, $--theme-saturation, 0%, 0.15);
$--tree-font-color: $--theme-text-2;
$--tree-node-hover-background-color: hsl(
$--theme-hue,
$--theme-saturation,
16%
);
$--menu-item-font-color: $--theme-text-2;
$--menu-background-color: $--theme-bg-2;
@@ -117,10 +193,8 @@ $--collapse-header-background-color: $--theme-bg-5;
$--collapse-content-background-color: $--theme-bg-4;
$--collapse-border-color: $--theme-border-2;
$--collapse-header-font-color: $--theme-text-2;
$--collapse-content-font-color: hsl($--theme-hue, $--theme-saturation, 66%);
$--message-background-color: $--theme-bg-5;
$--message-close-icon-color: hsl($--theme-hue, $--theme-saturation, 60%);
$--message-close-hover-color: $--theme-text-2;
$--message-success-font-color: #52c41a;
$--message-info-font-color: #1890ff;
@@ -129,13 +203,8 @@ $--message-danger-font-color: #ff4d4f;
$--pagination-hover-color: $--theme-text-4;
$--calendar-selected-background-color: rgba($--theme-primary, 0.1);
$--card-background-color: $--theme-bg-4;
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';
:root {
--group-calendar-event-bg: #{$--calendar-selected-background-color};
--group-calendar-badge-following: #{$--color-success};
@@ -170,35 +239,35 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
background-color: $--theme-border-2 !important;
}
.el-popper[x-placement^='top'] .popper__arrow {
.el-popper[data-popper-placement^='top'] .el-popper__arrow {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='top'] .popper__arrow::after {
.el-popper[data-popper-placement^='top'] .el-popper__arrow::after {
border-top-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow::after {
border-bottom-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow {
.el-popper[data-popper-placement^='left'] .el-popper__arrow {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='left'] .popper__arrow::after {
.el-popper[data-popper-placement^='left'] .el-popper__arrow::after {
border-left-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow {
.el-popper[data-popper-placement^='right'] .el-popper__arrow {
border-right-color: $--theme-bg-4 !important;
}
.el-popper[x-placement^='right'] .popper__arrow::after {
.el-popper[data-popper-placement^='right'] .el-popper__arrow::after {
border-right-color: $--theme-bg-4 !important;
}

View File

@@ -598,7 +598,7 @@ img.friends-list-avatar {
border-radius: var(--dv_lg-rounded) !important;
}
.el-tag--mini {
.el-tag--small {
height: 30px;
padding: 5px 15px;
font-size: 10pt;
@@ -627,7 +627,7 @@ img.friends-list-avatar {
background-color: var(--dv_muted);
}
.el-input--mini .el-textarea__inner:hover {
.el-input--small .el-textarea__inner:hover {
background-color: transparent !important;
}
@@ -730,7 +730,7 @@ i[class='el-icon-star-off']:not(.el-menu-item div.el-tooltip i) {
background-color: transparent;
}
.el-collapse-item .el-tag--mini {
.el-collapse-item .el-tag--small {
border: transparent;
background-color: var(--dv_bg-bot);
}
@@ -786,7 +786,7 @@ i[class='el-icon-star-off']:not(.el-menu-item div.el-tooltip i) {
div.x-friend-list
> div:nth-child(1)
> div
> div.el-textarea.el-input--mini
> div.el-textarea.el-input--small
> span.el-input__count {
background-color: var(--mid) !important;
}
@@ -831,3 +831,27 @@ div.x-friend-list
.group-header {
border-bottom: 2px solid var(--dv_muted) !important;
}
.el-select__wrapper {
background-color: var(--dv_bg-top) !important;
border: 1px solid var(--dv_muted) !important;
color: var(--dv_bright);
}
.el-select__wrapper.is-focused {
border-color: var(--dv_bright) !important;
}
.el-select__wrapper.is-disabled {
background-color: var(--dv_bg-bot) !important;
color: var(--dv_muted) !important;
}
.el-input__wrapper {
background-color: var(--dv_bg-top) !important;
border: 1px solid var(--dv_muted) !important;
}
.el-input__wrapper.is-focus {
border-color: var(--dv_bright) !important;
}

View File

@@ -325,7 +325,7 @@ path[stroke='#e5e9f2'] {
stroke: var(--farback) !important;
}
.el-collapse-item .el-tag--mini {
.el-collapse-item .el-tag--small {
border: transparent;
background-color: #333 !important;
}
@@ -372,7 +372,7 @@ path[stroke='#e5e9f2'] {
div.x-friend-list
> div:nth-child(1)
> div
> div.el-textarea.el-input--mini
> div.el-textarea.el-input--small
> span.el-input__count {
background-color: var(--mid) !important;
}
@@ -402,3 +402,27 @@ div.x-friend-list
.el-timeline-item__node {
background-color: var(--theme-text-muted) !important;
}
.el-select__wrapper {
background-color: var(--top) !important;
border: 1px solid var(--top-border) !important;
color: var(--theme-text);
}
.el-select__wrapper.is-focused {
border-color: var(--theme-text) !important;
}
.el-select__wrapper.is-disabled {
background-color: var(--farback) !important;
color: var(--theme-text-muted) !important;
}
.el-input__wrapper {
background-color: var(--top) !important;
border: 1px solid var(--top-border) !important;
}
.el-input__wrapper.is-focus {
border-color: var(--theme-text) !important;
}

View File

@@ -1651,35 +1651,35 @@ img.x-link.el-popover__reference {
border-color: rgb(var(--md-sys-color-outline-variant)) !important;
}
.el-popper[x-placement^='top'] .popper__arrow {
.el-popper[data-popper-placement^='top'] .el-popper__arrow {
border-top-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='top'] .popper__arrow::after {
.el-popper[data-popper-placement^='top'] .el-popper__arrow::after {
border-top-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow {
border-bottom-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='bottom'] .popper__arrow::after {
.el-popper[data-popper-placement^='bottom'] .el-popper__arrow::after {
border-bottom-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='left'] .popper__arrow {
.el-popper[data-popper-placement^='left'] .el-popper__arrow {
border-left-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='left'] .popper__arrow::after {
.el-popper[data-popper-placement^='left'] .el-popper__arrow::after {
border-left-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='right'] .popper__arrow {
.el-popper[data-popper-placement^='right'] .el-popper__arrow {
border-right-color: var(--md-sys-color-surface-3) !important;
}
.el-popper[x-placement^='right'] .popper__arrow::after {
.el-popper[data-popper-placement^='right'] .el-popper__arrow::after {
border-right-color: var(--md-sys-color-surface-3) !important;
}
@@ -2065,7 +2065,7 @@ i.x-user-status {
text-align: center !important;
}
.el-collapse-item .el-tag--mini {
.el-collapse-item .el-tag--small {
background-color: transparent;
border: transparent;
padding-top: 6px;
@@ -2074,7 +2074,7 @@ i.x-user-status {
justify-content: space-between;
}
.el-dialog__body .el-tag--mini {
.el-dialog__body .el-tag--small {
line-height: 28px;
}
.el-divider {
@@ -2149,7 +2149,7 @@ i.x-user-status {
div.x-friend-list
> div:nth-child(1)
> div
> div.el-textarea.el-input--mini
> div.el-textarea.el-input--small
> span.el-input__count {
background-color: var(--md-sys-color-surface-3) !important;
}
@@ -2192,3 +2192,44 @@ div.x-friend-list
.group-header {
border-bottom: 2px solid rgba(var(--md-sys-color-outline), 0.5) !important;
}
.el-select__wrapper {
background-color: rgb(var(--md-sys-color-surface-variant)) !important;
border: 1px solid rgb(var(--md-sys-color-outline-variant)) !important;
}
.el-select__wrapper.is-focused {
border-color: rgb(var(--md-sys-color-primary)) !important;
}
.el-select__wrapper.is-disabled {
background-color: rgba(var(--md-sys-color-on-surface), 0.12) !important;
color: rgba(var(--md-sys-color-on-surface), 0.38) !important;
}
.el-input__wrapper {
background-color: rgb(var(--md-sys-color-surface-variant)) !important;
border: 1px solid rgb(var(--md-sys-color-outline-variant)) !important;
}
.el-input__wrapper.is-focus {
border-color: rgb(var(--md-sys-color-primary)) !important;
}
.el-table {
background-color: rgb(var(--md-sys-color-surface)) !important;
}
.el-table tr,
.el-table td.el-table__cell,
.el-table th.el-table__cell {
background-color: transparent !important;
}
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
background-color: rgba(var(--md-sys-color-primary), 0.05) !important;
}
.el-table__body-wrapper .el-table__row:hover > .el-table__cell {
background-color: rgba(var(--md-sys-color-primary), 0.08) !important;
}

View File

@@ -375,7 +375,7 @@ input[type='checkbox']:checked + .el-switch__core {
background-color: var(--lighter-lighter-bg);
}
.el-collapse-item .el-tag--mini {
.el-collapse-item .el-tag--small {
border: transparent;
background-color: var(--lighter-lighter-bg);
}
@@ -506,3 +506,45 @@ input[type='checkbox']:checked + .el-switch__core {
.group-header {
border-bottom: 2px solid var(--lighter-bg) !important;
}
.el-select__wrapper {
background-color: var(--light-bg) !important;
border: 1px solid var(--lighter-bg) !important;
color: var(--theme);
}
.el-select__wrapper.is-focused {
border-color: var(--theme) !important;
}
.el-select__wrapper.is-disabled {
background-color: var(--bg) !important;
color: var(--lighter-border) !important;
}
.el-input__wrapper {
background-color: var(--light-bg) !important;
border: 1px solid var(--lighter-bg) !important;
}
.el-input__wrapper.is-focus {
border-color: var(--theme) !important;
}
.el-table {
background-color: var(--bg) !important;
}
.el-table tr,
.el-table td.el-table__cell,
.el-table th.el-table__cell {
background-color: transparent !important;
}
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
background-color: var(--light-bg) !important;
}
.el-table__body-wrapper .el-table__row:hover > .el-table__cell {
background-color: var(--lighter-bg) !important;
}

44
src/bootstrap.js vendored
View File

@@ -1,16 +1,6 @@
import '@fontsource/noto-sans-kr';
import '@fontsource/noto-sans-jp';
import '@fontsource/noto-sans-sc';
import '@fontsource/noto-sans-tc';
import 'remixicon/fonts/remixicon.css';
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,
@@ -18,8 +8,10 @@ import {
refreshCustomScript,
systemIsDarkMode
} from './shared/utils';
import { i18n } from './plugin';
import { i18n } from './plugin/i18n';
import { initNoty } from './plugin/noty';
import './plugin/ipc';
import './plugin/dayjs';
initNoty(false);
@@ -42,7 +34,7 @@ function setLoginContainerStyle(isDarkMode) {
background-color: ${backgroundColor} !important;
transition: background-color 0.3s ease;
}
.x-login-container .el-input__inner {
background-color: ${inputBackgroundColor} !important;
border: ${inputBorder} !important;
@@ -88,33 +80,7 @@ try {
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

@@ -0,0 +1,244 @@
<template>
<div class="data-table-wrapper">
<el-table
ref="tableRef"
v-loading="loading"
:data="paginatedData"
v-bind="mergedTableProps"
default-sort-prop="created_at"
default-sort-order="descending"
lazy
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
@row-click="handleRowClick">
<slot></slot>
</el-table>
<div v-if="showPagination" class="pagination-wrapper">
<el-pagination
:current-page="internalCurrentPage"
:page-size="internalPageSize"
:total="filteredData.length"
v-bind="mergedPaginationProps"
@size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</div>
</template>
<script>
import { computed, ref, watch, toRefs } from 'vue';
export default {
name: 'DataTable',
props: {
data: {
type: Array,
default: () => []
},
tableProps: {
type: Object,
default: () => ({})
},
paginationProps: {
type: Object,
default: () => ({})
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 20
},
filters: {
type: [Array, Object],
default: () => []
},
loading: {
type: Boolean,
default: false
},
layout: {
type: String,
default: 'table, pagination'
},
defaultSort: {
type: Object,
default: () => ({ prop: 'created_at', order: 'descending' })
}
},
emits: [
'update:currentPage',
'update:pageSize',
'size-change',
'current-change',
'selection-change',
'row-click',
'filtered-data'
],
setup(props, { emit }) {
const { data, currentPage, pageSize, tableProps, paginationProps, filters } = toRefs(props);
const internalCurrentPage = ref(currentPage.value);
const internalPageSize = ref(pageSize.value);
const sortData = ref({
prop: props.defaultSort?.prop || null,
order: props.defaultSort?.order || null
});
const showPagination = computed(() => {
return props.layout.includes('pagination');
});
const mergedTableProps = computed(() => ({
stripe: true,
...tableProps.value
}));
const mergedPaginationProps = computed(() => ({
layout: 'sizes, prev, pager, next, total',
pageSizes: [20, 50, 100, 200],
small: true,
...paginationProps.value
}));
const applyFilter = function (row, filter) {
if (Array.isArray(filter.prop)) {
return filter.prop.some((propItem) => applyFilter(row, { prop: propItem, value: filter.value }));
}
const cellValue = row[filter.prop];
if (cellValue === undefined || cellValue === null) return false;
if (Array.isArray(filter.value)) {
return filter.value.some((val) =>
String(cellValue).toLowerCase().includes(String(val).toLowerCase())
);
} else {
return String(cellValue).toLowerCase().includes(String(filter.value).toLowerCase());
}
};
const filteredData = computed(() => {
let result = [...data.value];
if (filters.value && Array.isArray(filters.value) && filters.value.length > 0) {
filters.value.forEach((filter) => {
if (filter.value && (!Array.isArray(filter.value) || filter.value.length > 0)) {
result = result.filter((row) => applyFilter(row, filter));
}
});
}
if (sortData.value.prop && sortData.value.order) {
const { prop, order } = sortData.value;
result.sort((a, b) => {
const aVal = a[prop];
const bVal = b[prop];
let comparison = 0;
if (aVal == null && bVal == null) return 0;
if (aVal == null) return 1;
if (bVal == null) return -1;
if (typeof aVal === 'number' && typeof bVal === 'number') {
comparison = aVal - bVal;
} else if (aVal instanceof Date && bVal instanceof Date) {
comparison = aVal.getTime() - bVal.getTime();
} else {
const aStr = String(aVal).toLowerCase();
const bStr = String(bVal).toLowerCase();
if (aStr > bStr) comparison = 1;
else if (aStr < bStr) comparison = -1;
}
return order === 'descending' ? -comparison : comparison;
});
}
emit('filtered-data', result);
return result;
});
const paginatedData = computed(() => {
if (!showPagination.value) {
return filteredData.value;
}
const start = (internalCurrentPage.value - 1) * internalPageSize.value;
const end = start + internalPageSize.value;
return filteredData.value.slice(start, end);
});
const handleSortChange = ({ prop, order }) => {
sortData.value = { prop, order };
};
const handleSelectionChange = (selection) => {
emit('selection-change', selection);
};
const handleRowClick = (row, column, event) => {
emit('row-click', row, column, event);
};
const handleSizeChange = (size) => {
internalPageSize.value = size;
};
const handleCurrentChange = (page) => {
internalCurrentPage.value = page;
};
watch(currentPage, (newVal) => {
internalCurrentPage.value = newVal;
});
watch(pageSize, (newVal) => {
internalPageSize.value = newVal;
});
watch(
() => props.defaultSort,
(newSort) => {
if (newSort) {
sortData.value = {
prop: newSort.prop,
order: newSort.order
};
}
},
{ immediate: true }
);
return {
internalCurrentPage,
internalPageSize,
showPagination,
mergedTableProps,
mergedPaginationProps,
filteredData,
paginatedData,
handleSortChange,
handleSelectionChange,
handleRowClick,
handleSizeChange,
handleCurrentChange
};
}
};
</script>
<style scoped>
.data-table-wrapper {
width: 100%;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: space-around;
}
</style>

View File

@@ -4,7 +4,7 @@
<div
class="avatar"
:class="isFriendActiveOrOffline ? undefined : userStatusClass(friend.ref, friend.pendingOffline)">
<img v-lazy="userImage(friend.ref, true)" />
<img :src="userImage(friend.ref, true)" loading="lazy" />
</div>
<div class="detail">
<span v-if="!hideNicknames && friend.$nickName" class="name" :style="{ color: friend.ref.$userColour }">
@@ -17,21 +17,18 @@
<span v-if="isFriendActiveOrOffline" class="extra">{{ friend.ref.statusDescription }}</span>
<template v-else>
<span v-if="friend.pendingOffline" class="extra">
<i class="el-icon-warning-outline" /> {{ $t('side_panel.pending_offline') }}
<el-icon><WarningFilled /></el-icon> {{ t('side_panel.pending_offline') }}
</span>
<template v-else-if="isGroupByInstance">
<i v-if="isFriendTraveling" class="el-icon el-icon-loading"></i>
<el-icon v-if="isFriendTraveling" class="is-loading" style="margin-right: 3px"
><Loading
/></el-icon>
<Timer
class="extra"
:epoch="epoch"
:style="isFriendTraveling ? { display: 'inline-block', overflow: 'unset' } : undefined" />
</template>
<Location
v-else
class="extra"
:location="friend.ref.location"
:traveling="friend.ref.travelingToLocation"
:link="false" />
<Location v-else class="extra" :location="locationProp" :traveling="travelingProp" :link="false" />
</template>
</div>
</template>
@@ -39,15 +36,15 @@
<span>{{ friend.name || friend.id }}</span>
<el-button
ttype="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
style="margin-left: 5px"
@click.stop="$emit('confirm-delete-friend', friend.id)">
</el-button>
</template>
<el-skeleton v-else animated class="skeleton" :throttle="100">
<template slot="template">
<template #template>
<div>
<el-skeleton-item variant="circle" />
<div>
@@ -61,8 +58,10 @@
</template>
<script setup>
import { WarningFilled, Close, Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { userImage, userStatusClass } from '../shared/utils';
import { useAppearanceSettingsStore, useFriendStore } from '../stores';
@@ -71,21 +70,27 @@
isGroupByInstance: Boolean
});
defineEmits(['click', 'confirm-delete-friend']);
const { hideNicknames } = storeToRefs(useAppearanceSettingsStore());
const { isRefreshFriendsLoading } = storeToRefs(useFriendStore());
const { t } = useI18n();
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
);
const locationProp = computed(() => props.friend.ref?.location || '');
const travelingProp = computed(() => props.friend.ref?.travelingToLocation || '');
</script>
<style scoped>
.skeleton {
height: 40px;
width: 100%;
::v-deep .el-skeleton {
:deep(.el-skeleton) {
height: 100%;
> div {
height: 100%;

View File

@@ -7,7 +7,7 @@
<template v-if="state.canCloseInstance">
<el-button
:disabled="state.isClosed"
size="mini"
size="small"
type="primary"
@click="closeInstance(props.location)">
{{ t('dialog.user.info.close_instance') }} </el-button
@@ -24,17 +24,14 @@
>{{ 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 v-for="user in state.userList" :key="user.id">
<span style="cursor: pointer; margin-right: 5px" @click="showUserDialog(user.id)">{{
user.displayName
}}</span>
</template>
</div>
</template>
<i class="el-icon-caret-bottom"></i>
<el-icon><CaretBottom /></el-icon>
</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>
@@ -57,8 +54,10 @@
</template>
<script setup>
import { getCurrentInstance, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage, ElMessageBox } from 'element-plus';
import { CaretBottom } from '@element-plus/icons-vue';
import { reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { miscRequest } from '../api';
import { formatDateFilter, hasGroupPermission } from '../shared/utils';
import { useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../stores';
@@ -94,8 +93,6 @@
disabledContentSettings: ''
});
const { proxy } = getCurrentInstance();
function parse() {
Object.assign(state, {
isValidInstance: false,
@@ -153,18 +150,19 @@
}
function closeInstance(location) {
proxy.$confirm('Continue? Close Instance, nobody will be able to join', 'Confirm', {
ElMessageBox.confirm('Continue? Close Instance, nobody will be able to join', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: async (action) => {
type: 'warning'
})
.then(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' });
ElMessage({ message: t('message.instance.closed'), type: 'success' });
instanceStore.applyInstance(args.json);
}
}
});
})
.catch(() => {});
}
</script>

View File

@@ -1,25 +1,25 @@
<template>
<el-tooltip
v-if="!canOpenInstanceInGame()"
placement="top"
:content="t('dialog.user.info.self_invite_tooltip')"
:disabled="hideTooltips">
<el-button v-show="isVisible" @click="confirmInvite" size="mini" icon="el-icon-message" circle />
</el-tooltip>
<el-tooltip v-else placement="top" :content="t('dialog.user.info.open_in_vrchat_tooltip')" :disabled="hideTooltips">
<el-button @click="openInstance" size="mini" icon="el-icon-message" circle />
</el-tooltip>
<div v-if="isVisible" :class="['inline-block']">
<el-tooltip
v-if="!canOpenInstanceInGame()"
placement="top"
:content="t('dialog.user.info.self_invite_tooltip')">
<el-button v-show="isVisible" @click="confirmInvite" size="small" :icon="Message" circle />
</el-tooltip>
<el-tooltip v-else placement="top" :content="t('dialog.user.info.open_in_vrchat_tooltip')">
<el-button @click="openInstance" size="small" :icon="Message" circle />
</el-tooltip>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { Message } from '@element-plus/icons-vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { instanceRequest } from '../api';
import { checkCanInviteSelf, parseLocation } from '../shared/utils';
import { useAppearanceSettingsStore } from '../stores/settings/appearance';
import { useLaunchStore } from '../stores/launch';
import { useInviteStore } from '../stores/invite';
import { useInviteStore, useLaunchStore } from '../stores';
const props = defineProps({
location: String,
@@ -28,13 +28,9 @@
const { t } = useI18n();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { canOpenInstanceInGame } = useInviteStore();
const { tryOpenInstanceInVrc } = useLaunchStore();
const { proxy } = getCurrentInstance();
const isVisible = computed(() => checkCanInviteSelf(props.location));
function confirmInvite() {
@@ -50,7 +46,7 @@
shortName: props.shortname
})
.then((args) => {
proxy.$message({ message: 'Self invite sent', type: 'success' });
ElMessage({ message: 'Self invite sent', type: 'success' });
return args;
});
}
@@ -64,3 +60,9 @@
tryOpenInstanceInVrc(L.tag, props.shortname);
}
</script>
<style scoped>
.inline-block {
display: inline-block;
}
</style>

View File

@@ -1,20 +1,23 @@
<template>
<span v-if="lastJoin">
<el-tooltip placement="top" style="margin-left: 5px">
<span v-if="lastJoin" :class="['inline-block', 'ml-5']">
<el-tooltip placement="top" class="ml-5">
<template #content>
<span>{{ $t('dialog.user.info.last_join') }} <Timer :epoch="lastJoin" /></span>
<span>{{ t('dialog.user.info.last_join') }} <Timer :epoch="lastJoin" /></span>
</template>
<i class="el-icon el-icon-location-outline" style="display: inline-block" />
<el-icon style="display: inline-block"><Location /></el-icon>
</el-tooltip>
</span>
</template>
<script setup>
import { Location } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useInstanceStore } from '../stores';
const { instanceJoinHistory } = storeToRefs(useInstanceStore());
const { t } = useI18n();
const props = defineProps({
location: String,
@@ -30,3 +33,12 @@
watch(() => props.location, parse, { immediate: true });
watch(() => props.currentlocation, parse);
</script>
<style scoped>
.ml-5 {
margin-left: 5px;
}
.inline-block {
display: inline-block;
}
</style>

View File

@@ -1,16 +1,15 @@
<template>
<el-tooltip
v-show="isVisible"
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-tooltip>
<div v-if="isVisible" class="inline-block">
<el-tooltip placement="top" :content="t('dialog.user.info.launch_invite_tooltip')"
><el-button @click="confirm" size="small" :icon="SwitchButton" circle />
</el-tooltip>
</div>
</template>
<script setup>
import { SwitchButton } from '@element-plus/icons-vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { checkCanInviteSelf } from '../shared/utils';
import { useLaunchStore } from '../stores';
@@ -18,8 +17,7 @@
const { t } = useI18n();
const props = defineProps({
location: String,
hideTooltips: Boolean
location: String
});
const isVisible = computed(() => {
@@ -30,3 +28,9 @@
launchStore.showLaunchDialog(props.location);
}
</script>
<style scoped>
.inline-block {
display: inline-block;
}
</style>

View File

@@ -1,28 +1,29 @@
<template>
<div>
<span v-if="!text" style="color: transparent">-</span>
<span v-if="!text" class="transparent">-</span>
<span v-show="text">
<span
:class="{ 'x-link': link && location !== 'private' && location !== 'offline' }"
@click="handleShowWorldDialog">
<i v-if="isTraveling" class="el-icon el-icon-loading" style="display: inline-block"></i>
<el-icon :class="['is-loading', 'inline-block']" style="margin-right: 3px" v-if="isTraveling"
><Loading
/></el-icon>
<span>{{ text }}</span>
</span>
<span v-if="groupName" :class="{ 'x-link': link }" @click="handleShowGroupDialog">({{ groupName }})</span>
<span v-if="region" 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 v-if="region" :class="['flags', 'inline-block', 'ml-5', region]"></span>
<el-icon v-if="strict" :class="['inline-block', 'ml-5']"><Lock /></el-icon>
</span>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { Loading, Lock } from '@element-plus/icons-vue';
import { ref, watchEffect } from 'vue';
import { getGroupName, getWorldName, parseLocation } from '../shared/utils';
import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
const { cachedWorlds } = storeToRefs(useWorldStore());
const { showWorldDialog } = useWorldStore();
const { cachedWorlds, showWorldDialog } = useWorldStore();
const { showGroupDialog } = useGroupStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { verifyShortName } = useSearchStore();
@@ -51,14 +52,9 @@
const isTraveling = ref(false);
const groupName = ref('');
watch(
() => props.location,
() => {
parse();
}
);
parse();
watchEffect(() => {
parse();
});
function parse() {
isTraveling.value = false;
@@ -82,7 +78,7 @@
text.value = props.hint;
}
} else if (L.worldId) {
const ref = cachedWorlds.value.get(L.worldId);
const ref = cachedWorlds.get(L.worldId);
if (typeof ref === 'undefined') {
getWorldName(L.worldId).then((worldName) => {
if (L.tag === instanceId) {
@@ -152,3 +148,17 @@
showGroupDialog(L.groupId);
}
</script>
<style scoped>
.inline-block {
display: inline-block;
}
.ml-5 {
margin-left: 5px;
}
.transparent {
color: transparent;
}
</style>

View File

@@ -1,16 +1,17 @@
<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>
<el-icon v-if="isUnlocked" style="display: inline-block; margin-right: 5px"><Unlock /></el-icon>
<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>
<el-icon v-if="strict" style="display: inline-block; margin-left: 5px"><Lock /></el-icon>
</span>
</template>
<script setup>
import { Lock, Unlock } from '@element-plus/icons-vue';
import { ref, watch } from 'vue';
import { getGroupName, parseLocation } from '../shared/utils';
import { useGroupStore, useLaunchStore } from '../stores';

View File

@@ -11,8 +11,8 @@
<div v-else-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
<el-button
type="default"
size="mini"
icon="el-icon-download"
size="small"
:icon="Download"
circle
style="font-size: 14px; height: 50px; width: 50px"
@click="showVRCXUpdateDialog" />
@@ -30,7 +30,7 @@
:class="{ notify: notifiedMenus.includes(item.index) }">
<i :class="item.icon"></i>
<template #title>
<span>{{ $t(item.tooltip) }}</span>
<span>{{ t(item.tooltip) }}</span>
</template>
</el-menu-item>
</el-menu>
@@ -38,9 +38,13 @@
</template>
<script setup>
import { Download } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useUiStore, useVRCXUpdaterStore } from '../stores';
const { t } = useI18n();
const navItems = [
{ index: 'feed', icon: 'ri-rss-line', tooltip: 'nav_tooltip.feed' },
{ index: 'gameLog', icon: 'ri-history-line', tooltip: 'nav_tooltip.game_log' },

View File

@@ -3,15 +3,16 @@
<div class="name" :style="{ width: longLabel ? '300px' : undefined }">
{{ label }}
<el-tooltip v-if="tooltip" placement="top" class="tooltip" :content="tooltip"
><i class="el-icon-info"
/></el-tooltip>
><el-icon><InfoFilled /></el-icon
></el-tooltip>
</div>
<el-switch class="switch" :value="value" @change="change" :disabled="disabled"></el-switch>
<el-switch class="switch" :model-value="value" @change="change" :disabled="disabled"></el-switch>
</div>
</template>
<script setup>
import { InfoFilled } from '@element-plus/icons-vue';
defineProps({
label: String,
value: Boolean,
@@ -30,13 +31,13 @@
<style scoped>
.simple-switch {
font-size: 12px;
margin-top: 5px;
display: flex;
}
.simple-switch > .name {
width: 225px;
min-width: 225px;
word-wrap: break-word;
padding-top: 7px;
}
.simple-switch > .switch {
margin-left: 10px;

View File

@@ -1,84 +0,0 @@
<template>
<span>
<span>{{ text }}</span>
<span v-if="groupName">({{ groupName }})</span>
<span
v-if="region"
class="flags"
:class="region"
style="display:inline-block;margin-bottom:2px;margin-left:5px">
</span>
<i
v-if="strict"
class="el-icon el-icon-lock"
style="display:inline-block;margin-left:5px">
</i>
</span>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { parseLocation } from '../shared/utils/location';
const props = defineProps({
location: String,
hint: {
type: String,
default: ''
},
grouphint: {
type: String,
default: ''
}
});
const text = ref('');
const region = ref('');
const strict = ref(false);
const groupName = ref('');
function parse() {
text.value = props.location;
const L = parseLocation(props.location);
if (L.isOffline) {
text.value = 'Offline';
} else if (L.isPrivate) {
text.value = 'Private';
} else if (L.isTraveling) {
text.value = 'Traveling';
} else if (typeof props.hint === 'string' && props.hint !== '') {
if (L.instanceId) {
text.value = `${props.hint} #${L.instanceName} ${L.accessTypeName}`;
} else {
text.value = props.hint;
}
} else if (L.worldId) {
if (L.instanceId) {
text.value = ` #${L.instanceName} ${L.accessTypeName}`;
} else {
text.value = props.location;
}
}
region.value = '';
if (
props.location !== '' &&
L.instanceId &&
!L.isOffline &&
!L.isPrivate
) {
region.value = L.region;
if (!L.region) {
region.value = 'us';
}
}
strict.value = L.strict;
groupName.value = props.grouphint;
}
watch(() => props.location, parse);
onMounted(parse);
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="changeAvatarImageDialogVisible"
:model-value="changeAvatarImageDialogVisible"
:title="t('dialog.change_content_image.avatar')"
width="850px"
append-to-body
@@ -16,73 +16,47 @@
<span>{{ t('dialog.change_content_image.description') }}</span>
<br />
<el-button-group style="padding-bottom: 10px; padding-top: 10px">
<el-button type="default" size="small" icon="el-icon-refresh" @click="refresh">
{{ t('dialog.change_content_image.refresh') }}
</el-button>
<el-button type="default" size="small" icon="el-icon-upload2" @click="uploadAvatarImage">
<el-button type="default" size="small" :icon="Upload" @click="uploadAvatarImage">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<div
v-if="image.file"
class="x-change-image-item"
style="cursor: pointer"
:class="{ 'current-image': compareCurrentImage(image) }"
@click="setAvatarImage(image)">
<img v-lazy="image.file.url" class="image" />
</div>
<div class="x-change-image-item">
<img :src="currentImageUrl" class="img-size" loading="lazy" />
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useAvatarStore, useGalleryStore } from '../../../stores';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { avatarDialog } = storeToRefs(useAvatarStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const { applyAvatar } = useAvatarStore();
const props = defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
default: false
required: true
},
previousImagesFileId: {
previousImageUrl: {
type: String,
default: ''
}
});
const changeAvatarImageDialogLoading = ref(false);
const avatarImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
const currentImageUrl = computed(() => props.previousImageUrl);
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'refresh']);
function refresh() {
emit('refresh', 'Change');
}
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'update:previousImageUrl']);
function closeDialog() {
emit('update:changeAvatarImageDialogVisible', false);
@@ -93,23 +67,9 @@
return response;
}
async function genMd5(file) {
const response = await AppApi.MD5File(file);
return response;
}
async function genSig(file) {
const response = await AppApi.SignFile(file);
return response;
}
async function genLength(file) {
const response = await AppApi.FileLength(file);
return response;
}
function onFileChangeAvatarImage(e) {
const clearFile = function () {
changeAvatarImageDialogLoading.value = false;
const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#AvatarImageUploadButton'));
if (fileInput) {
fileInput.value = '';
@@ -124,7 +84,7 @@
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -132,7 +92,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
@@ -141,50 +101,14 @@
}
const r = new FileReader();
r.onload = async function (file) {
r.onload = async function () {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total.toString(), 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const avatarId = avatarDialog.value.id;
const { imageUrl } = avatarDialog.value.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
$message({
message: t('message.avatar.image_invalid'),
type: 'error'
});
clearFile();
return;
}
avatarImage.value = {
base64File,
fileMd5,
base64SignatureFile,
signatureMd5,
fileId,
avatarId
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
await initiateUpload(base64File);
} catch (error) {
console.error('Avatar image upload process failed:', error);
} finally {
changeAvatarImageDialogLoading.value = false;
clearFile();
}
};
@@ -193,169 +117,32 @@
r.readAsBinaryString(files[0]);
}
// ------------ Upload Process Start ------------
async function initiateUpload(params, fileId) {
const res = await imageRequest.uploadAvatarImage(params, fileId);
return avatarImageInit(res);
}
async function avatarImageInit(args) {
const fileId = args.json.id;
const fileVersion = args.json.versions[args.json.versions.length - 1].version;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileStart(params);
return avatarImageFileStart(res);
}
async function avatarImageFileStart(args) {
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadAvatarImageFileAWS(params);
}
async function uploadAvatarImageFileAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: avatarImage.value.base64File,
fileMIME: 'image/png',
headers: {
'Content-MD5': avatarImage.value.fileMd5
}
async function initiateUpload(base64File) {
const args = await avatarRequest.uploadAvatarImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
const avatarArgs = await avatarRequest.saveAvatar({
id: avatarDialog.value.id,
imageUrl: fileUrl
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
$throw(json.status, 'Avatar image upload failed', params.url);
}
const args = {
json,
params
};
return avatarImageFileAWS(args);
}
async function avatarImageFileAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileFinish(params);
return avatarImageFileFinish(res);
}
async function avatarImageFileFinish(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigStart(params);
return avatarImageSigStart(res);
}
async function avatarImageSigStart(args) {
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadAvatarImageSigAWS(params);
}
async function uploadAvatarImageSigAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: avatarImage.value.base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
headers: {
'Content-MD5': avatarImage.value.signatureMd5
}
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
$throw(json.status, 'Avatar image upload failed', params.url);
}
const args = {
json,
params
};
return avatarImageSigAWS(args);
}
async function avatarImageSigAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigFinish(params);
return avatarImageSigFinish(res);
}
async function avatarImageSigFinish(args) {
const { fileId, fileVersion } = args.params;
const parmas = {
id: avatarImage.value.avatarId,
imageUrl: `${AppGlobal.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setAvatarImage(parmas);
return avatarImageSet(res);
}
async function avatarImageSet(args) {
applyAvatar(args.json);
const ref = applyAvatar(avatarArgs.json);
emit('update:previousImageUrl', ref.imageUrl);
changeAvatarImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.avatar.image_changed'),
type: 'success'
});
refresh();
} else {
$throw(0, 'Avatar image change failed', args.params.imageUrl);
}
}
ElMessage({
message: t('message.avatar.image_changed'),
type: 'success'
});
// ------------ Upload Process End ------------
// closeDialog();
}
function uploadAvatarImage() {
document.getElementById('AvatarImageUploadButton').click();
}
function setAvatarImage(image) {
changeAvatarImageDialogLoading.value = true;
const parmas = {
id: avatarDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setAvatarImage(parmas)
.then((args) => applyAvatar(args.json))
.finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
return (
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
avatarDialog.value.ref.imageUrl
);
}
</script>
<style lang="scss" scoped>
.img-size {
width: 500px;
height: 375px;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<safe-dialog
<el-dialog
ref="setAvatarStylesDialog"
class="x-dialog"
:visible.sync="setAvatarStylesDialog.visible"
v-model="setAvatarStylesDialog.visible"
:title="t('dialog.set_avatar_styles.header')"
width="400px"
append-to-body>
@@ -39,11 +39,11 @@
</el-select>
</div>
<br />
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.author_tags') }}<br /></div>
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.author_tags') }}<br /></div>
<el-input
v-model="setAvatarStylesDialog.authorTags"
type="textarea"
size="mini"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
@@ -57,21 +57,18 @@
t('dialog.set_avatar_styles.save')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { watch, getCurrentInstance } from 'vue';
import { watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { arraysMatch } from '../../../shared/utils';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { applyAvatar } = useAvatarStore();
const props = defineProps({

View File

@@ -1,8 +1,8 @@
<template>
<safe-dialog
<el-dialog
ref="setAvatarTagsDialog"
class="x-dialog"
:visible.sync="setAvatarTagsDialog.visible"
v-model="setAvatarTagsDialog.visible"
:title="t('dialog.set_avatar_tags.header')"
width="770px"
append-to-body>
@@ -29,7 +29,7 @@
<br />
<el-input
v-model="setAvatarTagsDialog.selectedTagsCsv"
size="mini"
size="small"
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="t('dialog.set_avatar_tags.custom_tags_placeholder')"
style="margin-top: 10px"
@@ -47,19 +47,18 @@
<span style="margin-left: 5px"
>{{ setAvatarTagsDialog.selectedCount }} / {{ setAvatarTagsDialog.ownAvatars.length }}</span
>
<span v-if="setAvatarTagsDialog.loading" style="margin-left: 5px">
<i class="el-icon-loading"></i>
</span>
<el-icon v-if="setAvatarTagsDialog.loading" class="is-loading" style="margin-left: 5px"
><Loading
/></el-icon>
<br />
<div class="x-friend-list" style="margin-top: 10px; min-height: 60px; max-height: 280px">
<div
v-for="avatar in setAvatarTagsDialog.ownAvatars"
:key="avatar.id"
class="x-friend-item x-friend-item-border"
style="width: 350px"
:class="['item-width', 'x-friend-item', 'x-friend-item-border']"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl" />
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
@@ -76,7 +75,7 @@
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.$tagString"></span>
</div>
<el-button type="text" size="mini" style="margin-left: 5px" @click.stop>
<el-button type="text" size="small" style="margin-left: 5px" @click.stop>
<el-checkbox v-model="avatar.$selected" @change="updateAvatarTagsSelection"></el-checkbox>
</el-button>
</div>
@@ -90,21 +89,21 @@
t('dialog.set_avatar_tags.save')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Loading } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { showAvatarDialog, applyAvatar } = useAvatarStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const props = defineProps({
setAvatarTagsDialog: {
type: Object,
@@ -229,7 +228,7 @@
}
} catch (err) {
console.error(err);
$message({
ElMessage({
message: 'Error saving avatar tags',
type: 'error'
});
@@ -273,4 +272,8 @@
}
</script>
<style scoped></style>
<style scoped>
.item-width {
width: 335px;
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<safe-dialog ref="favoriteDialogRef" :visible.sync="isVisible" :title="t('dialog.favorite.header')" width="300px">
<el-dialog :z-index="favoriteDialogIndex" v-model="isVisible" :title="t('dialog.favorite.header')" width="300px">
<div v-loading="loading">
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
<el-button
style="display: block; width: 100%; margin: 10px 0"
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} /
{{ favoriteDialog.currentGroup.capacity }})
</el-button>
@@ -23,18 +23,16 @@
</div>
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
<template v-for="group in localWorldFavoriteGroups">
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-button
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
</el-button>
<el-button
v-else
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
@@ -43,18 +41,16 @@
</div>
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
<template v-for="group in localAvatarFavoriteGroups">
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-button
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
</el-button>
<el-button
v-else
:key="group"
style="display: block; width: 100%; margin: 10px 0"
:disabled="!isLocalUserVrcplusSupporter"
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
@@ -62,16 +58,18 @@
</el-button>
</template>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Check } from '@element-plus/icons-vue';
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { computed, nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../api';
import { adjustDialogZ } from '../../shared/utils';
import { getNextDialogIndex } from '../../shared/utils';
import { useFavoriteStore, useUserStore } from '../../stores';
const { t } = useI18n();
@@ -98,7 +96,7 @@
} = favoriteStore;
const { currentUser } = storeToRefs(useUserStore());
const favoriteDialogRef = ref(null);
const favoriteDialogIndex = ref(2000);
const groups = ref([]);
const loading = ref(false);
@@ -113,11 +111,12 @@
watch(
() => favoriteDialog.value.visible,
async (value) => {
(value) => {
if (value) {
initFavoriteDialog();
await nextTick();
adjustDialogZ(favoriteDialogRef.value.$el);
nextTick(() => {
favoriteDialogIndex.value = getNextDialogIndex();
});
}
}
);

View File

@@ -1,49 +1,90 @@
<template>
<safe-dialog class="x-dialog" :visible.sync="fullscreenImageDialog.visible" append-to-body width="97vw">
<el-dialog class="x-dialog" v-model="fullscreenImageDialog.visible" append-to-body width="97vw">
<div>
<div style="margin: 0 0 5px 5px">
<el-button
size="mini"
icon="el-icon-s-order"
circle
@click="copyImageUrl(fullscreenImageDialog.imageUrl)"></el-button>
<el-button
type="default"
size="mini"
icon="el-icon-download"
circle
style="margin-left: 5px"
:disabled="!fullscreenImageDialog.imageUrl.startsWith('http')"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
<el-tooltip :content="t('dialog.fullscreen_image.copy_image_to_clipboard')" placement="top">
<el-button
size="small"
:icon="CopyDocument"
circle
@click="copyImageToClipboard(fullscreenImageDialog.imageUrl)"></el-button>
</el-tooltip>
<el-tooltip :content="t('dialog.fullscreen_image.download_and_save_image')" placement="top">
<el-button
type="default"
size="small"
:icon="Download"
circle
style="margin-left: 5px"
:disabled="!fullscreenImageDialog.imageUrl.startsWith('http')"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
</el-tooltip>
</div>
<img v-lazy="fullscreenImageDialog.imageUrl" style="width: 100%; height: 85vh; object-fit: contain" />
<img
:src="fullscreenImageDialog.imageUrl"
style="width: 100%; height: 85vh; object-fit: contain"
loading="lazy" />
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Download, CopyDocument } from '@element-plus/icons-vue';
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { copyToClipboard, escapeTag, extractFileId } from '../../shared/utils';
import { useI18n } from 'vue-i18n';
import { escapeTag, extractFileId } from '../../shared/utils';
import { useGalleryStore } from '../../stores';
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const { t } = useI18n();
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
function copyImageUrl(imageUrl) {
copyToClipboard(imageUrl, 'ImageUrl copied to clipboard');
async function copyImageToClipboard(url) {
if (!url) {
return;
}
const msg = ElMessage({
message: 'Downloading image...',
type: 'info'
});
try {
const response = await webApiService.execute({
url,
method: 'GET'
});
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
throw new Error(`Error: ${response.data}`);
}
await navigator.clipboard.write([
new ClipboardItem({
'image/png': await (await fetch(response.data)).blob()
})
]);
ElMessage({
message: 'Image copied to clipboard',
type: 'success'
});
} catch (error) {
console.error('Error downloading image:', error);
new Noty({
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally {
msg.close();
}
}
async function downloadAndSaveImage(url, fileName) {
if (!url) {
return;
}
$message({
const msg = ElMessage({
message: 'Downloading image...',
type: 'info'
});
@@ -77,6 +118,8 @@
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally {
msg.close();
}
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="gallerySelectDialog.visible"
v-model="gallerySelectDialog.visible"
:title="t('dialog.gallery_select.header')"
width="100%"
append-to-body>
@@ -16,16 +16,16 @@
style="display: none"
@change="onFileChangeGallery" />
<el-button-group>
<el-button type="default" size="small" icon="el-icon-close" @click="selectImageGallerySelect('', '')">{{
<el-button type="default" size="small" :icon="Close" @click="selectImageGallerySelect('', '')">{{
t('dialog.gallery_select.none')
}}</el-button>
<el-button type="default" size="small" icon="el-icon-refresh" @click="refreshGalleryTable">{{
<el-button type="default" size="small" :icon="Refresh" @click="refreshGalleryTable">{{
t('dialog.gallery_select.refresh')
}}</el-button>
<el-button
type="default"
size="small"
icon="el-icon-upload2"
:icon="Upload"
:disabled="!currentUser.$isVRCPlus"
@click="displayGalleryUpload"
>{{ t('dialog.gallery_select.upload') }}</el-button
@@ -42,24 +42,28 @@
v-if="image.versions[image.versions.length - 1].file.url"
class="vrcplus-icon"
@click="selectImageGallerySelect(image.versions[image.versions.length - 1].file.url, image.id)">
<img v-lazy="image.versions[image.versions.length - 1].file.url" class="avatar" /></div
<img
:src="image.versions[image.versions.length - 1].file.url"
class="avatar"
loading="lazy" /></div
></template>
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Close, Refresh, Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { vrcPlusImageRequest } from '../../../api';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const { galleryTable } = storeToRefs(useGalleryStore());
const { refreshGalleryTable, handleGalleryImageAdd } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -95,7 +99,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -103,7 +107,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
@@ -115,7 +119,7 @@
const base64Body = btoa(r.result.toString());
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
handleGalleryImageAdd(args);
$message({
ElMessage({
message: t('message.gallery.uploaded'),
type: 'success'
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="groupMemberModeration.visible"
v-model="groupMemberModeration.visible"
:title="t('dialog.group_member_moderation.header')"
append-to-body
width="90vw">
@@ -12,8 +12,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="loadAllGroupMembers" />
@@ -34,21 +34,23 @@
memberSearch.length ||
!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')
)
"
@click.native.stop>
<el-button size="mini">
">
<el-button size="small" @click.stop>
<span
>{{ t(memberSortOrder.name) }} <i class="el-icon-arrow-down el-icon--right"></i
></span>
>{{ t(memberSortOrder.name) }}
<el-icon style="margin-left: 5px"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click.native="setGroupMemberSortOrder(item)">
{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click="setGroupMemberSortOrder(item)">
{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span style="margin-right: 5px">{{ t('dialog.group.members.filter') }}</span>
<el-dropdown
@@ -61,33 +63,36 @@
memberSearch.length ||
!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')
)
"
@click.native.stop>
<el-button size="mini">
">
<el-button size="small" @click.stop>
<span
>{{ t(memberFilter.name) }} <i class="el-icon-arrow-down el-icon--right"></i
>{{ t(memberFilter.name) }}
<el-icon style="margin-left: 5px"><ArrowDown /></el-icon
></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
v-text="t(item.name)"></el-dropdown-item>
<el-dropdown-item
v-for="item in groupMemberModeration.groupRef.roles"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
><span v-if="!item.defaultRole">{{ t(item.name) }}</span></el-dropdown-item
>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click="setGroupMemberFilter(item)"
v-text="t(item.name)"></el-dropdown-item>
<template v-for="role in groupMemberModeration.groupRef.roles" :key="role.name">
<el-dropdown-item
v-if="!role.defaultRole"
@click="setGroupMemberFilter(role)">
{{ t(role.name) }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-input
v-model="memberSearch"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')"
clearable
size="mini"
size="small"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"
@input="groupMembersSearch"></el-input>
@@ -95,13 +100,13 @@
<el-button size="small" @click="selectAllGroupMembers">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables
<DataTable
v-if="groupMemberModerationTable.data.length"
v-bind="groupMemberModerationTable"
style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -115,16 +120,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -145,9 +153,11 @@
</el-table-column>
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
<template #default="scope">
<template v-for="(roleId, index) in scope.row.roleIds">
<template v-for="(role, rIndex) in groupMemberModeration.groupRef.roles">
<span v-if="role?.id === roleId" :key="roleId + rIndex"
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
<template
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
:key="roleId + rIndex">
<span v-if="role?.id === roleId"
>{{ role.name
}}<span v-if="index < scope.row.roleIds.length - 1">, </span></span
></template
@@ -181,7 +191,7 @@
<span v-text="scope.row.visibility"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
@@ -191,8 +201,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupBans(groupMemberModeration.id)"></el-button>
@@ -203,17 +213,17 @@
<el-input
v-model="groupBansModerationTable.filters[0].value"
clearable
size="mini"
size="small"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"></el-input>
<br />
<el-button size="small" @click="selectAllGroupBans">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupBansModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupBansModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -227,16 +237,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -257,16 +270,14 @@
</el-table-column>
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
<template #default="scope">
<template v-for="(roleId, index) in scope.row.roleIds">
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
<span
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
v-if="role.id === roleId"
:key="rIndex + roleId"
>{{ role.name }}</span
>
<span v-if="index < scope.row.roleIds.length - 1" :key="index + roleId"
>,
</span>
<span v-if="index < scope.row.roleIds.length - 1">, </span>
</template>
</template>
</el-table-column>
@@ -296,7 +307,7 @@
<span>{{ formatDateFilter(scope.row.bannedAt, 'long') }}</span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
@@ -306,29 +317,29 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupInvitesAndJoinRequests(groupMemberModeration.id)"></el-button>
<br />
<el-tabs>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.sent_invites')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupInvitesModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupInvites">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupInvitesModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupInvitesModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -342,16 +353,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -378,7 +392,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -396,21 +410,21 @@
</el-tab-pane>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.join_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupJoinRequestsModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupJoinRequests">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupJoinRequestsModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupJoinRequestsModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -424,16 +438,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -460,7 +477,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -504,21 +521,21 @@
</el-tab-pane>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.blocked_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupBlockedModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupBlocked">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupBlockedModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupBlockedModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -532,16 +549,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -568,7 +588,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -594,8 +614,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupLogs(groupMemberModeration.id)"></el-button>
@@ -637,7 +657,7 @@
</div>
</div>
<br />
<data-tables v-bind="groupLogsModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupLogsModerationTable" style="margin-top: 10px">
<el-table-column
:label="t('dialog.group_member_moderation.created_at')"
width="170"
@@ -684,7 +704,7 @@
v-text="JSON.stringify(scope.row.data)"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
</el-tabs>
@@ -695,7 +715,7 @@
<br />
<el-input
v-model="selectUserId"
size="mini"
size="small"
style="margin-top: 5px; width: 340px"
:placeholder="t('dialog.group_member_moderation.user_id_placeholder')"
clearable></el-input>
@@ -707,8 +727,8 @@
<span class="name">{{ t('dialog.group_member_moderation.selected_users') }}</span>
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="clearSelectedGroupMembers"></el-button>
@@ -725,7 +745,7 @@
<template #content>
<span>{{ t('dialog.group_member_moderation.user_isnt_in_group') }}</span>
</template>
<i class="el-icon el-icon-warning" style="display: inline-block" />
<el-icon style="margin-left: 3px; display: inline-block"><Warning /></el-icon>
</el-tooltip>
<span v-text="user.user?.displayName || user.userId" style="font-weight: bold; margin-left: 5px"></span>
</el-tag>
@@ -739,7 +759,7 @@
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.group_member_moderation.note_placeholder')"
size="mini"
size="small"
resize="none"
style="margin-top: 5px"></el-input>
<br />
@@ -826,7 +846,7 @@
>{{ t('dialog.group_member_moderation.unban') }}</el-button
>
<span v-if="progressCurrent" style="margin-top: 10px">
<i class="el-icon-loading" style="margin-left: 5px; margin-right: 5px"></i>
<el-icon class="is-loading" style="margin-left: 5px; margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.group_member_moderation.progress') }} {{ progressCurrent }}/{{ progressTotal }}
</span>
<el-button v-if="progressCurrent" style="margin-left: 5px" @click="progressTotal = 0">{{
@@ -834,15 +854,18 @@
}}</el-button>
</div>
<group-member-moderation-export-dialog
:is-group-logs-export-dialog-visible.sync="isGroupLogsExportDialogVisible"
:is-group-logs-export-dialog-visible="isGroupLogsExportDialogVisible"
:group-logs-moderation-table="groupLogsModerationTable" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Refresh, Delete, ArrowDown, Warning, Loading } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as workerTimers from 'worker-timers';
import { groupRequest, userRequest } from '../../../api';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
@@ -857,9 +880,6 @@
const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore();
const { showFullscreenImageDialog } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const selectedUsers = reactive({});
const selectedUsersArray = ref([]);
const isGroupMembersLoading = ref(false);
@@ -880,7 +900,7 @@
n: 100,
offset: 0,
groupId: '',
sort: '',
sort: 'joinedAt:desc',
roleId: ''
});
@@ -917,7 +937,7 @@
const groupInvitesModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -927,7 +947,7 @@
});
const groupJoinRequestsModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -937,7 +957,7 @@
});
const groupBlockedModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -948,7 +968,7 @@
const groupLogsModerationTable = reactive({
data: [],
filters: [{ prop: ['description'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -959,7 +979,7 @@
const groupBansModerationTable = reactive({
data: [],
filters: [{ prop: ['$displayName'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -969,7 +989,7 @@
});
const groupMemberModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -1084,7 +1104,7 @@
});
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to delete group invites: ${err}`,
type: 'error'
});
@@ -1092,7 +1112,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Deleted ${memberCount} group invites`,
type: 'success'
});
@@ -1136,7 +1156,7 @@
}
groupBansModerationTable.data = fetchedBans;
} catch {
$message({
ElMessage({
message: 'Failed to get group bans',
type: 'error'
});
@@ -1167,13 +1187,13 @@
await groupRequest.banGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to ban group member: ${err}`,
type: 'error'
});
}
}
$message({ message: `Banned ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Banned ${memberCount} group members`, type: 'success' });
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupBans(D.id);
@@ -1196,7 +1216,7 @@
await groupRequest.unbanGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to unban group member: ${err}`,
type: 'error'
});
@@ -1205,7 +1225,7 @@
}
if (allSuccess) {
$message({ message: `Unbanned ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Unbanned ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1230,7 +1250,7 @@
await groupRequest.kickGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to kick group member: ${err}`,
type: 'error'
});
@@ -1238,7 +1258,7 @@
}
}
if (allSuccess) {
$message({ message: `Kicked ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Kicked ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1266,7 +1286,7 @@
handleGroupMemberProps(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to set group member note for ${err}`,
type: 'error'
});
@@ -1274,7 +1294,7 @@
}
}
if (allSuccess) {
$message({ message: `Saved notes for ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Saved notes for ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1314,7 +1334,7 @@
handleGroupMemberRoleChange(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to remove group member roles: ${err}`,
type: 'error'
});
@@ -1323,7 +1343,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Roles removed`,
type: 'success'
});
@@ -1362,7 +1382,7 @@
handleGroupMemberRoleChange(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to add group member roles: ${err}`,
type: 'error'
});
@@ -1371,7 +1391,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Added group member roles`,
type: 'success'
});
@@ -1466,7 +1486,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group logs',
type: 'error'
});
@@ -1496,7 +1516,7 @@
await groupRequest.deleteBlockedGroupRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to delete blocked group requests: ${err}`,
type: 'error'
});
@@ -1504,7 +1524,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Deleted ${memberCount} blocked group requests`,
type: 'success'
});
@@ -1532,7 +1552,7 @@
await groupRequest.blockGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to block group join requests: ${err}`,
type: 'error'
});
@@ -1540,7 +1560,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Blocked ${memberCount} group join requests`,
type: 'success'
});
@@ -1569,7 +1589,7 @@
await groupRequest.rejectGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to reject group join requests: ${err}`,
type: 'error'
});
@@ -1577,7 +1597,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Rejected ${memberCount} group join requests`,
type: 'success'
});
@@ -1605,7 +1625,7 @@
await groupRequest.acceptGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to accept group join requests: ${err}`,
type: 'error'
});
@@ -1613,7 +1633,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Accepted ${memberCount} group join requests`,
type: 'success'
});
@@ -1661,7 +1681,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group join requests',
type: 'error'
});
@@ -1694,7 +1714,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group join requests',
type: 'error'
});
@@ -1731,7 +1751,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group invites',
type: 'error'
});
@@ -1823,7 +1843,7 @@
members.value = [];
isGroupMembersDone.value = false;
loadMoreGroupMembersParams.value = {
sort: '',
sort: 'joinedAt:desc',
roleId: '',
n: 100,
offset: 0,

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isGroupLogsExportDialogVisible"
:model-value="isGroupLogsExportDialogVisible"
:title="t('dialog.group_member_moderation.export_logs')"
width="650px"
append-to-body
@@ -10,8 +10,8 @@
v-model="checkedGroupLogsExportLogsOptions"
style="margin-bottom: 10px"
@change="updateGroupLogsExportContent">
<template v-for="option in checkGroupsLogsExportLogsOptions">
<el-checkbox :key="option.label" :label="option.label">
<template v-for="option in checkGroupsLogsExportLogsOptions" :key="option.label">
<el-checkbox :label="option.label">
{{ t(option.text) }}
</el-checkbox>
</template>
@@ -20,18 +20,18 @@
<el-input
v-model="groupLogsExportContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="handleCopyGroupLogsExportContent" />
</safe-dialog>
@click="handleCopyGroupLogsExportContent" />
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { copyToClipboard } from '../../../shared/utils';
const { t } = useI18n();

View File

@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="groupPostEditDialog.visible"
<el-dialog
v-model="groupPostEditDialog.visible"
:title="t('dialog.group_post_edit.header')"
width="650px"
append-to-body>
@@ -8,7 +8,7 @@
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
<el-form-item :label="t('dialog.group_post_edit.title')">
<el-input v-model="groupPostEditDialog.title" size="mini"></el-input>
<el-input v-model="groupPostEditDialog.title" size="small"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.message')">
<el-input
@@ -61,29 +61,32 @@
<el-form-item :label="t('dialog.group_post_edit.image')">
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<el-popover placement="right" width="500px" trigger="click">
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<img
:src="gallerySelectDialog.selectedImageUrl"
style="
flex: none;
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="gallerySelectDialog.selectedImageUrl"
style="
flex: none;
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
" />
<img
v-lazy="gallerySelectDialog.selectedImageUrl"
style="height: 500px"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />
:src="gallerySelectDialog.selectedImageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)"
loading="lazy" />
</el-popover>
<el-button size="mini" style="vertical-align: top" @click="clearImageGallerySelect">
<el-button size="small" style="vertical-align: top" @click="clearImageGallerySelect">
{{ t('dialog.invite_message.clear_selected_image') }}
</el-button>
</div>
</template>
<template v-else>
<el-button size="mini" style="margin-right: 5px" @click="showGallerySelectDialog">
<el-button size="small" style="margin-right: 5px" @click="showGallerySelectDialog">
{{ t('dialog.invite_message.select_image') }}
</el-button>
</template>
@@ -105,12 +108,13 @@
:gallery-select-dialog="gallerySelectDialog"
:gallery-table="galleryTable"
@refresh-gallery-table="refreshGalleryTable" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { groupRequest, vrcPlusIconRequest } from '../../../api';
import { useGalleryStore, useGroupStore } from '../../../stores';
import GallerySelectDialog from './GallerySelectDialog.vue';
@@ -125,7 +129,6 @@
const emit = defineEmits(['update:dialogData']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { showFullscreenImageDialog, handleFilesList } = useGalleryStore();
@@ -169,7 +172,7 @@
return;
}
if (!D.title || !D.text) {
proxy.$message({
ElMessage({
message: 'Title and text are required',
type: 'warning'
});
@@ -188,8 +191,8 @@
params.imageId = gallerySelectDialog.value.selectedFileId;
}
groupRequest.editGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
handleGroupPost(args);
ElMessage({
message: 'Group post edited',
type: 'success'
});
@@ -200,7 +203,7 @@
function createGroupPost() {
const D = groupPostEditDialog.value;
if (!D.title || !D.text) {
proxy.$message({
ElMessage({
message: 'Title and text are required',
type: 'warning'
});
@@ -220,7 +223,7 @@
}
groupRequest.createGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
ElMessage({
message: 'Group post created',
type: 'success'
});

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="editAndSendInviteDialog.visible"
:model-value="editAndSendInviteDialog.visible"
:title="t('dialog.edit_send_invite_message.header')"
width="400px"
append-to-body
@@ -13,7 +13,7 @@
<el-input
v-model="editAndSendInviteDialog.newMessage"
type="textarea"
size="mini"
size="small"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -21,28 +21,25 @@
style="margin-top: 10px"></el-input>
<template #footer>
<el-button type="small" @click="cancelEditAndSendInvite">
<el-button @click="cancelEditAndSendInvite">
{{ t('dialog.edit_send_invite_message.cancel') }}
</el-button>
<el-button type="primary" size="small" @click="saveEditAndSendInvite">
{{ t('dialog.edit_send_invite_message.send') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -86,13 +83,13 @@
})
.then((args) => {
if (args.json[slot].message === I.messageSlot.message) {
$message({
ElMessage({
message: "VRChat API didn't update message, try again",
type: 'error'
});
throw new Error("VRChat API didn't update message, try again");
} else {
$message('Invite message updated');
ElMessage('Invite message updated');
}
return args;
});
@@ -139,7 +136,7 @@
} else {
J.loading = false;
J.visible = false;
$message({
ElMessage({
message: 'Invite sent',
type: 'success'
});
@@ -155,7 +152,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite photo message sent',
type: 'success'
});
@@ -168,7 +165,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -185,7 +182,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite photo message sent',
type: 'success'
});
@@ -198,7 +195,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite message sent',
type: 'success'
});

View File

@@ -1,25 +1,25 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="inviteDialog.visible"
v-model="inviteDialog.visible"
:title="t('dialog.invite.header')"
width="500px"
append-to-body>
<div v-if="inviteDialog.visible" v-loading="inviteDialog.loading">
<Location :location="inviteDialog.worldId" :link="false" />
<br />
<el-button size="mini" style="margin-top: 10px" @click="addSelfToInvite">{{
<el-button size="small" style="margin-top: 10px" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
}}</el-button>
<el-button
size="mini"
size="small"
:disabled="inviteDialog.friendsInInstance.length === 0"
style="margin-top: 10px"
@click="addFriendsInInstanceToInvite"
>{{ t('dialog.invite.add_friends_in_instance') }}</el-button
>
<el-button
size="mini"
size="small"
:disabled="vipFriends.length === 0"
style="margin-top: 10px"
@click="addFavoriteFriendsToInvite"
@@ -34,108 +34,116 @@
filterable
:disabled="inviteDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="currentUser" :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="currentUser.displayName"
:value="currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(currentUser)]">
<img v-lazy="userImage(currentUser)" />
</div>
<div class="detail">
<span class="name">{{ currentUser.displayName }}</span>
</div>
</el-option>
</el-option-group>
<el-option-group
v-if="inviteDialog.friendsInInstance.length"
:label="t('dialog.invite.friends_in_instance')">
<el-option
v-for="friend in inviteDialog.friendsInInstance"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
<template v-if="currentUser">
<el-option-group :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="currentUser.displayName"
:value="currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(currentUser)]">
<img :src="userImage(currentUser)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
<span class="name">{{ currentUser.displayName }}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</el-option>
</el-option-group>
</template>
<el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">
<el-option
v-for="friend in vipFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<template v-if="inviteDialog.friendsInInstance.length">
<el-option-group :label="t('dialog.invite.friends_in_instance')">
<el-option
v-for="friend in inviteDialog.friendsInInstance"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</template>
<el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">
<el-option
v-for="friend in onlineFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<template v-if="vipFriends.length">
<el-option-group :label="t('side_panel.favorite')">
<el-option
v-for="friend in vipFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</template>
<el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">
<el-option
v-for="friend in activeFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div class="avatar"><img v-lazy="userImage(friend.ref)" /></div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<template v-if="onlineFriends.length">
<el-option-group :label="t('side_panel.online')">
<el-option
v-for="friend in onlineFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</template>
<template v-if="activeFriends.length">
<el-option-group :label="t('side_panel.active')">
<el-option
v-for="friend in activeFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div class="avatar"><img :src="userImage(friend.ref)" loading="lazy" /></div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</template>
</el-select>
</div>
@@ -155,17 +163,19 @@
>
</template>
<SendInviteDialog
:send-invite-dialog-visible.sync="sendInviteDialogVisible"
:send-invite-dialog-visible="sendInviteDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation, userImage, userStatusClass } from '../../../shared/utils';
import { useFriendStore, useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
@@ -177,10 +187,6 @@
const { clearInviteImageUpload } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const props = defineProps({
inviteDialog: {
type: Object,
@@ -238,11 +244,12 @@
}
function sendInvite() {
$confirm('Continue? Invite', 'Confirm', {
ElMessageBox.confirm('Continue? Invite', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
const D = props.inviteDialog;
if (action !== 'confirm' || D.loading === true) {
return;
@@ -275,14 +282,14 @@
} else {
D.loading = false;
D.visible = false;
$message({
ElMessage({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
}
});
})
.catch(() => {});
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="visible"
:model-value="visible"
:title="t('dialog.invite_message.header')"
width="400px"
append-to-body
@@ -18,22 +18,19 @@
{{ t('dialog.invite_message.confirm') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -54,10 +51,10 @@
}
});
const emit = defineEmits(['update:visible', 'closeInviteDialog']);
const emit = defineEmits(['update:model-value', 'closeInviteDialog']);
function cancelInviteConfirm() {
emit('update:visible', false);
emit('update:model-value', false);
}
function sendInviteConfirm() {
@@ -106,7 +103,7 @@
} else {
J.loading = false;
J.visible = false;
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -122,7 +119,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite photo message sent',
type: 'success'
});
@@ -135,7 +132,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -152,7 +149,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite photo message sent',
type: 'success'
});
@@ -165,7 +162,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite message sent',
type: 'success'
});

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="sendInviteDialogVisible"
:model-value="sendInviteDialogVisible"
:title="t('dialog.invite_message.header')"
width="800px"
append-to-body
@@ -9,46 +9,46 @@
<template v-if="currentUser.$isVRCPlus">
<!-- <template v-if="gallerySelectDialog.selectedFileId">-->
<!-- <div style="display: inline-block; flex: none; margin-right: 5px">-->
<!-- <el-popover placement="right" width="500px" trigger="click">-->
<!-- <el-popover placement="right" :width="500px" trigger="click">-->
<!-- <template #reference>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- :src="gallerySelectDialog.selectedImageUrl"-->
<!-- style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover" />-->
<!-- </template>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- :src="gallerySelectDialog.selectedImageUrl"-->
<!-- style="height: 500px"-->
<!-- @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />-->
<!-- </el-popover>-->
<!-- </div>-->
<!-- <el-button size="mini" @click="clearImageGallerySelect" style="vertical-align: top">-->
<!-- <el-button size="small" @click="clearImageGallerySelect" style="vertical-align: top">-->
<!-- {{ t('dialog.invite_message.clear_selected_image') }}-->
<!-- </el-button>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <el-button size="mini" @click="showGallerySelectDialog" style="margin-right: 5px">-->
<!-- <el-button size="small" @click="showGallerySelectDialog" style="margin-right: 5px">-->
<!-- {{ t('dialog.invite_message.select_image') }}-->
<!-- </el-button>-->
<!-- </template>-->
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
<DataTable
v-bind="inviteMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteConfirmDialog">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -59,38 +59,40 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteDialog(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInvite">
<el-button @click="cancelSendInvite">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('message')">
<el-button @click="refreshInviteMessageTableData('message')">
{{ t('dialog.invite_message.refresh') }}
</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
v-model="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:edit-and-send-invite-dialog="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';

View File

@@ -1,22 +1,22 @@
<template>
<safe-dialog
ref="inviteGroupDialogRef"
:visible.sync="inviteGroupDialog.visible"
:title="$t('dialog.invite_to_group.header')"
<el-dialog
:z-index="inviteGroupDialogIndex"
v-model="inviteGroupDialog.visible"
:title="t('dialog.invite_to_group.header')"
width="450px"
append-to-body>
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ $t('dialog.invite_to_group.description') }}</span>
<span>{{ t('dialog.invite_to_group.description') }}</span>
<br />
<el-select
v-model="inviteGroupDialog.groupId"
clearable
:placeholder="$t('dialog.invite_to_group.choose_group_placeholder')"
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
filterable
:disabled="inviteGroupDialog.loading"
style="margin-top: 15px; width: 100%">
<el-option-group
:label="$t('dialog.invite_to_group.groups_with_invite_permission')"
:label="t('dialog.invite_to_group.groups_with_invite_permission')"
style="width: 410px">
<el-option
v-for="group in groupsWithInvitePermission"
@@ -26,7 +26,7 @@
style="height: auto"
class="x-friend-item">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -38,11 +38,11 @@
v-model="inviteGroupDialog.userIds"
multiple
clearable
:placeholder="$t('dialog.invite_to_group.choose_friends_placeholder')"
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
filterable
:disabled="inviteGroupDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="inviteGroupDialog.userId" :label="$t('dialog.invite_to_group.selected_users')">
<el-option-group v-if="inviteGroupDialog.userId" :label="t('dialog.invite_to_group.selected_users')">
<el-option
:key="inviteGroupDialog.userObject.id"
:label="inviteGroupDialog.userObject.displayName"
@@ -50,7 +50,7 @@
class="x-friend-item">
<template v-if="inviteGroupDialog.userObject.id">
<div class="avatar" :class="userStatusClass(inviteGroupDialog.userObject)">
<img v-lazy="userImage(inviteGroupDialog.userObject)" />
<img :src="userImage(inviteGroupDialog.userObject)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -62,7 +62,7 @@
<span v-else v-text="inviteGroupDialog.userId"></span>
</el-option>
</el-option-group>
<el-option-group v-if="vipFriends.length" :label="$t('side_panel.favorite')">
<el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">
<el-option
v-for="friend in vipFriends"
:key="friend.id"
@@ -72,7 +72,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -84,7 +84,7 @@
<span v-else v-text="friend.id"></span>
</el-option>
</el-option-group>
<el-option-group v-if="onlineFriends.length" :label="$t('side_panel.online')">
<el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">
<el-option
v-for="friend in onlineFriends"
:key="friend.id"
@@ -94,7 +94,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -106,7 +106,7 @@
<span v-else v-text="friend.id"></span>
</el-option>
</el-option-group>
<el-option-group v-if="activeFriends.length" :label="$t('side_panel.active')">
<el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">
<el-option
v-for="friend in activeFriends"
:key="friend.id"
@@ -116,7 +116,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -128,7 +128,7 @@
<span v-else v-text="friend.id"></span>
</el-option>
</el-option-group>
<el-option-group v-if="offlineFriends.length" :label="$t('side_panel.offline')">
<el-option-group v-if="offlineFriends.length" :label="t('side_panel.offline')">
<el-option
v-for="friend in offlineFriends"
:key="friend.id"
@@ -138,7 +138,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -158,24 +158,25 @@
size="small"
:disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length || !inviteGroupDialog.groupId"
@click="sendGroupInvite">
{{ $t('dialog.invite_to_group.invite') }}
{{ t('dialog.invite_to_group.invite') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, getCurrentInstance, nextTick, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, watch, nextTick, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { groupRequest, userRequest } from '../../api';
import { adjustDialogZ, hasGroupPermission, userImage, userStatusClass } from '../../shared/utils';
import { getNextDialogIndex, hasGroupPermission, userImage, userStatusClass } from '../../shared/utils';
import { useFriendStore, useGroupStore } from '../../stores';
const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { currentUserGroups, inviteGroupDialog } = storeToRefs(useGroupStore());
const { applyGroup } = useGroupStore();
const { proxy } = getCurrentInstance();
const { t } = useI18n();
watch(
() => {
@@ -188,7 +189,7 @@
}
);
const inviteGroupDialogRef = ref(null);
const inviteGroupDialogIndex = ref(2000);
const groupsWithInvitePermission = computed(() => {
return Array.from(currentUserGroups.value.values()).filter((group) =>
@@ -197,7 +198,9 @@
});
function initDialog() {
nextTick(() => adjustDialogZ(inviteGroupDialogRef.value.$el));
nextTick(() => {
inviteGroupDialogIndex.value = getNextDialogIndex();
});
const D = inviteGroupDialog.value;
if (D.groupId) {
groupRequest
@@ -236,7 +239,7 @@
}
// not allowed to invite
inviteGroupDialog.value.groupId = '';
proxy.$message({
ElMessage({
type: 'error',
message: 'You are not allowed to invite to this group'
});
@@ -247,11 +250,12 @@
});
}
function sendGroupInvite() {
proxy.$confirm('Continue? Invite User(s) To Group', 'Confirm', {
ElMessageBox.confirm('Continue? Invite User(s) To Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
const D = inviteGroupDialog.value;
if (action !== 'confirm' || D.loading === true) {
return;
@@ -274,7 +278,7 @@
});
};
inviteLoop();
}
});
})
.catch(() => {});
}
</script>

View File

@@ -1,38 +1,38 @@
<template>
<safe-dialog ref="launchDialogRef" :visible.sync="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-dialog :z-index="launchDialogIndex" v-model="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-form :model="launchDialog" label-width="100px">
<el-form-item :label="t('dialog.launch.url')">
<el-input
v-model="launchDialog.url"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
size="small"
:icon="CopyDocument"
style="margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.url)" />
</el-tooltip>
</el-form-item>
<el-form-item v-if="launchDialog.shortUrl">
<template slot="label">
<template #label>
<span>{{ t('dialog.launch.short_url') }}</span>
<el-tooltip placement="top" style="margin-left: 5px" :content="t('dialog.launch.short_url_notice')">
<i class="el-icon-warning" />
<el-tooltip placement="top" :content="t('dialog.launch.short_url_notice')">
<el-icon style="display: inline-block; margin-left: 5px"><Warning /></el-icon>
</el-tooltip>
</template>
<el-input
v-model="launchDialog.shortUrl"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
size="small"
:icon="CopyDocument"
style="display: inline-block; margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.shortUrl)" />
</el-tooltip>
@@ -40,23 +40,26 @@
<el-form-item :label="t('dialog.launch.location')">
<el-input
v-model="launchDialog.location"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
size="small"
:icon="CopyDocument"
style="display: inline-block; margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.location)" />
</el-tooltip>
</el-form-item>
</el-form>
<el-checkbox v-model="launchDialog.desktop" style="float: left; margin-top: 5px" @change="saveLaunchDialog">
<el-checkbox
v-model="launchDialog.desktop"
style="display: inline-flex; align-items: center; margin-top: 5px"
@change="saveLaunchDialog">
{{ t('dialog.launch.start_as_desktop') }}
</el-checkbox>
<template slot="footer">
<template #footer>
<el-button size="small" @click="showPreviousInstancesInfoDialog(launchDialog.location)">
{{ t('dialog.launch.info') }}
</el-button>
@@ -68,7 +71,6 @@
</el-button>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
@@ -84,7 +86,6 @@
</template>
<template v-else>
<el-button
type="default"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="selfInvite(launchDialog.location, launchDialog.shortName)">
@@ -100,18 +101,26 @@
</template>
</template>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, nextTick, watch, getCurrentInstance } from 'vue';
import { CopyDocument, Warning } from '@element-plus/icons-vue';
import { ref, computed, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, worldRequest } from '../../api';
import configRepository from '../../service/config';
import { adjustDialogZ, checkCanInvite, getLaunchURL, isRealInstance, parseLocation } from '../../shared/utils';
import {
useAppearanceSettingsStore,
getNextDialogIndex,
checkCanInvite,
getLaunchURL,
isRealInstance,
parseLocation
} from '../../shared/utils';
import {
useFriendStore,
useInviteStore,
useInstanceStore,
@@ -121,19 +130,18 @@
} from '../../stores';
import InviteDialog from './InviteDialog/InviteDialog.vue';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { friends } = storeToRefs(useFriendStore());
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { lastLocation } = storeToRefs(useLocationStore());
const { launchGame, tryOpenInstanceInVrc } = useLaunchStore();
const { launchDialogData } = storeToRefs(useLaunchStore());
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { canOpenInstanceInGame } = useInviteStore();
const { isGameRunning } = storeToRefs(useGameStore());
const launchDialogRef = ref(null);
const launchDialogIndex = ref(2000);
const launchDialog = ref({
loading: false,
@@ -207,17 +215,18 @@
}
function handleLaunchGame(location, shortName, desktop) {
if (isGameRunning.value) {
proxy.$confirm(t('dialog.launch.game_running_warning'), t('dialog.launch.header'), {
ElMessageBox.confirm(t('dialog.launch.game_running_warning'), t('dialog.launch.header'), {
confirmButtonText: t('dialog.launch.confirm_yes'),
cancelButtonText: t('dialog.launch.confirm_no'),
type: 'warning',
callback: (action) => {
type: 'warning'
})
.then((action) => {
if (action === 'confirm') {
launchGame(location, shortName, desktop);
isVisible.value = false;
}
}
});
})
.catch(() => {});
return;
}
launchGame(location, shortName, desktop);
@@ -239,7 +248,7 @@
shortName
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Self invite sent',
type: 'success'
});
@@ -257,7 +266,9 @@
if (!isRealInstance(tag)) {
return;
}
nextTick(() => adjustDialogZ(launchDialogRef.value.$el));
nextTick(() => {
launchDialogIndex.value = getNextDialogIndex();
});
const D = launchDialog.value;
D.tag = tag;
D.secureOrShortName = shortName;
@@ -300,12 +311,12 @@
async function copyInstanceMessage(input) {
try {
await navigator.clipboard.writeText(input);
proxy.$message({
ElMessage({
message: 'Instance copied to clipboard',
type: 'success'
});
} catch (error) {
proxy.$message({
ElMessage({
message: 'Instance copied failed',
type: 'error'
});

View File

@@ -1,14 +1,14 @@
<template>
<safe-dialog
ref="moderateGroupDialogRef"
:visible.sync="moderateGroupDialog.visible"
:title="$t('dialog.moderate_group.header')"
<el-dialog
:z-index="moderateGroupDialogIndex"
v-model="moderateGroupDialog.visible"
:title="t('dialog.moderate_group.header')"
width="450px"
append-to-body>
<div v-if="moderateGroupDialog.visible">
<div class="x-friend-item" style="cursor: default">
<div class="avatar">
<img v-lazy="userImage(moderateGroupDialog.userObject)" />
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -22,12 +22,12 @@
<el-select
v-model="moderateGroupDialog.groupId"
clearable
:placeholder="$t('dialog.moderate_group.choose_group_placeholder')"
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
filterable
style="margin-top: 15px; width: 100%">
<el-option-group
v-if="currentUserGroups.size"
:label="$t('dialog.moderate_group.groups_with_moderation_permission')">
:label="t('dialog.moderate_group.groups_with_moderation_permission')">
<el-option
v-for="group in groupsWithModerationPermission"
:key="group.id"
@@ -36,7 +36,7 @@
style="height: auto"
class="x-friend-item">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -54,29 +54,23 @@
showGroupMemberModerationDialog(moderateGroupDialog.groupId, moderateGroupDialog.userId);
moderateGroupDialog.visible = false;
">
{{ $t('dialog.moderate_group.moderation_tools') }}
{{ t('dialog.moderate_group.moderation_tools') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, getCurrentInstance, nextTick, computed } from 'vue';
import { ref, watch, nextTick, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { groupRequest, userRequest } from '../../api';
import {
adjustDialogZ,
hasGroupPermission,
hasGroupModerationPermission,
userImage,
userStatusClass
} from '../../shared/utils';
import { getNextDialogIndex, hasGroupModerationPermission, userImage } from '../../shared/utils';
import { useGroupStore } from '../../stores';
const { currentUserGroups, moderateGroupDialog } = storeToRefs(useGroupStore());
const { applyGroup, showGroupMemberModerationDialog } = useGroupStore();
const { proxy } = getCurrentInstance();
const { showGroupMemberModerationDialog } = useGroupStore();
const { t } = useI18n();
const groupsWithModerationPermission = computed(() => {
return Array.from(currentUserGroups.value.values()).filter((group) => hasGroupModerationPermission(group));
@@ -93,10 +87,12 @@
}
);
const moderateGroupDialogRef = ref(null);
const moderateGroupDialogIndex = ref(2000);
function initDialog() {
nextTick(() => adjustDialogZ(moderateGroupDialogRef.value.$el));
nextTick(() => {
moderateGroupDialogIndex.value = getNextDialogIndex();
});
const D = moderateGroupDialog.value;
if (D.groupId) {
groupRequest

View File

@@ -1,15 +1,15 @@
<template>
<safe-dialog
ref="newInstanceDialogRef"
:visible.sync="newInstanceDialog.visible"
<el-dialog
:z-index="newInstanceDialogIndex"
v-model="newInstanceDialog.visible"
:title="t('dialog.new_instance.header')"
width="650px"
append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
<el-tab-pane :label="t('dialog.new_instance.normal')">
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<el-radio-group v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance">
<el-radio-group v-model="newInstanceDialog.accessType" size="small" @change="buildInstance">
<el-radio-button label="public">{{
t('dialog.new_instance.access_type_public')
}}</el-radio-button>
@@ -33,7 +33,10 @@
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_access_type')">
<el-radio-group v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildInstance">
<el-radio-group
v-model="newInstanceDialog.groupAccessType"
size="small"
@change="buildInstance">
<el-radio-button
label="members"
:disabled="
@@ -59,7 +62,7 @@
</el-radio-group>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildInstance">
<el-radio-group v-model="newInstanceDialog.region" size="small" @change="buildInstance">
<el-radio-button label="US West">{{ t('dialog.new_instance.region_usw') }}</el-radio-button>
<el-radio-button label="US East">{{ t('dialog.new_instance.region_use') }}</el-radio-button>
<el-radio-button label="Europe">{{ t('dialog.new_instance.region_eu') }}</el-radio-button>
@@ -84,8 +87,16 @@
<el-form-item :label="t('dialog.new_instance.world_id')">
<el-input
v-model="newInstanceDialog.worldId"
size="mini"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.display_name')">
<el-input
:disabled="!isLocalUserVrcplusSupporter"
v-model="newInstanceDialog.displayName"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance"></el-input>
</el-form-item>
<el-form-item
@@ -114,7 +125,7 @@
hasGroupPermission(group, 'group-instance-open-create'))
">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -155,22 +166,22 @@
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
v-model="newInstanceDialog.location"
size="mini"
size="small"
readonly
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
</el-form-item>
</template>
</el-form>
</el-tab-pane>
<el-tab-pane :label="t('dialog.new_instance.legacy')">
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<el-radio-group
v-model="newInstanceDialog.accessType"
size="mini"
size="small"
@change="buildLegacyInstance">
<el-radio-button label="public">{{
t('dialog.new_instance.access_type_public')
@@ -197,7 +208,7 @@
:label="t('dialog.new_instance.group_access_type')">
<el-radio-group
v-model="newInstanceDialog.groupAccessType"
size="mini"
size="small"
@change="buildLegacyInstance">
<el-radio-button label="members">{{
t('dialog.new_instance.group_access_type_members')
@@ -211,7 +222,7 @@
</el-radio-group>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildLegacyInstance">
<el-radio-group v-model="newInstanceDialog.region" size="small" @change="buildLegacyInstance">
<el-radio-button label="US West">{{ t('dialog.new_instance.region_usw') }}</el-radio-button>
<el-radio-button label="US East">{{ t('dialog.new_instance.region_use') }}</el-radio-button>
<el-radio-button label="Europe">{{ t('dialog.new_instance.region_eu') }}</el-radio-button>
@@ -226,15 +237,15 @@
<el-form-item :label="t('dialog.new_instance.world_id')">
<el-input
v-model="newInstanceDialog.worldId"
size="mini"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.instance_id')">
<el-input
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="mini"
size="small"
@change="buildLegacyInstance"></el-input>
</el-form-item>
<el-form-item
@@ -254,7 +265,7 @@
:value="currentUser.id"
style="height: auto">
<div class="avatar" :class="userStatusClass(currentUser)">
<img v-lazy="userImage(currentUser)" />
<img :src="userImage(currentUser)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="currentUser.displayName"></span>
@@ -271,7 +282,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -293,7 +304,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -315,7 +326,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -337,7 +348,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -371,7 +382,7 @@
style="height: auto; width: 478px">
<template v-if="group">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span></div
@@ -383,17 +394,17 @@
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
v-model="newInstanceDialog.location"
size="mini"
size="small"
readonly
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<template v-if="newInstanceDialog.selectedTab === '0'" #footer>
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
<template v-if="newInstanceDialog.instanceCreated">
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
@@ -412,13 +423,11 @@
>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</el-button
>
<el-button
type="primary"
size="small"
@click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
{{ t('dialog.new_instance.open_ingame') }}
@@ -439,7 +448,7 @@
}}</el-button>
</template>
</template>
<template v-else-if="newInstanceDialog.selectedTab === '1'" #footer>
<template v-else-if="newInstanceDialog.selectedTab === 'Legacy'" #footer>
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
}}</el-button>
@@ -457,7 +466,6 @@
>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</el-button
@@ -479,17 +487,18 @@
</template>
</template>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, watch, nextTick, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { groupRequest, instanceRequest, worldRequest } from '../../api';
import configRepository from '../../service/config';
import {
adjustDialogZ,
getNextDialogIndex,
copyToClipboard,
getLaunchURL,
hasGroupPermission,
@@ -518,11 +527,9 @@
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { friends, vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { currentUserGroups, cachedGroups } = storeToRefs(useGroupStore());
const { handleGroupPermissions } = useGroupStore();
const { currentUserGroups } = storeToRefs(useGroupStore());
const { cachedGroups, handleGroupPermissions } = useGroupStore();
const { lastLocation } = storeToRefs(useLocationStore());
const { showLaunchDialog } = useLaunchStore();
const { createNewInstance } = useInstanceStore();
@@ -530,12 +537,12 @@
const { tryOpenInstanceInVrc } = useLaunchStore();
const { canOpenInstanceInGame } = useInviteStore();
const newInstanceDialogRef = ref(null);
const newInstanceDialogIndex = ref(2000);
const newInstanceDialog = ref({
visible: false,
// loading: false,
selectedTab: '0',
selectedTab: 'Normal',
instanceCreated: false,
queueEnabled: false,
worldId: '',
@@ -551,6 +558,7 @@
strict: false,
location: '',
shortName: '',
displayName: '',
url: '',
secureOrShortName: '',
lastSelectedGroupId: '',
@@ -575,6 +583,8 @@
}
);
const isLocalUserVrcplusSupporter = computed(() => currentUser.value.$isVRCPlus);
initializeNewInstanceDialog();
function closeInviteDialog() {
@@ -617,7 +627,9 @@
if (!isRealInstance(tag)) {
return;
}
nextTick(() => adjustDialogZ(newInstanceDialogRef.value.$el));
nextTick(() => {
newInstanceDialogIndex.value = getNextDialogIndex();
});
const D = newInstanceDialog.value;
const L = parseLocation(tag);
if (D.worldId === L.worldId) {
@@ -634,6 +646,9 @@
D.strict = false;
D.shortName = '';
D.secureOrShortName = '';
if (!isLocalUserVrcplusSupporter.value) {
D.displayName = '';
}
const args = await groupRequest.getGroupPermissions({ userId: currentUser.value.id });
handleGroupPermissions(args);
buildInstance();
@@ -673,10 +688,23 @@
configRepository
.getBool('instanceDialogAgeGate', false)
.then((value) => (newInstanceDialog.value.ageGate = value));
configRepository
.getString('instanceDialogDisplayName', '')
.then((value) => (newInstanceDialog.value.displayName = value));
}
function saveNewInstanceDialog() {
const { accessType, region, instanceName, userId, groupId, groupAccessType, queueEnabled, ageGate } =
newInstanceDialog.value;
const {
accessType,
region,
instanceName,
userId,
groupId,
groupAccessType,
queueEnabled,
ageGate,
displayName
} = newInstanceDialog.value;
configRepository.setString('instanceDialogAccessType', accessType);
configRepository.setString('instanceRegion', region);
@@ -686,9 +714,10 @@
configRepository.setString('instanceDialogGroupAccessType', groupAccessType);
configRepository.setBool('instanceDialogQueueEnabled', queueEnabled);
configRepository.setBool('instanceDialogAgeGate', ageGate);
configRepository.setString('instanceDialogDisplayName', displayName);
}
function newInstanceTabClick(tab) {
if (tab === '1') {
function newInstanceTabClick(obj) {
if (obj.props.name === 'Normal') {
buildInstance();
} else {
buildLegacyInstance();
@@ -720,7 +749,7 @@
worldId: L.worldId
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Self invite sent',
type: 'success'
});
@@ -749,7 +778,7 @@
}
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
D.roleIds = [];
const ref = cachedGroups.value.get(D.groupId);
const ref = cachedGroups.get(D.groupId);
if (typeof ref !== 'undefined') {
D.groupRef = ref;
D.selectedGroupRoles = ref.roles;
@@ -824,7 +853,7 @@
}
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
D.roleIds = [];
const ref = cachedGroups.value.get(D.groupId);
const ref = cachedGroups.get(D.groupId);
if (typeof ref !== 'undefined') {
D.groupRef = ref;
D.selectedGroupRoles = ref.roles;

View File

@@ -1,42 +0,0 @@
<template>
<safe-dialog
class="x-dialog"
:visible="previousImagesDialogVisible"
:title="t('dialog.previous_images.header')"
width="800px"
append-to-body
@close="closeDialog">
<div>
<div v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<el-popover
class="x-change-image-item"
placement="right"
width="500px"
trigger="click"
v-if="image.file">
<img slot="reference" v-lazy="image.file.url" class="x-link" />
<img
v-lazy="image.file.url"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(image.file.url)" />
</el-popover>
</div>
</div>
</safe-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore } from '../../stores';
const { t } = useI18n();
const { previousImagesDialogVisible, previousImagesTable } = storeToRefs(useGalleryStore());
const { showFullscreenImageDialog } = useGalleryStore();
function closeDialog() {
previousImagesDialogVisible.value = false;
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="previousInstancesGroupDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="previousInstancesGroupDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
@@ -13,7 +13,7 @@
style="width: 150px" />
</div>
<data-tables v-loading="loading" v-bind="previousInstancesGroupDialogTable" style="margin-top: 10px">
<DataTable v-loading="loading" v-bind="previousInstancesGroupDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
@@ -39,48 +39,50 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
@click="showPreviousInstancesInfoDialog(scope.row.location)" />
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deleteGameLogGroupInstance(scope.row)" />
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deleteGameLogGroupInstancePrompt(scope.row)" />
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue';
import {
parseLocation,
compareByCreatedAt,
timeToText,
removeFromArray,
adjustDialogZ,
getNextDialogIndex,
formatDateFilter
} from '../../../shared/utils';
import { database } from '../../../service/database';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useInstanceStore, useUiStore } from '../../../stores';
const { proxy } = getCurrentInstance();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = useUiStore();
const { t } = useI18n();
const previousInstancesGroupDialogRef = ref(null);
const previousInstancesGroupDialogIndex = ref(2000);
const loading = ref(false);
const previousInstancesGroupDialogTable = reactive({
@@ -88,7 +90,7 @@
filters: [{ prop: 'groupName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -115,7 +117,7 @@
() => {
if (props.previousInstancesGroupDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesGroupDialogRef.value.$el);
previousInstancesGroupDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesGroupTable();
}
@@ -144,15 +146,16 @@
}
function deleteGameLogGroupInstancePrompt(row) {
proxy.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteGameLogGroupInstance(row);
}
}
});
})
.catch(() => {});
}
</script>

View File

@@ -1,8 +1,8 @@
<template>
<safe-dialog
ref="dialogRef"
:visible="previousInstancesInfoDialogVisible"
:title="$t('dialog.previous_instances.info')"
<el-dialog
:z-index="previousInstancesInfoDialogIndex"
:model-value="previousInstancesInfoDialogVisible"
:title="t('dialog.previous_instances.info')"
width="800px"
:fullscreen="fullscreen"
destroy-on-close
@@ -11,12 +11,12 @@
<Location :location="location.tag" style="font-size: 14px" />
<el-input
v-model="dataTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="width: 150px"
clearable></el-input>
</div>
<data-tables v-loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="110">
<DataTable v-loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="110">
<template #default="scope">
<el-tooltip placement="left">
<template #content>
@@ -26,7 +26,7 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
<el-table-column :label="t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
<template #default="scope">
<template v-if="gameLogIsFriend(scope.row)">
<el-tooltip v-if="gameLogIsFavorite(scope.row)" placement="top" content="Favorite">
@@ -36,33 +36,35 @@
<span>💚</span>
</el-tooltip>
</template>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.display_name')" prop="displayName" sortable>
<el-table-column :label="t('table.previous_instances.display_name')" prop="displayName" sortable>
<template #default="scope">
<span class="x-link" @click="lookupUser(scope.row)">{{ scope.row.displayName }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
<el-table-column :label="t('table.previous_instances.time')" prop="time" width="100" sortable>
<template #default="scope">
<span>{{ scope.row.timer }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.count')" prop="count" width="100" sortable>
<el-table-column :label="t('table.previous_instances.count')" prop="count" width="100" sortable>
<template #default="scope">
<span>{{ scope.row.count }}</span>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
timeToText,
@@ -74,8 +76,9 @@
const { previousInstancesInfoDialogVisible, previousInstancesInfoDialogInstanceId } =
storeToRefs(useInstanceStore());
const { gameLogIsFriend, gameLogIsFavorite } = useGameLogStore();
const { t } = useI18n();
const dialogRef = ref(null);
const previousInstancesInfoDialogIndex = ref(2000);
const loading = ref(false);
const location = ref({
@@ -111,7 +114,7 @@
],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created_at',
order: 'descending'
@@ -139,7 +142,7 @@
);
function init() {
adjustDialogZ(dialogRef.value.$el);
previousInstancesInfoDialogIndex.value = getNextDialogIndex();
loading.value = true;
location.value = parseLocation(previousInstancesInfoDialogInstanceId.value);
}

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="previousInstancesWorldDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="previousInstancesWorldDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
@@ -12,7 +12,7 @@
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
</div>
<data-tables v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<DataTable v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
@@ -43,35 +43,41 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
class="button-pd-0"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogWorldInstance(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogWorldInstancePrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
removeFromArray,
@@ -80,7 +86,6 @@
} from '../../../shared/utils';
import { useInstanceStore, useUiStore, useUserStore } from '../../../stores';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const props = defineProps({
@@ -100,7 +105,7 @@
filters: [{ prop: 'groupName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -111,7 +116,7 @@
}
});
const loading = ref(false);
const previousInstancesWorldDialogRef = ref(null);
const previousInstancesWorldDialogIndex = ref(2000);
const isVisible = computed({
get: () => props.previousInstancesWorldDialog.visible,
@@ -145,16 +150,17 @@
}
function deleteGameLogWorldInstancePrompt(row) {
proxy.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteGameLogWorldInstance(row);
}
}
});
})
.catch(() => {});
}
watch(
@@ -162,10 +168,16 @@
() => {
if (props.previousInstancesWorldDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesWorldDialogRef.value.$el);
previousInstancesWorldDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesWorldTable();
}
}
);
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>

View File

@@ -1,159 +0,0 @@
<template>
<el-dialog
ref="elDialogRef"
:visible="props.visible"
v-bind="attrs"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose"
:top="marginTop">
<slot></slot>
<template v-if="slots.title" #title>
<slot name="title"></slot>
</template>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onBeforeUnmount, nextTick, useAttrs, useSlots } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:visible', 'open', 'close']);
const attrs = useAttrs();
const slots = useSlots();
const elDialogRef = ref(null);
const wrapperElement = ref(null);
const mouseDownOnWrapper = ref(false);
const styleObserver = ref(null);
const resizeObserver = ref(null);
const marginTop = ref('5px');
let handleResize = null;
const handleOpen = () => {
emit('open');
nextTick(() => {
addWrapperListeners();
removeTitleAttribute();
adjustDialogMarginTop();
});
};
const removeTitleAttribute = () => {
const wrapper = elDialogRef.value?.$el;
if (wrapper && wrapper.nodeType === Node.ELEMENT_NODE) {
wrapper.removeAttribute('title');
}
};
const handleClose = () => {
emit('close');
removeWrapperListeners();
if (styleObserver.value) {
styleObserver.value.disconnect();
styleObserver.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (handleResize) {
window.removeEventListener('resize', handleResize);
handleResize = null;
}
emit('update:visible', false);
};
const handleWrapperMouseDown = (event) => {
if (event.target === wrapperElement.value) {
mouseDownOnWrapper.value = true;
}
};
const handleWrapperMouseUp = (event) => {
if (event.target === wrapperElement.value && mouseDownOnWrapper.value) {
handleClose();
}
mouseDownOnWrapper.value = false;
};
const addWrapperListeners = () => {
const wrapper = elDialogRef.value?.$el;
if (
wrapper &&
wrapper.nodeType === Node.ELEMENT_NODE &&
wrapper.classList &&
wrapper.classList.contains('el-dialog__wrapper')
) {
wrapperElement.value = wrapper;
wrapperElement.value.addEventListener('mousedown', handleWrapperMouseDown);
wrapperElement.value.addEventListener('mouseup', handleWrapperMouseUp);
} else {
wrapperElement.value = null;
}
};
const removeWrapperListeners = () => {
if (wrapperElement.value) {
wrapperElement.value.removeEventListener('mousedown', handleWrapperMouseDown);
wrapperElement.value.removeEventListener('mouseup', handleWrapperMouseUp);
wrapperElement.value = null;
}
mouseDownOnWrapper.value = false;
};
const adjustDialogMarginTop = () => {
const wrapper = elDialogRef.value?.$el;
if (!wrapper) return;
const dialog = wrapper.querySelector('.el-dialog');
if (!dialog) return;
const applyStyle = () => {
const dialogHeight = dialog.offsetHeight;
const viewportHeight = window.innerHeight;
const topOffset = Math.max(0, (viewportHeight - dialogHeight) * 0.2);
marginTop.value = `${topOffset}px`;
};
applyStyle();
};
onBeforeUnmount(() => {
removeWrapperListeners();
if (styleObserver.value) {
styleObserver.value.disconnect();
styleObserver.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (handleResize) {
window.removeEventListener('resize', handleResize);
handleResize = null;
}
});
</script>

View File

@@ -1,7 +1,7 @@
<!--<template>-->
<!-- <safe-dialog-->
<!-- <el-dialog-->
<!-- class="x-dialog"-->
<!-- :visible="sendBoopDialog.visible"-->
<!-- :model-value="sendBoopDialog.visible"-->
<!-- :title="t('dialog.boop_dialog.header')"-->
<!-- width="450px"-->
<!-- @close="closeDialog">-->
@@ -20,7 +20,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -42,7 +42,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -64,7 +64,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -86,7 +86,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -135,7 +135,7 @@
<!-- </template>-->
<!-- <template v-else>-->
<!-- <img-->
<!-- v-lazy="image.versions[image.versions.length - 1].file.url"-->
<!-- :src="image.versions[image.versions.length - 1].file.url"-->
<!-- class="avatar"-->
<!-- style="width: 200px; height: 200px" />-->
<!-- </template>-->
@@ -162,7 +162,7 @@
<!-- t('dialog.boop_dialog.send')-->
<!-- }}</el-button>-->
<!-- </template>-->
<!-- </safe-dialog>-->
<!-- </el-dialog>-->
<!--</template>-->
<!--<script setup>-->

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="bioDialog.visible"
v-model="bioDialog.visible"
:title="t('dialog.bio.header')"
width="600px"
append-to-body>
@@ -9,7 +9,7 @@
<el-input
v-model="bioDialog.bio"
type="textarea"
size="mini"
size="small"
maxlength="512"
show-word-limit
:autosize="{ minRows: 5, maxRows: 20 }"
@@ -23,16 +23,13 @@
v-model="bioDialog.bioLinks[index]"
size="small"
style="margin-top: 5px">
<img
slot="prepend"
:src="getFaviconUrl(link)"
style="width: 16px; height: 16px; vertical-align: middle" />
<el-button slot="append" icon="el-icon-delete" @click="bioDialog.bioLinks.splice(index, 1)" />
<img :src="getFaviconUrl(link)" style="width: 16px; height: 16px; vertical-align: middle" />
<el-button :icon="Delete" @click="bioDialog.bioLinks.splice(index, 1)" />
</el-input>
<el-button
:disabled="bioDialog.bioLinks.length >= 3"
size="mini"
size="small"
style="margin-top: 5px"
@click="bioDialog.bioLinks.push('')">
{{ t('dialog.bio.add_link') }}
@@ -44,18 +41,19 @@
{{ t('dialog.bio.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { getFaviconUrl } from '../../../shared/utils';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const props = defineProps({
bioDialog: {
type: Object,
@@ -79,7 +77,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Bio updated',
type: 'success'
});

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="languageDialog.visible"
v-model="languageDialog.visible"
:title="t('dialog.language.header')"
width="400px"
append-to-body>
@@ -22,7 +22,6 @@
</el-tag>
</div>
<el-select
value=""
:disabled="languageDialog.loading || (currentUser.$languages && currentUser.$languages.length === 3)"
:placeholder="t('dialog.language.select_language')"
style="margin-top: 14px"
@@ -40,12 +39,12 @@
</el-option>
</el-select>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { languageClass } from '../../../shared/utils';
import { useUserStore } from '../../../stores';

View File

@@ -1,24 +1,24 @@
<template>
<safe-dialog
ref="previousInstancesUserDialogRef"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
<el-dialog
:z-index="previousInstancesUserDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span>
<el-input
v-model="previousInstancesUserDialogTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
</div>
<data-tables v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
<DataTable v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.world')" prop="name" sortable>
<el-table-column :label="t('table.previous_instances.world')" prop="name" sortable>
<template #default="scope">
<Location
:location="scope.row.location"
@@ -26,53 +26,61 @@
:grouphint="scope.row.groupName" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location" width="170">
<el-table-column :label="t('table.previous_instances.instance_creator')" prop="location" width="170">
<template #default="scope">
<DisplayName :userid="scope.row.$location.userId" :location="scope.row.$location.tag" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
<el-table-column :label="t('table.previous_instances.time')" prop="time" width="100" sortable>
<template #default="scope">
<span v-text="scope.row.timer"></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.action')" width="90" align="right">
<el-table-column :label="t('table.previous_instances.action')" width="90" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-switch-button"
size="mini"
:icon="SwitchButton"
size="small"
class="button-pd-0"
@click="showLaunchDialog(scope.row.location)"></el-button>
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
class="button-pd-0"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogUserInstance(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogUserInstancePrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { SwitchButton, DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
removeFromArray,
@@ -93,22 +101,20 @@
previousInstancesTable: {
data: [],
filters: [{ prop: 'displayName', value: '' }],
tableProps: { stripe: true, size: 'mini', height: '400px' }
tableProps: { stripe: true, size: 'small', height: '400px' }
}
})
}
});
const emit = defineEmits(['update:previous-instances-user-dialog']);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const previousInstancesUserDialogTable = reactive({
data: [],
filters: [{ prop: 'worldName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -122,8 +128,9 @@
const { showLaunchDialog } = useLaunchStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { t } = useI18n();
const previousInstancesUserDialogRef = ref(null);
const previousInstancesUserDialogIndex = ref(2000);
const isVisible = computed({
get: () => props.previousInstancesUserDialog.visible,
@@ -154,7 +161,7 @@
() => {
if (props.previousInstancesUserDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesUserDialogRef.value.$el);
previousInstancesUserDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesUserTable();
}
@@ -172,13 +179,20 @@
}
function deleteGameLogUserInstancePrompt(row) {
proxy.$confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') deleteGameLogUserInstance(row);
}
});
})
.catch(() => {});
}
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="pronounsDialog.visible"
v-model="pronounsDialog.visible"
:title="t('dialog.pronouns.header')"
width="600px"
append-to-body>
@@ -9,7 +9,7 @@
<el-input
type="textarea"
v-model="pronounsDialog.pronouns"
size="mini"
size="small"
maxlength="32"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -21,18 +21,15 @@
{{ t('dialog.pronouns.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const props = defineProps({
pronounsDialog: {
type: Object,
@@ -55,7 +52,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Pronouns updated',
type: 'success'
});

View File

@@ -1,7 +1,8 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="sendInviteRequestDialogVisible"
:model-value="sendInviteRequestDialogVisible"
@update:model-value="$emit('update:sendInviteRequestDialogVisible', $event)"
:title="t('dialog.invite_request_message.header')"
width="800px"
append-to-body
@@ -10,20 +11,20 @@
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
<DataTable
v-bind="inviteRequestMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteConfirmDialog">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -34,38 +35,38 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteDialog(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInviteRequest">{{
t('dialog.invite_request_message.cancel')
}}</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('request')">{{
<el-button @click="cancelSendInviteRequest">{{ t('dialog.invite_request_message.cancel') }}</el-button>
<el-button @click="refreshInviteMessageTableData('request')">{{
t('dialog.invite_request_message.refresh')
}}</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
v-model="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:edit-and-send-invite-dialog="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="socialStatusDialog.visible"
v-model="socialStatusDialog.visible"
:title="t('dialog.social_status.header')"
append-to-body
width="400px">
@@ -11,13 +11,13 @@
<template #title>
<span style="font-size: 16px">{{ t('dialog.social_status.history') }}</span>
</template>
<data-tables
<DataTable
v-bind="socialStatusHistoryTable"
style="cursor: pointer"
@row-click="setSocialStatusFromHistory">
<el-table-column :label="t('table.social_status.no')" prop="no" width="50"></el-table-column>
<el-table-column :label="t('table.social_status.status')" prop="status"></el-table-column>
</data-tables>
</DataTable>
</el-collapse-item>
</el-collapse>
@@ -53,19 +53,18 @@
{{ t('dialog.social_status.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { useUserStore } from '../../../stores';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
@@ -103,7 +102,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Status updated',
type: 'success'
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
<template>
<safe-dialog
ref="VRCXUpdateDialogRef"
<el-dialog
:z-index="VRCXUpdateDialogIndex"
class="x-dialog"
:visible.sync="VRCXUpdateDialog.visible"
v-model="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')"
append-to-body
width="400px">
@@ -59,15 +59,15 @@
{{ t('dialog.vrcx_updater.install') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { branches } from '../../shared/constants';
import { adjustDialogZ } from '../../shared/utils';
import { getNextDialogIndex } from '../../shared/utils';
import { useVRCXUpdaterStore } from '../../stores';
const VRCXUpdaterStore = useVRCXUpdaterStore();
@@ -85,14 +85,14 @@
const { t } = useI18n();
const VRCXUpdateDialogRef = ref(null);
const VRCXUpdateDialogIndex = ref(2000);
watch(
() => VRCXUpdateDialog,
(newVal) => {
if (newVal.value.visible) {
nextTick(() => {
adjustDialogZ(VRCXUpdateDialogRef.value.$el);
VRCXUpdateDialogIndex.value = getNextDialogIndex();
});
}
}

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="changeWorldImageDialogVisible"
:model-value="changeWorldImageDialogVisible"
:title="t('dialog.change_content_image.world')"
width="850px"
append-to-body
@@ -16,106 +16,60 @@
<span>{{ t('dialog.change_content_image.description') }}</span>
<br />
<el-button-group style="padding-bottom: 10px; padding-top: 10px">
<el-button type="default" size="small" icon="el-icon-refresh" @click="refresh">{{
t('dialog.change_content_image.refresh')
}}</el-button>
<el-button type="default" size="small" icon="el-icon-upload2" @click="uploadWorldImage">{{
t('dialog.change_content_image.upload')
}}</el-button>
<!-- el-button(type="default" size="small" @click="deleteWorldImage" icon="el-icon-delete") Delete Latest Image-->
<el-button type="default" size="small" :icon="Upload" @click="uploadWorldImage">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<div
v-if="image.file"
class="x-change-image-item"
style="cursor: pointer"
:class="{ 'current-image': compareCurrentImage(image) }"
@click="setWorldImage(image)">
<img v-lazy="image.file.url" class="image" />
</div>
<div class="x-change-image-item">
<img :src="previousImageUrl" class="img-size" loading="lazy" />
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useGalleryStore, useWorldStore } from '../../../stores';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
import { useWorldStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { worldDialog } = storeToRefs(useWorldStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const { applyWorld } = useWorldStore();
const props = defineProps({
changeWorldImageDialogVisible: {
type: Boolean,
default: false
required: true
},
previousImagesFileId: {
previousImageUrl: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'refresh']);
const changeWorldImageDialogLoading = ref(false);
const worldImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: '',
worldId: ''
});
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'update:previousImageUrl']);
function closeDialog() {
emit('update:changeWorldImageDialogVisible', false);
}
function refresh() {
emit('refresh', 'Change');
}
async function resizeImageToFitLimits(file) {
const response = await AppApi.ResizeImageToFitLimits(file);
return response;
}
async function genMd5(file) {
const response = await AppApi.MD5File(file);
return response;
}
async function genSig(file) {
const response = await AppApi.SignFile(file);
return response;
}
async function genLength(file) {
const response = await AppApi.FileLength(file);
return response;
}
function onFileChangeWorldImage(e) {
const clearFile = function () {
const fileInput = /** @type {HTMLInputElement} */ (document.querySelector('#WorldImageUploadButton'));
changeWorldImageDialogLoading.value = false;
const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#WorldImageUploadButton'));
if (fileInput) {
fileInput.value = '';
}
@@ -125,9 +79,11 @@
clearFile();
return;
}
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -135,223 +91,57 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
changeWorldImageDialogLoading.value = true;
const r = new FileReader();
r.onload = async function (file) {
r.onload = async function () {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total.toString(), 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const worldId = worldDialog.value.id;
const { imageUrl } = worldDialog.value.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
$message({
message: t('message.world.image_invalid'),
type: 'error'
});
clearFile();
return;
}
worldImage.value = {
base64File,
fileMd5,
base64SignatureFile,
signatureMd5,
fileId,
worldId,
...worldImage.value
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
await initiateUpload(base64File);
} catch (error) {
console.error('World image upload process failed:', error);
} finally {
changeWorldImageDialogLoading.value = false;
clearFile();
}
};
changeWorldImageDialogLoading.value = true;
r.readAsBinaryString(files[0]);
}
// ------------ Upload Process Start ------------
async function initiateUpload(params, fileId) {
const res = await imageRequest.uploadWorldImage(params, fileId);
return worldImageInit(res);
}
async function worldImageInit(args) {
const fileId = args.json.id;
const fileVersion = args.json.versions[args.json.versions.length - 1].version;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageFileStart(params);
return worldImageFileStart(res);
}
async function worldImageFileStart(args) {
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadWorldImageFileAWS(params);
}
async function uploadWorldImageFileAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: worldImage.value.base64File,
fileMIME: 'image/png',
headers: {
'Content-MD5': worldImage.value.fileMd5
}
});
if (json.status !== 200) {
changeWorldImageDialogLoading.value = false;
$throw(json.status, 'World image upload failed', params.url);
}
const args = {
json,
params
};
return worldImageFileAWS(args);
}
async function worldImageFileAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageFileFinish(params);
return worldImageFileFinish(res);
}
async function worldImageFileFinish(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigStart(params);
return worldImageSigStart(res);
}
async function worldImageSigStart(args) {
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadWorldImageSigAWS(params);
}
async function uploadWorldImageSigAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: worldImage.value.base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
headers: {
'Content-MD5': worldImage.value.signatureMd5
}
});
if (json.status !== 200) {
changeWorldImageDialogLoading.value = false;
$throw(json.status, 'World image upload failed', params.url);
}
const args = {
json,
params
};
return worldImageSigAWS(args);
}
async function worldImageSigAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigFinish(params);
return worldImageSigFinish(res);
}
async function worldImageSigFinish(args) {
const { fileId, fileVersion } = args.params;
const parmas = {
id: worldImage.value.worldId,
imageUrl: `${AppGlobal.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setWorldImage(parmas);
return worldImageSet(res);
}
function worldImageSet(args) {
changeWorldImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.world.image_changed'),
type: 'success'
});
refresh();
} else {
$throw(0, 'World image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function setWorldImage(image) {
changeWorldImageDialogLoading.value = true;
const parmas = {
async function initiateUpload(base64File) {
const args = await worldRequest.uploadWorldImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
const worldArgs = await worldRequest.saveWorld({
id: worldDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setWorldImage(parmas)
.then((args) => worldImageSet(args))
.finally(() => {
changeWorldImageDialogLoading.value = false;
closeDialog();
});
imageUrl: fileUrl
});
const ref = applyWorld(worldArgs.json);
changeWorldImageDialogLoading.value = false;
emit('update:previousImageUrl', ref.imageUrl);
ElMessage({
message: t('message.world.image_changed'),
type: 'success'
});
// closeDialog();
}
function compareCurrentImage(image) {
if (
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
worldDialog.value.ref.imageUrl
) {
return true;
}
return false;
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
</script>
<style lang="scss" scoped>
.img-size {
width: 500px;
height: 375px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="isVisible"
<el-dialog
v-model="isVisible"
:title="t('dialog.set_world_tags.header')"
width="400px"
destroy-on-close
@@ -20,7 +20,7 @@
<el-input
v-model="setWorldTagsDialog.authorTags"
type="textarea"
size="mini"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
@@ -81,12 +81,13 @@
</el-button>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
import { useWorldStore } from '../../../stores';
@@ -115,8 +116,6 @@
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const setWorldTagsDialog = ref({
authorTags: '',
contentTags: '',
@@ -296,7 +295,7 @@
tags
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Tags updated',
type: 'success'
});

View File

@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="isVisible"
<el-dialog
v-model="isVisible"
:title="t('dialog.allowed_video_player_domains.header')"
width="600px"
destroy-on-close
@@ -12,9 +12,9 @@
v-model="urlList[index]"
size="small"
style="margin-top: 5px">
<el-button slot="append" icon="el-icon-delete" @click="urlList.splice(index, 1)"></el-button>
<el-button :icon="Delete" @click="urlList.splice(index, 1)"></el-button>
</el-input>
<el-button size="mini" style="margin-top: 5px" @click="urlList.push('')">
<el-button size="small" style="margin-top: 5px" @click="urlList.push('')">
{{ t('dialog.allowed_video_player_domains.add_domain') }}
</el-button>
</div>
@@ -27,12 +27,16 @@
{{ t('dialog.allowed_video_player_domains.save') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
const props = defineProps({
@@ -44,8 +48,6 @@
const emit = defineEmits(['update:worldAllowedDomainsDialog']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const urlList = ref([]);
@@ -79,7 +81,7 @@
urlList: urlList.value
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Allowed Video Player Domains updated',
type: 'success'
});

File diff suppressed because it is too large Load Diff

25
src/index.html Normal file
View File

@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<meta name="referrer" content="no-referrer" />
<meta http-equiv="Cache-Control" content="no-cache" />
<title>VRCX</title>
<!-- <link rel="stylesheet" href="app.css" /> -->
<link rel="preconnect" href="https://api.vrchat.cloud" />
<link rel="preconnect" href="https://files.vrchat.cloud" />
<link rel="preconnect" href="https://d348imysud55la.cloudfront.net" />
<link rel="dns-prefetch" href="https://assets.vrchat.com" />
</head>
<body>
<div id="root"></div>
<script type="module" src="app.js"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"language": "Čeština (CZ)",
"language": "Čeština (cs)",
"translator": "-",
"nav_tooltip": {
"feed": "Zdroj",

View File

@@ -617,7 +617,7 @@
"launch_commands": {
"header": "Launch Commands (Deep Links)",
"docs": "Launch Command Docs",
"show_confirmation_on_switch_avatar_enable": "Show confirmation diaglog before switching avatars",
"show_confirmation_on_switch_avatar_enable": "Show confirmation dialog before switching avatars",
"show_confirmation_on_switch_avatar_tooltip": "When disabled VRCX will not come to the front when switching avatars and ask for confirmation",
"website_userscript": "VRC Website Userscript"
},
@@ -653,6 +653,7 @@
"avatar_name_cache": "Avatar Name cache:",
"instance_cache": "Instance cache:",
"clear_cache": "Clear Cache",
"refresh_cache": "Refresh Cache",
"auto_clear_cache": "Auto Clear Cache",
"show_console": "Show Console"
},
@@ -766,7 +767,6 @@
"show_avatar_author": "Show Avatar Author",
"show_fallback_avatar": "Show Fallback Avatar Details",
"show_previous_instances": "Show Previous Instances",
"show_previous_images": "Show Previous Images",
"moderation_block": "Block",
"moderation_unblock": "Unblock",
"moderation_mute": "Mute",
@@ -915,7 +915,6 @@
"make_home": "Make Home",
"reset_home": "Reset Home",
"show_previous_instances": "Show Previous Instances",
"show_previous_images": "Show Previous Images",
"rename": "Rename",
"change_description": "Change Description",
"change_capacity": "Change Capacity",
@@ -1002,7 +1001,6 @@
"select_fallback": "Select Fallback Avatar",
"block": "Block Avatar",
"unblock": "Unblock Avatar",
"show_previous_images": "Show Previous Images",
"make_public": "Make Public",
"make_private": "Make Private",
"rename": "Rename",
@@ -1118,7 +1116,6 @@
"load_more": "Load more...",
"sort_by": "Sort By:",
"sorting": {
"user_id": "User ID (Ascending)",
"joined_at_asc": "Joined At (Ascending)",
"joined_at_desc": "Joined At (Descending)"
},
@@ -1227,7 +1224,8 @@
"group": "Group",
"legacy": "Legacy",
"roles": "Roles",
"open_ingame": "Open in-game"
"open_ingame": "Open in-game",
"display_name": "Display Name (VRC+)"
},
"launch_options": {
"header": "VRChat Launch Options",
@@ -1518,12 +1516,8 @@
"avatar": "Change Avatar Image",
"world": "Change World Image",
"description": "Recommended image size: 1200x900px (4:3)",
"refresh": "Refresh",
"upload": "Upload Image"
},
"previous_images": {
"header": "Previous Images"
},
"previous_instances": {
"header": "Previous Instances",
"info": "Previous Instance Info",
@@ -1663,6 +1657,10 @@
"choose_group_placeholder": "Choose Group",
"groups_with_moderation_permission": "Groups with Moderation Permission",
"moderation_tools": "Moderation Tools"
},
"fullscreen_image": {
"download_and_save_image": "Download and save image",
"copy_image_to_clipboard": "Copy image to clipboard"
}
},
"confirm": {

View File

@@ -1,73 +1,44 @@
import en from './en/en.json' assert { type: 'JSON' };
import elements_en from 'element-ui/lib/locale/lang/en';
import es from './es/en.json' assert { type: 'JSON' };
import elements_es from 'element-ui/lib/locale/lang/es';
import fr from './fr/en.json' assert { type: 'JSON' };
import elements_fr from 'element-ui/lib/locale/lang/fr';
// import hu from './hu/en.json' assert { type: 'JSON' };
// import elements_hu from 'element-ui/lib/locale/lang/hu';
import hu from './hu/en.json' assert { type: 'JSON' };
import ja from './ja/en.json' assert { type: 'JSON' };
import elements_ja from 'element-ui/lib/locale/lang/ja';
import ko from './ko/en.json' assert { type: 'JSON' };
import elements_ko from 'element-ui/lib/locale/lang/ko';
import pl from './pl/en.json' assert { type: 'JSON' };
import elements_pl from 'element-ui/lib/locale/lang/pl';
import pt from './pt/en.json' assert { type: 'JSON' };
import elements_pt from 'element-ui/lib/locale/lang/pt';
import cz from './cz/en.json' assert { type: 'JSON' };
import elements_cz from 'element-ui/lib/locale/lang/cs-CZ';
import cs from './cs/en.json' assert { type: 'JSON' };
import ru_RU from './ru/en.json' assert { type: 'JSON' };
import elements_ru from 'element-ui/lib/locale/lang/ru-RU';
import ru from './ru/en.json' assert { type: 'JSON' };
import vi from './vi/en.json' assert { type: 'JSON' };
import elements_vi from 'element-ui/lib/locale/lang/vi';
import zh_CN from './zh-CN/en.json' assert { type: 'JSON' };
import elements_zh_CN from 'element-ui/lib/locale/lang/zh-CN';
import zh_TW from './zh-TW/en.json' assert { type: 'JSON' };
import elements_zh_TW from 'element-ui/lib/locale/lang/zh-TW';
import th from './th/en.json' assert { type: 'JSON' };
import elements_th from 'element-ui/lib/locale/lang/th';
const localized_en = { ...en, ...elements_en };
const localized_es = { ...es, ...elements_es };
const localized_fr = { ...fr, ...elements_fr };
// const localized_hu = { ...hu, ...elements_hu };
const localized_ja = { ...ja, ...elements_ja };
const localized_ko = { ...ko, ...elements_ko };
const localized_pl = { ...pl, ...elements_pl };
const localized_pt = { ...pt, ...elements_pt };
const localized_cz = { ...cz, ...elements_cz };
const localized_ru = { ...ru_RU, ...elements_ru };
const localized_vi = { ...vi, ...elements_vi };
const localized_zh_CN = { ...zh_CN, ...elements_zh_CN };
const localized_zh_TW = { ...zh_TW, ...elements_zh_TW };
const localized_th = { ...th, ...elements_th };
export {
localized_en as en,
localized_es as es,
localized_fr as fr,
// localized_hu as hu,
localized_ja as ja_JP,
localized_ko as ko,
localized_pl as pl,
localized_pt as pt,
localized_cz as cz,
localized_ru as ru_RU,
localized_vi as vi,
localized_zh_CN as zh_CN,
localized_zh_TW as zh_TW,
localized_th as th
en,
es,
fr,
hu,
ja,
ko,
pl,
pt,
cs,
ru,
vi,
zh_CN,
zh_TW,
th
};

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