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
+143 -65
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>
-681
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>
+20 -2
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}>}
-297
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;
-3
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,
+5 -5
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;
+4 -3
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;
});
+18
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;
});
}
};
+14 -5
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 };
-75
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
+218 -188
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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+1 -1
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);
@@ -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%);
}
+107 -38
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;
}
+108 -39
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;
}
+108 -39
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;
}
+28 -4
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;
}
@@ -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;
}
+52 -11
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;
}
+43 -1
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;
}
+5 -39
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);
};
}
+244
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>
+18 -13
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%;
+17 -19
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>
+23 -21
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>
+16 -4
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>
+14 -10
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>
+27 -17
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>
+3 -2
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';
+7 -3
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' },
+5 -4
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;
-84
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
@@ -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>
@@ -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({
@@ -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>
@@ -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();
});
}
}
);
@@ -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>
@@ -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
@@ -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,
@@ -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();
@@ -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'
});
@@ -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'
});
@@ -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>
@@ -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'
});
@@ -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';
+37 -33
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>
+56 -45
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'
});
+19 -23
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
+81 -52
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;
@@ -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>
@@ -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>
@@ -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);
}
@@ -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>
-159
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>
+8 -8
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>-->
+13 -15
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'
});
@@ -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';
@@ -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>
@@ -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'
});
@@ -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';
@@ -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
+8 -8
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();
});
}
}
@@ -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>
@@ -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'
});
@@ -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
+10 -4
View File
@@ -1,13 +1,18 @@
<!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>
<!-- <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" />
@@ -15,5 +20,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="app.js"></script>
</body>
</html>
@@ -1,5 +1,5 @@
{
"language": "Čeština (CZ)",
"language": "Čeština (cs)",
"translator": "-",
"nav_tooltip": {
"feed": "Zdroj",
+8 -10
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": {
+17 -46
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
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"language": "简体中文 (zh_CN)",
"language": "简体中文 (zh-CN)",
"translator": "flower_elf,Yingxue,Map1en",
"nav_tooltip": {
"feed": "好友动态",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"language": "繁體中文 (zh_TW)",
"language": "繁體中文 (zh-TW)",
"translator": "Kamiya,XoF_eLtTiL",
"nav_tooltip": {
"feed": "動態",
+15 -14
View File
@@ -1,7 +1,5 @@
import Vue from 'vue';
import AvatarInfo from '../components/AvatarInfo.vue';
import CountdownTimer from '../components/CountdownTimer.vue';
import SafeDialog from '../components/dialogs/SafeDialog.vue';
import DisplayName from '../components/DisplayName.vue';
import InstanceInfo from '../components/InstanceInfo.vue';
import InviteYourself from '../components/InviteYourself.vue';
@@ -11,16 +9,19 @@ import Location from '../components/Location.vue';
import LocationWorld from '../components/LocationWorld.vue';
import SimpleSwitch from '../components/SimpleSwitch.vue';
import Timer from '../components/Timer.vue';
import DataTable from '../components/DataTable.vue';
Vue.component('SafeDialog', SafeDialog);
Vue.component('SimpleSwitch', SimpleSwitch);
Vue.component('Location', Location);
Vue.component('Timer', Timer);
Vue.component('InstanceInfo', InstanceInfo);
Vue.component('LastJoin', LastJoin);
Vue.component('CountdownTimer', CountdownTimer);
Vue.component('AvatarInfo', AvatarInfo);
Vue.component('DisplayName', DisplayName);
Vue.component('InviteYourself', InviteYourself);
Vue.component('Launch', Launch);
Vue.component('LocationWorld', LocationWorld);
export default function registerComponents(app) {
app.component('SimpleSwitch', SimpleSwitch);
app.component('Location', Location);
app.component('Timer', Timer);
app.component('InstanceInfo', InstanceInfo);
app.component('LastJoin', LastJoin);
app.component('CountdownTimer', CountdownTimer);
app.component('AvatarInfo', AvatarInfo);
app.component('DisplayName', DisplayName);
app.component('InviteYourself', InviteYourself);
app.component('Launch', Launch);
app.component('LocationWorld', LocationWorld);
app.component('DataTable', DataTable);
}
+16 -26
View File
@@ -1,30 +1,20 @@
import ElementUI from 'element-ui';
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import { createI18n } from 'vue-i18n-bridge';
import { createI18n } from 'vue-i18n';
import * as localizedStrings from '../localization/localizedStrings';
// i18n: execution order matters here
Vue.use(VueI18n, { bridge: true });
const i18n = createI18n(
{
locale: 'en',
fallbackLocale: 'en',
messages: localizedStrings,
legacy: false,
globalInjection: true,
missingWarn: false,
warnHtmlMessage: false,
fallbackWarn: false
},
VueI18n
);
Vue.use(i18n);
Vue.use(ElementUI, {
i18n: (key, value) => i18n.global.t(key, value)
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: Object.fromEntries(
Object.entries(localizedStrings).map(([key, value]) => [
key.replaceAll('_', '-'),
value
])
),
legacy: false,
globalInjection: false,
missingWarn: false,
warnHtmlMessage: false,
fallbackWarn: false
});
const t = i18n.global.t;
export { i18n, t };
export { i18n };
-5
View File
@@ -1,5 +0,0 @@
import './ipc';
import './dayjs';
import './components';
export { t, i18n } from './i18n';
+4 -4
View File
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { reactive } from 'vue';
const AppGlobal = Vue.observable({
const AppDebug = reactive({
debug: false,
debugWebSocket: false,
debugUserDiff: false,
@@ -16,6 +16,6 @@ const AppGlobal = Vue.observable({
websocketDomainVrchat: 'wss://pipeline.vrchat.cloud'
});
window.__APP_GLOBALS__ = AppGlobal;
window.$debug = AppDebug;
export { AppGlobal };
export { AppDebug };
+30 -14
View File
@@ -1,6 +1,6 @@
import Noty from 'noty';
import { $app } from '../app.js';
import { t } from '../plugin';
import { ElMessageBox, ElMessage } from 'element-plus';
import { i18n } from '../plugin/i18n';
import { statusCodes } from '../shared/constants/api.js';
import { escapeTag } from '../shared/utils';
import {
@@ -10,13 +10,15 @@ import {
useUpdateLoopStore,
useUserStore
} from '../stores';
import { AppGlobal } from './appConfig.js';
import { AppDebug } from './appConfig.js';
import webApiService from './webapi.js';
import { watchState } from './watchState';
const pendingGetRequests = new Map();
export let failedGetRequests = new Map();
const t = i18n.global.t;
/**
* @template T
* @param {string} endpoint
@@ -38,7 +40,7 @@ export function request(endpoint, options) {
}
let req;
const init = {
url: `${AppGlobal.endpointDomain}/${endpoint}`,
url: `${AppDebug.endpointDomain}/${endpoint}`,
method: 'GET',
...options
};
@@ -50,7 +52,7 @@ export function request(endpoint, options) {
if (lastRun >= Date.now() - 900000) {
// 15mins
$throw(
0,
-1,
t('api.error.message.403_404_bailing_request'),
endpoint
);
@@ -102,14 +104,14 @@ export function request(endpoint, options) {
throw `API request blocked while logged out: ${endpoint}`;
}
if (!response.data) {
if (AppGlobal.debugWebRequests) {
if (AppDebug.debugWebRequests) {
console.log(init, 'no data', response);
}
return response;
}
try {
response.data = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
if (AppDebug.debugWebRequests) {
console.log(init, 'parsed data', response.data);
}
return response;
@@ -179,7 +181,7 @@ export function request(endpoint, options) {
}
}
if (status === 403 && endpoint === 'config') {
$app.$alert(
ElMessageBox.alert(
t('api.error.message.vpn_in_use'),
`403 ${t('api.error.message.login_error')}`
);
@@ -191,7 +193,7 @@ export function request(endpoint, options) {
status === 404 &&
endpoint.startsWith('avatars/')
) {
$app.$message({
ElMessage({
message: t('message.api_handler.avatar_private_or_deleted'),
type: 'error'
});
@@ -276,15 +278,29 @@ export function $throw(code, error, endpoint) {
);
}
const text = message.map((s) => escapeTag(s)).join('<br>');
if (text.length) {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
let ignoreError = false;
if (
(code === 404 || code === -1) &&
endpoint.split('/').length === 2 &&
(endpoint.startsWith('users/') ||
endpoint.startsWith('worlds/') ||
endpoint.startsWith('avatars/') ||
endpoint.startsWith('file/'))
) {
ignoreError = true;
}
if (endpoint.startsWith('analysis/')) {
ignoreError = true;
}
if (text.length && !ignoreError) {
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'error',
text
});
AppGlobal.errorNoty.show();
AppDebug.errorNoty.show();
}
const e = new Error(text);
e.status = code;
+11 -11
View File
@@ -13,7 +13,7 @@ import {
useUiStore,
useUserStore
} from '../stores';
import { AppGlobal } from './appConfig';
import { AppDebug } from './appConfig';
import { request } from './request';
import { watchState } from './watchState';
@@ -49,9 +49,9 @@ function connectWebSocket(token) {
if (webSocket !== null) {
return;
}
const socket = new WebSocket(`${AppGlobal.websocketDomain}/?auth=${token}`);
const socket = new WebSocket(`${AppDebug.websocketDomain}/?auth=${token}`);
socket.onopen = () => {
if (AppGlobal.debugWebSocket) {
if (AppDebug.debugWebSocket) {
console.log('WebSocket connected');
}
};
@@ -64,7 +64,7 @@ function connectWebSocket(token) {
} catch (err) {
console.error('Error closing WebSocket:', err);
}
if (AppGlobal.debugWebSocket) {
if (AppDebug.debugWebSocket) {
console.log('WebSocket closed');
}
workerTimers.setTimeout(() => {
@@ -78,10 +78,10 @@ function connectWebSocket(token) {
}, 5000);
};
socket.onerror = () => {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'error',
text: 'WebSocket Error'
}).show();
@@ -109,7 +109,7 @@ function connectWebSocket(token) {
handlePipeline({
json
});
if (AppGlobal.debugWebSocket && json.content) {
if (AppDebug.debugWebSocket && json.content) {
let displayName = '';
const user = userStore.cachedUsers.get(json.content.userId);
if (user) {
@@ -167,10 +167,10 @@ function handlePipeline(args) {
const { type, content, err } = args.json;
if (typeof err !== 'undefined') {
console.error('PIPELINE: error', args);
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'error',
text: escapeTag(`WebSocket Error: ${err}`)
}).show();
+1 -1
View File
@@ -153,7 +153,7 @@ function feedFiltersOptions() {
name: 'Video Play',
options: getOptions(['Off', 'On']),
tooltip: 'Requires VRCX YouTube API option enabled',
tooltipIcon: 'el-icon-warning'
tooltipWarning: true
},
{
key: 'Event',
-4
View File
@@ -6,10 +6,6 @@ const groupDialogSortingOptions = {
joinedAtAsc: {
name: 'dialog.group.members.sorting.joined_at_asc',
value: 'joinedAt:asc'
},
userId: {
name: 'dialog.group.members.sorting.user_id',
value: ''
}
};
-2
View File
@@ -1,8 +1,6 @@
export * from './emoji';
export * from './feedFilters';
export * from './language';
export * from './ossLicenses';
export * from './photon';
export * from './settings';
export * from './group';
export * from './user';
+110 -63
View File
@@ -104,30 +104,6 @@ const openSourceSoftwareLicenses = [
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'librsync.net',
licenseText: `The MIT License (MIT)
Copyright (c) 2015 Brad Dodson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'Newtonsoft.Json',
licenseText: `The MIT License (MIT)
@@ -140,6 +116,7 @@ const openSourceSoftwareLicenses = [
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
},
{
name: 'normalize',
licenseText: `The MIT License (MIT)
@@ -276,52 +253,39 @@ const openSourceSoftwareLicenses = [
THE SOFTWARE.`
},
{
name: 'vue-data-tables',
licenseText: `The MIT License (MIT)
name: 'NLog',
licenseText: `BSD 3-Clause License
Copyright (c) 2018 Leon Zhang
Copyright (c) 2004-2024 Jaroslaw Kowalski <jaak@jkowalski.net>, Kim Christensen, Julian Verdurmen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
All rights reserved.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'vue-lazyload',
licenseText: `The MIT License (MIT)
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Copyright (c) 2016 Awe
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
* Neither the name of Jaroslaw Kowalski nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.`
},
{
name: 'Encode Sans Font (from Dark Vanilla)',
@@ -386,6 +350,54 @@ const openSourceSoftwareLicenses = [
ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.`
},
{
name: 'SixLabors ImageSharp',
licenseText: `Apache License 2.0
Six Labors Split License
Version 1.0, June 2022
Copyright (c) Six Labors
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source
code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including
but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" (or "Works") shall mean any Six Labors software made available under the License, as indicated by a
copyright notice that is included in or attached to the work.
"Direct Package Dependency" shall mean any Work in Source or Object form that is installed directly by You.
"Transitive Package Dependency" shall mean any Work in Object form that is installed indirectly by a third party
dependency unrelated to Six Labors.
2. License
Works in Source or Object form are split licensed and may be licensed under the Apache License, Version 2.0 or a
Six Labors Commercial Use License.
Licenses are granted based upon You meeting the qualified criteria as stated. Once granted,
You must reference the granted license only in all documentation.
Works in Source or Object form are licensed to You under the Apache License, Version 2.0 if.
- You are consuming the Work in for use in software licensed under an Open Source or Source Available license.
- You are consuming the Work as a Transitive Package Dependency.
- You are consuming the Work as a Direct Package Dependency in the capacity of a For-profit company/individual with
less than 1M USD annual gross revenue.
- You are consuming the Work as a Direct Package Dependency in the capacity of a Non-profit organization
or Registered Charity.
For all other scenarios, Works in Source or Object form are licensed to You under the Six Labors Commercial License
which may be purchased by visiting https://sixlabors.com/pricing/.`
},
{
name: 'Apache ECharts',
licenseText: `Apache License 2.0
@@ -418,6 +430,41 @@ const openSourceSoftwareLicenses = [
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'Electron',
licenseText: `MIT License
Copyright (c) Electron contributors
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
},
{
name: 'Remix Icon',
licenseText: `Apache License 2.0
Copyright 2017-2025 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).`
}
];
+15 -15
View File
@@ -1,50 +1,50 @@
import dark from '../../assets/scss/themes/theme.dark.scss?url';
import darkblue from '../../assets/scss/themes/theme.darkblue.scss?url';
import amoled from '../../assets/scss/themes/theme.amoled.scss?url';
import darkvanillaold from '../../assets/scss/themes/theme.darkvanillaold.scss?url';
import darkvanilla from '../../assets/scss/themes/theme.darkvanilla.scss?url';
import pink from '../../assets/scss/themes/theme.pink.scss?url';
import material3 from '../../assets/scss/themes/theme.material3.scss?url';
export const THEME_CONFIG = {
system: {
cssFile: '',
requiresDarkBase: false,
isDark: 'system',
name: 'System'
},
light: {
cssFile: '',
requiresDarkBase: false,
isDark: false,
name: 'Light'
},
dark: { cssFile: '', requiresDarkBase: true, isDark: true, name: 'Dark' },
dark: { cssFile: dark, isDark: true, name: 'Dark' },
darkblue: {
cssFile: 'theme.darkblue.css',
requiresDarkBase: true,
cssFile: darkblue,
isDark: true,
name: 'Dark Blue'
},
amoled: {
cssFile: 'theme.amoled.css',
requiresDarkBase: true,
cssFile: amoled,
isDark: true,
name: 'Amoled'
},
darkvanillaold: {
cssFile: 'theme.darkvanillaold.css',
requiresDarkBase: true,
cssFile: darkvanillaold,
isDark: true,
name: 'Dark Vanilla Old'
},
darkvanilla: {
cssFile: 'theme.darkvanilla.css',
requiresDarkBase: true,
cssFile: darkvanilla,
isDark: true,
name: 'Dark Vanilla'
},
pink: {
cssFile: 'theme.pink.css',
requiresDarkBase: true,
cssFile: pink,
isDark: true,
name: 'Pink'
},
material3: {
cssFile: 'theme.material3.css',
requiresDarkBase: true,
cssFile: material3,
isDark: true,
name: 'Material 3'
}
+4
View File
@@ -16,6 +16,10 @@ function timeToText(sec, isNeedSeconds = false) {
if (n < 0) {
n = -n;
}
if (n >= 31536000) {
arr.push(`${Math.floor(n / 31536000)}y`);
n %= 31536000;
}
if (n >= 86400) {
arr.push(`${Math.floor(n / 86400)}d`);
n %= 86400;
+42 -63
View File
@@ -23,13 +23,24 @@ function changeAppDarkStyle(isDark) {
}
function changeAppThemeStyle(themeMode) {
if (themeMode === 'system') {
themeMode = systemIsDarkMode() ? 'dark' : 'light';
}
const themeConfig = THEME_CONFIG[themeMode];
if (!themeConfig) return;
if (!themeConfig) {
console.error('Invalid theme mode:', themeMode);
return;
}
let filePathPrefix = 'file://vrcx/';
if (LINUX) {
filePathPrefix = './';
}
if (process.env.NODE_ENV === 'development') {
filePathPrefix = 'http://localhost:9000/';
console.log('Using development file path prefix:', filePathPrefix);
}
let $appThemeStyle = document.getElementById('app-theme-style');
if (!$appThemeStyle) {
@@ -38,63 +49,31 @@ function changeAppThemeStyle(themeMode) {
$appThemeStyle.rel = 'stylesheet';
document.head.appendChild($appThemeStyle);
}
$appThemeStyle.href = themeConfig.cssFile
? `${filePathPrefix}${themeConfig.cssFile}`
: '';
$appThemeStyle.href = themeConfig.cssFile ? themeConfig.cssFile : '';
let $appThemeDarkStyle = document.getElementById('app-theme-dark-style');
const darkThemeCssPath = `${filePathPrefix}theme.dark.css`;
const shouldApplyDarkBase =
themeConfig.requiresDarkBase ||
(themeMode === 'system' && systemIsDarkMode());
if (shouldApplyDarkBase) {
if (!$appThemeDarkStyle) {
$appThemeDarkStyle = document.createElement('link');
$appThemeDarkStyle.setAttribute('id', 'app-theme-dark-style');
$appThemeDarkStyle.rel = 'stylesheet';
$appThemeDarkStyle.href = darkThemeCssPath;
document.head.insertBefore($appThemeDarkStyle, $appThemeStyle);
} else if ($appThemeDarkStyle.href !== darkThemeCssPath) {
$appThemeDarkStyle.href = darkThemeCssPath;
}
if (themeConfig.isDark) {
document.documentElement.classList.add('dark');
} else {
$appThemeDarkStyle && $appThemeDarkStyle.remove();
document.documentElement.classList.remove('dark');
}
changeAppDarkStyle(themeConfig.isDark);
let isDarkForExternalApp = themeConfig.isDark;
if (isDarkForExternalApp === 'system') {
isDarkForExternalApp = systemIsDarkMode();
}
changeAppDarkStyle(isDarkForExternalApp);
}
/**
* CJK character in Japanese, Korean, Chinese are different
* so change font-family order when users change language to display CJK character correctly
* @param {string} lang
*/
function changeCJKFontsOrder(lang) {
const otherFonts = window
.getComputedStyle(document.body)
.fontFamily.split(',')
.filter((item) => !item.includes('Noto Sans'))
.join(', ');
const notoSans = 'Noto Sans';
const fontFamilies = {
ja_JP: ['JP', 'KR', 'TC', 'SC'],
ko: ['KR', 'JP', 'TC', 'SC'],
zh_TW: ['TC', 'JP', 'KR', 'SC'],
zh_CN: ['SC', 'JP', 'KR', 'TC']
};
if (fontFamilies[lang]) {
const CJKFamily = fontFamilies[lang]
.map((item) => `${notoSans} ${item}`)
.join(', ');
document.body.style.fontFamily = `${CJKFamily}, ${otherFonts}`;
}
// let $appThemeDarkStyle = document.getElementById('app-theme-dark-style');
// const darkThemeCssPath = `${filePathPrefix}theme.dark.css`;
// const shouldApplyDarkBase = themeConfig.isDark;
// if (shouldApplyDarkBase) {
// if (!$appThemeDarkStyle) {
// $appThemeDarkStyle = document.createElement('link');
// $appThemeDarkStyle.setAttribute('id', 'app-theme-dark-style');
// $appThemeDarkStyle.rel = 'stylesheet';
// $appThemeDarkStyle.href = darkThemeCssPath;
// document.head.insertBefore($appThemeDarkStyle, $appThemeStyle);
// } else if ($appThemeDarkStyle.href !== darkThemeCssPath) {
// $appThemeDarkStyle.href = darkThemeCssPath;
// }
// } else {
// $appThemeDarkStyle && $appThemeDarkStyle.remove();
// }
}
/**
@@ -224,28 +203,28 @@ function HSVtoRGB(h, s, v) {
return `#${decColor.toString(16).substr(1)}`;
}
function adjustDialogZ(el) {
let z = 0;
document.querySelectorAll('.v-modal,.el-dialog__wrapper').forEach((v) => {
function getNextDialogIndex() {
let z = 2000;
document.querySelectorAll('.el-overlay,.el-modal-dialog').forEach((v) => {
if (v.style.display === 'none') {
return;
}
const _z = Number(v.style.zIndex) || 0;
if (_z && _z > z && v !== el) {
if (_z > z) {
z = _z;
}
});
if (z) {
el.style.zIndex = z + 1;
}
return z + 1;
}
export {
systemIsDarkMode,
changeAppDarkStyle,
changeAppThemeStyle,
changeCJKFontsOrder,
updateTrustColorClasses,
refreshCustomCss,
refreshCustomScript,
HueToHex,
HSVtoRGB,
adjustDialogZ
getNextDialogIndex
};
+27 -29
View File
@@ -1,7 +1,7 @@
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { ElMessageBox, ElMessage } from 'element-plus';
import { miscRequest } from '../../api';
import { $app } from '../../app';
import {
useAvatarStore,
useInstanceStore,
@@ -156,14 +156,14 @@ function copyToClipboard(text, message = 'Copied successfully!') {
navigator.clipboard
.writeText(text)
.then(() => {
$app.$message({
ElMessage({
message: message,
type: 'success'
});
})
.catch((err) => {
console.error('Copy failed:', err);
$app.$message.error('Copy failed!');
ElMessage.error('Copy failed!');
});
}
@@ -387,19 +387,20 @@ function openExternalLink(link) {
return;
}
$app.$confirm(`${link}`, 'Open External Link', {
ElMessageBox.confirm(`${link}`, 'Open External Link', {
distinguishCancelAndClose: true,
confirmButtonText: 'Open',
cancelButtonText: 'Copy',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
AppApi.OpenLink(link);
} else if (action === 'cancel') {
copyLink(link);
}
}
});
})
.catch(() => {});
}
/**
@@ -407,7 +408,7 @@ function openExternalLink(link) {
* @param {string} text
*/
function copyLink(text) {
$app.$message({
ElMessage({
message: 'Link copied to clipboard',
type: 'success'
});
@@ -485,26 +486,23 @@ async function getBundleDateSize(ref) {
fileSize
};
if (unityPackage.variant === 'standard') {
if (avatarDialog.value.id === ref.id) {
// update avatar dialog
avatarDialog.value.bundleSizes[platform] =
bundleSizes[platform];
avatarDialog.value.lastUpdated = createdAt;
avatarDialog.value.fileAnalysis = buildTreeData(bundleJson);
}
// update world dialog
if (worldDialog.value.id === ref.id) {
worldDialog.value.bundleSizes[platform] = bundleSizes[platform];
worldDialog.value.lastUpdated = createdAt;
worldDialog.value.fileAnalysis = buildTreeData(bundleJson);
}
// update player list
if (currentInstanceLocation.value.worldId === ref.id) {
currentInstanceWorld.value.bundleSizes[platform] =
bundleSizes[platform];
currentInstanceWorld.value.lastUpdated = createdAt;
}
if (avatarDialog.value.id === ref.id) {
// update avatar dialog
avatarDialog.value.bundleSizes[platform] = bundleSizes[platform];
avatarDialog.value.lastUpdated = createdAt;
avatarDialog.value.fileAnalysis = buildTreeData(bundleJson);
}
// update world dialog
if (worldDialog.value.id === ref.id) {
worldDialog.value.bundleSizes[platform] = bundleSizes[platform];
worldDialog.value.lastUpdated = createdAt;
worldDialog.value.fileAnalysis = buildTreeData(bundleJson);
}
// update player list
if (currentInstanceLocation.value.worldId === ref.id) {
currentInstanceWorld.value.bundleSizes[platform] =
bundleSizes[platform];
currentInstanceWorld.value.lastUpdated = createdAt;
}
}
-15
View File
@@ -1,15 +0,0 @@
<!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>VRCXVR</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
+97 -87
View File
@@ -1,12 +1,12 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import { authRequest } from '../api';
import { $app } from '../app';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import configRepository from '../service/config';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { AppDebug } from '../service/appConfig';
import { request } from '../service/request';
import security from '../service/security';
import webApiService from '../service/webapi';
@@ -59,7 +59,7 @@ export const useAuthStore = defineStore('Auth', () => {
password: '',
rePassword: '',
beforeClose(done) {
$app._data.enablePrimaryPassword = false;
// $app._data.enablePrimaryPassword = false;
done();
}
},
@@ -211,8 +211,8 @@ export const useAuthStore = defineStore('Auth', () => {
state.loginForm.lastUserLoggedIn
];
if (user?.loginParmas?.endpoint) {
AppGlobal.endpointDomain = user.loginParmas.endpoint;
AppGlobal.websocketDomain = user.loginParmas.websocket;
AppDebug.endpointDomain = user.loginParmas.endpoint;
AppDebug.websocketDomain = user.loginParmas.websocket;
}
// login at startup
state.loginForm.loading = true;
@@ -283,7 +283,7 @@ export const useAuthStore = defineStore('Auth', () => {
if (advancedSettingsStore.enablePrimaryPassword) {
state.enablePrimaryPasswordDialog.visible = true;
} else {
$app.$prompt(
ElMessageBox.prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
@@ -415,7 +415,7 @@ export const useAuthStore = defineStore('Auth', () => {
if (!advancedSettingsStore.enablePrimaryPassword) {
resolve(args.password);
}
$app.$prompt(
ElMessageBox.prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
@@ -443,11 +443,12 @@ export const useAuthStore = defineStore('Auth', () => {
}
function logout() {
$app.$confirm('Continue? Logout', 'Confirm', {
ElMessageBox.confirm('Continue? Logout', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
const existingStyle = document.getElementById(
'login-container-style'
@@ -457,8 +458,8 @@ export const useAuthStore = defineStore('Auth', () => {
}
handleLogoutEvent();
}
}
});
})
.catch(() => {});
}
async function relogin(user) {
@@ -468,11 +469,11 @@ export const useAuthStore = defineStore('Auth', () => {
}
state.loginForm.lastUserLoggedIn = user.user.id; // for resend email 2fa
if (loginParmas.endpoint) {
AppGlobal.endpointDomain = loginParmas.endpoint;
AppGlobal.websocketDomain = loginParmas.websocket;
AppDebug.endpointDomain = loginParmas.endpoint;
AppDebug.websocketDomain = loginParmas.websocket;
} else {
AppGlobal.endpointDomain = AppGlobal.endpointDomainVrchat;
AppGlobal.websocketDomain = AppGlobal.websocketDomainVrchat;
AppDebug.endpointDomain = AppDebug.endpointDomainVrchat;
AppDebug.websocketDomain = AppDebug.websocketDomainVrchat;
}
return new Promise((resolve, reject) => {
state.loginForm.loading = true;
@@ -501,7 +502,7 @@ export const useAuthStore = defineStore('Auth', () => {
});
})
.catch((_) => {
$app.$message({
ElMessage({
message: 'Incorrect primary password',
type: 'error'
});
@@ -559,11 +560,11 @@ export const useAuthStore = defineStore('Auth', () => {
if (!state.loginForm.loading) {
state.loginForm.loading = true;
if (state.loginForm.endpoint) {
AppGlobal.endpointDomain = state.loginForm.endpoint;
AppGlobal.websocketDomain = state.loginForm.websocket;
AppDebug.endpointDomain = state.loginForm.endpoint;
AppDebug.websocketDomain = state.loginForm.websocket;
} else {
AppGlobal.endpointDomain = AppGlobal.endpointDomainVrchat;
AppGlobal.websocketDomain = AppGlobal.websocketDomainVrchat;
AppDebug.endpointDomain = AppDebug.endpointDomainVrchat;
AppDebug.websocketDomain = AppDebug.websocketDomainVrchat;
}
authRequest
.getConfig()
@@ -576,7 +577,7 @@ export const useAuthStore = defineStore('Auth', () => {
state.loginForm.saveCredentials &&
advancedSettingsStore.enablePrimaryPassword
) {
$app.$prompt(
ElMessageBox.prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
@@ -649,36 +650,40 @@ export const useAuthStore = defineStore('Auth', () => {
}
AppApi.FlashWindow();
state.twoFactorAuthDialogVisible = true;
$app.$prompt(t('prompt.totp.description'), t('prompt.totp.header'), {
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.totp.use_otp'),
confirmButtonText: t('prompt.totp.verify'),
inputPlaceholder: t('prompt.totp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: t('prompt.totp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyTOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptOTP();
ElMessageBox.prompt(
t('prompt.totp.description'),
t('prompt.totp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.totp.use_otp'),
confirmButtonText: t('prompt.totp.verify'),
inputPlaceholder: t('prompt.totp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: t('prompt.totp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyTOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptOTP();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
});
);
}
function promptOTP() {
@@ -686,36 +691,40 @@ export const useAuthStore = defineStore('Auth', () => {
return;
}
state.twoFactorAuthDialogVisible = true;
$app.$prompt(t('prompt.otp.description'), t('prompt.otp.header'), {
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.otp.use_totp'),
confirmButtonText: t('prompt.otp.verify'),
inputPlaceholder: t('prompt.otp.input_placeholder'),
inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/,
inputErrorMessage: t('prompt.otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptTOTP();
ElMessageBox.prompt(
t('prompt.otp.description'),
t('prompt.otp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.otp.use_totp'),
confirmButtonText: t('prompt.otp.verify'),
inputPlaceholder: t('prompt.otp.input_placeholder'),
inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/,
inputErrorMessage: t('prompt.otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptTOTP();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
});
);
}
function promptEmailOTP() {
@@ -724,7 +733,7 @@ export const useAuthStore = defineStore('Auth', () => {
}
AppApi.FlashWindow();
state.twoFactorAuthDialogVisible = true;
$app.$prompt(
ElMessageBox.prompt(
t('prompt.email_otp.description'),
t('prompt.email_otp.header'),
{
@@ -840,20 +849,20 @@ export const useAuthStore = defineStore('Auth', () => {
state.autoLoginAttempts.add(new Date().getTime());
relogin(user)
.then(() => {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'success',
text: 'Automatically logged in.'
}).show();
console.log('Automatically logged in.');
})
.catch((err) => {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'error',
text: 'Failed to login automatically.'
}).show();
@@ -861,7 +870,7 @@ export const useAuthStore = defineStore('Auth', () => {
})
.finally(() => {
if (!navigator.onLine) {
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'error',
text: `You're offline.`
}).show();
@@ -879,6 +888,7 @@ export const useAuthStore = defineStore('Auth', () => {
return {
state,
loginForm,
enablePrimaryPasswordDialog,
saveCredentials,
+51 -74
View File
@@ -1,10 +1,9 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { avatarRequest, imageRequest } from '../api';
import { $app } from '../app';
import { computed, reactive, watch, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { avatarRequest, miscRequest } from '../api';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { AppDebug } from '../service/appConfig';
import webApiService from '../service/webapi';
import { watchState } from '../service/watchState';
import {
@@ -53,15 +52,16 @@ export const useAvatarStore = defineStore('Avatar', () => {
cacheSize: '',
cacheLocked: false,
cachePath: '',
fileAnalysis: {}
fileAnalysis: []
},
cachedAvatarModerations: new Map(),
avatarHistory: new Set(),
avatarHistoryArray: [],
cachedAvatars: new Map(),
cachedAvatarNames: new Map()
avatarHistoryArray: []
});
let cachedAvatarModerations = new Map();
let cachedAvatars = new Map();
let cachedAvatarNames = new Map();
const avatarDialog = computed({
get: () => state.avatarDialog,
set: (value) => {
@@ -76,34 +76,13 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
});
const cachedAvatarModerations = computed({
get: () => state.cachedAvatarModerations,
set: (value) => {
state.cachedAvatarModerations = value;
}
});
const cachedAvatars = computed({
get: () => state.cachedAvatars,
set: (value) => {
state.cachedAvatars = value;
}
});
const cachedAvatarNames = computed({
get: () => state.cachedAvatarNames,
set: (value) => {
state.cachedAvatarNames = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.avatarDialog.visible = false;
state.cachedAvatars.clear();
state.cachedAvatarNames.clear();
state.cachedAvatarModerations.clear();
cachedAvatars.clear();
cachedAvatarNames.clear();
cachedAvatarModerations.clear();
state.avatarHistory.clear();
state.avatarHistoryArray = [];
if (isLoggedIn) {
@@ -120,7 +99,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
function applyAvatar(json) {
json.name = replaceBioSymbols(json.name);
json.description = replaceBioSymbols(json.description);
let ref = state.cachedAvatars.get(json.id);
let ref = cachedAvatars.get(json.id);
if (typeof ref === 'undefined') {
ref = {
acknowledgements: '',
@@ -152,7 +131,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
version: 0,
...json
};
state.cachedAvatars.set(ref.id, ref);
cachedAvatars.set(ref.id, ref);
} else {
const { unityPackages } = ref;
Object.assign(ref, json);
@@ -212,7 +191,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
D.cacheSize = '';
D.cacheLocked = false;
D.cachePath = '';
D.fileAnalysis = {};
D.fileAnalysis = [];
D.isQuestFallback = false;
D.isPC = false;
D.isQuest = false;
@@ -228,8 +207,8 @@ export const useAvatarStore = defineStore('Avatar', () => {
favoriteStore.cachedFavoritesByObjectId.has(avatarId) ||
(userStore.currentUser.$isVRCPlus &&
favoriteStore.localAvatarFavoritesList.includes(avatarId));
D.isBlocked = state.cachedAvatarModerations.has(avatarId);
const ref2 = state.cachedAvatars.get(avatarId);
D.isBlocked = cachedAvatarModerations.has(avatarId);
const ref2 = cachedAvatars.get(avatarId);
if (typeof ref2 !== 'undefined') {
D.ref = ref2;
updateVRChatAvatarCache();
@@ -277,12 +256,12 @@ export const useAvatarStore = defineStore('Avatar', () => {
throw err;
})
.finally(() => {
$app.$nextTick(() => (D.loading = false));
nextTick(() => (D.loading = false));
});
}
/**
* aka: `$app.methods.getAvatarGallery`
*
* @param {string} avatarId
* @returns {Promise<string[]>}
*/
@@ -325,7 +304,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
json.created = new Date(json.created).toJSON();
}
let ref = state.cachedAvatarModerations.get(json.targetAvatarId);
let ref = cachedAvatarModerations.get(json.targetAvatarId);
if (typeof ref === 'undefined') {
ref = {
avatarModerationType: '',
@@ -333,7 +312,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
targetAvatarId: '',
...json
};
state.cachedAvatarModerations.set(ref.targetAvatarId, ref);
cachedAvatarModerations.set(ref.targetAvatarId, ref);
} else {
Object.assign(ref, json);
}
@@ -370,7 +349,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
/**
* aka: `$app.methods.getAvatarHistory`
*
* @returns {Promise<void>}
*/
async function getAvatarHistory() {
@@ -428,16 +407,15 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
function promptClearAvatarHistory() {
$app.$confirm('Continue? Clear Avatar History', 'Confirm', {
ElMessageBox.confirm('Continue? Clear Avatar History', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
clearAvatarHistory();
}
}
});
type: 'info'
})
.then(() => {
clearAvatarHistory();
})
.catch(() => {});
}
/**
@@ -453,12 +431,12 @@ export const useAvatarStore = defineStore('Avatar', () => {
avatarName: '-'
};
}
if (state.cachedAvatarNames.has(fileId)) {
return state.cachedAvatarNames.get(fileId);
if (cachedAvatarNames.has(fileId)) {
return cachedAvatarNames.get(fileId);
}
try {
const args = await imageRequest.getAvatarImages({ fileId });
return storeAvatarImage(args, state.cachedAvatarNames);
const args = await miscRequest.getFile({ fileId });
return storeAvatarImage(args, cachedAvatarNames);
} catch (error) {
console.error('Failed to get avatar images:', error);
return {
@@ -483,7 +461,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
});
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
if (AppDebug.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
@@ -511,7 +489,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
} catch (err) {
const msg = `Avatar search failed for ${search} with ${avatarProviderStore.avatarRemoteDatabaseProvider}\n${err}`;
console.error(msg);
$app.$message({
ElMessage({
message: msg,
type: 'error'
});
@@ -563,7 +541,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
});
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
if (AppDebug.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
@@ -589,7 +567,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
} catch (err) {
const msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`;
console.error(msg);
$app.$message({
ElMessage({
message: msg,
type: 'error'
});
@@ -598,22 +576,20 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
function selectAvatarWithConfirmation(id) {
$app.$confirm(`Continue? Select Avatar`, 'Confirm', {
ElMessageBox.confirm(`Continue? Select Avatar`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action !== 'confirm') {
return;
}
type: 'info'
})
.then(() => {
selectAvatarWithoutConfirmation(id);
}
});
})
.catch(() => {});
}
function selectAvatarWithoutConfirmation(id) {
if (userStore.currentUser.currentAvatar === id) {
$app.$message({
ElMessage({
message: 'Avatar already selected',
type: 'info'
});
@@ -624,7 +600,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
avatarId: id
})
.then(() => {
$app.$message({
ElMessage({
message: 'Avatar changed',
type: 'success'
});
@@ -633,7 +609,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
function checkAvatarCache(fileId) {
let avatarId = '';
for (let ref of state.cachedAvatars.values()) {
for (let ref of cachedAvatars.values()) {
if (extractFileId(ref.imageUrl) === fileId) {
avatarId = ref.id;
}
@@ -659,7 +635,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
) {
const fileId = extractFileId(currentAvatarImageUrl);
if (!fileId) {
$app.$message({
ElMessage({
message: 'Sorry, the author is unknown',
type: 'error'
});
@@ -682,13 +658,13 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
if (!avatarId) {
if (avatarInfo.ownerId === refUserId) {
$app.$message({
ElMessage({
message:
"It's personal (own) avatar or not found in avatar database",
type: 'warning'
});
} else {
$app.$message({
ElMessage({
message: 'Avatar not found in avatar database',
type: 'warning'
});
@@ -712,6 +688,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
return {
state,
avatarDialog,
avatarHistory,
avatarHistoryArray,
+99 -188
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { favoriteRequest } from '../api';
import { $app } from '../app';
import { database } from '../service/database';
import { processBulk } from '../service/request';
import { watchState } from '../service/watchState';
@@ -12,7 +12,7 @@ import { useAppearanceSettingsStore } from './settings/appearance';
import { useGeneralSettingsStore } from './settings/general';
import { useUserStore } from './user';
import { useWorldStore } from './world';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
export const useFavoriteStore = defineStore('Favorite', () => {
const appearanceSettingsStore = useAppearanceSettingsStore();
@@ -41,7 +41,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
},
cachedFavoriteGroupsByTypeName: new Map(),
cachedFavorites: new Map(),
favoriteWorldGroups: [],
favoriteAvatarGroups: [],
isFavoriteLoading: false,
@@ -77,6 +76,21 @@ export const useFavoriteStore = defineStore('Favorite', () => {
cachedFavoritesByObjectId: new Map()
});
let cachedFavorites = new Map();
const cachedFavoriteGroups = computed({
get: () => state.cachedFavoriteGroups,
set: (value) => {
state.cachedFavoriteGroups = value;
}
});
const cachedFavoriteGroupsByTypeName = computed({
get: () => state.cachedFavoriteGroupsByTypeName,
set: (value) => {
state.cachedFavoriteGroupsByTypeName = value;
}
});
const favoriteFriends = computed(() => {
if (state.sortFavoriteFriends) {
state.sortFavoriteFriends = false;
@@ -144,10 +158,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
});
const cachedFavoriteGroups = state.cachedFavoriteGroups;
const cachedFavoriteGroupsByTypeName = state.cachedFavoriteGroupsByTypeName;
const cachedFavorites = state.cachedFavorites;
const favoriteLimits = computed({
get() {
return state.favoriteLimits;
@@ -265,15 +275,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
});
const favoriteObjects = computed({
get() {
return state.favoriteObjects;
},
set(value) {
state.favoriteObjects = value;
}
});
const localWorldFavoritesList = computed({
get() {
return state.localWorldFavoritesList;
@@ -283,87 +284,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
});
const favoriteFriends_ = computed({
get() {
return state.favoriteFriends_;
},
set(value) {
state.favoriteFriends_ = value;
}
});
const favoriteFriendsSorted = computed({
get() {
return state.favoriteFriendsSorted;
},
set(value) {
state.favoriteFriendsSorted = value;
}
});
const favoriteWorlds_ = computed({
get() {
return state.favoriteWorlds_;
},
set(value) {
state.favoriteWorlds_ = value;
}
});
const favoriteWorldsSorted = computed({
get() {
return state.favoriteWorldsSorted;
},
set(value) {
state.favoriteWorldsSorted = value;
}
});
const favoriteAvatars_ = computed({
get() {
return state.favoriteAvatars_;
},
set(value) {
state.favoriteAvatars_ = value;
}
});
const favoriteAvatarsSorted = computed({
get() {
return state.favoriteAvatarsSorted;
},
set(value) {
state.favoriteAvatarsSorted = value;
}
});
const sortFavoriteFriends = computed({
get() {
return state.sortFavoriteFriends;
},
set(value) {
state.sortFavoriteFriends = value;
}
});
const sortFavoriteWorlds = computed({
get() {
return state.sortFavoriteWorlds;
},
set(value) {
state.sortFavoriteWorlds = value;
}
});
const sortFavoriteAvatars = computed({
get() {
return state.sortFavoriteAvatars;
},
set(value) {
state.sortFavoriteAvatars = value;
}
});
const cachedFavoritesByObjectId = computed({
get() {
return state.cachedFavoritesByObjectId;
@@ -399,7 +319,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
() => watchState.isLoggedIn,
(isLoggedIn) => {
friendStore.localFavoriteFriends.clear();
state.cachedFavorites.clear();
cachedFavorites.clear();
state.cachedFavoritesByObjectId.clear();
state.cachedFavoriteGroups.clear();
state.cachedFavoriteGroupsByTypeName.clear();
@@ -536,7 +456,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function handleFavoriteGroupClear(args) {
const key = `${args.params.type}:${args.params.group}`;
for (const ref of state.cachedFavorites.values()) {
for (const ref of cachedFavorites.values()) {
if (ref.$isDeleted || ref.$groupKey !== key) {
continue;
}
@@ -573,7 +493,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function expireFavorites() {
friendStore.localFavoriteFriends.clear();
state.cachedFavorites.clear();
cachedFavorites.clear();
state.cachedFavoritesByObjectId.clear();
state.favoriteObjects.clear();
state.favoriteFriends_ = [];
@@ -625,7 +545,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.applyFavorite`
*
* @param {'friend' | 'world' | 'avatar'} type
* @param {string} objectId
* @param {boolean} sortTop
@@ -952,7 +872,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
state.cachedFavoriteGroupsByTypeName.set(group.key, group);
}
}
for (ref of state.cachedFavorites.values()) {
for (ref of cachedFavorites.values()) {
ref.$groupRef = null;
if (ref.$isDeleted) {
continue;
@@ -1054,7 +974,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
* @returns {any}
*/
function applyFavoriteCached(json) {
let ref = state.cachedFavorites.get(json.id);
let ref = cachedFavorites.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
@@ -1069,7 +989,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
//
...json
};
state.cachedFavorites.set(ref.id, ref);
cachedFavorites.set(ref.id, ref);
state.cachedFavoritesByObjectId.set(ref.favoriteId, ref);
if (
ref.type === 'friend' &&
@@ -1103,7 +1023,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
*
*/
function deleteExpiredFavorites() {
for (const ref of state.cachedFavorites.values()) {
for (const ref of cachedFavorites.values()) {
if (ref.$isDeleted || ref.$isExpired === false) {
continue;
}
@@ -1141,7 +1061,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
avatar: [0, favoriteRequest.getFavoriteAvatars]
};
const tags = [];
for (const ref of state.cachedFavorites.values()) {
for (const ref of cachedFavorites.values()) {
if (ref.$isDeleted) {
continue;
}
@@ -1187,9 +1107,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
* aka: `$app.methods.clearBulkFavoriteSelection`
*/
function clearBulkFavoriteSelection() {
let ctx;
for (ctx of state.favoriteFriends_) {
@@ -1216,7 +1133,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.getLocalWorldFavoriteGroupLength`
*
* @param {string} group
* @returns {*|number}
*/
@@ -1229,7 +1146,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.addLocalWorldFavorite`
*
* @param {string} worldId
* @param {string} group
*/
@@ -1271,7 +1188,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.hasLocalWorldFavorite`
*
* @param {string} worldId
* @param {string} group
* @returns {boolean}
@@ -1290,7 +1207,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.addLocalAvatarFavorite`
*
* @param {string} avatarId
* @param {string} group
*/
@@ -1332,7 +1249,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.hasLocalAvatarFavorite`
*
* @param {string} avatarId
* @param {string} group
* @returns {boolean}
@@ -1351,7 +1268,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.getLocalAvatarFavoriteGroupLength`
*
* @param {string} group
* @returns {*|number}
*/
@@ -1394,7 +1311,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.deleteLocalAvatarFavoriteGroup`
*
* @param {string} group
*/
function deleteLocalAvatarFavoriteGroup(group) {
@@ -1465,9 +1382,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
* aka: `$app.methods.sortLocalAvatarFavorites`
*/
function sortLocalAvatarFavorites() {
state.localAvatarFavoriteGroups.sort();
if (!appearanceSettingsStore.sortFavorites) {
@@ -1481,13 +1395,13 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.renameLocalAvatarFavoriteGroup`
*
* @param {string} newName
* @param {string} group
*/
function renameLocalAvatarFavoriteGroup(newName, group) {
if (state.localAvatarFavoriteGroups.includes(newName)) {
$app.$message({
ElMessage({
message: t('prompt.local_favorite_group_rename.message.error', {
name: newName
}),
@@ -1505,12 +1419,12 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.newLocalAvatarFavoriteGroup`
*
* @param {string} group
*/
function newLocalAvatarFavoriteGroup(group) {
if (state.localAvatarFavoriteGroups.includes(group)) {
$app.$message({
ElMessage({
message: t('prompt.new_local_favorite_group.message.error', {
name: group
}),
@@ -1528,52 +1442,56 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.getLocalAvatarFavorites`
*
* @returns {Promise<void>}
*/
async function getLocalAvatarFavorites() {
let ref;
let i;
state.localAvatarFavoriteGroups = [];
state.localAvatarFavoritesList = [];
state.localAvatarFavorites = {};
const localGroups = new Set();
const localListSet = new Set();
const localFavorites = Object.create(null);
const avatarCache = await database.getAvatarCache();
for (i = 0; i < avatarCache.length; ++i) {
ref = avatarCache[i];
for (let i = 0; i < avatarCache.length; ++i) {
const ref = avatarCache[i];
if (!avatarStore.cachedAvatars.has(ref.id)) {
avatarStore.applyAvatar(ref);
}
}
const favorites = await database.getAvatarFavorites();
for (i = 0; i < favorites.length; ++i) {
for (let i = 0; i < favorites.length; ++i) {
const favorite = favorites[i];
if (!state.localAvatarFavoritesList.includes(favorite.avatarId)) {
state.localAvatarFavoritesList.push(favorite.avatarId);
localListSet.add(favorite.avatarId);
if (!localFavorites[favorite.groupName]) {
localFavorites[favorite.groupName] = [];
}
if (!state.localAvatarFavorites[favorite.groupName]) {
state.localAvatarFavorites[favorite.groupName] = [];
}
if (!state.localAvatarFavoriteGroups.includes(favorite.groupName)) {
state.localAvatarFavoriteGroups.push(favorite.groupName);
}
ref = avatarStore.cachedAvatars.get(favorite.avatarId);
localGroups.add(favorite.groupName);
let ref = avatarStore.cachedAvatars.get(favorite.avatarId);
if (typeof ref === 'undefined') {
ref = {
id: favorite.avatarId
};
ref = { id: favorite.avatarId };
}
state.localAvatarFavorites[favorite.groupName].unshift(ref);
localFavorites[favorite.groupName].push(ref);
}
if (state.localAvatarFavoriteGroups.length === 0) {
let groupsArr = Array.from(localGroups);
if (groupsArr.length === 0) {
// default group
state.localAvatarFavorites.Favorites = [];
state.localAvatarFavoriteGroups.push('Favorites');
localFavorites.Favorites = [];
groupsArr = ['Favorites'];
}
state.localAvatarFavoriteGroups = groupsArr;
state.localAvatarFavoritesList = Array.from(localListSet);
state.localAvatarFavorites = localFavorites;
sortLocalAvatarFavorites();
}
/**
* aka: `$app.methods.removeLocalAvatarFavorite`
*
* @param {string} avatarId
* @param {string} group
*/
@@ -1631,7 +1549,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.deleteLocalWorldFavoriteGroup`
*
* @param {string} group
*/
function deleteLocalWorldFavoriteGroup(group) {
@@ -1671,9 +1589,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
* aka: `$app.methods.sortLocalWorldFavorites`
*/
function sortLocalWorldFavorites() {
state.localWorldFavoriteGroups.sort();
if (!appearanceSettingsStore.sortFavorites) {
@@ -1687,13 +1602,13 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.renameLocalWorldFavoriteGroup`
*
* @param {string} newName
* @param {string} group
*/
function renameLocalWorldFavoriteGroup(newName, group) {
if (state.localWorldFavoriteGroups.includes(newName)) {
$app.$message({
ElMessage({
message: t('prompt.local_favorite_group_rename.message.error', {
name: newName
}),
@@ -1711,7 +1626,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.removeLocalWorldFavorite`
*
* @param {string} worldId
* @param {string} group
*/
@@ -1767,13 +1682,14 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.getLocalWorldFavorites`
*
* @returns {Promise<void>}
*/
async function getLocalWorldFavorites() {
state.localWorldFavoriteGroups = [];
state.localWorldFavoritesList = [];
state.localWorldFavorites = {};
const localGroups = new Set();
const localListSet = new Set();
const localFavorites = Object.create(null);
const worldCache = await database.getWorldCache();
for (let i = 0; i < worldCache.length; ++i) {
const ref = worldCache[i];
@@ -1781,41 +1697,46 @@ export const useFavoriteStore = defineStore('Favorite', () => {
worldStore.applyWorld(ref);
}
}
const favorites = await database.getWorldFavorites();
for (let i = 0; i < favorites.length; ++i) {
const favorite = favorites[i];
if (!state.localWorldFavoritesList.includes(favorite.worldId)) {
state.localWorldFavoritesList.push(favorite.worldId);
}
if (!state.localWorldFavorites[favorite.groupName]) {
state.localWorldFavorites[favorite.groupName] = [];
}
if (!state.localWorldFavoriteGroups.includes(favorite.groupName)) {
state.localWorldFavoriteGroups.push(favorite.groupName);
localListSet.add(favorite.worldId);
if (!localFavorites[favorite.groupName]) {
localFavorites[favorite.groupName] = [];
}
localGroups.add(favorite.groupName);
let ref = worldStore.cachedWorlds.get(favorite.worldId);
if (typeof ref === 'undefined') {
ref = {
id: favorite.worldId
};
ref = { id: favorite.worldId };
}
state.localWorldFavorites[favorite.groupName].unshift(ref);
localFavorites[favorite.groupName].push(ref);
}
if (state.localWorldFavoriteGroups.length === 0) {
let groupsArr = Array.from(localGroups);
if (groupsArr.length === 0) {
localFavorites.Favorites = [];
// default group
state.localWorldFavorites.Favorites = [];
state.localWorldFavoriteGroups.push('Favorites');
groupsArr = ['Favorites'];
}
state.localWorldFavoriteGroups = groupsArr;
state.localWorldFavoritesList = Array.from(localListSet);
state.localWorldFavorites = localFavorites;
sortLocalWorldFavorites();
}
/**
* aka: `$app.methods.newLocalWorldFavoriteGroup`
*
* @param {string} group
*/
function newLocalWorldFavoriteGroup(group) {
if (state.localWorldFavoriteGroups.includes(group)) {
$app.$message({
ElMessage({
message: t('prompt.new_local_favorite_group.message.error', {
name: group
}),
@@ -1833,7 +1754,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
/**
* aka: `$app.methods.deleteFavoriteNoConfirm`
*
* @param {string} objectId
*/
function deleteFavoriteNoConfirm(objectId) {
@@ -1898,17 +1819,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
localAvatarFavoritesList,
localAvatarFavoriteGroups,
favoriteDialog,
favoriteObjects,
localWorldFavoritesList,
favoriteFriends_,
favoriteFriendsSorted,
favoriteWorlds_,
favoriteWorldsSorted,
favoriteAvatars_,
favoriteAvatarsSorted,
sortFavoriteFriends,
sortFavoriteWorlds,
sortFavoriteAvatars,
cachedFavoritesByObjectId,
localWorldFavoriteGroups,
groupedByGroupKeyFavoriteFriends,
+2 -1
View File
@@ -25,7 +25,7 @@ export const useFeedStore = defineStore('Feed', () => {
filter: [],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created_at',
order: 'descending'
@@ -229,6 +229,7 @@ export const useFeedStore = defineStore('Feed', () => {
return {
state,
feedTable,
feedSessionTable,
initFeedTable,
+98 -84
View File
@@ -1,11 +1,11 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as workerTimers from 'worker-timers';
import { friendRequest, userRequest } from '../api';
import { $app } from '../app';
import configRepository from '../service/config';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { AppDebug } from '../service/appConfig';
import { reconnectWebSocket } from '../service/websocket';
import { watchState } from '../service/watchState';
import {
@@ -30,7 +30,7 @@ import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
export const useFriendStore = defineStore('Friend', () => {
const appearanceSettingsStore = useAppearanceSettingsStore();
@@ -58,7 +58,6 @@ export const useFriendStore = defineStore('Friend', () => {
localFavoriteFriends: new Set(),
isRefreshFriendsLoading: false,
onlineFriendCount: 0,
friendLog: new Map(),
friendLogTable: {
data: [],
filters: [
@@ -81,7 +80,7 @@ export const useFriendStore = defineStore('Friend', () => {
],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created_at',
order: 'descending'
@@ -97,6 +96,26 @@ export const useFriendStore = defineStore('Friend', () => {
friendNumber: 0
});
let friendLog = new Map();
const friends = computed({
get() {
return state.friends;
},
set(value) {
state.friends = value;
}
});
const localFavoriteFriends = computed({
get() {
return state.localFavoriteFriends;
},
set(value) {
state.localFavoriteFriends = value;
}
});
async function init() {
const friendLogTableFiltersValue = JSON.parse(
await configRepository.getString('VRCX_friendLogTableFilters', '[]')
@@ -106,8 +125,6 @@ export const useFriendStore = defineStore('Friend', () => {
init();
const friends = state.friends;
// friends_(array) may not have change records in pinia because does not use action
const onlineFriends_ = computed({
get() {
@@ -173,7 +190,6 @@ export const useFriendStore = defineStore('Friend', () => {
state.sortOfflineFriends = value;
}
});
const localFavoriteFriends = state.localFavoriteFriends;
// VIP friends
const vipFriends = computed(() => {
@@ -247,15 +263,6 @@ export const useFriendStore = defineStore('Friend', () => {
}
});
const friendLog = computed({
get() {
return state.friendLog;
},
set(value) {
state.friendLog = value;
}
});
const friendLogTable = computed({
get() {
return state.friendLogTable;
@@ -270,7 +277,7 @@ export const useFriendStore = defineStore('Friend', () => {
(isLoggedIn) => {
state.friends.clear();
state.friendNumber = 0;
state.friendLog.clear();
friendLog.clear();
state.friendLogTable.data = [];
groupStore.groupInstances = [];
state.vipFriends_ = [];
@@ -343,7 +350,7 @@ export const useFriendStore = defineStore('Friend', () => {
if (
watchState.isFriendsLoaded &&
ref.isFriend &&
!state.friendLog.has(ref.id) &&
!friendLog.has(ref.id) &&
ref.id !== userStore.currentUser.id
) {
addFriendship(ref.id);
@@ -429,7 +436,7 @@ export const useFriendStore = defineStore('Friend', () => {
}
}
if (stateInput === 'online') {
if (AppGlobal.debugFriendState && ctx.pendingOffline) {
if (AppDebug.debugFriendState && ctx.pendingOffline) {
const time = (Date.now() - ctx.pendingOfflineTime) / 1000;
console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
}
@@ -534,12 +541,12 @@ export const useFriendStore = defineStore('Friend', () => {
}
// prevent status flapping
if (ctx.pendingOffline) {
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineAlreadyWaiting');
}
return;
}
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineBegin');
}
ctx.pendingOffline = true;
@@ -547,7 +554,7 @@ export const useFriendStore = defineStore('Friend', () => {
// wait 2minutes then check if user came back online
workerTimers.setTimeout(() => {
if (!ctx.pendingOffline) {
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineAlreadyCancelled');
}
return;
@@ -555,7 +562,7 @@ export const useFriendStore = defineStore('Friend', () => {
ctx.pendingOffline = false;
ctx.pendingOfflineTime = '';
if (ctx.pendingState === ctx.state) {
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(
ctx.name,
'pendingOfflineCancelledStateMatched'
@@ -563,7 +570,7 @@ export const useFriendStore = defineStore('Friend', () => {
}
return;
}
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineEnd');
}
updateFriendDelayedCheck(ctx, location, $location_at);
@@ -602,7 +609,7 @@ export const useFriendStore = defineStore('Friend', () => {
let worldName;
const id = ctx.id;
const newState = ctx.pendingState;
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(
`${ctx.name} updateFriendState ${ctx.state} -> ${newState}`
);
@@ -736,7 +743,7 @@ export const useFriendStore = defineStore('Friend', () => {
}
/**
* aka: `$app.refreshFriends`
*
* @param ref
*/
function refreshFriendsStatus(ref) {
@@ -780,11 +787,11 @@ export const useFriendStore = defineStore('Friend', () => {
const ref = userStore.cachedUsers.get(id);
const isVIP = state.localFavoriteFriends.has(id);
let name = '';
const friend = state.friendLog.get(id);
const friend = friendLog.get(id);
if (friend) {
name = friend.displayName;
}
const ctx = {
const ctx = reactive({
id,
state: state_input || 'offline',
isVIP,
@@ -795,7 +802,7 @@ export const useFriendStore = defineStore('Friend', () => {
pendingOfflineTime: '',
pendingState: '',
$nickName: ''
};
});
if (watchState.isFriendsLoaded) {
getUserMemo(id).then((memo) => {
if (memo.userId === id) {
@@ -809,7 +816,7 @@ export const useFriendStore = defineStore('Friend', () => {
});
}
if (typeof ref === 'undefined') {
const friendLogRef = state.friendLog.get(id);
const friendLogRef = friendLog.get(id);
if (friendLogRef?.displayName) {
ctx.name = friendLogRef.displayName;
}
@@ -961,7 +968,7 @@ export const useFriendStore = defineStore('Friend', () => {
}
const ref = state.friends.get(friend.id);
if (ref?.state !== state_input) {
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(
`Refetching friend state it does not match ${friend.displayName} from ${ref?.state} to ${state_input}`,
friend
@@ -972,7 +979,7 @@ export const useFriendStore = defineStore('Friend', () => {
});
friends[i] = args.json;
} else if (friend.location === 'traveling') {
if (AppGlobal.debugFriendState) {
if (AppDebug.debugFriendState) {
console.log(
'Refetching traveling friend',
friend.displayName
@@ -995,8 +1002,9 @@ export const useFriendStore = defineStore('Friend', () => {
* @returns {Promise<*>}
*/
async function refreshRemainingFriends(friends) {
const friendsSet = new Set(friends.map((x) => x.id));
for (const userId of userStore.currentUser.friends) {
if (!friends.some((x) => x.id === userId)) {
if (!friendsSet.has(userId)) {
try {
if (!watchState.isLoggedIn) {
console.error(`User isn't logged in`);
@@ -1062,26 +1070,33 @@ export const useFriendStore = defineStore('Friend', () => {
}
const data = await database.getAllUserStats(userIds, displayNames);
const dataByDisplayName = new Map();
const friendsByDisplayName = new Map();
for (const ref of data) {
if (ref.displayName && ref.userId) {
dataByDisplayName.set(ref.displayName, ref.userId);
}
}
for (const ref of state.friends.values()) {
if (ref?.ref?.id && ref.ref.displayName) {
friendsByDisplayName.set(ref.ref.displayName, ref.id);
}
}
const friendListMap = new Map();
for (item of data) {
if (!item.userId) {
// find userId from previous data with matching displayName
for (ref of data) {
if (ref.displayName === item.displayName && ref.userId) {
item.userId = ref.userId;
}
}
item.userId = dataByDisplayName.get(item.displayName);
// if still no userId, find userId from friends list
if (!item.userId) {
for (ref of state.friends.values()) {
if (
ref?.ref?.id &&
ref.ref.displayName === item.displayName
) {
item.userId = ref.id;
}
}
item.userId = friendsByDisplayName.get(item.displayName);
}
// if still no userId, skip
if (!item.userId) {
continue;
@@ -1118,7 +1133,7 @@ export const useFriendStore = defineStore('Friend', () => {
function addFriendship(id) {
if (
!watchState.isFriendsLoaded ||
state.friendLog.has(id) ||
friendLog.has(id) ||
id === userStore.currentUser.id
) {
return;
@@ -1139,7 +1154,7 @@ export const useFriendStore = defineStore('Friend', () => {
return;
}
handleFriendStatus(args);
if (args.json.isFriend && !state.friendLog.has(id)) {
if (args.json.isFriend && !friendLog.has(id)) {
if (state.friendNumber === 0) {
state.friendNumber = state.friends.size;
}
@@ -1165,7 +1180,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
state.friendLog.set(id, friendLogCurrent);
friendLog.set(id, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
uiStore.notifyMenu('friendLog');
deleteFriendRequest(id);
@@ -1208,7 +1223,7 @@ export const useFriendStore = defineStore('Friend', () => {
* @param {string} id
*/
function deleteFriendship(id) {
const ctx = state.friendLog.get(id);
const ctx = friendLog.get(id);
if (typeof ctx === 'undefined') {
return;
}
@@ -1223,7 +1238,7 @@ export const useFriendStore = defineStore('Friend', () => {
return;
}
handleFriendStatus(args);
if (!args.json.isFriend && state.friendLog.has(id)) {
if (!args.json.isFriend && friendLog.has(id)) {
const friendLogHistory = {
created_at: new Date().toJSON(),
type: 'Unfriend',
@@ -1233,7 +1248,7 @@ export const useFriendStore = defineStore('Friend', () => {
state.friendLogTable.data.push(friendLogHistory);
database.addFriendLogHistory(friendLogHistory);
notificationStore.queueFriendLogNoty(friendLogHistory);
state.friendLog.delete(id);
friendLog.delete(id);
database.deleteFriendLogCurrent(id);
if (!appearanceSettingsStore.hideUnfriends) {
uiStore.notifyMenu('friendLog');
@@ -1255,9 +1270,9 @@ export const useFriendStore = defineStore('Friend', () => {
set.add(id);
addFriendship(id);
}
for (id of state.friendLog.keys()) {
for (id of friendLog.keys()) {
if (id === userStore.currentUser.id) {
state.friendLog.delete(id);
friendLog.delete(id);
database.deleteFriendLogCurrent(id);
} else if (!set.has(id)) {
deleteFriendship(id);
@@ -1270,7 +1285,7 @@ export const useFriendStore = defineStore('Friend', () => {
* @param {object} ref
*/
function updateFriendship(ref) {
const ctx = state.friendLog.get(ref.id);
const ctx = friendLog.get(ref.id);
if (!watchState.isFriendsLoaded || typeof ctx === 'undefined') {
return;
}
@@ -1301,7 +1316,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
state.friendLog.set(ref.id, friendLogCurrent);
friendLog.set(ref.id, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
ctx.displayName = ref.displayName;
uiStore.notifyMenu('friendLog');
@@ -1325,7 +1340,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
state.friendLog.set(ref.id, friendLogCurrent3);
friendLog.set(ref.id, friendLogCurrent3);
database.setFriendLogCurrent(friendLogCurrent3);
return;
}
@@ -1347,7 +1362,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
state.friendLog.set(ref.id, friendLogCurrent2);
friendLog.set(ref.id, friendLogCurrent2);
database.setFriendLogCurrent(friendLogCurrent2);
uiStore.notifyMenu('friendLog');
sharedFeedStore.updateSharedFeed(true);
@@ -1372,7 +1387,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: 0
};
state.friendLog.set(friend.id, row);
friendLog.set(friend.id, row);
sqlValues.unshift(row);
}
database.setFriendLogCurrentArray(sqlValues);
@@ -1414,7 +1429,7 @@ export const useFriendStore = defineStore('Friend', () => {
const friendLogCurrentArray = await database.getFriendLogCurrent();
for (friend of friendLogCurrentArray) {
state.friendLog.set(friend.userId, friend);
friendLog.set(friend.userId, friend);
}
refreshFriendsStatus(currentUser);
@@ -1443,12 +1458,12 @@ export const useFriendStore = defineStore('Friend', () => {
* @param {string} userId
*/
function setFriendNumber(friendNumber, userId) {
const ref = state.friendLog.get(userId);
const ref = friendLog.get(userId);
if (!ref) {
return;
}
ref.friendNumber = friendNumber;
state.friendLog.set(ref.userId, ref);
friendLog.set(ref.userId, ref);
database.setFriendLogCurrent(ref);
const friendRef = state.friends.get(userId);
if (friendRef?.ref) {
@@ -1467,7 +1482,7 @@ export const useFriendStore = defineStore('Friend', () => {
// reset friendNumber and friendLog
state.friendNumber = 0;
for (const ref of state.friendLog.values()) {
for (const ref of friendLog.values()) {
ref.friendNumber = 0;
}
@@ -1504,7 +1519,7 @@ export const useFriendStore = defineStore('Friend', () => {
}
var status = false;
state.friendNumber = 0;
for (const ref of state.friendLog.values()) {
for (const ref of friendLog.values()) {
ref.friendNumber = 0;
}
try {
@@ -1595,7 +1610,7 @@ export const useFriendStore = defineStore('Friend', () => {
const friendLogTable = getFriendLogFriendOrder();
for (let i = friendLogTable.length - 1; i > -1; i--) {
const friendLog = friendLogTable[i];
const ref = state.friendLog.get(friendLog.id);
const ref = friendLog.get(friendLog.id);
if (!ref) {
continue;
}
@@ -1603,7 +1618,7 @@ export const useFriendStore = defineStore('Friend', () => {
break;
}
ref.friendNumber = --state.friendNumber;
state.friendLog.set(ref.userId, ref);
friendLog.set(ref.userId, ref);
database.setFriendLogCurrent(ref);
const friendRef = state.friends.get(friendLog.id);
if (friendRef?.ref) {
@@ -1714,7 +1729,7 @@ export const useFriendStore = defineStore('Friend', () => {
trustLevel: ref.$trustLevel,
friendNumber: i + 1
};
state.friendLog.set(userId, friendLogCurrent);
friendLog.set(userId, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
state.friendNumber = i + 1;
}
@@ -1729,12 +1744,12 @@ export const useFriendStore = defineStore('Friend', () => {
return;
}
for (const friendLog of friendLogTable) {
const ref = state.friendLog.get(friendLog.id);
const ref = friendLog.get(friendLog.id);
if (!ref || ref.friendNumber) {
continue;
}
ref.friendNumber = ++state.friendNumber;
state.friendLog.set(ref.userId, ref);
friendLog.set(ref.userId, ref);
database.setFriendLogCurrent(ref);
const friendRef = state.friends.get(friendLog.id);
if (friendRef?.ref) {
@@ -1744,19 +1759,18 @@ export const useFriendStore = defineStore('Friend', () => {
}
function confirmDeleteFriend(id) {
$app.$confirm('Continue? Unfriend', 'Confirm', {
ElMessageBox.confirm('Continue? Unfriend', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: async (action) => {
if (action === 'confirm') {
const args = await friendRequest.deleteFriend({
userId: id
});
handleFriendDelete(args);
}
}
});
type: 'info'
})
.then(async () => {
const args = await friendRequest.deleteFriend({
userId: id
});
handleFriendDelete(args);
})
.catch(() => {});
}
async function saveSidebarSortOrder() {
@@ -1770,7 +1784,7 @@ export const useFriendStore = defineStore('Friend', () => {
const userId = userStore.currentUser.id;
state.isRefreshFriendsLoading = true;
watchState.isFriendsLoaded = false;
state.friendLog = new Map();
friendLog = new Map();
initFriendLogHistoryTable();
try {
@@ -1780,8 +1794,8 @@ export const useFriendStore = defineStore('Friend', () => {
await initFriendLog(userStore.currentUser);
}
} catch (err) {
if (!AppGlobal.dontLogMeOut) {
$app.$message({
if (!AppDebug.dontLogMeOut) {
ElMessage({
message: t('message.friend.load_failed'),
type: 'error'
});
+9 -47
View File
@@ -1,6 +1,7 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import * as workerTimers from 'worker-timers';
import {
inventoryRequest,
@@ -8,8 +9,7 @@ import {
vrcPlusIconRequest,
vrcPlusImageRequest
} from '../api';
import { $app } from '../app';
import { AppGlobal } from '../service/appConfig';
import { AppDebug } from '../service/appConfig';
import { watchState } from '../service/watchState';
import {
getEmojiFileName,
@@ -17,7 +17,7 @@ import {
getPrintLocalDate
} from '../shared/utils';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
export const useGalleryStore = defineStore('Gallery', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
@@ -45,8 +45,6 @@ export const useGalleryStore = defineStore('Gallery', () => {
printTable: [],
emojiTable: [],
inventoryTable: [],
previousImagesDialogVisible: false,
previousImagesTable: [],
fullscreenImageDialog: {
visible: false,
imageUrl: '',
@@ -176,20 +174,6 @@ export const useGalleryStore = defineStore('Gallery', () => {
}
});
const previousImagesDialogVisible = computed({
get: () => state.previousImagesDialogVisible,
set: (value) => {
state.previousImagesDialogVisible = value;
}
});
const previousImagesTable = computed({
get: () => state.previousImagesTable,
set: (value) => {
state.previousImagesTable = value;
}
});
const fullscreenImageDialog = computed({
get: () => state.fullscreenImageDialog,
set: (value) => {
@@ -200,14 +184,12 @@ export const useGalleryStore = defineStore('Gallery', () => {
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.previousImagesTable = [];
state.galleryTable = [];
state.VRCPlusIconsTable = [];
state.stickerTable = [];
state.printTable = [];
state.emojiTable = [];
state.galleryDialogVisible = false;
state.previousImagesDialogVisible = false;
state.fullscreenImageDialog.visible = false;
if (isLoggedIn) {
tryDeleteOldPrints();
@@ -290,7 +272,7 @@ export const useGalleryStore = defineStore('Gallery', () => {
}
if (files[0].size >= 100000000) {
// 100MB
$app.$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -298,7 +280,7 @@ export const useGalleryStore = defineStore('Gallery', () => {
return;
}
if (!files[0].type.match(/image.*/)) {
$app.$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
@@ -540,10 +522,10 @@ export const useGalleryStore = defineStore('Gallery', () => {
for (const printId of idList) {
await vrcPlusImageRequest.deletePrint(printId);
const text = `Old print automatically deleted: ${printId}`;
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
AppDebug.errorNoty = new Noty({
type: 'info',
text
}).show();
@@ -554,24 +536,6 @@ export const useGalleryStore = defineStore('Gallery', () => {
await refreshPrintTable();
}
async function checkPreviousImageAvailable(images) {
state.previousImagesTable = [];
for (const image of images) {
if (image.file && image.file.url) {
const response = await fetch(image.file.url, {
method: 'HEAD',
redirect: 'follow'
}).catch((error) => {
console.error('Failed to check image availability:', error);
return null;
});
if (response && response.status === 200) {
state.previousImagesTable.push(image);
}
}
}
}
function showFullscreenImageDialog(imageUrl, fileName) {
if (!imageUrl) {
return;
@@ -656,6 +620,7 @@ export const useGalleryStore = defineStore('Gallery', () => {
return {
state,
galleryTable,
galleryDialogVisible,
galleryDialogGalleryLoading,
@@ -673,8 +638,6 @@ export const useGalleryStore = defineStore('Gallery', () => {
printTable,
emojiTable,
inventoryTable,
previousImagesDialogVisible,
previousImagesTable,
fullscreenImageDialog,
showGalleryDialog,
@@ -689,7 +652,6 @@ export const useGalleryStore = defineStore('Gallery', () => {
refreshEmojiTable,
getInventory,
tryDeleteOldPrints,
checkPreviousImageAvailable,
showFullscreenImageDialog,
handleStickerAdd,
handleGalleryImageAdd,
+5 -4
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as workerTimers from 'worker-timers';
import { $app } from '../app';
import configRepository from '../service/config.js';
import { database } from '../service/database';
import {
@@ -158,7 +158,7 @@ export const useGameStore = defineStore('Game', () => {
}
AppApi.FocusWindow();
const message = 'VRChat crashed, attempting to rejoin last instance';
$app.$message({
ElMessage({
message,
type: 'info'
});
@@ -255,14 +255,14 @@ export const useGameStore = defineStore('Game', () => {
);
if (!result) {
// failed to set key
$app.$alert(
ElMessageBox.alert(
'VRCX has noticed VRChat debug logging is disabled. VRCX requires debug logging in order to function correctly. Please enable debug logging in VRChat quick menu settings > debug > enable debug logging, then rejoin the instance or restart VRChat.',
'Enable debug logging'
);
console.error('Failed to enable debug logging', result);
return;
}
$app.$alert(
ElMessageBox.alert(
'VRCX has noticed VRChat debug logging is disabled and automatically re-enabled it. VRCX requires debug logging in order to function correctly.',
'Enabled debug logging'
);
@@ -281,6 +281,7 @@ export const useGameStore = defineStore('Game', () => {
return {
state,
VRChatUsedCacheSize,
VRChatTotalCacheSize,
VRChatCacheSizeLoading,
+10 -8
View File
@@ -1,12 +1,12 @@
import dayjs from 'dayjs';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import * as workerTimers from 'worker-timers';
import { userRequest } from '../api';
import { $app } from '../app';
import configRepository from '../service/config';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { AppDebug } from '../service/appConfig';
import gameLogService from '../service/gamelog.js';
import { watchState } from '../service/watchState';
import {
@@ -69,7 +69,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
filter: [],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created_at',
order: 'descending'
@@ -627,7 +627,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
// set $location_at to join time if user isn't a friend
ref.$location_at = joinTime;
} else {
if (AppGlobal.debugGameLog || AppGlobal.debugWebRequests) {
if (AppDebug.debugGameLog || AppDebug.debugWebRequests) {
console.log('Fetching user from gameLog:', userId);
}
userRequest.getUser({ userId });
@@ -744,7 +744,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
vrcxStore.processScreenshot(gameLog.screenshotPath);
break;
case 'api-request':
if (AppGlobal.debugWebRequests) {
if (AppDebug.debugWebRequests) {
console.log('API Request:', gameLog.url);
}
// const userId = '';
@@ -1387,7 +1387,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
rawLogs.slice(3)
);
if (
AppGlobal.debugGameLog &&
AppDebug.debugGameLog &&
gameLog.type !== 'photon-id' &&
gameLog.type !== 'api-request' &&
gameLog.type !== 'udon-exception'
@@ -1399,7 +1399,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
async function disableGameLogDialog() {
if (gameStore.isGameRunning) {
$app.$message({
ElMessage({
message:
'VRChat needs to be closed before this option can be changed',
type: 'error'
@@ -1407,7 +1407,7 @@ export const useGameLogStore = defineStore('GameLog', () => {
return;
}
if (!advancedSettingsStore.gameLogDisabled) {
$app.$confirm('Continue? Disable GameLog', 'Confirm', {
ElMessageBox.confirm('Continue? Disable GameLog', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
@@ -1431,11 +1431,13 @@ export const useGameLogStore = defineStore('GameLog', () => {
return {
state,
nowPlaying,
gameLogTable,
gameLogSessionTable,
lastVideoUrl,
lastResourceloadUrl,
initGameLogTable,
clearNowPlaying,
tryLoadPlayerList,
+28 -30
View File
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { computed, reactive, watch, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as workerTimers from 'worker-timers';
import {
groupRequest,
@@ -7,7 +8,6 @@ import {
userRequest,
worldRequest
} from '../api';
import { $app } from '../app';
import configRepository from '../service/config';
import { watchState } from '../service/watchState';
import { database } from '../service/database.js';
@@ -83,12 +83,13 @@ export const useGroupStore = defineStore('Group', () => {
auditLogTypes: [],
openWithUserId: ''
},
cachedGroups: new Map(),
inGameGroupOrder: [],
groupInstances: [],
currentUserGroupsInit: false
});
let cachedGroups = new Map();
const groupDialog = computed({
get: () => state.groupDialog,
set: (value) => {
@@ -124,13 +125,6 @@ export const useGroupStore = defineStore('Group', () => {
}
});
const cachedGroups = computed({
get: () => state.cachedGroups,
set: (value) => {
state.cachedGroups = value;
}
});
const inGameGroupOrder = computed({
get: () => state.inGameGroupOrder,
set: (value) => {
@@ -160,7 +154,7 @@ export const useGroupStore = defineStore('Group', () => {
state.moderateGroupDialog.visible = false;
state.groupMemberModeration.visible = false;
state.currentUserGroupsInit = false;
state.cachedGroups.clear();
cachedGroups.clear();
state.currentUserGroups.clear();
if (isLoggedIn) {
initUserGroups();
@@ -198,7 +192,7 @@ export const useGroupStore = defineStore('Group', () => {
.catch((err) => {
D.loading = false;
D.visible = false;
$app.$message({
ElMessage({
message: 'Failed to load group',
type: 'error'
});
@@ -506,7 +500,7 @@ export const useGroupStore = defineStore('Group', () => {
});
}
}
$app.$nextTick(() => (D.isGetGroupDialogGroupLoading = false));
nextTick(() => (D.isGetGroupDialogGroupLoading = false));
return args;
});
}
@@ -566,16 +560,19 @@ export const useGroupStore = defineStore('Group', () => {
}
function leaveGroupPrompt(groupId) {
$app.$confirm('Are you sure you want to leave this group?', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
leaveGroup(groupId);
}
ElMessageBox.confirm(
'Are you sure you want to leave this group?',
'Confirm',
{
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info'
}
});
)
.then(() => {
leaveGroup(groupId);
})
.catch(() => {});
}
function updateGroupPostSearch() {
@@ -602,7 +599,7 @@ export const useGroupStore = defineStore('Group', () => {
})
.then((args) => {
handleGroupMemberProps(args);
$app.$message({
ElMessage({
message: 'Group visibility updated',
type: 'success'
});
@@ -616,7 +613,7 @@ export const useGroupStore = defineStore('Group', () => {
* @returns {object} ref
*/
function applyGroup(json) {
let ref = state.cachedGroups.get(json.id);
let ref = cachedGroups.get(json.id);
if (json.rules) {
json.rules = replaceBioSymbols(json.rules);
}
@@ -684,7 +681,7 @@ export const useGroupStore = defineStore('Group', () => {
$languages: [],
...json
};
state.cachedGroups.set(ref.id, ref);
cachedGroups.set(ref.id, ref);
} else {
if (state.currentUserGroups.has(ref.id)) {
// compare group props
@@ -845,7 +842,7 @@ export const useGroupStore = defineStore('Group', () => {
const json = args.json;
for (const groupId in json) {
const permissions = json[groupId];
const group = state.cachedGroups.get(groupId);
const group = cachedGroups.get(groupId);
if (group) {
group.myMember.permissions = permissions;
}
@@ -896,7 +893,7 @@ export const useGroupStore = defineStore('Group', () => {
json.$fetchedAt = args.json.fetchedAt;
}
const instanceRef = instanceStore.applyInstance(json);
const groupRef = state.cachedGroups.get(json.ownerId);
const groupRef = cachedGroups.get(json.ownerId);
if (typeof groupRef === 'undefined') {
if (watchState.isFriendsLoaded) {
const args = await groupRequest.getGroup({
@@ -936,7 +933,7 @@ export const useGroupStore = defineStore('Group', () => {
}
// update myMember without fetching member
if (json?.userId === userStore.currentUser.id) {
ref = state.cachedGroups.get(json.groupId);
ref = cachedGroups.get(json.groupId);
if (typeof ref !== 'undefined') {
const newJson = {
id: json.groupId,
@@ -980,7 +977,7 @@ export const useGroupStore = defineStore('Group', () => {
'[]'
)
);
state.cachedGroups.clear();
cachedGroups.clear();
state.currentUserGroups.clear();
for (const group of savedGroups) {
const json = {
@@ -999,7 +996,7 @@ export const useGroupStore = defineStore('Group', () => {
if (groups) {
const promises = groups.map(async (groupId) => {
const groupRef = state.cachedGroups.get(groupId);
const groupRef = cachedGroups.get(groupId);
if (
typeof groupRef !== 'undefined' &&
@@ -1096,6 +1093,7 @@ export const useGroupStore = defineStore('Group', () => {
return {
state,
groupDialog,
currentUserGroups,
inviteGroupDialog,

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