refactor: dialogs (#1224)

* refactor: dialogs

* fix: storeAvatarImage

* FriendLog.vue

* FriendLog.vue

* FriendLog.vue

* GameLog.vue

* fix: next day button jumping to the wrong date

* sync master

* fix: launchGame

* Notification.vue

* Feed.vue

* Search.vue

* Profile.vue

* PlayerList.vue

* Login.vue

* utils

* update dialog

* del gameLog.pug

* fix

* fix: group role cannot be displayed currently

* fix: "Hide Friends in Same Instance" hides players in unrelated private instances (#1210)

* fix

* fix: "Hide Friends in Same Instance" does not work when "Split Favorite Friends" is enabled

* fix Notification.vue message

* fix: deleteFavoriteNoConfirm

* fix: feed status style

* fix: infinite loading when deleting note

* fix: private players will not be hidden when 'Hide Friends in Same Instance', and 'Hide Friends in Same Instance' will not work when 'Split Favorite Friends'
This commit is contained in:
pa
2025-05-14 19:01:15 +09:00
committed by GitHub
parent 5ca028b30a
commit e792ed481b
130 changed files with 14208 additions and 10462 deletions

View File

@@ -49,7 +49,7 @@ const friendReq = {
json,
params
};
window.API.$emit('FRIEND:REQUEST:CANCEL', args);
// window.API.$emit('FRIEND:REQUEST:CANCEL', args);
return args;
});
},

View File

@@ -14,7 +14,7 @@ const imageReq = {
method: 'PUT'
});
window.$app.avatarDialog.loading = false;
window.$app.changeAvatarImageDialogLoading = false;
// window.$app.changeAvatarImageDialogLoading = false;
},
async uploadAvatarImage(params, fileId) {
@@ -28,14 +28,15 @@ const imageReq = {
params,
fileId
};
window.API.$emit('AVATARIMAGE:INIT', args);
// window.API.$emit('AVATARIMAGE:INIT', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadAvatarFailCleanup(fileId);
imageReq.uploadAvatarFailCleanup(fileId);
throw err;
}
return void 0;
// return void 0;
},
async uploadAvatarImageFileStart(params) {
@@ -50,12 +51,12 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:FILESTART', args);
// window.API.$emit('AVATARIMAGE:FILESTART', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadAvatarFailCleanup(params.fileId);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
return void 0;
},
@@ -75,7 +76,7 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:FILEFINISH', args);
// window.API.$emit('AVATARIMAGE:FILEFINISH', args);
return args;
});
},
@@ -92,12 +93,12 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:SIGSTART', args);
// window.API.$emit('AVATARIMAGE:SIGSTART', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadAvatarFailCleanup(params.fileId);
imageReq.uploadAvatarFailCleanup(params.fileId);
}
return void 0;
},
@@ -117,7 +118,7 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:SIGFINISH', args);
// window.API.$emit('AVATARIMAGE:SIGFINISH', args);
return args;
});
},
@@ -131,7 +132,7 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:SET', args);
// window.API.$emit('AVATARIMAGE:SET', args);
window.API.$emit('AVATAR', args);
return args;
});
@@ -152,7 +153,7 @@ const imageReq = {
method: 'PUT'
});
window.$app.worldDialog.loading = false;
window.$app.changeWorldImageDialogLoading = false;
// window.$app.changeWorldImageDialogLoading = false;
},
async uploadWorldImage(params, fileId) {
@@ -166,12 +167,12 @@ const imageReq = {
params,
fileId
};
window.API.$emit('WORLDIMAGE:INIT', args);
// window.API.$emit('WORLDIMAGE:INIT', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadWorldFailCleanup(fileId);
imageReq.uploadWorldFailCleanup(fileId);
}
return void 0;
},
@@ -188,12 +189,12 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:FILESTART', args);
// window.API.$emit('WORLDIMAGE:FILESTART', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadWorldFailCleanup(params.fileId);
imageReq.uploadWorldFailCleanup(params.fileId);
}
return void 0;
},
@@ -213,7 +214,7 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:FILEFINISH', args);
// window.API.$emit('WORLDIMAGE:FILEFINISH', args);
return args;
});
},
@@ -230,12 +231,12 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:SIGSTART', args);
// window.API.$emit('WORLDIMAGE:SIGSTART', args);
return args;
});
} catch (err) {
console.error(err);
window.API.uploadWorldFailCleanup(params.fileId);
imageReq.uploadWorldFailCleanup(params.fileId);
}
return void 0;
},
@@ -255,7 +256,7 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:SIGFINISH', args);
// window.API.$emit('WORLDIMAGE:SIGFINISH', args);
return args;
});
},
@@ -269,7 +270,7 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:SET', args);
// window.API.$emit('WORLDIMAGE:SET', args);
window.API.$emit('WORLD', args);
return args;
});
@@ -283,7 +284,7 @@ const imageReq = {
json,
params
};
window.API.$emit('AVATARIMAGE:GET', args);
// window.API.$emit('AVATARIMAGE:GET', args);
return args;
});
},
@@ -297,7 +298,7 @@ const imageReq = {
json,
params
};
window.API.$emit('WORLDIMAGE:GET', args);
// window.API.$emit('WORLDIMAGE:GET', args);
return args;
});
}

View File

@@ -19,7 +19,7 @@ const miscReq = {
json,
params
};
window.API.$emit('NOTE', args);
// window.API.$emit('NOTE', args);
return args;
});
},
@@ -46,7 +46,7 @@ const miscReq = {
json,
params
};
window.API.$emit('FEEDBACK:REPORT:USER', args);
// window.API.$emit('FEEDBACK:REPORT:USER', args);
return args;
});
},
@@ -81,7 +81,7 @@ const miscReq = {
const args = {
json
};
window.API.$emit('VRCCREDITS', args);
// window.API.$emit('VRCCREDITS', args);
return args;
});
},
@@ -170,10 +170,43 @@ const miscReq = {
json,
params
};
window.API.$emit('BADGE:UPDATE', args);
// window.API.$emit('BADGE:UPDATE', args);
return args;
});
},
getVisits() {
return window.API.call('visits', {
method: 'GET'
}).then((json) => {
const args = {
json
};
// window.API.$emit('VISITS', args);
return args;
});
}
// /**
// * @params {{
// userId: string,
// emojiId: string
// }} params
// * @returns {Promise<{json: any, params}>}
// */
// sendBoop(params) {
// return window.API.call(`users/${params.userId}/boop`, {
// method: 'POST',
// params
// }).then((json) => {
// const args = {
// json,
// params
// };
// this.$emit('BOOP:SEND', args);
// return args;
// });
// }
};
export default miscReq;

View File

@@ -265,7 +265,8 @@ const notificationReq = {
notificationId
}
};
window.API.$emit('NOTIFICATION:V2:HIDE', args);
// useless
// window.API.$emit('NOTIFICATION:V2:HIDE', args);
return args;
});
}

View File

@@ -27,7 +27,7 @@ const playerModerationReq = {
json,
params
};
window.API.$emit('PLAYER-MODERATION:SEND', args);
// window.API.$emit('PLAYER-MODERATION:SEND', args);
return args;
});
},

View File

@@ -126,7 +126,33 @@ const userReq = {
json,
params
};
window.API.$emit('USER:FEEDBACK', args);
// window.API.$emit('USER:FEEDBACK', args);
return args;
});
},
/**
* @typedef {{
* status: 'active' | 'offline' | 'busy' | 'ask me' | 'join me',
* statusDescription: string
* }} SaveCurrentUserParameters
*/
/**
* Updates current user's status.
* @param params {SaveCurrentUserParameters} new status to be set
* @returns {Promise<{json: any, params}>}
*/
saveCurrentUser(params) {
return window.API.call(`users/${window.API.currentUser.id}`, {
method: 'PUT',
params
}).then((json) => {
var args = {
json,
params
};
window.API.$emit('USER:CURRENT:SAVE', args);
return args;
});
}

View File

@@ -56,7 +56,7 @@ const vrcPlusImageReq = {
json,
printId
};
window.API.$emit('PRINT:DELETE', args);
// window.API.$emit('PRINT:DELETE', args);
return args;
});
},

4994
src/app.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
doctype html
#x-app.x-app(@dragenter.prevent @dragover.prevent @drop.prevent)
//- login
include ./mixins/loginPage.pug
+loginPage
LoginPage(v-if="!API.isLoggedIn" v-bind="loginPageBind" v-on="loginPageEvent")
VRCXUpdateDialog(v-bind="vrcxUpdateDialogBind" v-on="vrcxUpdateDialogEvent")
//- menu
.x-menu-container
@@ -23,47 +23,30 @@ doctype html
circle
style='font-size: 14px; height: 50px; width: 50px')
nav-menu(ref='menu' @select='selectMenu' :menu-active-index='menuActiveIndex')
NavMenu(ref='menu' @select='selectMenu' :menu-active-index='menuActiveIndex')
//- ### Tabs ###
template(v-if='API.isLoggedIn')
//- feed
include ./mixins/tabs/feed.pug
+feedTab
FeedTab(v-bind='feedTabBind' v-on='feedTabEvent')
//- gameLog
include ./mixins/tabs/gameLog.pug
+gameLogTab
GameLogTab(v-bind='gameLogTabBind' v-on='gameLogTabEvent')
//- playerList
include ./mixins/tabs/playerList.pug
+playerListTab
PlayerListTab(v-bind='playerListTabBind' v-on='playerListTabEvent')
//- search
include ./mixins/tabs/search.pug
+searchTab
SearchTab(v-bind='searchTabBind' v-on='searchTabEvent')
FavoritesTab(v-bind='favoritesTabBind' v-on='favoritesTabEvent')
//- friendLog
include ./mixins/tabs/friendLog.pug
+friendLogTab
FriendLogTab(v-bind='friendLogTabBind')
//- moderation
ModerationTab(v-bind='moderationTabBind')
//- notification
include ./mixins/tabs/notifications.pug
+notificationsTab
NotificationTab(v-bind='notificationTabBind' v-on='notificationTabEvent')
//- profile
include ./mixins/tabs/profile.pug
+profileTab
ProfileTab(v-bind='profileTabBind' v-on='profileTabEvent')
//- friends list
FriendListTab(v-bind='friendsListTabBind' v-on='friendsListTabEvent')
//- charts
KeepAlive
ChartsTab(v-if='menuActiveIndex === "charts"' v-bind='chartsTabBind' v-on='chartsTabEvent')
@@ -73,89 +56,8 @@ doctype html
SideBar(v-bind='sideBarTabBind' v-on='sideBarTabEvent')
//- ## Dialogs ## -\\
include ./mixins/dialogs/userDialog.pug
+userDialog
include ./mixins/dialogs/images.pug
+images
include ./mixins/dialogs/currentUser.pug
+currentUser
include ./mixins/dialogs/invites.pug
+invites
include ./mixins/dialogs/boops.pug
+boops
//- previous instances
PreviousInstancesInfoDialog(v-bind='previousInstancesInfoDialogBind' v-on='previousInstancesInfoDialogEvent')
PreviousInstancesUserDialog(v-bind='previousInstancesUserDialogBind' v-on='previousInstancesUserDialogEvent')
//- favorites
FriendImportDialog(v-bind='friendImportDialogBind' v-on='friendImportDialogEvent')
WorldImportDialog(v-bind='worldImportDialogBind' v-on='worldImportDialogEvent')
AvatarImportDialog(v-bind='avatarImportDialogBind' v-on='avatarImportDialogEvent')
//- favorites dialog
ChooseFavoriteGroupDialog(v-bind='favoriteDialogBind' v-on='favoriteDialogEvent')
ExportFriendsListDialog(v-bind='exportFriendsListDialogBind' v-on='exportFriendsListDialogEvent')
ExportAvatarsListDialog(v-bind='exportAvatarsListDialogBind' v-on='exportAvatarsListDialogEvent')
//- launch
LaunchDialog(v-bind='launchDialogBind' v-on='launchDialogEvent')
//- world
WorldDialog(v-bind='worldDialogBind' v-on='worldDialogEvent')
//- group
GroupDialog(v-bind='groupDialogBind' v-on='groupDialogEvent')
InviteGroupDialog(v-bind='inviteGroupDialogBind' v-on='inviteGroupDialogEvent')
//- avatar
AvatarDialog(v-bind='avatarDialogBind' v-on='avatarDialogEvent')
//- settings
FeedFiltersDialog(v-bind='feedFiltersDialogBind' v-on='feedFiltersDialogEvent')
LaunchOptionsDialog(:is-launch-options-dialog-visible.sync='isLaunchOptionsDialogVisible')
OpenSourceSoftwareNoticeDialog(:oss-dialog.sync='ossDialog')
ChangelogDialog(:change-log-dialog.sync='changeLogDialog')
VRCXUpdateDialog(v-bind='vrcxUpdateDialogBind' v-on='vrcxUpdateDialogEvent')
ScreenshotMetadataDialog(v-bind='screenshotMetadataDialogBind' v-on='screenshotMetadataDialogEvent')
DiscordNamesDialog(:discord-names-dialog-visible.sync='discordNamesDialogVisible' :friends='friends')
EditInviteMessageDialog(:edit-invite-message-dialog.sync='editInviteMessageDialog')
NoteExportDialog(:is-note-export-dialog-visible.sync='isNoteExportDialogVisible' :friends='friends')
VRChatConfigDialog(v-bind='vrchatConfigDialogBind' v-on='vrchatConfigDialogEvent')
YouTubeApiDialog(v-bind='youTubeApiDialogBind' v-on='youTubeApiDialogEvent')
NotificationPositionDialog(v-bind='notificationPositionDialogBind' v-on='notificationPositionDialogEvent')
AvatarProviderDialog(v-bind='avatarProviderDialogBind' v-on='avatarProviderDialogEvent')
RegistryBackupDialog(
:isRegistryBackupDialogVisible.sync='isRegistryBackupDialogVisible'
:backupVrcRegistry='backupVrcRegistry')
PrimaryPasswordDialog(:enablePrimaryPasswordDialog.sync='enablePrimaryPasswordDialog' @setPrimaryPassword="setPrimaryPassword")
//- player list
ChatboxBlacklistDialog(:chatboxBlacklistDialog="chatboxBlacklistDialog" :chatboxUserBlacklist="chatboxUserBlacklist" @deleteChatboxUserBlacklist="deleteChatboxUserBlacklist")
//- ## Dialogs ## -\\
include ./mixins/dialogs/dialogs.pug
+dialogs
//- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="templateDialog" :visible.sync="templateDialog.visible" :title="$t('dialog.template_dialog.header')" width="450px")

View File

@@ -159,7 +159,7 @@ a {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden auto;
overflow: hidden;
cursor: default;
}

View File

@@ -590,6 +590,10 @@ input[type='number'],
.el-table table tr td:first-child {
border-left: none;
}
.feed .el-table .el-table_1_column_5.el-table__cell > div {
display: flex;
align-items: center;
}
.el-table .el-table__body-wrapper table tr:last-child th,
.el-table table tr:last-child td,
.el-table tr,

View File

@@ -239,13 +239,12 @@ export default class extends baseClass {
API.websocketDomain = API.websocketDomainVrchat;
}
return new Promise((resolve, reject) => {
this.loginForm.loading = true;
if (this.enablePrimaryPassword) {
this.checkPrimaryPassword(loginParmas)
.then((pwd) => {
this.loginForm.loading = true;
return API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
reject(err);
})
.then(() => {
@@ -257,12 +256,10 @@ export default class extends baseClass {
websocket: loginParmas.websocket
})
.catch((err2) => {
this.loginForm.loading = false;
// API.logout();
reject(err2);
})
.then(() => {
this.loginForm.loading = false;
resolve();
});
});
@@ -277,7 +274,6 @@ export default class extends baseClass {
} else {
API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
reject(err);
})
.then(() => {
@@ -288,17 +284,15 @@ export default class extends baseClass {
websocket: loginParmas.websocket
})
.catch((err2) => {
this.loginForm.loading = false;
API.logout();
reject(err2);
})
.then(() => {
this.loginForm.loading = false;
resolve();
});
});
}
});
}).finally(() => (this.loginForm.loading = false));
},
async deleteSavedLogin(userId) {
@@ -325,100 +319,90 @@ export default class extends baseClass {
async login() {
await webApiService.clearCookies();
this.$refs.loginForm.validate((valid) => {
if (valid && !this.loginForm.loading) {
this.loginForm.loading = true;
if (this.loginForm.endpoint) {
API.endpointDomain = this.loginForm.endpoint;
API.websocketDomain = this.loginForm.websocket;
} else {
API.endpointDomain = API.endpointDomainVrchat;
API.websocketDomain = API.websocketDomainVrchat;
}
API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
throw err;
})
.then((args) => {
if (
this.loginForm.saveCredentials &&
this.enablePrimaryPassword
) {
$app.$prompt(
$t('prompt.primary_password.description'),
$t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
let saveCredential =
this.loginForm.savedCredentials[
Object.keys(
this.loginForm
.savedCredentials
)[0]
];
security
.decrypt(
saveCredential.loginParmas
.password,
value
)
.then(() => {
security
.encrypt(
this.loginForm.password,
value
)
.then((pwd) => {
API.login({
username:
this.loginForm
.username,
password:
this.loginForm
.password,
endpoint:
this.loginForm
.endpoint,
websocket:
this.loginForm
.websocket,
saveCredentials:
this.loginForm
.saveCredentials,
cipher: pwd
}).then(() => {
this.$refs.loginForm.resetFields();
});
if (!this.loginForm.loading) {
this.loginForm.loading = true;
if (this.loginForm.endpoint) {
API.endpointDomain = this.loginForm.endpoint;
API.websocketDomain = this.loginForm.websocket;
} else {
API.endpointDomain = API.endpointDomainVrchat;
API.websocketDomain = API.websocketDomainVrchat;
}
API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
throw err;
})
.then((args) => {
if (
this.loginForm.saveCredentials &&
this.enablePrimaryPassword
) {
$app.$prompt(
$t('prompt.primary_password.description'),
$t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
let saveCredential =
this.loginForm.savedCredentials[
Object.keys(
this.loginForm.savedCredentials
)[0]
];
security
.decrypt(
saveCredential.loginParmas.password,
value
)
.then(() => {
security
.encrypt(
this.loginForm.password,
value
)
.then((pwd) => {
API.login({
username:
this.loginForm
.username,
password:
this.loginForm
.password,
endpoint:
this.loginForm
.endpoint,
websocket:
this.loginForm
.websocket,
saveCredentials:
this.loginForm
.saveCredentials,
cipher: pwd
});
});
})
.finally(() => {
this.loginForm.loading = false;
});
return args;
}
API.login({
username: this.loginForm.username,
password: this.loginForm.password,
endpoint: this.loginForm.endpoint,
websocket: this.loginForm.websocket,
saveCredentials: this.loginForm.saveCredentials
})
.then(() => {
this.$refs.loginForm.resetFields();
});
});
})
.finally(() => {
this.loginForm.loading = false;
});
return args;
}
API.login({
username: this.loginForm.username,
password: this.loginForm.password,
endpoint: this.loginForm.endpoint,
websocket: this.loginForm.websocket,
saveCredentials: this.loginForm.saveCredentials
}).finally(() => {
this.loginForm.loading = false;
});
}
});
return args;
});
}
},
logout() {

View File

@@ -1,104 +0,0 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { notificationRequest } from '../api';
export default class extends baseClass {
constructor(_app, _API, _t) {
super(_app, _API, _t);
}
init() {
/**
* @params {{
userId: string,
emojiId: string
}} params
* @returns {Promise<{json: any, params}>}
*/
API.sendBoop = function (params) {
return this.call(`users/${params.userId}/boop`, {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('BOOP:SEND', args);
return args;
});
};
}
_data = {
sendBoopDialog: {
visible: false,
userId: '',
fileId: ''
}
};
_methods = {
sendBoop() {
var D = this.sendBoopDialog;
this.dismissBoop(D.userId);
var params = {
userId: D.userId
};
if (D.fileId) {
params.emojiId = D.fileId;
}
API.sendBoop(params);
D.visible = false;
},
dismissBoop(userId) {
// JANK: This is a hack to remove boop notifications when responding
var array = this.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
var ref = array[i];
if (
ref.type !== 'boop' ||
ref.$isExpired ||
ref.senderUserId !== userId
) {
continue;
}
notificationRequest.sendNotificationResponse({
notificationId: ref.id,
responseType: 'delete',
responseData: ''
});
}
},
showSendBoopDialog(userId) {
this.$nextTick(() =>
$app.adjustDialogZ(this.$refs.sendBoopDialog.$el)
);
var D = this.sendBoopDialog;
D.userId = userId;
D.visible = true;
if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) {
this.refreshEmojiTable();
}
},
getEmojiValue(emojiName) {
if (!emojiName) {
return '';
}
return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`;
},
getEmojiName(emojiValue) {
// uppercase first letter of each word
if (!emojiValue) {
return '';
}
return emojiValue
.replace('vrchat_', '')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase());
}
};
}

View File

@@ -1,4 +1,5 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { isRealInstance, parseLocation } from '../composables/instance/utils';
import { $app, API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -83,8 +84,8 @@ export default class extends baseClass {
args.ref = this.applyCurrentUser(json);
// when isGameRunning use gameLog instead of API
var $location = $app.parseLocation($app.lastLocation.location);
var $travelingLocation = $app.parseLocation(
var $location = parseLocation($app.lastLocation.location);
var $travelingLocation = parseLocation(
$app.lastLocationDestination
);
var location = $app.lastLocation.location;
@@ -94,12 +95,12 @@ export default class extends baseClass {
var travelingToWorld = $travelingLocation.worldId;
var travelingToInstance = $travelingLocation.instanceId;
if (!$app.isGameRunning && json.presence) {
if ($utils.isRealInstance(json.presence.world)) {
if (isRealInstance(json.presence.world)) {
location = `${json.presence.world}:${json.presence.instance}`;
} else {
location = json.presence.world;
}
if ($utils.isRealInstance(json.presence.travelingToWorld)) {
if (isRealInstance(json.presence.travelingToWorld)) {
travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`;
} else {
travelingToLocation = json.presence.travelingToWorld;
@@ -175,7 +176,7 @@ export default class extends baseClass {
}
Object.assign(ref, json);
if (ref.homeLocation !== ref.$homeLocation.tag) {
ref.$homeLocation = $app.parseLocation(ref.homeLocation);
ref.$homeLocation = parseLocation(ref.homeLocation);
// apply home location name to user dialog
if (
$app.userDialog.visible &&
@@ -295,13 +296,12 @@ export default class extends baseClass {
$languages: [],
$locationTag: '',
$travelingToLocation: '',
$vrchatcredits: null,
...json
};
if ($app.isGameRunning) {
ref.$previousAvatarSwapTime = Date.now();
}
ref.$homeLocation = $app.parseLocation(ref.homeLocation);
ref.$homeLocation = parseLocation(ref.homeLocation);
ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref);
@@ -316,32 +316,6 @@ export default class extends baseClass {
}
return ref;
};
/**
* @typedef {{
* status: 'active' | 'offline' | 'busy' | 'ask me' | 'join me',
* statusDescription: string
* }} SaveCurrentUserParameters
*/
/**
* Updates current user's status.
* @param params {SaveCurrentUserParameters} new status to be set
* @returns {Promise<{json: any, params}>}
*/
API.saveCurrentUser = function (params) {
return this.call(`users/${this.currentUser.id}`, {
method: 'PUT',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('USER:CURRENT:SAVE', args);
return args;
});
};
}
_data = {};

View File

@@ -1,6 +1,8 @@
import configRepository from '../service/config.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { worldRequest } from '../api';
import { parseLocation } from '../composables/instance/utils';
import { getLaunchURL } from '../composables/shared/utils';
import configRepository from '../service/config.js';
import { API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -36,7 +38,7 @@ export default class extends baseClass {
var L = this.lastLocation$;
if (currentLocation !== this.lastLocation$.tag) {
Discord.SetTimestamps(timeStamp, 0);
L = $app.parseLocation(currentLocation);
L = parseLocation(currentLocation);
L.worldName = '';
L.thumbnailImageUrl = '';
L.worldCapacity = 0;
@@ -76,7 +78,7 @@ export default class extends baseClass {
}
switch (L.accessType) {
case 'public':
L.joinUrl = $utils.getLaunchURL(L);
L.joinUrl = getLaunchURL(L);
L.accessName = `Public #${L.instanceName} (${platform})`;
break;
case 'invite+':

View File

@@ -1,8 +1,9 @@
import * as workerTimers from 'worker-timers';
import { parseLocation } from '../composables/instance/utils';
import gameLogService from '../service/gamelog.js';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { baseClass, $app, API, $utils } from './baseClass.js';
import { userRequest } from '../api';
import dayjs from 'dayjs';
@@ -80,7 +81,7 @@ export default class extends baseClass {
this.lastLocation.location,
gameLog.dt
);
var worldName = this.replaceBioSymbols(gameLog.worldName);
var worldName = $utils.replaceBioSymbols(gameLog.worldName);
if (this.isGameRunning) {
this.lastLocationReset(gameLog.dt);
this.clearNowPlaying();
@@ -100,7 +101,7 @@ export default class extends baseClass {
this.applyGroupDialogInstances();
}
this.addInstanceJoinHistory(gameLog.location, gameLog.dt);
var L = $utils.parseLocation(gameLog.location);
var L = parseLocation(gameLog.location);
var entry = {
created_at: gameLog.dt,
type: 'Location',
@@ -789,7 +790,7 @@ export default class extends baseClass {
var videoPos = Number(data[1]);
var videoLength = Number(data[2]);
var displayName = data[3];
var videoName = this.replaceBioSymbols(data[4]);
var videoName = $utils.replaceBioSymbols(data[4]);
var videoUrl = videoName;
var videoId = 'LSMedia';
if (videoUrl === this.nowPlaying.url) {
@@ -981,29 +982,6 @@ export default class extends baseClass {
this.addGameLogEntry(gameLog, this.lastLocation.location);
},
deleteGameLogEntryPrompt(row) {
this.$confirm('Continue? Delete Log', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
this.deleteGameLogEntry(row);
}
}
});
},
deleteGameLogEntry(row) {
$app.removeFromArray(this.gameLogTable.data, row);
database.deleteGameLogEntry(row);
console.log(row);
database.getGamelogDatabase().then((data) => {
this.gameLogSessionTable = data;
this.updateSharedFeed(true);
});
},
gameLogSearch(row) {
var value = this.gameLogTable.search.toUpperCase();
if (!value) {

View File

@@ -1,8 +1,14 @@
import * as workerTimers from 'worker-timers';
import { displayLocation, parseLocation } from '../composables/instance/utils';
import { checkVRChatCache } from '../composables/shared/utils';
import configRepository from '../service/config.js';
import database from '../service/database.js';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { baseClass, $app, API, $utils } from './baseClass.js';
import { instanceRequest, userRequest } from '../api';
import {
photonEmojis,
photonEventType
} from '../composables/shared/constants/photon.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -80,111 +86,6 @@ export default class extends baseClass {
}
},
photonEventType: [
'MeshVisibility',
'AnimationFloat',
'AnimationBool',
'AnimationTrigger',
'AudioTrigger',
'PlayAnimation',
'SendMessage',
'SetParticlePlaying',
'TeleportPlayer',
'RunConsoleCommand',
'SetGameObjectActive',
'SetWebPanelURI',
'SetWebPanelVolume',
'SpawnObject',
'SendRPC',
'ActivateCustomTrigger',
'DestroyObject',
'SetLayer',
'SetMaterial',
'AddHealth',
'AddDamage',
'SetComponentActive',
'AnimationInt',
'AnimationIntAdd',
'AnimationIntSubtract',
'AnimationIntMultiply',
'AnimationIntDivide',
'AddVelocity',
'SetVelocity',
'AddAngularVelocity',
'SetAngularVelocity',
'AddForce',
'SetUIText',
'CallUdonMethod'
],
photonEmojis: [
'Angry',
'Blushing',
'Crying',
'Frown',
'Hand Wave',
'Hang Ten',
'In Love',
'Jack O Lantern',
'Kiss',
'Laugh',
'Skull',
'Smile',
'Spooky Ghost',
'Stoic',
'Sunglasses',
'Thinking',
'Thumbs Down',
'Thumbs Up',
'Tongue Out',
'Wow',
'Arrow Point',
"Can't see",
'Hourglass',
'Keyboard',
'No Headphones',
'No Mic',
'Portal',
'Shush',
'Bats',
'Cloud',
'Fire',
'Snow Fall',
'Snowball',
'Splash',
'Web',
'Beer',
'Candy',
'Candy Cane',
'Candy Corn',
'Champagne',
'Drink',
'Gingerbread',
'Ice Cream',
'Pineapple',
'Pizza',
'Tomato',
'Beachball',
'Coal',
'Confetti',
'Gift',
'Gifts',
'Life Ring',
'Mistletoe',
'Money',
'Neon Shades',
'Sun Lotion',
'Boo',
'Broken Heart',
'Exclamation',
'Go',
'Heart',
'Music Note',
'Question',
'Stop',
'Zzz'
],
photonEventTableFilter: '',
photonEventTableTypeFilter: [],
photonEventTableTypeOverlayFilter: [],
@@ -894,7 +795,7 @@ export default class extends baseClass {
var imageUrl = '';
if (type === 0) {
var emojiId = data.Parameters[245][2];
emojiName = this.photonEmojis[emojiId];
emojiName = photonEmojis[emojiId];
} else if (type === 1) {
emojiName = 'Custom';
var fileId = data.Parameters[245][1];
@@ -982,7 +883,7 @@ export default class extends baseClass {
if (this.debugPhotonLogging) {
var displayName = this.getDisplayNameFromPhotonId(senderId);
var feed = `RPC ${displayName} ${
this.photonEventType[eventData.EventType]
photonEventType[eventData.EventType]
}${eventName}`;
console.log('VrcRpc:', feed);
}
@@ -1026,7 +927,7 @@ export default class extends baseClass {
shortName
});
var location = instance.json.location;
var L = $utils.parseLocation(location);
var L = parseLocation(location);
var groupName = '';
if (L.groupId) {
groupName = await this.getGroupName(L.groupId);
@@ -1040,14 +941,14 @@ export default class extends baseClass {
// if (shortName === newShortName) {
// portalType = 'Unlocked';
// }
var displayLocation = this.displayLocation(
var _displayLocation = displayLocation(
location,
worldName,
groupName
);
this.addEntryPhotonEvent({
photonId: this.getPhotonIdFromUserId(userId),
text: `PortalSpawn to ${displayLocation}`,
text: `PortalSpawn to ${_displayLocation}`,
type: 'PortalSpawn',
shortName,
location,
@@ -1210,10 +1111,10 @@ export default class extends baseClass {
type: 'ChangeStatus',
status: photonUser.status,
previousStatus: ref.status,
statusDescription: this.replaceBioSymbols(
statusDescription: $utils.replaceBioSymbols(
photonUser.statusDescription
),
previousStatusDescription: this.replaceBioSymbols(
previousStatusDescription: $utils.replaceBioSymbols(
ref.statusDescription
),
created_at: Date.parse(gameLogDate)
@@ -1227,8 +1128,8 @@ export default class extends baseClass {
return;
}
var avatar = user.avatarDict;
avatar.name = this.replaceBioSymbols(avatar.name);
avatar.description = this.replaceBioSymbols(avatar.description);
avatar.name = $utils.replaceBioSymbols(avatar.name);
avatar.description = $utils.replaceBioSymbols(avatar.description);
var platform = '';
if (user.last_platform === 'android') {
platform = 'Android';
@@ -1240,7 +1141,7 @@ export default class extends baseClass {
platform = 'Desktop';
}
this.photonUserSusieCheck(photonId, user, gameLogDate);
$utils.checkVRChatCache(avatar).then((cacheInfo) => {
checkVRChatCache(avatar).then((cacheInfo) => {
var inCache = false;
if (cacheInfo.Item1 > 0) {
inCache = true;
@@ -1410,9 +1311,11 @@ export default class extends baseClass {
oldAvatarId !== avatar.id &&
photonId !== this.photonLobbyCurrentUser
) {
avatar.name = this.replaceBioSymbols(avatar.name);
avatar.description = this.replaceBioSymbols(avatar.description);
$utils.checkVRChatCache(avatar).then((cacheInfo) => {
avatar.name = $utils.replaceBioSymbols(avatar.name);
avatar.description = $utils.replaceBioSymbols(
avatar.description
);
checkVRChatCache(avatar).then((cacheInfo) => {
var inCache = false;
if (cacheInfo.Item1 > 0) {
inCache = true;

View File

@@ -7,6 +7,7 @@ import {
instanceRequest,
groupRequest
} from '../api';
import $utils from './utils';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -171,8 +172,8 @@ export default class extends baseClass {
var D = $app.groupDialog;
if (D.id === args.params.groupId) {
for (var post of args.posts) {
post.title = $app.replaceBioSymbols(post.title);
post.text = $app.replaceBioSymbols(post.text);
post.title = $utils.replaceBioSymbols(post.title);
post.text = $utils.replaceBioSymbols(post.text);
}
if (args.posts.length > 0) {
D.announcement = args.posts[0];
@@ -189,8 +190,8 @@ export default class extends baseClass {
}
var newPost = args.json;
newPost.title = $app.replaceBioSymbols(newPost.title);
newPost.text = $app.replaceBioSymbols(newPost.text);
newPost.title = $utils.replaceBioSymbols(newPost.title);
newPost.text = $utils.replaceBioSymbols(newPost.text);
var hasPost = false;
// update existing post
for (var post of D.posts) {
@@ -275,9 +276,9 @@ export default class extends baseClass {
API.applyGroup = function (json) {
var ref = this.cachedGroups.get(json.id);
json.rules = $app.replaceBioSymbols(json.rules);
json.name = $app.replaceBioSymbols(json.name);
json.description = $app.replaceBioSymbols(json.description);
json.rules = $utils.replaceBioSymbols(json.rules);
json.name = $utils.replaceBioSymbols(json.name);
json.description = $utils.replaceBioSymbols(json.description);
if (typeof ref === 'undefined') {
ref = {
id: '',
@@ -912,9 +913,6 @@ export default class extends baseClass {
case 'Unsubscribe To Announcements':
this.setGroupSubscription(D.id, false);
break;
case 'Invite To Group':
this.showInviteGroupDialog(D.id, '');
break;
}
},
@@ -1050,17 +1048,6 @@ export default class extends baseClass {
this.userDialog.representedGroup = args.json;
return args;
});
},
showInviteGroupDialog(groupId, userId) {
const D = this.inviteGroupDialog;
D.userIds = '';
D.groups = [];
D.groupId = groupId;
D.groupName = groupId;
D.userId = userId;
D.userObject = {};
D.visible = true;
}
};
}

View File

@@ -24,85 +24,9 @@ export default class extends baseClass {
}
$app.languageDialog.languages = data;
});
API.$on('LOGOUT', function () {
$app.languageDialog.visible = false;
});
}
_data = {
// vrchat to famfamfam language mappings
languageMappings: {
eng: 'us',
kor: 'kr',
rus: 'ru',
spa: 'es',
por: 'pt',
zho: 'cn',
deu: 'de',
jpn: 'jp',
fra: 'fr',
swe: 'se',
nld: 'nl',
pol: 'pl',
dan: 'dk',
nor: 'no',
ita: 'it',
tha: 'th',
fin: 'fi',
hun: 'hu',
ces: 'cz',
tur: 'tr',
ara: 'ae',
ron: 'ro',
vie: 'vn',
ukr: 'ua',
ase: 'us',
bfi: 'gb',
dse: 'nl',
fsl: 'fr',
jsl: 'jp',
kvk: 'kr',
mlt: 'mt',
ind: 'id',
hrv: 'hr',
heb: 'he',
afr: 'af',
ben: 'be',
bul: 'bg',
cmn: 'cn',
cym: 'cy',
ell: 'el',
est: 'et',
fil: 'ph',
gla: 'gd',
gle: 'ga',
hin: 'hi',
hmn: 'cn',
hye: 'hy',
isl: 'is',
lav: 'lv',
lit: 'lt',
ltz: 'lb',
mar: 'hi',
mkd: 'mk',
msa: 'my',
sco: 'gd',
slk: 'sk',
slv: 'sl',
tel: 'hi',
mri: 'nz',
wuu: 'cn',
yue: 'cn',
tws: 'cn',
asf: 'au',
nzs: 'nz',
gsg: 'de',
epo: 'eo',
tok: 'tok'
},
subsetOfLanguages: [],
languageDialog: {
@@ -113,54 +37,5 @@ export default class extends baseClass {
}
};
_methods = {
languageClass(language) {
var style = {};
var mapping = this.languageMappings[language];
if (typeof mapping !== 'undefined') {
style[mapping] = true;
} else {
style.unknown = true;
}
return style;
},
addUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = this.languageDialog;
D.loading = true;
userRequest
.addUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
},
removeUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = this.languageDialog;
D.loading = true;
userRequest
.removeUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
},
showLanguageDialog() {
this.$nextTick(() =>
$app.adjustDialogZ(this.$refs.languageDialog.$el)
);
var D = this.languageDialog;
D.visible = true;
}
};
_methods = {};
}

View File

@@ -27,11 +27,6 @@ export default class extends baseClass {
}
},
onUserMemoChange() {
var D = this.userDialog;
this.saveUserMemo(D.id, D.memo);
},
async getUserMemo(userId) {
try {
return await database.getUserMemo(userId);

View File

@@ -132,139 +132,6 @@ export default class extends baseClass {
);
},
promptUserIdDialog() {
this.$prompt(
$t('prompt.direct_access_user_id.description'),
$t('prompt.direct_access_user_id.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.direct_access_user_id.ok'),
cancelButtonText: $t('prompt.direct_access_user_id.cancel'),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.direct_access_user_id.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
var testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
var userId = this.parseUserUrl(
instance.inputValue
);
if (userId) {
this.showUserDialog(userId);
} else {
this.$message({
message: $t(
'prompt.direct_access_user_id.message.error'
),
type: 'error'
});
}
} else {
this.showUserDialog(instance.inputValue);
}
}
}
}
);
},
promptUsernameDialog() {
this.$prompt(
$t('prompt.direct_access_username.description'),
$t('prompt.direct_access_username.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.direct_access_username.ok'),
cancelButtonText: $t(
'prompt.direct_access_username.cancel'
),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.direct_access_username.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
this.lookupUser({
displayName: instance.inputValue
});
}
}
}
);
},
promptWorldDialog() {
this.$prompt(
$t('prompt.direct_access_world_id.description'),
$t('prompt.direct_access_world_id.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.direct_access_world_id.ok'),
cancelButtonText: $t(
'prompt.direct_access_world_id.cancel'
),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.direct_access_world_id.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
if (!this.directAccessWorld(instance.inputValue)) {
this.$message({
message: $t(
'prompt.direct_access_world_id.message.error'
),
type: 'error'
});
}
}
}
}
);
},
promptAvatarDialog() {
this.$prompt(
$t('prompt.direct_access_avatar_id.description'),
$t('prompt.direct_access_avatar_id.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.direct_access_avatar_id.ok'),
cancelButtonText: $t(
'prompt.direct_access_avatar_id.cancel'
),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.direct_access_avatar_id.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
var testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
var avatarId = this.parseAvatarUrl(
instance.inputValue
);
if (avatarId) {
this.showAvatarDialog(avatarId);
} else {
this.$message({
message: $t(
'prompt.direct_access_avatar_id.message.error'
),
type: 'error'
});
}
} else {
this.showAvatarDialog(instance.inputValue);
}
}
}
}
);
},
promptOmniDirectDialog() {
this.$prompt(
$t('prompt.direct_access_omni.description'),

View File

@@ -1,8 +1,9 @@
import Vue from 'vue';
import VueMarkdown from 'vue-markdown';
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { instanceRequest, userRequest } from '../api';
import utils from './utils';
import { hasGroupPermission } from '../composables/group/utils';
import { parseLocation } from '../composables/instance/utils';
import { $app, $t, API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -60,7 +61,7 @@ export default class extends baseClass {
this.selfInvite(this.location, this.shortname);
},
selfInvite(location, shortName) {
const L = utils.parseLocation(location);
const L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
@@ -165,7 +166,7 @@ export default class extends baseClass {
if (!this.location) {
return;
}
var L = $utils.parseLocation(this.location);
var L = parseLocation(this.location);
if (!L.groupId) {
return;
}
@@ -320,7 +321,7 @@ export default class extends baseClass {
// check group perms
var groupId = this.instance.ownerId;
var group = API.cachedGroups.get(groupId);
this.canCloseInstance = utils.hasGroupPermission(
this.canCloseInstance = hasGroupPermission(
group,
'group-instance-moderate'
);

View File

@@ -1,8 +1,5 @@
import Noty from 'noty';
let echarts = null;
// messy here, organize later
const _utils = {
removeFromArray(array, item) {
var { length } = array;
@@ -14,7 +11,6 @@ const _utils = {
}
return false;
},
arraysMatch(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) {
return false;
@@ -27,12 +23,10 @@ const _utils = {
)
);
},
escapeTag(tag) {
var s = String(tag);
return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
},
escapeTagRecursive(obj) {
if (typeof obj === 'string') {
return this.escapeTag(obj);
@@ -44,7 +38,6 @@ const _utils = {
}
return obj;
},
timeToText(sec, isNeedSeconds = false) {
let n = Number(sec);
if (isNaN(n)) {
@@ -72,7 +65,6 @@ const _utils = {
}
return arr.join(' ');
},
textToHex(text) {
var s = String(text);
return s
@@ -80,7 +72,6 @@ const _utils = {
.map((c) => c.charCodeAt(0).toString(16))
.join(' ');
},
commaNumber(num) {
if (!num) {
return '0';
@@ -88,182 +79,6 @@ const _utils = {
var s = String(Number(num));
return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
},
isRealInstance(instanceId) {
if (!instanceId) {
return false;
}
switch (instanceId) {
case ':':
case 'offline':
case 'offline:offline':
case 'private':
case 'private:private':
case 'traveling':
case 'traveling:traveling':
case instanceId.startsWith('local'):
return false;
}
return true;
},
parseLocation(tag) {
var _tag = String(tag || '');
var ctx = {
tag: _tag,
isOffline: false,
isPrivate: false,
isTraveling: false,
isRealInstance: false,
worldId: '',
instanceId: '',
instanceName: '',
accessType: '',
accessTypeName: '',
region: '',
shortName: '',
userId: null,
hiddenId: null,
privateId: null,
friendsId: null,
groupId: null,
groupAccessType: null,
canRequestInvite: false,
strict: false,
ageGate: false
};
if (_tag === 'offline' || _tag === 'offline:offline') {
ctx.isOffline = true;
} else if (_tag === 'private' || _tag === 'private:private') {
ctx.isPrivate = true;
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
ctx.isTraveling = true;
} else if (!_tag.startsWith('local')) {
ctx.isRealInstance = true;
var sep = _tag.indexOf(':');
// technically not part of instance id, but might be there when coping id from url so why not support it
var shortNameQualifier = '&shortName=';
var shortNameIndex = _tag.indexOf(shortNameQualifier);
if (shortNameIndex >= 0) {
ctx.shortName = _tag.substr(
shortNameIndex + shortNameQualifier.length
);
_tag = _tag.substr(0, shortNameIndex);
}
if (sep >= 0) {
ctx.worldId = _tag.substr(0, sep);
ctx.instanceId = _tag.substr(sep + 1);
ctx.instanceId.split('~').forEach((s, i) => {
if (i) {
var A = s.indexOf('(');
var Z = A >= 0 ? s.lastIndexOf(')') : -1;
var key = Z >= 0 ? s.substr(0, A) : s;
var value = A < Z ? s.substr(A + 1, Z - A - 1) : '';
if (key === 'hidden') {
ctx.hiddenId = value;
} else if (key === 'private') {
ctx.privateId = value;
} else if (key === 'friends') {
ctx.friendsId = value;
} else if (key === 'canRequestInvite') {
ctx.canRequestInvite = true;
} else if (key === 'region') {
ctx.region = value;
} else if (key === 'group') {
ctx.groupId = value;
} else if (key === 'groupAccessType') {
ctx.groupAccessType = value;
} else if (key === 'strict') {
ctx.strict = true;
} else if (key === 'ageGate') {
ctx.ageGate = true;
}
} else {
ctx.instanceName = s;
}
});
ctx.accessType = 'public';
if (ctx.privateId !== null) {
if (ctx.canRequestInvite) {
// InvitePlus
ctx.accessType = 'invite+';
} else {
// InviteOnly
ctx.accessType = 'invite';
}
ctx.userId = ctx.privateId;
} else if (ctx.friendsId !== null) {
// FriendsOnly
ctx.accessType = 'friends';
ctx.userId = ctx.friendsId;
} else if (ctx.hiddenId !== null) {
// FriendsOfGuests
ctx.accessType = 'friends+';
ctx.userId = ctx.hiddenId;
} else if (ctx.groupId !== null) {
// Group
ctx.accessType = 'group';
}
ctx.accessTypeName = ctx.accessType;
if (ctx.groupAccessType !== null) {
if (ctx.groupAccessType === 'public') {
ctx.accessTypeName = 'groupPublic';
} else if (ctx.groupAccessType === 'plus') {
ctx.accessTypeName = 'groupPlus';
}
}
} else {
ctx.worldId = _tag;
}
}
return ctx;
},
displayLocation(location, worldName, groupName) {
var text = worldName;
var L = this.parseLocation(location);
if (L.isOffline) {
text = 'Offline';
} else if (L.isPrivate) {
text = 'Private';
} else if (L.isTraveling) {
text = 'Traveling';
} else if (L.worldId) {
if (groupName) {
text = `${worldName} ${L.accessTypeName}(${groupName})`;
} else if (L.instanceId) {
text = `${worldName} ${L.accessTypeName}`;
}
}
return text;
},
extractFileId(s) {
var match = String(s).match(/file_[0-9A-Za-z-]+/);
return match ? match[0] : '';
},
extractFileVersion(s) {
var match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s);
return match ? match[1] : '';
},
extractVariantVersion(url) {
if (!url) {
return '0';
}
try {
const params = new URLSearchParams(new URL(url).search);
const version = params.get('v');
if (version) {
return version;
}
return '0';
} catch {
return '0';
}
},
buildTreeData(json) {
var node = [];
for (var key in json) {
@@ -325,8 +140,6 @@ const _utils = {
});
return node;
},
// app.js 4900ln
// descending
compareByCreatedAt(a, b) {
if (
@@ -406,298 +219,69 @@ const _utils = {
}
return false;
},
convertFileUrlToImageUrl(url, resolution = 128) {
if (!url) {
compareByName(a, b) {
if (typeof a.name !== 'string' || typeof b.name !== 'string') {
return 0;
}
return a.name.localeCompare(b.name);
},
replaceBioSymbols(text) {
if (!text) {
return '';
}
/**
* possible patterns?
* /file/file_fileId/version
* /file/file_fileId/version/
* /file/file_fileId/version/file
* /file/file_fileId/version/file/
*/
const pattern = /file\/file_([a-f0-9-]+)\/(\d+)(\/file)?\/?$/;
const match = url.match(pattern);
if (match) {
const fileId = match[1];
const version = match[2];
return `https://api.vrchat.cloud/api/1/image/file_${fileId}/${version}/${resolution}`;
var symbolList = {
'@': '',
'#': '',
$: '',
'%': '',
'&': '',
'=': '',
'+': '',
'/': '',
'\\': '',
';': ';',
':': '˸',
',': '',
'?': '',
'!': 'ǃ',
'"': '',
'<': '≺',
'>': '≻',
'.': '',
'^': '',
'{': '',
'}': '',
'[': '',
']': '',
'(': '',
')': '',
'|': '',
'*': ''
};
var newText = text;
for (var key in symbolList) {
var regex = new RegExp(symbolList[key], 'g');
newText = newText.replace(regex, key);
}
// no match return origin url
return url;
return newText.replace(/ {1,}/g, ' ').trimRight();
},
replaceVrcPackageUrl(url) {
if (!url) {
return '';
}
return url.replace('https://api.vrchat.cloud/', 'https://vrchat.com/');
},
getLaunchURL(instance) {
var L = instance;
if (L.instanceId) {
if (L.shortName) {
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(
L.instanceId
)}&shortName=${encodeURIComponent(L.shortName)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(L.instanceId)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}`;
},
getFaviconUrl(resource) {
try {
const url = new URL(resource);
return `https://icons.duckduckgo.com/ip2/${url.host}.ico`;
} catch (err) {
return '';
}
},
copyToClipboard(text) {
navigator.clipboard
.writeText(text)
.then(() => {
window.$app.$message({
message: 'Copied successfully!',
type: 'success'
});
})
.catch((err) => {
console.error('Copy failed:', err);
this.$message.error('Copy failed!');
});
},
hasGroupPermission(ref, permission) {
// descending
compareByUpdatedAt(a, b) {
if (
ref &&
ref.myMember &&
ref.myMember.permissions &&
(ref.myMember.permissions.includes('*') ||
ref.myMember.permissions.includes(permission))
typeof a.updated_at !== 'string' ||
typeof b.updated_at !== 'string'
) {
return true;
return 0;
}
return false;
},
compareUnityVersion(unitySortNumber) {
if (!window.API.cachedConfig.sdkUnityVersion) {
console.error('No cachedConfig.sdkUnityVersion');
return false;
var A = a.updated_at.toUpperCase();
var B = b.updated_at.toUpperCase();
if (A < B) {
return 1;
}
// 2022.3.6f1 2022 03 06 000
// 2019.4.31f1 2019 04 31 000
// 5.3.4p1 5 03 04 010
// 2019.4.31f1c1 is a thing
var array = API.cachedConfig.sdkUnityVersion.split('.');
if (array.length < 3) {
console.error('Invalid cachedConfig.sdkUnityVersion');
return false;
if (A > B) {
return -1;
}
var currentUnityVersion = array[0];
currentUnityVersion += array[1].padStart(2, '0');
var indexFirstLetter = array[2].search(/[a-zA-Z]/);
if (indexFirstLetter > -1) {
currentUnityVersion += array[2]
.substr(0, indexFirstLetter)
.padStart(2, '0');
currentUnityVersion += '0';
var letter = array[2].substr(indexFirstLetter, 1);
if (letter === 'p') {
currentUnityVersion += '1';
} else {
// f
currentUnityVersion += '0';
}
currentUnityVersion += '0';
} else {
// just in case
currentUnityVersion += '000';
}
// just in case
currentUnityVersion = currentUnityVersion.replace(/\D/g, '');
if (
parseInt(unitySortNumber, 10) <= parseInt(currentUnityVersion, 10)
) {
return true;
}
return false;
},
async checkVRChatCache(ref) {
if (!ref.unityPackages) {
return { Item1: -1, Item2: false, Item3: '' };
}
var assetUrl = '';
var variant = '';
for (var i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (unityPackage.variant && unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
_utils.compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
if (unityPackage.variant !== 'standard') {
variant = unityPackage.variant;
}
break;
}
}
if (!assetUrl) {
assetUrl = ref.assetUrl;
}
var id = _utils.extractFileId(assetUrl);
var version = parseInt(_utils.extractFileVersion(assetUrl), 10);
var variantVersion = parseInt(
_utils.extractVariantVersion(assetUrl),
10
);
if (!id || !version) {
return { Item1: -1, Item2: false, Item3: '' };
}
return AssetBundleManager.CheckVRChatCache(
id,
version,
variant,
variantVersion
);
},
async deleteVRChatCache(ref) {
var assetUrl = '';
var variant = '';
for (var i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
$utils.compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
if (unityPackage.variant !== 'standard') {
variant = unityPackage.variant;
}
break;
}
}
var id = $utils.extractFileId(assetUrl);
var version = parseInt($utils.extractFileVersion(assetUrl), 10);
var variantVersion = parseInt(
$utils.extractVariantVersion(assetUrl),
10
);
await AssetBundleManager.DeleteCache(
id,
version,
variant,
variantVersion
);
},
downloadAndSaveJson(fileName, data) {
if (!fileName || !data) {
return;
}
try {
var link = document.createElement('a');
link.setAttribute(
'href',
`data:application/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(data, null, 2)
)}`
);
link.setAttribute('download', `${fileName}.json`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
new Noty({
type: 'error',
text: $app.escapeTag('Failed to download JSON.')
}).show();
}
},
getAvailablePlatforms(unityPackages) {
var isPC = false;
var isQuest = false;
var isIos = false;
if (typeof unityPackages === 'object') {
for (var unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
isPC = true;
} else if (unityPackage.platform === 'android') {
isQuest = true;
} else if (unityPackage.platform === 'ios') {
isIos = true;
}
}
}
return { isPC, isQuest, isIos };
},
getPlatformInfo(unityPackages) {
var pc = {};
var android = {};
var ios = {};
if (typeof unityPackages === 'object') {
for (var unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
if (
unityPackage.performanceRating === 'None' &&
pc.performanceRating
) {
continue;
}
pc = unityPackage;
} else if (unityPackage.platform === 'android') {
if (
unityPackage.performanceRating === 'None' &&
android.performanceRating
) {
continue;
}
android = unityPackage;
} else if (unityPackage.platform === 'ios') {
if (
unityPackage.performanceRating === 'None' &&
ios.performanceRating
) {
continue;
}
ios = unityPackage;
}
}
}
return { pc, android, ios };
return 0;
}
};

View File

@@ -1,5 +1,7 @@
import { baseClass, $app, API, $t, $utils } from './baseClass.js';
import { userRequest } from '../api';
import { displayLocation } from '../composables/instance/utils';
import { extractFileId, extractFileVersion } from '../composables/shared/utils';
import { $app, API, baseClass } from './baseClass.js';
export default class extends baseClass {
constructor(_app, _API, _t) {
@@ -356,8 +358,8 @@ export default class extends baseClass {
async notySaveImage(noty) {
var imageUrl = await this.notyGetImage(noty);
var fileId = this.extractFileId(imageUrl);
var fileVersion = this.extractFileVersion(imageUrl);
var fileId = extractFileId(imageUrl);
var fileVersion = extractFileVersion(imageUrl);
var imageLocation = '';
try {
if (fileId && fileVersion) {
@@ -414,7 +416,7 @@ export default class extends baseClass {
break;
case 'GPS':
this.speak(
`${displayName} is in ${this.displayLocation(
`${displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -424,7 +426,7 @@ export default class extends baseClass {
case 'Online':
var locationName = '';
if (noty.worldName) {
locationName = ` to ${this.displayLocation(
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -442,7 +444,7 @@ export default class extends baseClass {
break;
case 'invite':
this.speak(
`${displayName} has invited you to ${this.displayLocation(
`${displayName} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName,
noty.groupName
@@ -513,7 +515,7 @@ export default class extends baseClass {
case 'PortalSpawn':
if (displayName) {
this.speak(
`${displayName} has spawned a portal to ${this.displayLocation(
`${displayName} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
@@ -599,7 +601,7 @@ export default class extends baseClass {
case 'GPS':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} is in ${this.displayLocation(
`${noty.displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -611,7 +613,7 @@ export default class extends baseClass {
case 'Online':
var locationName = '';
if (noty.worldName) {
locationName = ` to ${this.displayLocation(
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -645,7 +647,7 @@ export default class extends baseClass {
'VRCX',
`${
noty.senderUsername
} has invited you to ${this.displayLocation(
} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
@@ -755,7 +757,7 @@ export default class extends baseClass {
'VRCX',
`${
noty.displayName
} has spawned a portal to ${this.displayLocation(
} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
@@ -915,7 +917,7 @@ export default class extends baseClass {
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} is in ${this.displayLocation(
`${noty.displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -927,7 +929,7 @@ export default class extends baseClass {
case 'Online':
var locationName = '';
if (noty.worldName) {
locationName = ` to ${this.displayLocation(
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -969,7 +971,7 @@ export default class extends baseClass {
'VRCX',
`${
noty.senderUsername
} has invited you to ${this.displayLocation(
} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
@@ -1155,7 +1157,7 @@ export default class extends baseClass {
'VRCX',
`${
noty.displayName
} has spawned a portal to ${this.displayLocation(
} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
@@ -1345,7 +1347,7 @@ export default class extends baseClass {
case 'GPS':
this.desktopNotification(
noty.displayName,
`is in ${this.displayLocation(
`is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -1356,7 +1358,7 @@ export default class extends baseClass {
case 'Online':
var locationName = '';
if (noty.worldName) {
locationName = ` to ${this.displayLocation(
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
@@ -1385,7 +1387,7 @@ export default class extends baseClass {
case 'invite':
this.desktopNotification(
noty.senderUsername,
`has invited you to ${this.displayLocation(
`has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
@@ -1515,7 +1517,7 @@ export default class extends baseClass {
if (noty.displayName) {
this.desktopNotification(
noty.displayName,
`has spawned a portal to ${this.displayLocation(
`has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName

View File

@@ -1,5 +1,6 @@
import * as workerTimers from 'worker-timers';
import Noty from 'noty';
import { parseLocation } from '../composables/instance/utils';
import { baseClass, $app, API, $utils } from './baseClass.js';
import { groupRequest } from '../api';
@@ -265,8 +266,8 @@ export default class extends baseClass {
case 'friend-online':
// Where is instanceId, travelingToWorld, travelingToInstance?
// More JANK, what a mess
var $location = $utils.parseLocation(content.location);
var $travelingToLocation = $utils.parseLocation(
var $location = parseLocation(content.location);
var $travelingToLocation = parseLocation(
content.travelingToLocation
);
if (content?.user?.id) {
@@ -367,8 +368,8 @@ export default class extends baseClass {
break;
case 'friend-location':
var $location = $utils.parseLocation(content.location);
var $travelingToLocation = $utils.parseLocation(
var $location = parseLocation(content.location);
var $travelingToLocation = parseLocation(
content.travelingToLocation
);
if (!content?.user?.id) {

View File

@@ -16,7 +16,7 @@
</template>
<script>
import utils from '../classes/utils';
import { parseLocation } from '../composables/instance/utils';
export default {
// eslint-disable-next-line vue/multi-word-component-names
@@ -74,7 +74,7 @@
instanceId = this.traveling;
this.isTraveling = true;
}
const L = utils.parseLocation(instanceId);
const L = parseLocation(instanceId);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
@@ -150,7 +150,7 @@
if (!location || !this.link) {
return;
}
const L = utils.parseLocation(location);
const L = parseLocation(location);
if (!L.groupId) {
return;
}

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="avatarDialogRef"
class="x-dialog x-avatar-dialog"
:before-close="beforeDialogClose"
:visible.sync="avatarDialog.visible"
:show-close="false"
width="600px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="600px">
<div v-loading="avatarDialog.loading">
<div style="display: flex">
<el-popover placement="right" width="500px" trigger="click">
@@ -506,26 +503,40 @@
</div>
<SetAvatarTagsDialog :set-avatar-tags-dialog="setAvatarTagsDialog" />
<SetAvatarStylesDialog :set-avatar-styles-dialog="setAvatarStylesDialog" />
</el-dialog>
<ChangeAvatarImageDialog
:change-avatar-image-dialog-visible.sync="changeAvatarImageDialogVisible"
:previous-images-table="previousImagesTable"
:avatar-dialog="avatarDialog"
:previous-images-file-id="previousImagesFileId"
@refresh="displayPreviousImages" />
<PreviousImagesDialog
:previous-images-dialog-visible.sync="previousImagesDialogVisible"
:previous-images-table="previousImagesTable" />
</safe-dialog>
</template>
<script setup>
import { inject, computed, getCurrentInstance, reactive, nextTick, watch, ref } from 'vue';
import utils from '../../../classes/utils';
import database from '../../../service/database';
import { avatarModerationRequest, avatarRequest, favoriteRequest, miscRequest } from '../../../api';
import { computed, getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import SetAvatarTagsDialog from './SetAvatarTagsDialog.vue';
import { avatarModerationRequest, avatarRequest, favoriteRequest, imageRequest, miscRequest } from '../../../api';
import utils from '../../../classes/utils';
import { compareUnityVersion, storeAvatarImage } from '../../../composables/avatar/utils';
import {
copyToClipboard,
downloadAndSaveJson,
extractFileId,
extractFileVersion,
replaceVrcPackageUrl
} from '../../../composables/shared/utils';
import database from '../../../service/database';
import PreviousImagesDialog from '../PreviousImagesDialog.vue';
import ChangeAvatarImageDialog from './ChangeAvatarImageDialog.vue';
import SetAvatarStylesDialog from './SetAvatarStylesDialog.vue';
import SetAvatarTagsDialog from './SetAvatarTagsDialog.vue';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const showUserDialog = inject('showUserDialog');
const displayPreviousImages = inject('displayPreviousImages');
const showAvatarDialog = inject('showAvatarDialog');
const showFavoriteDialog = inject('showFavoriteDialog');
const openExternalLink = inject('openExternalLink');
@@ -533,11 +544,9 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const $prompt = instance.proxy.$prompt;
const { $message, $confirm, $prompt } = instance.proxy;
const emit = defineEmits(['openFolderGeneric', 'deleteVRChatCache']);
const emit = defineEmits(['openFolderGeneric', 'deleteVRChatCache', 'openPreviousImagesDialog']);
const props = defineProps({
avatarDialog: {
@@ -555,6 +564,10 @@
});
const avatarDialogRef = ref(null);
const changeAvatarImageDialogVisible = ref(false);
const previousImagesFileId = ref('');
const previousImagesDialogVisible = ref(false);
const previousImagesTable = ref([]);
const treeData = ref([]);
const timeSpent = ref(0);
@@ -671,16 +684,16 @@
showAvatarDialog(D.id);
break;
case 'Share':
utils.copyToClipboard(D.id);
copyToClipboard(D.id);
break;
case 'Rename':
promptRenameAvatar(D);
break;
case 'Change Image':
displayPreviousImages('Avatar', 'Change');
displayPreviousImages('Change');
break;
case 'Previous Images':
displayPreviousImages('Avatar', 'Display');
displayPreviousImages('Display');
break;
case 'Change Description':
promptChangeAvatarDescription(D);
@@ -692,7 +705,7 @@
showSetAvatarStylesDialog(D.id);
break;
case 'Download Unity Package':
openExternalLink(utils.replaceVrcPackageUrl(props.avatarDialog.ref.unityPackageUrl));
openExternalLink(replaceVrcPackageUrl(props.avatarDialog.ref.unityPackageUrl));
break;
case 'Add Favorite':
showFavoriteDialog('avatar', D.id);
@@ -858,6 +871,54 @@
}
}
function displayPreviousImages(command) {
previousImagesTable.value = [];
previousImagesFileId.value = '';
const { imageUrl } = props.avatarDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
return;
}
const params = {
fileId
};
if (command === 'Display') {
previousImagesDialogVisible.value = true;
}
if (command === 'Change') {
changeAvatarImageDialogVisible.value = true;
}
imageRequest.getAvatarImages(params).then((args) => {
storeAvatarImage(args);
previousImagesFileId.value = args.json.id;
const images = [];
args.json.versions.forEach((item) => {
if (!item.deleted) {
images.unshift(item);
}
});
checkPreviousImageAvailable(images);
});
}
async function checkPreviousImageAvailable(images) {
previousImagesTable.value = [];
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.log(error);
});
if (response.status === 200) {
previousImagesTable.value.push(image);
}
}
}
}
function selectAvatar(id) {
avatarRequest
.selectAvatar({
@@ -937,11 +998,11 @@
}
function copyAvatarId(id) {
utils.copyToClipboard(id);
copyToClipboard(id);
}
function copyAvatarUrl(id) {
utils.copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
}
function timeToText(time) {
@@ -963,10 +1024,7 @@
if (unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
utils.compareUnityVersion(unityPackage.unitySortNumber)
) {
if (unityPackage.platform === 'standalonewindows' && compareUnityVersion(unityPackage.unitySortNumber)) {
assetUrl = unityPackage.assetUrl;
break;
}
@@ -979,7 +1037,7 @@
}
if (
unityPackage.platform === 'standalonewindows' &&
utils.compareUnityVersion(unityPackage.unitySortNumber)
compareUnityVersion(unityPackage.unitySortNumber)
) {
variant = 'standard';
assetUrl = unityPackage.assetUrl;
@@ -990,8 +1048,8 @@
if (!assetUrl) {
assetUrl = D.ref.assetUrl;
}
const fileId = utils.extractFileId(assetUrl);
const version = parseInt(utils.extractFileVersion(assetUrl), 10);
const fileId = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
if (!fileId || !version) {
$message({
message: 'File Analysis unavailable',
@@ -1100,8 +1158,4 @@
D.loading = false;
});
}
function downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
}
</script>

View File

@@ -0,0 +1,393 @@
<template>
<safe-dialog
class="x-dialog"
:visible="changeAvatarImageDialogVisible"
:title="t('dialog.change_content_image.avatar')"
width="850px"
append-to-body
@close="closeDialog">
<div v-loading="changeAvatarImageDialogLoading">
<input
id="AvatarImageUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeAvatarImage" />
<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">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div
v-for="image in previousImagesTable"
v-if="image.file"
:key="image.version"
style="display: inline-block">
<div
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>
</div>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { extractFileId } from '../../../composables/shared/utils';
import webApiService from '../../../service/webapi';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
avatarDialog: {
type: Object,
default: () => ({})
},
previousImagesFileId: {
type: String,
default: ''
}
});
const changeAvatarImageDialogLoading = ref(false);
const avatarImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'refresh']);
function refresh() {
emit('refresh', 'Change');
}
function closeDialog() {
emit('update:changeAvatarImageDialogVisible', false);
}
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 onFileChangeAvatarImage(e) {
const clearFile = function () {
if (document.querySelector('#AvatarImageUploadButton')) {
document.querySelector('#AvatarImageUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.avatarDialog.visible || props.avatarDialog.loading) {
clearFile();
return;
}
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
message: t('message.file.too_large'),
type: 'error'
});
clearFile();
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
const r = new FileReader();
r.onload = async function (file) {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total, 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const avatarId = props.avatarDialog.id;
const { imageUrl } = props.avatarDialog.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);
} catch (error) {
console.error('Avatar image upload process failed:', error);
} finally {
changeAvatarImageDialogLoading.value = false;
clearFile();
}
};
changeAvatarImageDialogLoading.value = true;
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) {
// API.$on('AVATARIMAGE:INIT')
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) {
// API.$on('AVATARIMAGE:FILESTART')
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
}
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
API.$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
params
};
return avatarImageFileAWS(args);
}
async function avatarImageFileAWS(args) {
// API.$on('AVATARIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileFinish(params);
return avatarImageFileFinish(res);
}
async function avatarImageFileFinish(args) {
// API.$on('AVATARIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigStart(params);
return avatarImageSigStart(res);
}
async function avatarImageSigStart(args) {
// API.$on('AVATARIMAGE:SIGSTART')
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;
API.$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
params
};
return avatarImageSigAWS(args);
}
async function avatarImageSigAWS(args) {
// API.$on('AVATARIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigFinish(params);
return avatarImageSigFinish(res);
}
async function avatarImageSigFinish(args) {
// API.$on('AVATARIMAGE:SIGFINISH')
const { fileId, fileVersion } = args.params;
const parmas = {
id: avatarImage.value.avatarId,
imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setAvatarImage(parmas);
return avatarImageSet(res);
}
async function avatarImageSet(args) {
// API.$on('AVATARIMAGE:SET')
changeAvatarImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.avatar.image_changed'),
type: 'success'
});
refresh();
} else {
API.$throw(0, 'Avatar image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function uploadAvatarImage() {
document.getElementById('AvatarImageUploadButton').click();
}
function setAvatarImage(image) {
changeAvatarImageDialogLoading.value = true;
const parmas = {
id: props.avatarDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest.setAvatarImage(parmas).finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
return (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
props.avatarDialog.ref.imageUrl
);
}
// $app.methods.deleteAvatarImage = function () {
// this.changeAvatarImageDialogLoading = true;
// var parmas = {
// fileId: this.previousImagesFileId,
// version: this.previousImagesTable[0].version
// };
// vrcPlusIconRequest
// .deleteFileVersion(parmas)
// .then((args) => {
// this.previousImagesFileId = args.json.id;
// var images = [];
// args.json.versions.forEach((item) => {
// if (!item.deleted) {
// images.unshift(item);
// }
// });
// this.checkPreviousImageAvailable(images);
// })
// .finally(() => {
// this.changeAvatarImageDialogLoading = false;
// });
// };
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="setAvatarStylesDialog"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="setAvatarStylesDialog.visible"
:title="t('dialog.set_avatar_styles.header')"
width="400px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<template v-if="setAvatarStylesDialog.visible">
<div>
<span>{{ t('dialog.set_avatar_styles.primary_style') }}</span>
@@ -50,19 +47,15 @@
t('dialog.set_avatar_styles.save')
}}</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { inject, watch, getCurrentInstance } from 'vue';
import { watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { avatarRequest } from '../../../api';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="setAvatarTagsDialog"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="setAvatarTagsDialog.visible"
:title="t('dialog.set_avatar_tags.header')"
width="770px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<template v-if="setAvatarTagsDialog.visible">
<el-checkbox v-model="setAvatarTagsDialog.contentHorror" @change="updateSelectedAvatarTags">{{
t('dialog.set_avatar_tags.content_horror')
@@ -93,7 +90,7 @@
t('dialog.set_avatar_tags.save')
}}</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
@@ -102,9 +99,6 @@
import { useI18n } from 'vue-i18n-bridge';
import { avatarRequest } from '../../../api';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showAvatarDialog = inject('showAvatarDialog');
const { t } = useI18n();

View File

@@ -1,12 +1,5 @@
<template>
<el-dialog
ref="favoriteDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.favorite.header')"
width="300px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<safe-dialog ref="favoriteDialog" :visible.sync="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">
@@ -69,7 +62,7 @@
</el-button>
</template>
</div>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -78,7 +71,7 @@
export default {
name: 'ChooseFavoriteGroupDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'adjustDialogZ'],
inject: ['API', 'adjustDialogZ'],
props: {
favoriteDialog: {
type: Object,
@@ -186,7 +179,7 @@
this.$emit('remove-local-avatar-favorite', ...args);
},
deleteFavoriteNoConfirm(...args) {
this.$emit('delete-favorite-no-confirm', ...args);
this.$emit('deleteFavoriteNoConfirm', ...args);
}
}
};

View File

@@ -0,0 +1,91 @@
<template>
<safe-dialog
ref="fullscreenImageDialog"
class="x-dialog"
:visible.sync="fullscreenImageDialog.visible"
append-to-body
top="1vh"
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"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
</div>
<img v-lazy="fullscreenImageDialog.imageUrl" style="width: 100%; height: 85vh; object-fit: contain" />
</div>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import utils from '../../classes/utils';
import { copyToClipboard, extractFileId } from '../../composables/shared/utils';
import webApiService from '../../service/webapi';
import Noty from 'noty';
const { proxy } = getCurrentInstance();
const { $message } = proxy;
defineProps({
fullscreenImageDialog: {
type: Object,
default: () => ({})
}
});
function copyImageUrl(imageUrl) {
copyToClipboard(imageUrl, 'ImageUrl copied to clipboard');
}
async function downloadAndSaveImage(url, fileName) {
if (!url) {
return;
}
$message({
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}`);
}
const link = document.createElement('a');
link.href = response.data;
const fileId = extractFileId(url);
if (!fileName && fileId) {
fileName = `${fileId}.png`;
}
if (!fileName) {
fileName = `${url.split('/').pop()}.png`;
}
if (!fileName) {
fileName = 'image.png';
}
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
new Noty({
type: 'error',
text: utils.escapeTag(`Failed to download image. ${url}`)
}).show();
}
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="gallerySelectDialog.visible"
:title="t('dialog.gallery_select.header')"
width="100%"
append-to-body>
<div>
<span>{{ t('dialog.gallery_select.gallery') }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ galleryTable.length }}/64</span>
<br />
<input
id="GalleryUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeGallery" />
<el-button-group>
<el-button type="default" size="small" icon="el-icon-close" @click="selectImageGallerySelect('', '')">{{
t('dialog.gallery_select.none')
}}</el-button>
<el-button type="default" size="small" icon="el-icon-refresh" @click="refreshGalleryTable">{{
t('dialog.gallery_select.refresh')
}}</el-button>
<el-button
type="default"
size="small"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus"
@click="displayGalleryUpload"
>{{ t('dialog.gallery_select.upload') }}</el-button
>
</el-button-group>
<br />
<div
v-for="image in galleryTable"
v-if="image.versions && image.versions.length > 0"
:key="image.id"
class="x-friend-item"
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
<div
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>
</div>
</div>
</safe-dialog>
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { vrcPlusImageRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const API = inject('API');
const props = defineProps({
gallerySelectDialog: {
type: Object,
required: true
},
galleryTable: {
type: Array,
required: true
}
});
const emit = defineEmits(['refreshGalleryTable']);
function selectImageGallerySelect(imageUrl, fileId) {
const D = props.gallerySelectDialog;
D.selectedFileId = fileId;
D.selectedImageUrl = imageUrl;
D.visible = false;
}
function displayGalleryUpload() {
document.getElementById('GalleryUploadButton').click();
}
function onFileChangeGallery(e) {
const clearFile = function () {
if (document.querySelector('#GalleryUploadButton')) {
document.querySelector('#GalleryUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
if (files[0].size >= 100000000) {
// 100MB
$message({
message: t('message.file.too_large'),
type: 'error'
});
clearFile();
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
const r = new FileReader();
r.onload = function () {
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
$message({
message: t('message.gallery.uploaded'),
type: 'success'
});
// API.$on('GALLERYIMAGE:ADD')
if (Object.keys(props.galleryTable).length !== 0) {
props.galleryTable.unshift(args.json);
}
return args;
});
};
r.readAsBinaryString(files[0]);
clearFile();
}
function refreshGalleryTable() {
emit('refreshGalleryTable');
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="groupDialogRef"
:before-close="beforeDialogClose"
:visible.sync="groupDialog.visible"
:show-close="false"
width="770px"
top="10vh"
class="x-dialog x-group-dialog"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
class="x-dialog x-group-dialog">
<div class="group-banner-image">
<el-popover placement="right" width="500px" trigger="click">
<img
@@ -1151,10 +1148,7 @@
</el-tabs>
</div>
<!--Nested-->
<GroupPostEditDialog
:gallery-select-dialog="gallerySelectDialog"
:dialog-data.sync="groupPostEditDialog"
@clear-image-gallery-select="clearImageGallerySelect" />
<GroupPostEditDialog :dialog-data.sync="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
<GroupMemberModerationDialog
:group-dialog="groupDialog"
:is-group-members-loading.sync="isGroupMembersLoading"
@@ -1167,25 +1161,32 @@
@load-all-group-members="loadAllGroupMembers"
@set-group-member-filter="setGroupMemberFilter"
@set-group-member-sort-order="setGroupMemberSortOrder" />
</el-dialog>
<InviteGroupDialog
:dialog-data.sync="inviteGroupDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:offline-friends="offlineFriends"
:active-friends="activeFriends" />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, nextTick, reactive, ref, watch, inject } from 'vue';
import { getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { groupRequest } from '../../../api';
import Location from '../../Location.vue';
import GroupPostEditDialog from './GroupPostEditDialog.vue';
import GroupMemberModerationDialog from './GroupMemberModerationDialog.vue';
import * as workerTimers from 'worker-timers';
import { groupRequest } from '../../../api';
import utils from '../../../classes/utils';
import { hasGroupPermission } from '../../../composables/group/utils';
import { refreshInstancePlayerCount } from '../../../composables/instance/utils';
import { copyToClipboard, downloadAndSaveJson, getFaviconUrl } from '../../../composables/shared/utils';
import { languageClass } from '../../../composables/user/utils';
import Location from '../../Location.vue';
import InviteGroupDialog from '../InviteGroupDialog.vue';
import GroupMemberModerationDialog from './GroupMemberModerationDialog.vue';
import GroupPostEditDialog from './GroupPostEditDialog.vue';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const languageClass = inject('languageClass');
const showUserDialog = inject('showUserDialog');
const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage');
@@ -1222,28 +1223,33 @@
type: Object,
required: true
},
gallerySelectDialog: {
type: Object,
default: () => ({})
},
randomUserColours: {
type: Boolean,
default: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
offlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
}
});
const emit = defineEmits([
'update:group-dialog',
'update:gallery-select-dialog',
'update:group-member-moderation',
'group-dialog-command',
'update:group-dialog',
'groupDialogCommand',
'get-group-dialog-group',
'get-group-dialog-group-members',
'refresh-instance-player-count',
'update-group-post-search',
'set-group-member-sort-order',
'clear-image-gallery-select'
'updateGroupPostSearch'
]);
const groupDialogRef = ref(null);
@@ -1254,6 +1260,10 @@
const groupDialogGalleryCurrentName = ref('0');
const groupDialogTabCurrentName = ref('0');
const isGroupGalleryLoading = ref(false);
const selectedGalleryFile = ref({
selectedFileId: '',
selectedImageUrl: ''
});
const groupPostEditDialog = reactive({
visible: false,
groupRef: {},
@@ -1273,6 +1283,16 @@
auditLogTypes: []
});
const inviteGroupDialog = ref({
visible: false,
loading: false,
groupId: '',
groupName: '',
userId: '',
userIds: [],
userObject: {}
});
let loadMoreGroupMembersParams = {};
watch(
@@ -1293,8 +1313,15 @@
}
);
function getFaviconUrl(link) {
return utils.getFaviconUrl(link);
function showInviteGroupDialog(groupId, userId) {
const D = inviteGroupDialog.value;
D.userIds = '';
D.groups = [];
D.groupId = groupId;
D.groupName = groupId;
D.userId = userId;
D.userObject = {};
D.visible = true;
}
function setGroupRepresentation(groupId) {
@@ -1439,9 +1466,7 @@
}
});
}
function copyToClipboard(text) {
utils.copyToClipboard(text);
}
function groupGalleryStatus(gallery) {
const style = {};
if (!gallery.membersOnly) {
@@ -1455,6 +1480,10 @@
}
function groupDialogCommand(command) {
const D = props.groupDialog;
if (D.visible === false) {
return;
}
switch (command) {
case 'Share':
copyToClipboard(props.groupDialog.ref.$url);
@@ -1465,8 +1494,11 @@
case 'Moderation Tools':
showGroupMemberModerationDialog(props.groupDialog.id);
break;
case 'Invite To Group':
showInviteGroupDialog(D.id, '');
break;
default:
emit('group-dialog-command', command);
emit('groupDialogCommand', command);
}
}
@@ -1481,7 +1513,7 @@
D.auditLogTypes = [];
API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
if (utils.hasGroupPermission(D.groupRef, 'group-audit-view')) {
if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
// API.$on('GROUP:AUDITLOGTYPES', function (args) {
if (groupMemberModeration.id !== args.params.groupId) {
@@ -1569,18 +1601,21 @@
D.roleIds = [];
D.postId = '';
D.groupId = groupId;
emit('update:gallery-select-dialog', { ...D, selectedFileId: '', selectedImageUrl: '' });
selectedGalleryFile.value = {
selectedFileId: '',
selectedImageUrl: ''
};
if (post) {
D.title = post.title;
D.text = post.text;
D.visibility = post.visibility;
D.roleIds = post.roleIds;
D.postId = post.id;
emit('update:gallery-select-dialog', {
...D,
selectedGalleryFile.value = {
selectedFileId: post.imageId,
selectedImageUrl: post.imageUrl
});
};
}
API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
@@ -1763,9 +1798,6 @@
await getGroupDialogGroupMembers();
}
function hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
}
function updateGroupDialogData(obj) {
// Be careful with the deep merge
emit('update:group-dialog', obj);
@@ -1773,16 +1805,7 @@
function getGroupDialogGroup(groupId) {
emit('get-group-dialog-group', groupId);
}
function refreshInstancePlayerCount(tag) {
emit('refresh-instance-player-count', tag);
}
function updateGroupPostSearch() {
emit('update-group-post-search');
}
function downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
}
function clearImageGallerySelect() {
emit('clear-image-gallery-select');
emit('updateGroupPostSearch');
}
</script>

View File

@@ -1,15 +1,12 @@
<template>
<el-dialog
<safe-dialog
class="x-dialog"
:before-close="beforeDialogClose"
:visible="groupMemberModeration.visible"
:title="t('dialog.group_member_moderation.header')"
append-to-body
top="5vh"
width="90vw"
@close="closeDialog"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
@close="closeDialog">
<div>
<h3>{{ groupMemberModeration.groupRef.name }}</h3>
<el-tabs type="card" style="height: 100%">
@@ -806,21 +803,18 @@
<group-member-moderation-export-dialog
:is-group-logs-export-dialog-visible.sync="isGroupLogsExportDialogVisible"
:group-logs-moderation-table="groupLogsModerationTable" />
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { groupRequest, userRequest } from '../../../api';
import { useModerationTable, useSelectedUsers } from '../../../composables/group/useGroupMemberModeration';
import { hasGroupPermission } from '../../../composables/group/utils';
import GroupMemberModerationExportDialog from './GroupMemberModerationExportDialog.vue';
import { useModerationTable, useSelectedUsers } from '../../../composables/groups/useGroupMemberModeration';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showUserDialog = inject('showUserDialog');
const userImage = inject('userImage');
const userImageFull = inject('userImageFull');
@@ -1687,8 +1681,4 @@
.replace(/\./g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase());
}
function hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
class="x-dialog"
:before-close="beforeDialogClose"
:visible="isGroupLogsExportDialogVisible"
:title="t('dialog.group_member_moderation.export_logs')"
width="650px"
append-to-body
@close="setIsGroupLogsExportDialogVisible"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
@close="setIsGroupLogsExportDialogVisible">
<el-checkbox-group
v-model="checkedGroupLogsExportLogsOptions"
style="margin-bottom: 10px"
@@ -29,17 +26,14 @@
readonly
style="margin-top: 15px"
@click.native="handleCopyGroupLogsExportContent" />
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { inject, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { copyToClipboard } from '../../../composables/shared/utils';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const { t } = useI18n();
const props = defineProps({
@@ -101,7 +95,7 @@
}
function handleCopyGroupLogsExportContent() {
utils.copyToClipboard(groupLogsExportContent.value);
copyToClipboard(groupLogsExportContent.value);
}
function setIsGroupLogsExportDialogVisible() {

View File

@@ -1,13 +1,9 @@
<template>
<el-dialog
ref="groupPostEditDialog"
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="groupPostEditDialog.visible"
:title="$t('dialog.group_post_edit.header')"
width="650px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
@@ -107,30 +103,39 @@
{{ $t('dialog.group_post_edit.create_post') }}
</el-button>
</template>
</el-dialog>
<GallerySelectDialog
:gallery-select-dialog="gallerySelectDialog"
:gallery-table="galleryTable"
@refresh-gallery-table="refreshGalleryTable" />
</safe-dialog>
</template>
<script>
import { groupRequest } from '../../../api';
import { groupRequest, vrcPlusIconRequest } from '../../../api';
import GallerySelectDialog from './GallerySelectDialog.vue';
export default {
name: 'GroupPostEditDialog',
inject: [
'beforeDialogClose',
'showFullscreenImageDialog',
'dialogMouseDown',
'dialogMouseUp',
'showGallerySelectDialog'
],
components: {
GallerySelectDialog
},
inject: ['showFullscreenImageDialog'],
props: {
dialogData: {
type: Object,
required: true
},
gallerySelectDialog: {
type: Object,
required: true
}
selectedGalleryFile: { type: Object, default: () => ({}) }
},
data() {
return {
gallerySelectDialog: {
visible: false,
selectedFileId: '',
selectedImageUrl: ''
},
galleryTable: []
};
},
computed: {
groupPostEditDialog: {
@@ -143,6 +148,22 @@
}
},
methods: {
showGallerySelectDialog() {
const D = this.gallerySelectDialog;
D.visible = true;
this.refreshGalleryTable();
},
async refreshGalleryTable() {
const params = {
n: 100,
tag: 'gallery'
};
const args = await vrcPlusIconRequest.getFileList(params);
// API.$on('FILES:LIST')
if (args.params.tag === 'gallery') {
this.galleryTable = args.json.reverse();
}
},
editGroupPost() {
const D = this.groupPostEditDialog;
if (!D.groupId || !D.postId) {
@@ -193,7 +214,9 @@
D.visible = false;
},
clearImageGallerySelect() {
this.$emit('clear-image-gallery-select');
const D = this.gallerySelectDialog;
D.selectedFileId = '';
D.selectedImageUrl = '';
}
}
};

View File

@@ -0,0 +1,212 @@
<template>
<safe-dialog
class="x-dialog"
:visible="editAndSendInviteDialog.visible"
:title="t('dialog.edit_send_invite_message.header')"
width="400px"
append-to-body
@close="cancelEditAndSendInvite">
<div style="font-size: 12px">
<span>{{ t('dialog.edit_send_invite_message.description') }}</span>
</div>
<el-input
v-model="editAndSendInviteDialog.newMessage"
type="textarea"
size="mini"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
style="margin-top: 10px"></el-input>
<template #footer>
<el-button type="small" @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>
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
editAndSendInviteDialog: {
type: Object,
required: true
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
const emit = defineEmits(['update:editAndSendInviteDialog', 'closeInviteDialog']);
function cancelEditAndSendInvite() {
emit('update:editAndSendInviteDialog', { ...props.editAndSendInviteDialog, visible: false });
}
async function saveEditAndSendInvite() {
const D = props.editAndSendInviteDialog;
D.visible = false;
const messageType = D.messageType;
const slot = D.inviteMessage.slot;
if (D.inviteMessage.message !== D.newMessage) {
const params = {
message: D.newMessage
};
await inviteMessagesRequest
.editInviteMessage(params, messageType, slot)
.catch((err) => {
throw err;
})
.then((args) => {
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
if (args.json[slot].message === D.inviteMessage.message) {
$message({
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');
}
return args;
});
}
const I = props.sendInviteDialog;
const J = props.inviteDialog;
if (J?.visible) {
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: slot
},
receiverUserId
)
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: slot
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
J.loading = false;
J.visible = false;
$message({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
} else if (I.messageType === 'invite') {
I.params.messageSlot = slot;
if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendInvite(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite message sent',
type: 'success'
});
return args;
});
}
} else if (I.messageType === 'requestInvite') {
I.params.requestSlot = slot;
if (props.uploadImage) {
notificationRequest
.sendRequestInvitePhoto(I.params, I.userId)
.catch((err) => {
this.clearInviteImageUpload();
throw err;
})
.then((args) => {
$message({
message: 'Request invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendRequestInvite(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Request invite message sent',
type: 'success'
});
return args;
});
}
}
emit('closeInviteDialog');
}
</script>

View File

@@ -0,0 +1,309 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="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"></location>
<br />
<el-button size="mini" style="margin-top: 10px" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
}}</el-button>
<el-button
size="mini"
:disabled="inviteDialog.friendsInInstance.length === 0"
style="margin-top: 10px"
@click="addFriendsInInstanceToInvite"
>{{ t('dialog.invite.add_friends_in_instance') }}</el-button
>
<el-button
size="mini"
:disabled="vipFriends.length === 0"
style="margin-top: 10px"
@click="addFavoriteFriendsToInvite"
>{{ t('dialog.invite.add_favorite_friends') }}</el-button
>
<el-select
v-model="inviteDialog.userIds"
multiple
clearable
:placeholder="t('dialog.invite.select_placeholder')"
filterable
:disabled="inviteDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="API.currentUser" :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="API.currentUser.displayName"
:value="API.currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(API.currentUser)]">
<img v-lazy="userImage(API.currentUser)" />
</div>
<div class="detail">
<span class="name">{{ API.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)" />
</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>
<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>
<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>
<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>
</el-select>
</div>
<template #footer>
<el-button
size="small"
:disabled="inviteDialog.loading || !inviteDialog.userIds.length"
@click="showSendInviteDialog"
>{{ t('dialog.invite.invite_with_message') }}</el-button
>
<el-button
type="primary"
size="small"
:disabled="inviteDialog.loading || !inviteDialog.userIds.length"
@click="sendInvite"
>{{ t('dialog.invite.invite') }}</el-button
>
</template>
<SendInviteDialog
:send-invite-dialog-visible.sync="sendInviteDialogVisible"
:invite-message-table="inviteMessageTable"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
import Location from '../../Location.vue';
import SendInviteDialog from './SendInviteDialog.vue';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage');
const API = inject('API');
const props = defineProps({
inviteDialog: {
type: Object,
required: true
},
vipFriends: {
type: Array,
required: true
},
onlineFriends: {
type: Array,
required: true
},
activeFriends: {
type: Array,
required: true
},
// SendInviteDialog
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['clearInviteImageUpload', 'inviteImageUpload', 'closeInviteDialog']);
const sendInviteDialogVisible = ref(false);
const sendInviteDialog = ref({ message: '', messageSlot: 0, userId: '', messageType: '', params: {} });
function closeInviteDialog() {
emit('closeInviteDialog');
}
function showSendInviteDialog(params, userId) {
sendInviteDialog.value = {
params,
userId,
messageType: 'invite'
};
inviteMessagesRequest.refreshInviteMessageTableData('message');
clearInviteImageUpload();
sendInviteDialogVisible.value = true;
}
function clearInviteImageUpload() {
emit('clearInviteImageUpload');
}
function addSelfToInvite() {
const D = props.inviteDialog;
if (!D.userIds.includes(API.currentUser.id)) {
D.userIds.push(API.currentUser.id);
}
}
function addFriendsInInstanceToInvite() {
const D = props.inviteDialog;
for (const friend of D.friendsInInstance) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
function addFavoriteFriendsToInvite() {
const D = props.inviteDialog;
for (const friend of props.vipFriends) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
function sendInvite() {
$confirm('Continue? Invite', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
const D = props.inviteDialog;
if (action !== 'confirm' || D.loading === true) {
return;
}
D.loading = true;
const inviteLoop = () => {
if (D.userIds.length > 0) {
const receiverUserId = D.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(D.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: D.worldId,
worldId: D.worldId,
worldName: D.worldName
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
D.loading = false;
D.visible = false;
$message({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
}
});
}
</script>

View File

@@ -0,0 +1,176 @@
<template>
<safe-dialog
class="x-dialog"
:visible="visible"
:title="t('dialog.invite_message.header')"
width="400px"
append-to-body
@close="cancelInviteConfirm">
<div style="font-size: 12px">
<span>{{ t('dialog.invite_message.confirmation') }}</span>
</div>
<template #footer>
<el-button type="small" @click="cancelInviteConfirm">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="primary" size="small" @click="sendInviteConfirm">
{{ t('dialog.invite_message.confirm') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
visible: {
type: Boolean,
required: true
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
const emit = defineEmits(['update:visible', 'closeInviteDialog']);
function cancelInviteConfirm() {
emit('update:visible', false);
}
function sendInviteConfirm() {
const D = props.sendInviteDialog;
const J = props.inviteDialog;
if (J?.visible) {
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: D.messageSlot
},
receiverUserId
)
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: D.messageSlot
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
J.loading = false;
J.visible = false;
$message({
message: 'Invite message sent',
type: 'success'
});
}
};
inviteLoop();
} else if (D.messageType === 'invite') {
D.params.messageSlot = D.messageSlot;
if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendInvite(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite message sent',
type: 'success'
});
return args;
});
}
} else if (D.messageType === 'requestInvite') {
D.params.requestSlot = D.messageSlot;
if (props.uploadImage) {
notificationRequest
.sendRequestInvitePhoto(D.params, D.userId)
.catch((err) => {
this.clearInviteImageUpload();
throw err;
})
.then((args) => {
$message({
message: 'Request invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendRequestInvite(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Request invite message sent',
type: 'success'
});
return args;
});
}
}
cancelInviteConfirm();
emit('closeInviteDialog');
}
</script>

View File

@@ -0,0 +1,165 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="sendInviteDialogVisible"
:title="t('dialog.invite_message.header')"
width="800px"
append-to-body
@close="cancelSendInvite">
<template v-if="API.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">-->
<!-- <template #reference>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover" />-->
<!-- </template>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- style="height: 500px"-->
<!-- @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />-->
<!-- </el-popover>-->
<!-- </div>-->
<!-- <el-button size="mini" @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">-->
<!-- {{ t('dialog.invite_message.select_image') }}-->
<!-- </el-button>-->
<!-- </template>-->
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
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"
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"
width="110"
align="right">
<template #default="scope">
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
</template>
</el-table-column>
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
@click.stop="showEditAndSendInviteDialog('message', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
<template #footer>
<el-button type="small" @click="cancelSendInvite">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('message')">
{{ t('dialog.invite_message.refresh') }}
</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
sendInviteDialogVisible: {
type: Boolean,
default: false
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['inviteImageUpload', 'update:sendInviteDialogVisible', 'closeInviteDialog']);
const isSendInviteConfirmDialogVisible = ref(false);
const editAndSendInviteDialog = ref({
visible: false,
messageType: '',
newMessage: '',
inviteMessage: {}
});
function inviteImageUpload(event) {
emit('inviteImageUpload', event);
}
function showSendInviteConfirmDialog(val) {
isSendInviteConfirmDialogVisible.value = true;
//
props.sendInviteDialog.messageSlot = val.slot;
}
function showEditAndSendInviteDialog(messageType, inviteMessage) {
// todo
editAndSendInviteDialog.value = {
newMessage: inviteMessage.message,
visible: true,
messageType,
inviteMessage
};
}
function cancelSendInvite() {
emit('update:sendInviteDialogVisible', false);
}
function closeInviteDialog() {
cancelSendInvite();
emit('closeInviteDialog');
}
</script>

View File

@@ -1,12 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="inviteGroupDialog"
:visible.sync="inviteGroupDialog.visible"
:before-close="beforeDialogClose"
:title="$t('dialog.invite_to_group.header')"
width="450px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ $t('dialog.invite_to_group.description') }}</span>
<br />
@@ -165,24 +163,16 @@
Invite
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
import { groupRequest, userRequest } from '../../../api';
import utils from '../../../classes/utils';
import { groupRequest, userRequest } from '../../api';
import { hasGroupPermission } from '../../composables/group/utils';
export default {
name: 'InviteGroupDialog',
inject: [
'API',
'dialogMouseDown',
'dialogMouseUp',
'beforeDialogClose',
'userStatusClass',
'userImage',
'adjustDialogZ'
],
inject: ['API', 'userStatusClass', 'userImage', 'adjustDialogZ'],
props: {
dialogData: {
type: Object,
@@ -256,7 +246,7 @@
groupRequest
.getGroup({ groupId })
.then((args) => {
if (utils.hasGroupPermission(args.ref, 'group-invites-manage')) {
if (hasGroupPermission(args.ref, 'group-invites-manage')) {
return args;
}
// not allowed to invite

View File

@@ -1,12 +1,5 @@
<template>
<el-dialog
ref="launchDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.launch.header')"
width="450px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<safe-dialog ref="launchDialog" :visible.sync="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
@@ -84,30 +77,58 @@
{{ $t('dialog.launch.launch') }}
</el-button>
</template>
</el-dialog>
<InviteDialog
:invite-dialog="inviteDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:active-friends="activeFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script>
import utils from '../classes/utils';
import configRepository from '../service/config';
import { instanceRequest } from '../api';
import { instanceRequest, worldRequest } from '../../api';
import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default {
name: 'LaunchDialog',
inject: [
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showPreviousInstancesInfoDialog',
'showInviteDialog',
'adjustDialogZ'
],
components: { InviteDialog },
inject: ['showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
hideTooltips: Boolean,
launchDialogData: { type: Object, required: true },
checkCanInvite: {
type: Function,
required: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
}
},
data() {
@@ -121,6 +142,14 @@
shortName: '',
shortUrl: '',
secureOrShortName: ''
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
}
};
},
@@ -147,8 +176,37 @@
this.getConfig();
},
methods: {
closeInviteDialog() {
this.inviteDialog.visible = false;
},
showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = this.inviteDialog;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = this.lastLocation.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = this.friends.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
},
launchGame(location, shortName, desktop) {
this.$emit('launch-game', location, shortName, desktop);
this.$emit('launchGame', location, shortName, desktop);
this.isVisible = false;
},
getConfig() {
@@ -159,7 +217,7 @@
},
async initLaunchDialog() {
const { tag, shortName } = this.launchDialogData;
if (!utils.isRealInstance(tag)) {
if (!isRealInstance(tag)) {
return;
}
this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el));
@@ -168,7 +226,7 @@
D.secureOrShortName = shortName;
D.shortUrl = '';
D.shortName = shortName;
const L = utils.parseLocation(tag);
const L = parseLocation(tag);
L.shortName = shortName;
if (shortName) {
D.shortUrl = `https://vrch.at/${shortName}`;
@@ -178,7 +236,7 @@
} else {
D.location = L.worldId;
}
D.url = utils.getLaunchURL(L);
D.url = getLaunchURL(L);
if (!shortName) {
const res = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
@@ -193,14 +251,14 @@
if (resLocation === this.launchDialog.tag) {
const resShortName = res.json.shortName;
const secureOrShortName = res.json.shortName || res.json.secureName;
const parsedL = utils.parseLocation(resLocation);
const parsedL = parseLocation(resLocation);
parsedL.shortName = resShortName;
this.launchDialog.shortName = resShortName;
this.launchDialog.secureOrShortName = secureOrShortName;
if (resShortName) {
this.launchDialog.shortUrl = `https://vrch.at/${resShortName}`;
}
this.launchDialog.url = utils.getLaunchURL(parsedL);
this.launchDialog.url = getLaunchURL(parsedL);
}
}
},

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="newInstanceDialog"
:before-close="beforeDialogClose"
:visible.sync="newInstanceDialog.visible"
:title="$t('dialog.new_instance.header')"
width="650px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
<el-tab-pane :label="$t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px">
@@ -486,27 +483,30 @@
>{{ $t('dialog.new_instance.launch') }}</el-button
>
</template>
</el-dialog>
<InviteDialog
:invite-dialog="inviteDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:active-friends="activeFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script>
import { groupRequest, instanceRequest } from '../../api';
import { groupRequest, instanceRequest, worldRequest } from '../../api';
import utils from '../../classes/utils';
import { hasGroupPermission as _hasGroupPermission } from '../../composables/group/utils';
import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default {
name: 'NewInstanceDialog',
inject: [
'API',
'userImage',
'userStatusClass',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showInviteDialog',
'showLaunchDialog',
'adjustDialogZ'
],
components: { InviteDialog },
inject: ['API', 'userImage', 'userStatusClass', 'showLaunchDialog', 'adjustDialogZ'],
props: {
vipFriends: {
type: Array,
@@ -535,6 +535,18 @@
newInstanceDialogLocationTag: {
type: String,
required: true
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
}
},
data() {
@@ -566,6 +578,14 @@
groupRef: {},
contentSettings: this.instanceContentSettings,
selectedContentSettings: []
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
}
};
},
@@ -578,13 +598,42 @@
this.initializeNewInstanceDialog();
},
methods: {
closeInviteDialog() {
this.inviteDialog.visible = false;
},
showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = this.inviteDialog;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = this.lastLocation.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = this.friends.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
},
initNewInstanceDialog(tag) {
if (!utils.isRealInstance(tag)) {
if (!isRealInstance(tag)) {
return;
}
this.$nextTick(() => this.adjustDialogZ(this.$refs.newInstanceDialog.$el));
const D = this.newInstanceDialog;
const L = utils.parseLocation(tag);
const L = parseLocation(tag);
if (D.worldId === L.worldId) {
// reopening dialog, keep last open instance
D.visible = true;
@@ -682,16 +731,16 @@
} else {
D.location = D.worldId;
}
const L = utils.parseLocation(D.location);
const L = parseLocation(D.location);
if (noChanges) {
L.shortName = D.shortName;
} else {
D.shortName = '';
}
D.url = utils.getLaunchURL(L);
D.url = getLaunchURL(L);
},
selfInvite(location) {
const L = utils.parseLocation(location);
const L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
@@ -831,7 +880,7 @@
this.saveNewInstanceDialog();
},
async copyInstanceUrl(location) {
const L = utils.parseLocation(location);
const L = parseLocation(location);
const args = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
@@ -870,7 +919,7 @@
}
},
hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
return _hasGroupPermission(ref, permission);
}
}
};

View File

@@ -0,0 +1,51 @@
<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"
v-if="image.file"
:key="image.version"
style="display: inline-block">
<el-popover class="x-change-image-item" placement="right" width="500px" trigger="click">
<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 { inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
const { t } = useI18n();
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
defineProps({
previousImagesDialogVisible: {
type: Boolean,
required: true
},
previousImagesTable: {
type: Array,
required: true
}
});
const emit = defineEmits(['update:previousImagesDialogVisible']);
function closeDialog() {
emit('update:previousImagesDialogVisible', false);
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="dialog"
:before-close="beforeDialogClose"
:visible="visible"
:title="$t('dialog.previous_instances.info')"
width="800px"
:fullscreen="fullscreen"
destroy-on-close
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp"
@close="$emit('update:visible', false)">
<div style="display: flex; align-items: center; justify-content: space-between">
<location :location="location.tag" style="font-size: 14px"></location>
@@ -57,13 +54,14 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import database from '../../../service/database';
import dayjs from 'dayjs';
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import Location from '../../Location.vue';
export default {
@@ -71,7 +69,7 @@
components: {
Location
},
inject: ['adjustDialogZ', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
inject: ['adjustDialogZ'],
props: {
visible: {
type: Boolean,
@@ -144,7 +142,7 @@
init() {
this.adjustDialogZ(this.$refs.dialog.$el);
this.loading = true;
this.location = utils.parseLocation(this.instanceId);
this.location = parseLocation(this.instanceId);
},
refreshPreviousInstancesInfoTable() {
database.getPlayersFromInstance(this.location.tag).then((data) => {

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="previousInstancesWorldDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
width="1000px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
<el-input
@@ -66,24 +63,17 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
export default {
name: 'PreviousInstancesWorldDialog',
inject: [
'API',
'showLaunchDialog',
'showPreviousInstancesInfoDialog',
'adjustDialogZ',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp'
],
inject: ['API', 'showLaunchDialog', 'showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
previousInstancesWorldDialog: {
type: Object,
@@ -149,7 +139,7 @@
database.getpreviousInstancesByWorldId(D.worldRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location);
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {

View File

@@ -0,0 +1,96 @@
<template>
<el-dialog
ref="elDialogRef"
:visible="props.visible"
v-bind="attrs"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose">
<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 handleOpen = () => {
emit('open');
nextTick(() => {
addWrapperListeners();
});
};
const handleClose = () => {
emit('close');
removeWrapperListeners();
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;
};
onBeforeUnmount(() => {
removeWrapperListeners();
});
</script>

View File

@@ -0,0 +1,277 @@
<!--<template>-->
<!-- <safe-dialog-->
<!-- class="x-dialog"-->
<!-- :visible="sendBoopDialog.visible"-->
<!-- :title="t('dialog.boop_dialog.header')"-->
<!-- width="450px"-->
<!-- @close="closeDialog">-->
<!-- <el-select-->
<!-- v-model="sendBoopDialog.userId"-->
<!-- :placeholder="t('dialog.new_instance.instance_creator_placeholder')"-->
<!-- filterable-->
<!-- style="width: 100%">-->
<!-- <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" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <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-->
<!-- 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" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <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-->
<!-- 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 }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <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-->
<!-- v-for="friend in offlineFriends"-->
<!-- :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 }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <span v-else v-text="friend.id"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- </el-select>-->
<!-- <br />-->
<!-- <br />-->
<!-- <el-select-->
<!-- v-model="fileId"-->
<!-- clearable-->
<!-- :placeholder="t('dialog.boop_dialog.select_emoji')"-->
<!-- size="small"-->
<!-- style="width: 100%"-->
<!-- popper-class="max-height-el-select">-->
<!-- <el-option-group :label="t('dialog.boop_dialog.my_emojis')">-->
<!-- <el-option-->
<!-- v-for="image in emojiTable"-->
<!-- v-if="image.versions && image.versions.length > 0"-->
<!-- :key="image.id"-->
<!-- :value="image.id"-->
<!-- style="width: 100%; height: 100%">-->
<!-- <div-->
<!-- v-if="image.versions[image.versions.length - 1].file.url"-->
<!-- class="vrcplus-icon"-->
<!-- style="overflow: hidden; width: 200px; height: 200px; padding: 10px">-->
<!-- <template v-if="image.frames">-->
<!-- <div-->
<!-- class="avatar"-->
<!-- :style="-->
<!-- generateEmojiStyle(-->
<!-- image.versions[image.versions.length - 1].file.url,-->
<!-- image.framesOverTime,-->
<!-- image.frames,-->
<!-- image.loopStyle-->
<!-- )-->
<!-- "></div>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <img-->
<!-- v-lazy="image.versions[image.versions.length - 1].file.url"-->
<!-- class="avatar"-->
<!-- style="width: 200px; height: 200px" />-->
<!-- </template>-->
<!-- </div>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- <el-option-group :label="t('dialog.boop_dialog.default_emojis')">-->
<!-- <el-option-->
<!-- v-for="emojiName in photonEmojis"-->
<!-- :key="emojiName"-->
<!-- :value="getEmojiValue(emojiName)"-->
<!-- style="width: 100%; height: 100%">-->
<!-- <span v-text="emojiName"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- </el-select>-->
<!-- <template #footer>-->
<!-- <el-button size="small" @click="showGalleryDialog(2)">{{-->
<!-- t('dialog.boop_dialog.emoji_manager')-->
<!-- }}</el-button>-->
<!-- <el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>-->
<!-- <el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{-->
<!-- t('dialog.boop_dialog.send')-->
<!-- }}</el-button>-->
<!-- </template>-->
<!-- </safe-dialog>-->
<!--</template>-->
<!--<script setup>-->
<!-- import { inject, ref } from 'vue';-->
<!-- import { useI18n } from 'vue-i18n-bridge';-->
<!-- import { photonEmojis } from '../../composables/shared/constants/photon.js';-->
<!-- import { notificationRequest } from '../../api';-->
<!-- // import { miscRequest } from '../../api';-->
<!-- const { t } = useI18n();-->
<!-- const userStatusClass = inject('userStatusClass');-->
<!-- const userImage = inject('userImage');-->
<!-- const showGalleryDialog = inject('showGalleryDialog');-->
<!-- const props = defineProps({-->
<!-- sendBoopDialog: {-->
<!-- type: Object,-->
<!-- required: true-->
<!-- },-->
<!-- emojiTable: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- vipFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- onlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- activeFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- offlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- generateEmojiStyle: {-->
<!-- type: Function,-->
<!-- required: true-->
<!-- },-->
<!-- notificationTable: {-->
<!-- type: Object,-->
<!-- required: true-->
<!-- }-->
<!-- });-->
<!-- const emit = defineEmits(['update:sendBoopDialog']);-->
<!-- const fileId = ref('');-->
<!-- // $app.data.sendBoopDialog = {-->
<!-- // visible: false,-->
<!-- // userId: ''-->
<!-- // };-->
<!-- // $app.methods.showSendBoopDialog = function (userId) {-->
<!-- // this.$nextTick(() =>-->
<!-- // $app.adjustDialogZ(this.$refs.sendBoopDialog.$el)-->
<!-- // );-->
<!-- // const D = this.sendBoopDialog;-->
<!-- // D.userId = userId;-->
<!-- // D.visible = true;-->
<!-- // if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) {-->
<!-- // this.refreshEmojiTable();-->
<!-- // }-->
<!-- // };-->
<!-- function closeDialog() {-->
<!-- emit('update:sendBoopDialog', {-->
<!-- ...props.sendBoopDialog,-->
<!-- visible: false-->
<!-- });-->
<!-- }-->
<!-- function getEmojiValue(emojiName) {-->
<!-- if (!emojiName) {-->
<!-- return '';-->
<!-- }-->
<!-- return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`;-->
<!-- }-->
<!-- function sendBoop() {-->
<!-- const D = props.sendBoopDialog;-->
<!-- dismissBoop(D.userId);-->
<!-- const params = {-->
<!-- userId: D.userId-->
<!-- };-->
<!-- if (fileId.value) {-->
<!-- params.emojiId = fileId.value;-->
<!-- }-->
<!-- // miscRequest.sendBoop(params);-->
<!-- D.visible = false;-->
<!-- }-->
<!-- function dismissBoop(userId) {-->
<!-- // JANK: This is a hack to remove boop notifications when responding-->
<!-- const array = props.notificationTable.data;-->
<!-- for (let i = array.length - 1; i >= 0; i&#45;&#45;) {-->
<!-- const ref = array[i];-->
<!-- if (ref.type !== 'boop' || ref.$isExpired || ref.senderUserId !== userId) {-->
<!-- continue;-->
<!-- }-->
<!-- notificationRequest.sendNotificationResponse({-->
<!-- notificationId: ref.id,-->
<!-- responseType: 'delete',-->
<!-- responseData: ''-->
<!-- });-->
<!-- }-->
<!-- }-->
<!--</script>-->

View File

@@ -0,0 +1,90 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="bioDialog.visible"
:title="t('dialog.bio.header')"
width="600px"
append-to-body>
<div v-loading="bioDialog.loading">
<el-input
v-model="bioDialog.bio"
type="textarea"
size="mini"
maxlength="512"
show-word-limit
:autosize="{ minRows: 5, maxRows: 20 }"
:placeholder="t('dialog.bio.bio_placeholder')"
style="margin-bottom: 10px">
</el-input>
<el-input
v-for="(link, index) in bioDialog.bioLinks"
:key="index"
v-model="bioDialog.bioLinks[index]"
:value="link"
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)" />
</el-input>
<el-button
:disabled="bioDialog.bioLinks.length >= 3"
size="mini"
style="margin-top: 5px"
@click="bioDialog.bioLinks.push('')">
{{ t('dialog.bio.add_link') }}
</el-button>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="bioDialog.loading" @click="saveBio">
{{ t('dialog.bio.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { getFaviconUrl } from '../../../composables/shared/utils';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const props = defineProps({
bioDialog: {
type: Object,
required: true
}
});
function saveBio() {
const D = props.bioDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
bio: D.bio,
bioLinks: D.bioLinks
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Bio updated',
type: 'success'
});
return args;
});
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="languageDialog.visible"
:title="t('dialog.language.header')"
width="400px"
append-to-body>
<div v-loading="languageDialog.loading">
<div v-for="item in API.currentUser.$languages" :key="item.key" style="margin: 6px 0">
<el-tag
size="small"
type="info"
effect="plain"
closable
style="margin-right: 5px"
@close="removeUserLanguage(item.key)">
<span
class="flags"
:class="languageClass(item.key)"
style="display: inline-block; margin-right: 5px"></span>
{{ item.value }} ({{ item.key.toUpperCase() }})
</el-tag>
</div>
<el-select
value=""
:disabled="
languageDialog.loading || (API.currentUser.$languages && API.currentUser.$languages.length === 3)
"
:placeholder="t('dialog.language.select_language')"
style="margin-top: 14px"
@change="addUserLanguage">
<el-option
v-for="item in languageDialog.languages"
:key="item.key"
:value="item.key"
:label="item.value">
<span
class="flags"
:class="languageClass(item.key)"
style="display: inline-block; margin-right: 5px"></span>
{{ item.value }} ({{ item.key.toUpperCase() }})
</el-option>
</el-select>
</div>
</safe-dialog>
</template>
<script setup>
import { inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { languageClass } from '../../../composables/user/utils';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
languageDialog: {
type: Object,
required: true
}
});
function removeUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
D.loading = true;
userRequest
.removeUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
}
function addUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
D.loading = true;
userRequest
.addUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
}
</script>

View File

@@ -1,12 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="previousInstancesUserDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
width="1000px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
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
@@ -68,11 +66,12 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import Location from '../../Location.vue';
@@ -81,14 +80,7 @@
components: {
Location
},
inject: [
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'adjustDialogZ',
'showLaunchDialog',
'showPreviousInstancesInfoDialog'
],
inject: ['adjustDialogZ', 'showLaunchDialog', 'showPreviousInstancesInfoDialog'],
props: {
previousInstancesUserDialog: {
type: Object,
@@ -176,7 +168,7 @@
database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location);
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {

View File

@@ -0,0 +1,65 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="pronounsDialog.visible"
:title="t('dialog.pronouns.header')"
width="600px"
append-to-body>
<div v-loading="pronounsDialog.loading">
<el-input
type="textarea"
v-model="pronounsDialog.pronouns"
size="mini"
maxlength="32"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="t('dialog.pronouns.pronouns_placeholder')">
</el-input>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="pronounsDialog.loading" @click="savePronouns">
{{ t('dialog.pronouns.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const props = defineProps({
pronounsDialog: {
type: Object,
required: true
}
});
function savePronouns() {
const D = props.pronounsDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
pronouns: D.pronouns
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Pronouns updated',
type: 'success'
});
return args;
});
}
</script>

View File

@@ -0,0 +1,138 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="sendInviteRequestDialogVisible"
:title="t('dialog.invite_request_message.header')"
width="800px"
append-to-body
@close="cancelSendInviteRequest">
<template v-if="API.currentUser.$isVRCPlus">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
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"
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"
width="110"
align="right">
<template #default="scope">
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
</template>
</el-table-column>
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
@click.stop="showEditAndSendInviteDialog('request', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
<template #footer>
<el-button type="small" @click="cancelSendInviteRequest">{{
t('dialog.invite_request_message.cancel')
}}</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('request')">{{
t('dialog.invite_request_message.refresh')
}}</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
sendInviteRequestDialogVisible: {
type: Boolean,
default: false
},
inviteRequestMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
default: () => ({})
},
inviteDialog: {
type: Object,
require: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['inviteImageUpload', 'update:sendInviteRequestDialogVisible', 'closeInviteDialog']);
const isSendInviteConfirmDialogVisible = ref(false);
const editAndSendInviteDialog = ref({
visible: false,
messageType: '',
newMessage: '',
inviteMessage: {}
});
function inviteImageUpload(event) {
emit('inviteImageUpload', event);
}
function showSendInviteConfirmDialog(val) {
isSendInviteConfirmDialogVisible.value = true;
//
props.sendInviteDialog.messageSlot = val.slot;
}
function showEditAndSendInviteDialog(messageType, inviteMessage) {
editAndSendInviteDialog.value = {
newMessage: inviteMessage.message,
visible: true,
messageType,
inviteMessage
};
}
function cancelSendInviteRequest() {
emit('update:sendInviteRequestDialogVisible', false);
}
function closeInviteDialog() {
cancelSendInviteRequest();
}
</script>

View File

@@ -0,0 +1,109 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="socialStatusDialog.visible"
:title="t('dialog.social_status.header')"
append-to-body
width="400px">
<div v-loading="socialStatusDialog.loading">
<el-collapse style="border: 0">
<el-collapse-item>
<template #title>
<span style="font-size: 16px">{{ t('dialog.social_status.history') }}</span>
</template>
<data-tables
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>
</el-collapse-item>
</el-collapse>
<el-select v-model="socialStatusDialog.status" style="display: block; margin-top: 10px">
<el-option :label="t('dialog.user.status.join_me')" value="join me">
<i class="x-user-status joinme"></i> {{ t('dialog.user.status.join_me') }}
</el-option>
<el-option :label="t('dialog.user.status.online')" value="active">
<i class="x-user-status online"></i> {{ t('dialog.user.status.online') }}
</el-option>
<el-option :label="t('dialog.user.status.ask_me')" value="ask me">
<i class="x-user-status askme"></i> {{ t('dialog.user.status.ask_me') }}
</el-option>
<el-option :label="t('dialog.user.status.busy')" value="busy">
<i class="x-user-status busy"></i> {{ t('dialog.user.status.busy') }}
</el-option>
<el-option v-if="API.currentUser.$isModerator" :label="t('dialog.user.status.offline')" value="offline">
<i class="x-user-status offline"></i> {{ t('dialog.user.status.offline') }}
</el-option>
</el-select>
<el-input
v-model="socialStatusDialog.statusDescription"
:placeholder="t('dialog.social_status.status_placeholder')"
maxlength="32"
show-word-limit
style="display: block; margin-top: 10px"></el-input>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="socialStatusDialog.loading" @click="saveSocialStatus">
{{ t('dialog.social_status.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const API = inject('API');
const props = defineProps({
socialStatusDialog: {
type: Object,
required: true
},
socialStatusHistoryTable: {
type: Object,
required: true
}
});
function setSocialStatusFromHistory(val) {
if (val === null) {
return;
}
const D = props.socialStatusDialog;
D.statusDescription = val.status;
}
function saveSocialStatus() {
const D = props.socialStatusDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
status: D.status,
statusDescription: D.statusDescription
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Status updated',
type: 'success'
});
return args;
});
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="VRCXUpdateDialogRef"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')"
width="400px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="400px">
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
<template v-if="updateInProgress">
<el-progress :percentage="updateProgress" :format="updateProgressText"></el-progress>
@@ -62,7 +59,7 @@
{{ t('dialog.vrcx_updater.install') }}
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
@@ -71,9 +68,6 @@
import { useI18n } from 'vue-i18n-bridge';
const { t } = useI18n();
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const adjustDialogZ = inject('adjustDialogZ');
const props = defineProps({

View File

@@ -0,0 +1,394 @@
<template>
<safe-dialog
class="x-dialog"
:visible="changeWorldImageDialogVisible"
:title="t('dialog.change_content_image.world')"
width="850px"
append-to-body
@close="closeDialog">
<div v-loading="changeWorldImageDialogLoading">
<input
id="WorldImageUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeWorldImage" />
<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-group>
<br />
<div
v-for="image in previousImagesTable"
v-if="image.file"
:key="image.version"
style="display: inline-block">
<div
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>
</div>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { extractFileId } from '../../../composables/shared/utils';
import webApiService from '../../../service/webapi';
const { t } = useI18n();
const API = inject('API');
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const props = defineProps({
changeWorldImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
previousImagesFileId: {
type: String,
default: ''
},
worldDialog: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'refresh']);
const changeWorldImageDialogLoading = ref(false);
const worldImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
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 () {
if (document.querySelector('#WorldImageUploadButton')) {
document.querySelector('#WorldImageUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.worldDialog.visible || props.worldDialog.loading) {
clearFile();
return;
}
if (files[0].size >= 100000000) {
// 100MB
$message({
message: t('message.file.too_large'),
type: 'error'
});
clearFile();
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
changeWorldImageDialogLoading.value = true;
const r = new FileReader();
r.onload = async function (file) {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total, 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const worldId = props.worldDialog.id;
const { imageUrl } = props.worldDialog.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
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
} catch (error) {
console.error('World image upload process failed:', error);
} finally {
changeWorldImageDialogLoading.value = false;
clearFile();
}
};
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) {
// API.$on('WORLDIMAGE:INIT')
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) {
// API.$on('WORLDIMAGE:FILESTART')
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) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
}
const args = {
json,
params
};
return worldImageFileAWS(args);
}
async function worldImageFileAWS(args) {
// API.$on('WORLDIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageFileFinish(params);
return worldImageFileFinish(res);
}
async function worldImageFileFinish(args) {
// API.$on('WORLDIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigStart(params);
return worldImageSigStart(res);
}
async function worldImageSigStart(args) {
// API.$on('WORLDIMAGE:SIGSTART')
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) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
}
const args = {
json,
params
};
return worldImageSigAWS(args);
}
async function worldImageSigAWS(args) {
// API.$on('WORLDIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigFinish(params);
return worldImageSigFinish(res);
}
async function worldImageSigFinish(args) {
// API.$on('WORLDIMAGE:SIGFINISH')
const { fileId, fileVersion } = args.params;
const parmas = {
id: worldImage.value.worldId,
imageUrl: `${API.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 {
API.$throw(0, 'World image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function setWorldImage(image) {
changeWorldImageDialogLoading.value = true;
const parmas = {
id: props.worldDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setWorldImage(parmas)
.then((args) => worldImageSet(args))
.finally(() => {
changeWorldImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
if (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
// FIXME: old:avatarDialog -> new:worldDialog, is this correct?
props.worldDialog.ref.imageUrl
) {
return true;
}
return false;
}
// $app.methods.deleteWorldImage = function () {
// this.changeWorldImageDialogLoading = true;
// var parmas = {
// fileId: this.previousImagesTableFileId,
// version: this.previousImagesTable[0].version
// };
// vrcPlusIconRequest
// .deleteFileVersion(parmas)
// .then((args) => {
// this.previousImagesTableFileId = args.json.id;
// var images = [];
// args.json.versions.forEach((item) => {
// if (!item.deleted) {
// images.unshift(item);
// }
// });
// this.checkPreviousImageAvailable(images);
// })
// .finally(() => {
// this.changeWorldImageDialogLoading = false;
// });
// };
</script>

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.set_world_tags.header')"
width="400px"
destroy-on-close
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled">
{{ $t('dialog.set_world_tags.avatar_scaling_disabled') }}
</el-checkbox>
@@ -80,7 +77,7 @@
</el-button>
</div>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -88,7 +85,7 @@
export default {
name: 'SetWorldTagsDialog',
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'showWorldDialog'],
inject: ['showWorldDialog'],
props: {
oldTags: {
type: Array,

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.allowed_video_player_domains.header')"
width="600px"
destroy-on-close
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div>
<el-input
v-for="(domain, index) in urlList"
@@ -31,7 +28,7 @@
{{ $t('dialog.allowed_video_player_domains.save') }}
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -39,7 +36,6 @@
export default {
name: 'WorldAllowedDomainsDialog',
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
props: {
worldAllowedDomainsDialog: {
type: Object,

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="worldDialog"
:before-close="beforeDialogClose"
class="x-dialog x-world-dialog"
:visible.sync="isDialogVisible"
:show-close="false"
width="770px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="770px">
<div v-loading="worldDialog.loading">
<div style="display: flex">
<el-popover placement="right" width="500px" trigger="click">
@@ -760,22 +757,49 @@
:offline-friends="offlineFriends"
:active-friends="activeFriends"
:online-friends="onlineFriends"
:vip-friends="vipFriends" />
</el-dialog>
:vip-friends="vipFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
:last-location="lastLocation" />
<ChangeWorldImageDialog
:change-world-image-dialog-visible.sync="changeWorldImageDialogVisible"
:previous-images-table="previousImagesTable"
:previous-images-file-id="previousImagesFileId"
:world-dialog="worldDialog"
@refresh="displayPreviousImages" />
<PreviousImagesDialog
:previous-images-dialog-visible.sync="previousImagesDialogVisible"
:previous-images-table="previousImagesTable" />
</safe-dialog>
</template>
<script>
import { favoriteRequest, imageRequest, miscRequest, userRequest, worldRequest } from '../../../api';
import utils from '../../../classes/utils';
import { refreshInstancePlayerCount as _refreshInstancePlayerCount } from '../../../composables/instance/utils';
import {
downloadAndSaveJson as _downloadAndSaveJson,
extractFileId,
replaceVrcPackageUrl as _replaceVrcPackageUrl
} from '../../../composables/shared/utils';
import database from '../../../service/database.js';
import WorldAllowedDomainsDialog from './WorldAllowedDomainsDialog.vue';
import SetWorldTagsDialog from './SetWorldTagsDialog.vue';
import PreviousInstancesWorldDialog from '../PreviousInstancesDialog/PreviousInstancesWorldDialog.vue';
import NewInstanceDialog from '../NewInstanceDialog.vue';
import { favoriteRequest, miscRequest, worldRequest } from '../../../api';
import PreviousImagesDialog from '../PreviousImagesDialog.vue';
import PreviousInstancesWorldDialog from '../PreviousInstancesDialog/PreviousInstancesWorldDialog.vue';
import ChangeWorldImageDialog from './ChangeWorldImageDialog.vue';
import SetWorldTagsDialog from './SetWorldTagsDialog.vue';
import WorldAllowedDomainsDialog from './WorldAllowedDomainsDialog.vue';
export default {
name: 'WorldDialog',
components: { SetWorldTagsDialog, WorldAllowedDomainsDialog, PreviousInstancesWorldDialog, NewInstanceDialog },
components: {
PreviousImagesDialog,
SetWorldTagsDialog,
WorldAllowedDomainsDialog,
PreviousInstancesWorldDialog,
NewInstanceDialog,
ChangeWorldImageDialog
},
inject: [
'API',
'showUserDialog',
@@ -785,10 +809,6 @@
'showPreviousInstancesInfoDialog',
'showLaunchDialog',
'showFullscreenImageDialog',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'displayPreviousImages',
'showWorldDialog',
'showFavoriteDialog',
'openExternalLink'
@@ -808,6 +828,8 @@
activeFriends: Array,
onlineFriends: Array,
vipFriends: Array,
inviteMessageTable: Object,
uploadImage: String,
// TODO: Remove
updateInstanceInfo: Number
@@ -826,7 +848,11 @@
openFlg: false,
worldRef: {}
},
newInstanceDialogLocationTag: ''
newInstanceDialogLocationTag: '',
changeWorldImageDialogVisible: false,
previousImagesFileId: '',
previousImagesDialogVisible: false,
previousImagesTable: []
};
},
computed: {
@@ -907,6 +933,51 @@
}
},
methods: {
displayPreviousImages(command) {
this.previousImagesFileId = '';
this.previousImagesTable = [];
const { imageUrl } = this.worldDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
return;
}
const params = {
fileId
};
if (command === 'Display') {
this.previousImagesDialogVisible = true;
}
if (command === 'Change') {
this.changeWorldImageDialogVisible = true;
}
imageRequest.getWorldImages(params).then((args) => {
this.previousImagesFileId = args.json.id;
const images = [];
args.json.versions.forEach((item) => {
if (!item.deleted) {
images.unshift(item);
}
});
this.checkPreviousImageAvailable(images);
});
},
async checkPreviousImageAvailable(images) {
this.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.log(error);
});
if (response.status === 200) {
this.previousImagesTable.push(image);
}
}
}
},
showNewInstanceDialog(tag) {
// trigger watcher
this.newInstanceDialogLocationTag = '';
@@ -946,26 +1017,30 @@
});
break;
case 'Make Home':
this.API.saveCurrentUser({
homeLocation: D.id
}).then((args) => {
this.$message({
message: 'Home world updated',
type: 'success'
userRequest
.saveCurrentUser({
homeLocation: D.id
})
.then((args) => {
this.$message({
message: 'Home world updated',
type: 'success'
});
return args;
});
return args;
});
break;
case 'Reset Home':
this.API.saveCurrentUser({
homeLocation: ''
}).then((args) => {
this.$message({
message: 'Home world has been reset',
type: 'success'
userRequest
.saveCurrentUser({
homeLocation: ''
})
.then((args) => {
this.$message({
message: 'Home world has been reset',
type: 'success'
});
return args;
});
return args;
});
break;
case 'Publish':
worldRequest
@@ -1040,10 +1115,10 @@
this.openExternalLink(this.replaceVrcPackageUrl(this.worldDialog.ref.unityPackageUrl));
break;
case 'Change Image':
this.displayPreviousImages('World', 'Change');
this.displayPreviousImages('Change');
break;
case 'Previous Images':
this.displayPreviousImages('World', 'Display');
this.displayPreviousImages('Display');
break;
case 'Refresh':
this.showWorldDialog(D.id);
@@ -1055,12 +1130,15 @@
this.showFavoriteDialog('world', D.id);
break;
default:
this.$emit('world-dialog-command', command);
this.$emit('worldDialogCommand', command);
break;
}
},
replaceVrcPackageUrl(url) {
_replaceVrcPackageUrl(url);
},
refreshInstancePlayerCount(tag) {
this.$emit('refresh-instance-player-count', tag);
_refreshInstancePlayerCount(tag);
},
onWorldMemoChange() {
const worldId = this.worldDialog.id;
@@ -1087,7 +1165,7 @@
this.treeData = utils.buildTreeData(this.worldDialog.ref);
},
downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
_downloadAndSaveJson(fileName, data);
},
copyWorldId() {
navigator.clipboard

View File

@@ -0,0 +1,125 @@
import utils from '../../classes/utils';
function storeAvatarImage(args) {
const refCreatedAt = args.json.versions[0];
const fileCreatedAt = refCreatedAt.created_at;
const fileId = args.params.fileId;
let avatarName = '';
const imageName = args.json.name;
const avatarNameRegex = /Avatar - (.*) - Image -/gi.exec(imageName);
if (avatarNameRegex) {
avatarName = utils.replaceBioSymbols(avatarNameRegex[1]);
}
const ownerId = args.json.ownerId;
const avatarInfo = {
ownerId,
avatarName,
fileCreatedAt
};
window.API.cachedAvatarNames.set(fileId, avatarInfo);
return avatarInfo;
}
function parseAvatarUrl(avatar) {
const url = new URL(avatar);
const urlPath = url.pathname;
if (urlPath.substring(5, 13) === '/avatar/') {
const avatarId = urlPath.substring(13);
return avatarId;
}
// return void 0;
}
function getPlatformInfo(unityPackages) {
var pc = {};
var android = {};
var ios = {};
if (typeof unityPackages === 'object') {
for (var unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
if (
unityPackage.performanceRating === 'None' &&
pc.performanceRating
) {
continue;
}
pc = unityPackage;
} else if (unityPackage.platform === 'android') {
if (
unityPackage.performanceRating === 'None' &&
android.performanceRating
) {
continue;
}
android = unityPackage;
} else if (unityPackage.platform === 'ios') {
if (
unityPackage.performanceRating === 'None' &&
ios.performanceRating
) {
continue;
}
ios = unityPackage;
}
}
}
return { pc, android, ios };
}
function compareUnityVersion(unitySortNumber) {
if (!window.API.cachedConfig.sdkUnityVersion) {
console.error('No cachedConfig.sdkUnityVersion');
return false;
}
// 2022.3.6f1 2022 03 06 000
// 2019.4.31f1 2019 04 31 000
// 5.3.4p1 5 03 04 010
// 2019.4.31f1c1 is a thing
var array = window.API.cachedConfig.sdkUnityVersion.split('.');
if (array.length < 3) {
console.error('Invalid cachedConfig.sdkUnityVersion');
return false;
}
var currentUnityVersion = array[0];
currentUnityVersion += array[1].padStart(2, '0');
var indexFirstLetter = array[2].search(/[a-zA-Z]/);
if (indexFirstLetter > -1) {
currentUnityVersion += array[2]
.substr(0, indexFirstLetter)
.padStart(2, '0');
currentUnityVersion += '0';
var letter = array[2].substr(indexFirstLetter, 1);
if (letter === 'p') {
currentUnityVersion += '1';
} else {
// f
currentUnityVersion += '0';
}
currentUnityVersion += '0';
} else {
// just in case
currentUnityVersion += '000';
}
// just in case
currentUnityVersion = currentUnityVersion.replace(/\D/g, '');
if (parseInt(unitySortNumber, 10) <= parseInt(currentUnityVersion, 10)) {
return true;
}
return false;
}
export {
storeAvatarImage,
parseAvatarUrl,
getPlatformInfo,
compareUnityVersion
};

View File

@@ -0,0 +1,14 @@
function hasGroupPermission(ref, permission) {
if (
ref &&
ref.myMember &&
ref.myMember.permissions &&
(ref.myMember.permissions.includes('*') ||
ref.myMember.permissions.includes(permission))
) {
return true;
}
return false;
}
export { hasGroupPermission };

View File

@@ -0,0 +1,168 @@
import { instanceRequest } from '../../api';
// TODO: launch, invite, refresh, etc. buttons, better to split into one component
function refreshInstancePlayerCount(instance) {
const L = parseLocation(instance);
if (L.isRealInstance) {
instanceRequest.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
}
function isRealInstance(instanceId) {
if (!instanceId) {
return false;
}
switch (instanceId) {
case ':':
case 'offline':
case 'offline:offline':
case 'private':
case 'private:private':
case 'traveling':
case 'traveling:traveling':
case instanceId.startsWith('local'):
return false;
}
return true;
}
function displayLocation(location, worldName, groupName) {
var text = worldName;
var L = parseLocation(location);
if (L.isOffline) {
text = 'Offline';
} else if (L.isPrivate) {
text = 'Private';
} else if (L.isTraveling) {
text = 'Traveling';
} else if (L.worldId) {
if (groupName) {
text = `${worldName} ${L.accessTypeName}(${groupName})`;
} else if (L.instanceId) {
text = `${worldName} ${L.accessTypeName}`;
}
}
return text;
}
function parseLocation(tag) {
var _tag = String(tag || '');
var ctx = {
tag: _tag,
isOffline: false,
isPrivate: false,
isTraveling: false,
isRealInstance: false,
worldId: '',
instanceId: '',
instanceName: '',
accessType: '',
accessTypeName: '',
region: '',
shortName: '',
userId: null,
hiddenId: null,
privateId: null,
friendsId: null,
groupId: null,
groupAccessType: null,
canRequestInvite: false,
strict: false,
ageGate: false
};
if (_tag === 'offline' || _tag === 'offline:offline') {
ctx.isOffline = true;
} else if (_tag === 'private' || _tag === 'private:private') {
ctx.isPrivate = true;
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
ctx.isTraveling = true;
} else if (!_tag.startsWith('local')) {
ctx.isRealInstance = true;
var sep = _tag.indexOf(':');
// technically not part of instance id, but might be there when coping id from url so why not support it
var shortNameQualifier = '&shortName=';
var shortNameIndex = _tag.indexOf(shortNameQualifier);
if (shortNameIndex >= 0) {
ctx.shortName = _tag.substr(
shortNameIndex + shortNameQualifier.length
);
_tag = _tag.substr(0, shortNameIndex);
}
if (sep >= 0) {
ctx.worldId = _tag.substr(0, sep);
ctx.instanceId = _tag.substr(sep + 1);
ctx.instanceId.split('~').forEach((s, i) => {
if (i) {
var A = s.indexOf('(');
var Z = A >= 0 ? s.lastIndexOf(')') : -1;
var key = Z >= 0 ? s.substr(0, A) : s;
var value = A < Z ? s.substr(A + 1, Z - A - 1) : '';
if (key === 'hidden') {
ctx.hiddenId = value;
} else if (key === 'private') {
ctx.privateId = value;
} else if (key === 'friends') {
ctx.friendsId = value;
} else if (key === 'canRequestInvite') {
ctx.canRequestInvite = true;
} else if (key === 'region') {
ctx.region = value;
} else if (key === 'group') {
ctx.groupId = value;
} else if (key === 'groupAccessType') {
ctx.groupAccessType = value;
} else if (key === 'strict') {
ctx.strict = true;
} else if (key === 'ageGate') {
ctx.ageGate = true;
}
} else {
ctx.instanceName = s;
}
});
ctx.accessType = 'public';
if (ctx.privateId !== null) {
if (ctx.canRequestInvite) {
// InvitePlus
ctx.accessType = 'invite+';
} else {
// InviteOnly
ctx.accessType = 'invite';
}
ctx.userId = ctx.privateId;
} else if (ctx.friendsId !== null) {
// FriendsOnly
ctx.accessType = 'friends';
ctx.userId = ctx.friendsId;
} else if (ctx.hiddenId !== null) {
// FriendsOfGuests
ctx.accessType = 'friends+';
ctx.userId = ctx.hiddenId;
} else if (ctx.groupId !== null) {
// Group
ctx.accessType = 'group';
}
ctx.accessTypeName = ctx.accessType;
if (ctx.groupAccessType !== null) {
if (ctx.groupAccessType === 'public') {
ctx.accessTypeName = 'groupPublic';
} else if (ctx.groupAccessType === 'plus') {
ctx.accessTypeName = 'groupPlus';
}
}
} else {
ctx.worldId = _tag;
}
}
return ctx;
}
export {
refreshInstancePlayerCount,
isRealInstance,
displayLocation,
parseLocation
};

View File

@@ -1,19 +1,3 @@
function getVRChatResolution(res) {
switch (res) {
case '1280x720':
return '1280x720 (720p)';
case '1920x1080':
return '1920x1080 (1080p)';
case '2560x1440':
return '2560x1440 (2K)';
case '3840x2160':
return '3840x2160 (4K)';
case '7680x4320':
return '7680x4320 (8K)';
}
return `${res} (Custom)`;
}
const VRChatScreenshotResolutions = [
{ name: '1280x720 (720p)', width: 1280, height: 720 },
{ name: '1920x1080 (1080p Default)', width: '', height: '' },
@@ -29,8 +13,4 @@ const VRChatCameraResolutions = [
{ name: '7680x4320 (8K)', width: 7680, height: 4320 }
];
export {
getVRChatResolution,
VRChatScreenshotResolutions,
VRChatCameraResolutions
};
export { VRChatScreenshotResolutions, VRChatCameraResolutions };

View File

@@ -0,0 +1,17 @@
function getVRChatResolution(res) {
switch (res) {
case '1280x720':
return '1280x720 (720p)';
case '1920x1080':
return '1920x1080 (1080p)';
case '2560x1440':
return '2560x1440 (2K)';
case '3840x2160':
return '3840x2160 (4K)';
case '7680x4320':
return '7680x4320 (8K)';
}
return `${res} (Custom)`;
}
export { getVRChatResolution };

View File

@@ -0,0 +1,106 @@
const photonEmojis = [
'Angry',
'Blushing',
'Crying',
'Frown',
'Hand Wave',
'Hang Ten',
'In Love',
'Jack O Lantern',
'Kiss',
'Laugh',
'Skull',
'Smile',
'Spooky Ghost',
'Stoic',
'Sunglasses',
'Thinking',
'Thumbs Down',
'Thumbs Up',
'Tongue Out',
'Wow',
'Arrow Point',
"Can't see",
'Hourglass',
'Keyboard',
'No Headphones',
'No Mic',
'Portal',
'Shush',
'Bats',
'Cloud',
'Fire',
'Snow Fall',
'Snowball',
'Splash',
'Web',
'Beer',
'Candy',
'Candy Cane',
'Candy Corn',
'Champagne',
'Drink',
'Gingerbread',
'Ice Cream',
'Pineapple',
'Pizza',
'Tomato',
'Beachball',
'Coal',
'Confetti',
'Gift',
'Gifts',
'Life Ring',
'Mistletoe',
'Money',
'Neon Shades',
'Sun Lotion',
'Boo',
'Broken Heart',
'Exclamation',
'Go',
'Heart',
'Music Note',
'Question',
'Stop',
'Zzz'
];
const photonEventType = [
'MeshVisibility',
'AnimationFloat',
'AnimationBool',
'AnimationTrigger',
'AudioTrigger',
'PlayAnimation',
'SendMessage',
'SetParticlePlaying',
'TeleportPlayer',
'RunConsoleCommand',
'SetGameObjectActive',
'SetWebPanelURI',
'SetWebPanelVolume',
'SpawnObject',
'SendRPC',
'ActivateCustomTrigger',
'DestroyObject',
'SetLayer',
'SetMaterial',
'AddHealth',
'AddDamage',
'SetComponentActive',
'AnimationInt',
'AnimationIntAdd',
'AnimationIntSubtract',
'AnimationIntMultiply',
'AnimationIntDivide',
'AddVelocity',
'SetVelocity',
'AddAngularVelocity',
'SetAngularVelocity',
'AddForce',
'SetUIText',
'CallUdonMethod'
];
export { photonEmojis, photonEventType };

View File

@@ -0,0 +1,318 @@
import Noty from 'noty';
import utils from '../../classes/utils';
import { compareUnityVersion } from '../avatar/utils';
function getAvailablePlatforms(unityPackages) {
var isPC = false;
var isQuest = false;
var isIos = false;
if (typeof unityPackages === 'object') {
for (var unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
isPC = true;
} else if (unityPackage.platform === 'android') {
isQuest = true;
} else if (unityPackage.platform === 'ios') {
isIos = true;
}
}
}
return { isPC, isQuest, isIos };
}
function downloadAndSaveJson(fileName, data) {
if (!fileName || !data) {
return;
}
try {
var link = document.createElement('a');
link.setAttribute(
'href',
`data:application/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(data, null, 2)
)}`
);
link.setAttribute('download', `${fileName}.json`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
new Noty({
type: 'error',
text: utils.escapeTag('Failed to download JSON.')
}).show();
}
}
async function deleteVRChatCache(ref) {
var assetUrl = '';
var variant = '';
for (var i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
if (unityPackage.variant !== 'standard') {
variant = unityPackage.variant;
}
break;
}
}
var id = extractFileId(assetUrl);
var version = parseInt(extractFileVersion(assetUrl), 10);
var variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
await AssetBundleManager.DeleteCache(id, version, variant, variantVersion);
}
async function checkVRChatCache(ref) {
if (!ref.unityPackages) {
return { Item1: -1, Item2: false, Item3: '' };
}
var assetUrl = '';
var variant = '';
for (var i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (unityPackage.variant && unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
if (unityPackage.variant !== 'standard') {
variant = unityPackage.variant;
}
break;
}
}
if (!assetUrl) {
assetUrl = ref.assetUrl;
}
var id = extractFileId(assetUrl);
var version = parseInt(extractFileVersion(assetUrl), 10);
var variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
if (!id || !version) {
return { Item1: -1, Item2: false, Item3: '' };
}
return AssetBundleManager.CheckVRChatCache(
id,
version,
variant,
variantVersion
);
}
function copyToClipboard(text, message = 'Copied successfully!') {
navigator.clipboard
.writeText(text)
.then(() => {
window.$app.$message({
message: message,
type: 'success'
});
})
.catch((err) => {
console.error('Copy failed:', err);
window.$app.$message.error('Copy failed!');
});
}
function getFaviconUrl(resource) {
try {
const url = new URL(resource);
return `https://icons.duckduckgo.com/ip2/${url.host}.ico`;
} catch (err) {
console.error('Invalid URL:', err);
return '';
}
}
function convertFileUrlToImageUrl(url, resolution = 128) {
if (!url) {
return '';
}
/**
* possible patterns?
* /file/file_fileId/version
* /file/file_fileId/version/
* /file/file_fileId/version/file
* /file/file_fileId/version/file/
*/
const pattern = /file\/file_([a-f0-9-]+)\/(\d+)(\/file)?\/?$/;
const match = url.match(pattern);
if (match) {
const fileId = match[1];
const version = match[2];
return `https://api.vrchat.cloud/api/1/image/file_${fileId}/${version}/${resolution}`;
}
// no match return origin url
return url;
}
function replaceVrcPackageUrl(url) {
if (!url) {
return '';
}
return url.replace('https://api.vrchat.cloud/', 'https://vrchat.com/');
}
function getLaunchURL(instance) {
var L = instance;
if (L.instanceId) {
if (L.shortName) {
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(
L.instanceId
)}&shortName=${encodeURIComponent(L.shortName)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(L.instanceId)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}`;
}
function extractFileId(s) {
var match = String(s).match(/file_[0-9A-Za-z-]+/);
return match ? match[0] : '';
}
function extractFileVersion(s) {
var match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s);
return match ? match[1] : '';
}
function extractVariantVersion(url) {
if (!url) {
return '0';
}
try {
const params = new URLSearchParams(new URL(url).search);
const version = params.get('v');
if (version) {
return version;
}
return '0';
} catch {
return '0';
}
}
export {
getAvailablePlatforms,
downloadAndSaveJson,
deleteVRChatCache,
checkVRChatCache,
copyToClipboard,
getFaviconUrl,
convertFileUrlToImageUrl,
replaceVrcPackageUrl,
getLaunchURL,
extractFileId,
extractFileVersion,
extractVariantVersion
};
// ---------------------- devtools method --------------------------
// not window.$app
window.getBundleLocation = async function (input) {
const $app = window.$app;
var assetUrl = input;
var variant = '';
if (assetUrl) {
// continue
} else if (
$app.avatarDialog.visible &&
$app.avatarDialog.ref.unityPackages.length > 0
) {
var unityPackages = $app.avatarDialog.ref.unityPackages;
for (let i = unityPackages.length - 1; i > -1; i--) {
var unityPackage = unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
if (unityPackage.variant !== 'standard') {
variant = unityPackage.variant;
}
break;
}
}
} else if ($app.avatarDialog.visible && $app.avatarDialog.ref.assetUrl) {
assetUrl = $app.avatarDialog.ref.assetUrl;
} else if (
$app.worldDialog.visible &&
$app.worldDialog.ref.unityPackages.length > 0
) {
var unityPackages = $app.worldDialog.ref.unityPackages;
for (let i = unityPackages.length - 1; i > -1; i--) {
var unityPackage = unityPackages[i];
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
break;
}
}
} else if ($app.worldDialog.visible && $app.worldDialog.ref.assetUrl) {
assetUrl = $app.worldDialog.ref.assetUrl;
}
if (!assetUrl) {
return null;
}
var fileId = extractFileId(assetUrl);
var fileVersion = parseInt(extractFileVersion(assetUrl), 10);
var variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
var assetLocation = await AssetBundleManager.GetVRChatCacheFullLocation(
fileId,
fileVersion,
variant,
variantVersion
);
var cacheInfo = await AssetBundleManager.CheckVRChatCache(
fileId,
fileVersion,
variant,
variantVersion
);
var inCache = false;
if (cacheInfo.Item1 > 0) {
inCache = true;
}
console.log(`InCache: ${inCache}`);
var fullAssetLocation = `${assetLocation}\\__data`;
console.log(fullAssetLocation);
return fullAssetLocation;
};

View File

@@ -0,0 +1,34 @@
const emojiAnimationStyleUrl =
'https://assets.vrchat.com/www/images/emoji-previews/';
const emojiAnimationStyleList = {
Aura: 'Preview_B2-Aura.gif',
Bats: 'Preview_B2-Fall_Bats.gif',
Bees: 'Preview_B2-Bees.gif',
Bounce: 'Preview_B2-Bounce.gif',
Cloud: 'Preview_B2-Cloud.gif',
Confetti: 'Preview_B2-Winter_Confetti.gif',
Crying: 'Preview_B2-Crying.gif',
Dislike: 'Preview_B2-Dislike.gif',
Fire: 'Preview_B2-Fire.gif',
Idea: 'Preview_B2-Idea.gif',
Lasers: 'Preview_B2-Lasers.gif',
Like: 'Preview_B2-Like.gif',
Magnet: 'Preview_B2-Magnet.gif',
Mistletoe: 'Preview_B2-Winter_Mistletoe.gif',
Money: 'Preview_B2-Money.gif',
Noise: 'Preview_B2-Noise.gif',
Orbit: 'Preview_B2-Orbit.gif',
Pizza: 'Preview_B2-Pizza.gif',
Rain: 'Preview_B2-Rain.gif',
Rotate: 'Preview_B2-Rotate.gif',
Shake: 'Preview_B2-Shake.gif',
Snow: 'Preview_B2-Spin.gif',
Snowball: 'Preview_B2-Winter_Snowball.gif',
Spin: 'Preview_B2-Spin.gif',
Splash: 'Preview_B2-SummerSplash.gif',
Stop: 'Preview_B2-Stop.gif',
ZZZ: 'Preview_B2-ZZZ.gif'
};
export { emojiAnimationStyleUrl, emojiAnimationStyleList };

View File

@@ -0,0 +1,73 @@
// vrchat to famfamfam language mappings
const languageMappings = {
eng: 'us',
kor: 'kr',
rus: 'ru',
spa: 'es',
por: 'pt',
zho: 'cn',
deu: 'de',
jpn: 'jp',
fra: 'fr',
swe: 'se',
nld: 'nl',
pol: 'pl',
dan: 'dk',
nor: 'no',
ita: 'it',
tha: 'th',
fin: 'fi',
hun: 'hu',
ces: 'cz',
tur: 'tr',
ara: 'ae',
ron: 'ro',
vie: 'vn',
ukr: 'ua',
ase: 'us',
bfi: 'gb',
dse: 'nl',
fsl: 'fr',
jsl: 'jp',
kvk: 'kr',
mlt: 'mt',
ind: 'id',
hrv: 'hr',
heb: 'he',
afr: 'af',
ben: 'be',
bul: 'bg',
cmn: 'cn',
cym: 'cy',
ell: 'el',
est: 'et',
fil: 'ph',
gla: 'gd',
gle: 'ga',
hin: 'hi',
hmn: 'cn',
hye: 'hy',
isl: 'is',
lav: 'lv',
lit: 'lt',
ltz: 'lb',
mar: 'hi',
mkd: 'mk',
msa: 'my',
sco: 'gd',
slk: 'sk',
slv: 'sl',
tel: 'hi',
mri: 'nz',
wuu: 'cn',
yue: 'cn',
tws: 'cn',
asf: 'au',
nzs: 'nz',
gsg: 'de',
epo: 'eo',
tok: 'tok'
};
export { languageMappings };

View File

@@ -0,0 +1,16 @@
const userDialogGroupSortingOptions = {
alphabetical: {
name: 'dialog.user.groups.sorting.alphabetical',
value: 'alphabetical'
},
members: {
name: 'dialog.user.groups.sorting.members',
value: 'members'
},
inGame: {
name: 'dialog.user.groups.sorting.in_game',
value: 'inGame'
}
};
export { userDialogGroupSortingOptions };

View File

@@ -0,0 +1,79 @@
import { languageMappings } from './constants/language';
function userOnlineForTimestamp(ctx) {
if (ctx.ref.state === 'online' && ctx.ref.$online_for) {
return ctx.ref.$online_for;
} else if (ctx.ref.state === 'active' && ctx.ref.$active_for) {
return ctx.ref.$active_for;
} else if (ctx.ref.$offline_for) {
return ctx.ref.$offline_for;
}
return 0;
}
function languageClass(language) {
const style = {};
const mapping = languageMappings[language];
if (typeof mapping !== 'undefined') {
style[mapping] = true;
} else {
style.unknown = true;
}
return style;
}
function getPrintFileName(print) {
const authorName = print.authorName;
// fileDate format: 2024-11-03_16-14-25.757
const createdAt = getPrintLocalDate(print);
const fileNameDate = createdAt
.toISOString()
.replace(/:/g, '-')
.replace(/T/g, '_')
.replace(/Z/g, '');
const fileName = `${authorName}_${fileNameDate}_${print.id}.png`;
return fileName;
}
function getPrintLocalDate(print) {
if (print.createdAt) {
const createdAt = new Date(print.createdAt);
// cursed convert to local time
createdAt.setMinutes(
createdAt.getMinutes() - createdAt.getTimezoneOffset()
);
return createdAt;
}
if (print.timestamp) {
return new Date(print.timestamp);
}
const createdAt = new Date();
// cursed convert to local time
createdAt.setMinutes(
createdAt.getMinutes() - createdAt.getTimezoneOffset()
);
return createdAt;
}
function isFriendOnline(friend) {
if (typeof friend === 'undefined' || typeof friend.ref === 'undefined') {
return false;
}
if (friend.state === 'online') {
return true;
}
if (friend.state !== 'online' && friend.ref.location !== 'private') {
// wat
return true;
}
return false;
}
export {
userOnlineForTimestamp,
languageClass,
getPrintFileName,
getPrintLocalDate,
isFriendOnline
};

View File

@@ -1,104 +0,0 @@
mixin boops
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendBoopDialog'
:visible.sync='sendBoopDialog.visible'
:title='$t("dialog.boop_dialog.header")'
width='450px')
div(v-if='sendBoopDialog.visible')
el-select(
v-model='sendBoopDialog.userId'
:placeholder='$t("dialog.new_instance.instance_creator_placeholder")'
filterable
style='width: 100%')
el-option-group(v-if='vipFriends.length' :label='$t("side_panel.favorite")')
el-option.x-friend-item(
v-for='friend in vipFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='onlineFriends.length' :label='$t("side_panel.online")')
el-option.x-friend-item(
v-for='friend in onlineFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='activeFriends.length' :label='$t("side_panel.active")')
el-option.x-friend-item(
v-for='friend in activeFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='offlineFriends.length' :label='$t("side_panel.offline")')
el-option.x-friend-item(
v-for='friend in offlineFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
br
br
el-select(
v-model='sendBoopDialog.fileId'
clearable
:placeholder='$t("dialog.boop_dialog.select_emoji")'
size='small'
style='width: 100%'
popper-class='max-height-el-select')
el-option-group(:label='$t("dialog.boop_dialog.my_emojis")')
el-option(
v-if='image.versions && image.versions.length > 0'
v-for='image in emojiTable'
:key='image.id'
:value='image.id'
style='width: 100%; height: 100%')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
style='overflow: hidden; width: 200px; height: 200px; padding: 10px')
template(v-if='image.frames')
.avatar(
:style='generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)')
template(v-else)
img.avatar(
v-lazy='image.versions[image.versions.length - 1].file.url'
style='width: 200px; height: 200px')
el-option-group(:label='$t("dialog.boop_dialog.default_emojis")')
el-option(
v-for='emojiName in photonEmojis'
:key='emojiName'
:value='getEmojiValue(emojiName)'
style='width: 100%; height: 100%')
span(v-text='emojiName')
template(#footer)
el-button(size='small' @click='showGalleryDialog(2)') {{ $t('dialog.boop_dialog.emoji_manager') }}
el-button(size='small' @click='sendBoopDialog.visible = false') {{ $t('dialog.boop_dialog.cancel') }}
el-button(size='small' @click='sendBoop' :disabled='!sendBoopDialog.userId') {{ $t('dialog.boop_dialog.send') }}

View File

@@ -1,444 +0,0 @@
mixin currentUser
//- dialog: social status
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='socialStatusDialog'
:visible.sync='socialStatusDialog.visible'
:title='$t("dialog.social_status.header")'
width='400px')
div(v-loading='socialStatusDialog.loading')
el-collapse(style='border: 0')
el-collapse-item
template(slot='title')
span(style='font-size: 16px') {{ $t('dialog.social_status.history') }}
data-tables(
v-bind='socialStatusHistoryTable'
@row-click='setSocialStatusFromHistory'
style='cursor: pointer')
el-table-column(:label='$t("table.social_status.no")' prop='no' width='50')
el-table-column(:label='$t("table.social_status.status")' prop='status')
el-select(v-model='socialStatusDialog.status' style='display: block; margin-top: 10px')
el-option(:label='$t("dialog.user.status.join_me")' value='join me').
#[i.x-user-status.joinme] {{ $t('dialog.user.status.join_me') }}
el-option(:label='$t("dialog.user.status.online")' value='active').
#[i.x-user-status.online] {{ $t('dialog.user.status.online') }}
el-option(:label='$t("dialog.user.status.ask_me")' value='ask me').
#[i.x-user-status.askme] {{ $t('dialog.user.status.ask_me') }}
el-option(:label='$t("dialog.user.status.busy")' value='busy').
#[i.x-user-status.busy] {{ $t('dialog.user.status.busy') }}
el-option(
v-if='API.currentUser.$isModerator'
:label='$t("dialog.user.status.offline")'
value='offline').
#[i.x-user-status.offline] {{ $t('dialog.user.status.offline') }}
el-input(
v-model='socialStatusDialog.statusDescription'
:placeholder='$t("dialog.social_status.status_placeholder")'
maxlength='32'
show-word-limit
style='display: block; margin-top: 10px')
template(#footer)
el-button(type='primary' size='small' :disabled='socialStatusDialog.loading' @click='saveSocialStatus') {{ $t('dialog.social_status.update') }}
//- dialog: language
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='languageDialog'
:visible.sync='languageDialog.visible'
:title='$t("dialog.language.header")'
width='400px')
div(v-loading='languageDialog.loading')
div(style='margin: 6px 0' v-for='item in API.currentUser.$languages' :key='item.key')
el-tag(
size='small'
type='info'
effect='plain'
closable
@close='removeUserLanguage(item.key)'
style='margin-right: 5px')
span.flags(:class='languageClass(item.key)' style='display: inline-block; margin-right: 5px')
| {{ item.value }} ({{ item.key.toUpperCase() }})
el-select(
value=''
:disabled='languageDialog.loading || (API.currentUser.$languages && API.currentUser.$languages.length === 3)'
:placeholder='$t("dialog.language.select_language")'
@change='addUserLanguage'
style='margin-top: 14px')
el-option(
v-for='item in languageDialog.languages'
:key='item.key'
:value='item.key'
:label='item.value')
span.flags(:class='languageClass(item.key)' style='display: inline-block; margin-right: 5px')
| {{ item.value }} ({{ item.key.toUpperCase() }})
//- dialog: bio
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='bioDialog'
:visible.sync='bioDialog.visible'
:title='$t("dialog.bio.header")'
width='600px')
div(v-loading='bioDialog.loading')
el-input(
type='textarea'
v-model='bioDialog.bio'
size='mini'
maxlength='512'
show-word-limit
:autosize='{ minRows: 5, maxRows: 20 }'
:placeholder='$t("dialog.bio.bio_placeholder")'
style='margin-bottom: 10px')
el-input(
v-for='(link, index) in bioDialog.bioLinks'
:key='index'
:value='link'
v-model='bioDialog.bioLinks[index]'
size='small'
style='margin-top: 5px')
img(slot='prepend' :src='getFaviconUrl(link)' style='width: 16px; height: 16px')
el-button(slot='append' icon='el-icon-delete' @click='bioDialog.bioLinks.splice(index, 1)')
el-button(
@click='bioDialog.bioLinks.push("")'
:disabled='bioDialog.bioLinks.length >= 3'
size='mini'
style='margin-top: 5px') {{ $t('dialog.bio.add_link') }}
template(#footer)
el-button(type='primary' size='small' :disabled='bioDialog.loading' @click='saveBio') {{ $t('dialog.bio.update') }}
//- dialog: pronouns
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='pronounsDialog'
:visible.sync='pronounsDialog.visible'
:title='$t("dialog.pronouns.header")'
width='600px')
div(v-loading='pronounsDialog.loading')
el-input(
type='textarea'
v-model='pronounsDialog.pronouns'
size='mini'
maxlength='32'
show-word-limit
:autosize='{ minRows: 2, maxRows: 5 }'
:placeholder='$t("dialog.pronouns.pronouns_placeholder")')
template(#footer)
el-button(type='primary' size='small' :disabled='pronounsDialog.loading' @click='savePronouns') {{ $t('dialog.pronouns.update') }}
//- dialog: Gallery/VRCPlusIcons
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='galleryDialog'
:visible.sync='galleryDialogVisible'
:title='$t("dialog.gallery_icons.header")'
width='97vw'
top='5vh'
destroy-on-close)
el-tabs(type='card' ref='galleryTabs')
el-tab-pane(v-if='galleryDialogVisible' v-loading='galleryDialogGalleryLoading')
span(slot='label') {{ $t('dialog.gallery_icons.gallery') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ galleryTable.length }}/64
input#GalleryUploadButton(
type='file'
accept='image/*'
@change='onFileChangeGallery'
style='display: none')
span {{ $t('dialog.gallery_icons.recommended_image_size') }}: 1200x900px (4:3)
br
br
el-button-group
el-button(type='default' size='small' @click='refreshGalleryTable' icon='el-icon-refresh') {{ $t('dialog.gallery_icons.refresh') }}
el-button(
type='default'
size='small'
@click='displayGalleryUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_icons.upload') }}
el-button(
type='default'
size='small'
@click='setProfilePicOverride("")'
icon='el-icon-close'
:disabled='!API.currentUser.profilePicOverride') {{ $t('dialog.gallery_icons.clear') }}
br
.x-friend-item(
v-if='image.versions && image.versions.length > 0'
v-for='image in galleryTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
@click='setProfilePicOverride(image.id)'
:class='{ "current-vrcplus-icon": compareCurrentProfilePic(image.id) }')
img.avatar(v-lazy='image.versions[image.versions.length - 1].file.url')
div(style='float: right; margin-top: 5px')
el-button(
type='default'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)'
size='mini'
icon='el-icon-picture-outline'
circle)
el-button(
type='default'
@click='deleteGalleryImage(image.id)'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tab-pane(v-if='galleryDialogVisible' v-loading='galleryDialogIconsLoading' lazy)
span(slot='label') {{ $t('dialog.gallery_icons.icons') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ VRCPlusIconsTable.length }}/64
input#VRCPlusIconUploadButton(
type='file'
accept='image/*'
@change='onFileChangeVRCPlusIcon'
style='display: none')
span {{ $t('dialog.gallery_icons.recommended_image_size') }}: 2048x2048px (1:1)
br
br
el-button-group
el-button(type='default' size='small' @click='refreshVRCPlusIconsTable' icon='el-icon-refresh') {{ $t('dialog.gallery_icons.refresh') }}
el-button(
type='default'
size='small'
@click='displayVRCPlusIconUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_icons.upload') }}
el-button(
type='default'
size='small'
@click='setVRCPlusIcon("")'
icon='el-icon-close'
:disabled='!API.currentUser.userIcon') {{ $t('dialog.gallery_icons.clear') }}
br
.x-friend-item(
v-if='image.versions && image.versions.length > 0'
v-for='image in VRCPlusIconsTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
@click='setVRCPlusIcon(image.id)'
:class='{ "current-vrcplus-icon": compareCurrentVRCPlusIcon(image.id) }')
img.avatar(v-lazy='image.versions[image.versions.length - 1].file.url')
div(style='float: right; margin-top: 5px')
el-button(
type='default'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)'
size='mini'
icon='el-icon-picture-outline'
circle)
el-button(
type='default'
@click='deleteVRCPlusIcon(image.id)'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tab-pane(v-if='galleryDialogVisible' v-loading='galleryDialogEmojisLoading' lazy)
span(slot='label') {{ $t('dialog.gallery_icons.emojis') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ emojiTable.length }}/9
input#EmojiUploadButton(type='file' accept='image/*' @change='onFileChangeEmoji' style='display: none')
span {{ $t('dialog.gallery_icons.recommended_image_size') }}: 1024x1024px (1:1)
br
br
div(style='display: flex; align-items: center')
el-button-group(style='margin-right: 10px')
el-button(type='default' size='small' @click='refreshEmojiTable' icon='el-icon-refresh') {{ $t('dialog.gallery_icons.refresh') }}
el-button(
type='default'
size='small'
@click='displayEmojiUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_icons.upload') }}
el-select(v-model='emojiAnimationStyle' popper-class='max-height-el-select')
el-option-group {{ $t('dialog.gallery_icons.emoji_animation_styles') }}
el-option.x-friend-item(
v-for='(fileName, styleName) in emojiAnimationStyleList'
:key='styleName'
:label='styleName'
:value='styleName'
style='height: auto')
.avatar(style='width: 200px; height: 200px')
img(v-lazy='`${emojiAnimationStyleUrl}${fileName}`')
.detail
span.name(v-text='styleName' style='margin-right: 100px')
el-checkbox(v-model='emojiAnimType' style='margin-left: 10px; margin-right: 10px')
span {{ $t('dialog.gallery_icons.emoji_animation_type') }}
template(v-if='emojiAnimType')
span(style='margin-right: 10px') {{ $t('dialog.gallery_icons.emoji_animation_fps') }}
el-input-number(
size='small'
v-model='emojiAnimFps'
:min='1'
:max='64'
style='margin-right: 10px; width: 112px')
span(style='margin-right: 10px') {{ $t('dialog.gallery_icons.emoji_animation_frame_count') }}
el-input-number(
size='small'
v-model='emojiAnimFrameCount'
:min='2'
:max='64'
style='margin-right: 10px; width: 112px')
el-checkbox(v-model='emojiAnimLoopPingPong' style='margin-left: 10px; margin-right: 10px')
span {{ $t('dialog.gallery_icons.emoji_loop_pingpong') }}
br
br
span {{ $t('dialog.gallery_icons.flipbook_info') }}
br
.x-friend-item(
v-if='image.versions && image.versions.length > 0'
v-for='image in emojiTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
style='overflow: hidden'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url, getEmojiFileName(image))')
template(v-if='image.frames')
.avatar(
:style='generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)')
template(v-else)
img.avatar(v-lazy='image.versions[image.versions.length - 1].file.url')
div(style='display: inline-block; margin: 5px')
span(v-if='image.loopStyle === "pingpong"') #[i.el-icon-refresh.el-icon--left]
span(style='margin-right: 5px') {{ image.animationStyle }}
span(v-if='image.framesOverTime' style='margin-right: 5px') {{ image.framesOverTime }}fps
span(v-if='image.frames' style='margin-right: 5px') {{ image.frames }}frames
br
div(style='float: right; margin-top: 5px')
el-button(
type='default'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url, getEmojiFileName(image))'
size='mini'
icon='el-icon-picture-outline'
circle)
el-button(
type='default'
@click='deleteEmoji(image.id)'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tab-pane(v-if='galleryDialogVisible' v-loading='galleryDialogStickersLoading' lazy)
span(slot='label') {{ $t('dialog.gallery_icons.stickers') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ stickerTable.length }}/9
input#StickerUploadButton(
type='file'
accept='image/*'
@change='onFileChangeSticker'
style='display: none')
span {{ $t('dialog.gallery_icons.recommended_image_size') }}: 1024x1024px (1:1)
br
br
el-button-group
el-button(type='default' size='small' @click='refreshStickerTable' icon='el-icon-refresh') {{ $t('dialog.gallery_icons.refresh') }}
el-button(
type='default'
size='small'
@click='displayStickerUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_icons.upload') }}
br
.x-friend-item(
v-if='image.versions && image.versions.length > 0'
v-for='image in stickerTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
style='overflow: hidden'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)')
img.avatar(v-lazy='image.versions[image.versions.length - 1].file.url')
div(style='float: right; margin-top: 5px')
el-button(
type='default'
@click='showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)'
size='mini'
icon='el-icon-picture-outline'
circle)
el-button(
type='default'
@click='deleteSticker(image.id)'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tab-pane(v-if='galleryDialogVisible' v-loading='galleryDialogPrintsLoading' lazy)
span(slot='label') {{ $t('dialog.gallery_icons.prints') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ printTable.length }}/64
input#PrintUploadButton(type='file' accept='image/*' @change='onFileChangePrint' style='display: none')
span {{ $t('dialog.gallery_icons.recommended_image_size') }}: 1920x1080px (16:9)
br
br
div(style='display: flex; align-items: center')
el-button-group
el-button(type='default' size='small' @click='refreshPrintTable' icon='el-icon-refresh') {{ $t('dialog.gallery_icons.refresh') }}
el-button(
type='default'
size='small'
@click='displayPrintUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_icons.upload') }}
el-input(
type='textarea'
v-model='printUploadNote'
size='mini'
rows='1'
resize='none'
maxlength='32'
style='margin-left: 10px; width: 300px'
:placeholder='$t("dialog.gallery_icons.note")')
el-checkbox(v-model='printCropBorder' style='margin-left: 10px; margin-right: 10px')
span {{ $t('dialog.gallery_icons.crop_print_border') }}
br
.x-friend-item(
v-for='image in printTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
style='overflow: hidden'
@click='showFullscreenImageDialog(image.files.image, getPrintFileName(image))')
img.avatar(v-lazy='image.files.image')
div(style='margin-top: 5px; width: 208px')
span.x-ellipsis(v-if='image.note' v-text='image.note' style='display: block')
span(v-else style='display: block') &nbsp;
location.x-ellipsis(
v-if='image.worldId'
:location='image.worldId'
:hint='image.worldName'
style='display: block')
span(v-else style='display: block') &nbsp;
display-name.x-ellipsis(
v-if='image.authorId'
:userid='image.authorId'
:hint='image.authorName'
style='color: #909399; font-family: monospace; display: block')
span(v-else style='font-family: monospace; display: block') &nbsp;
span.x-ellipsis(
v-if='image.createdAt'
style='color: #909399; font-family: monospace; font-size: 11px; display: block') {{ image.createdAt | formatDate('long') }}
span(v-else style='display: block') &nbsp;
div(style='float: right')
el-button(
type='default'
@click='showFullscreenImageDialog(image.files.image, getPrintFileName(image))'
size='mini'
icon='el-icon-picture-outline'
circle)
el-button(
type='default'
@click='deletePrint(image.id)'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')

View File

@@ -0,0 +1,215 @@
mixin dialogs
//- previous instances
PreviousInstancesInfoDialog(
:visible.sync='previousInstancesInfoDialogVisible'
:instanceId='previousInstancesInfoDialogInstanceId'
:gameLogIsFriend='gameLogIsFriend'
:gameLogIsFavorite='gameLogIsFavorite'
:lookupUser='lookupUser'
:isDarkMode='isDarkMode')
//- favorites
FriendImportDialog(
:friendImportDialogVisible.sync='friendImportDialogVisible'
:friendImportDialogInput.sync='friendImportDialogInput')
WorldImportDialog(
:worldImportDialogVisible.sync='worldImportDialogVisible'
:worldImportDialogInput.sync='worldImportDialogInput'
:getLocalWorldFavoriteGroupLength='getLocalWorldFavoriteGroupLength'
:localWorldFavoriteGroups='localWorldFavoriteGroups'
@addLocalWorldFavorite='addLocalWorldFavorite')
AvatarImportDialog(
:avatarImportDialogVisible.sync='avatarImportDialogVisible'
:avatarImportDialogInput.sync='avatarImportDialogInput'
:getLocalAvatarFavoriteGroupLength='getLocalAvatarFavoriteGroupLength'
:localAvatarFavoriteGroups='localAvatarFavoriteGroups'
@addLocalAvatarFavorite='addLocalAvatarFavorite')
//- favorites dialog
ChooseFavoriteGroupDialog(
:favoriteDialog.sync='favoriteDialog'
:localAvatarFavoriteGroups='localAvatarFavoriteGroups'
:localWorldFavoriteGroups='localWorldFavoriteGroups'
:hasLocalWorldFavorite='hasLocalWorldFavorite'
:getLocalWorldFavoriteGroupLength='getLocalWorldFavoriteGroupLength'
:hasLocalAvatarFavorite='hasLocalAvatarFavorite'
:getLocalAvatarFavoriteGroupLength='getLocalAvatarFavoriteGroupLength'
@addLocalWorldFavorite='addLocalWorldFavorite'
@removeLocalWorldFavorite='removeLocalWorldFavorite'
@addLocalAvatarFavorite='addLocalAvatarFavorite'
@removeLocalAvatarFavorite='removeLocalAvatarFavorite'
@deleteFavoriteNoConfirm='deleteFavoriteNoConfirm')
//- launch
LaunchDialog(
:checkCanInvite='checkCanInvite'
:launchDialogData.sync='launchDialogData'
:hideTooltips='hideTooltips'
:vipFriends='vipFriends'
:onlineFriends='onlineFriends'
:activeFriends='activeFriends'
:inviteMessageTable='inviteMessageTable'
:uploadImage='uploadImage'
:lastLocation='lastLocation'
@launchGame='launchGame')
UserDialog(
:userDialog='userDialog'
:languageDialog='languageDialog'
:hideTooltips='hideTooltips'
:lastLocation='lastLocation'
:lastLocationDestination='lastLocationDestination'
:isGameRunning='isGameRunning'
:checkCanInvite='checkCanInvite'
:updateInstanceInfo='updateInstanceInfo'
:hideUserNotes='hideUserNotes'
:hideUserMemos='hideUserMemos'
:userOnlineFor='userOnlineFor'
:userDialogWorldSortingOptions='userDialogWorldSortingOptions'
:userDialogWorldOrderOptions='userDialogWorldOrderOptions'
:avatarRemoteDatabase='avatarRemoteDatabase'
:friendLogTable='friendLogTable'
:lookupAvatars='lookupAvatars'
:updateInGameGroupOrder='updateInGameGroupOrder'
:inviteMessageTable='inviteMessageTable'
:uploadImage='uploadImage'
:inviteRequestMessageTable='inviteRequestMessageTable'
:inGameGroupOrder='inGameGroupOrder'
:shiftHeld='shiftHeld'
:vipFriends='vipFriends'
:onlineFriends='onlineFriends'
:offlineFriends='offlineFriends'
:activeFriends='activeFriends'
:galleryDialogVisible='galleryDialogVisible'
:galleryDialogGalleryLoading='galleryDialogGalleryLoading'
:galleryDialogIconsLoading='galleryDialogIconsLoading'
:galleryDialogEmojisLoading='galleryDialogEmojisLoading'
:galleryDialogStickersLoading='galleryDialogStickersLoading'
:galleryDialogPrintsLoading='galleryDialogPrintsLoading'
:galleryTable='galleryTable'
:VRCPlusIconsTable='VRCPlusIconsTable'
:emojiTable='emojiTable'
:stickerTable='stickerTable'
:printUploadNote='printUploadNote'
:printCropBorder='printCropBorder'
:printTable='printTable'
@refreshGalleryTable='refreshGalleryTable'
@refreshEmojiTable='refreshEmojiTable'
@refreshStickerTable='refreshStickerTable'
@refreshVRCPlusIconsTable='refreshVRCPlusIconsTable'
@refreshPrintTable='refreshPrintTable'
@sortUserDialogAvatars='sortUserDialogAvatars'
@logout='logout'
@showAvatarAuthorDialog='showAvatarAuthorDialog'
@showGalleryDialog='showGalleryDialog'
@saveCurrentUserGroups='saveCurrentUserGroups'
@refreshUserDialogAvatars='refreshUserDialogAvatars'
@refreshUserDialogTreeData='refreshUserDialogTreeData'
@saveUserMemo='saveUserMemo'
@setGroupVisibility='setGroupVisibility'
@leaveGroupPrompt='leaveGroupPrompt'
@clearInviteImageUpload='clearInviteImageUpload'
@closeGalleryDialog='galleryDialogVisible = false')
WorldDialog(
:worldDialog.sync='worldDialog'
:hideTooltips='hideTooltips'
:shiftHeld='shiftHeld'
:isGameRunning='isGameRunning'
:lastLocation='lastLocation'
:instanceJoinHistory='instanceJoinHistory'
:updateInstanceInfo='updateInstanceInfo'
:isAgeGatedInstancesVisible='isAgeGatedInstancesVisible'
:createNewInstance='createNewInstance'
:instanceContentSettings='instanceContentSettings'
:offlineFriends='offlineFriends'
:activeFriends='activeFriends'
:onlineFriends='onlineFriends'
:vipFriends='vipFriends'
:inviteMessageTable='inviteMessageTable'
:uploadImage='uploadImage'
@openFolderGeneric='openFolderGeneric'
@deleteVRChatCache='deleteVRChatCache'
@worldDialogCommand='worldDialogCommand')
GroupDialog(
:groupDialog.sync='groupDialog'
:hideTooltips='hideTooltips'
:lastLocation='lastLocation'
:updateInstanceInfo='updateInstanceInfo'
:groupDialogSortingOptions='groupDialogSortingOptions'
:groupDialogFilterOptions='groupDialogFilterOptions'
:isGroupGalleryLoading='isGroupGalleryLoading'
:randomUserColours='randomUserColours'
@updateGroupPostSearch='updateGroupPostSearch'
@groupDialogCommand='groupDialogCommand'
@getGroupDialogGroup='getGroupDialogGroup')
AvatarDialog(
:avatarDialog='avatarDialog'
:hideTooltips='hideTooltips'
:isGameRunning='isGameRunning'
@openFolderGeneric='openFolderGeneric'
@deleteVRChatCache='deleteVRChatCache')
FullscreenImageDialog(:fullscreenImageDialog='fullscreenImageDialog')
//- settings
FeedFiltersDialog(
:feedFiltersDialogMode.sync='feedFiltersDialogMode'
:photonLoggingEnabled='photonLoggingEnabled'
:sharedFeedFilters='sharedFeedFilters'
:sharedFeedFiltersDefaults='sharedFeedFiltersDefaults'
@updateSharedFeed='updateSharedFeed')
LaunchOptionsDialog(:isLaunchOptionsDialogVisible.sync='isLaunchOptionsDialogVisible')
OpenSourceSoftwareNoticeDialog(:ossDialog.sync='ossDialog')
ChangelogDialog(:changeLogDialog.sync='changeLogDialog')
ScreenshotMetadataDialog(
:screenshotMetadataDialog='screenshotMetadataDialog'
:currentlyDroppingFile='currentlyDroppingFile'
:fullscreenImageDialog='fullscreenImageDialog'
@lookupUser='lookupUser')
EditInviteMessageDialog(:editInviteMessageDialog.sync='editInviteMessageDialog')
NoteExportDialog(:isNoteExportDialogVisible.sync='isNoteExportDialogVisible' :friends='friends')
VRChatConfigDialog(
:isVRChatConfigDialogVisible.sync='isVRChatConfigDialogVisible'
:VRChatUsedCacheSize='VRChatUsedCacheSize'
:VRChatTotalCacheSize='VRChatTotalCacheSize'
:VRChatCacheSizeLoading='VRChatCacheSizeLoading'
:folderSelectorDialog='folderSelectorDialog'
:hideTooltips='hideTooltips'
@getVRChatCacheSize='getVRChatCacheSize'
@sweepVRChatCache='sweepVRChatCache')
YouTubeApiDialog(
:isYouTubeApiDialogVisible.sync='isYouTubeApiDialogVisible'
:lookupYouTubeVideo='lookupYouTubeVideo'
:youTubeApiKey.sync='youTubeApiKey')
NotificationPositionDialog(
:isNotificationPositionDialogVisible.sync='isNotificationPositionDialogVisible'
:notificationPosition.sync='notificationPosition'
@changeNotificationPosition='changeNotificationPosition')
AvatarProviderDialog(
:isAvatarProviderDialogVisible.sync='isAvatarProviderDialogVisible'
:avatarRemoteDatabaseProviderList='avatarRemoteDatabaseProviderList'
@saveAvatarProviderList='saveAvatarProviderList'
@removeAvatarProvider='removeAvatarProvider')
RegistryBackupDialog(
:isRegistryBackupDialogVisible.sync='isRegistryBackupDialogVisible'
:backupVrcRegistry='backupVrcRegistry')
PrimaryPasswordDialog(
:enablePrimaryPasswordDialog.sync='enablePrimaryPasswordDialog'
@setPrimaryPassword='setPrimaryPassword')

View File

@@ -1,150 +0,0 @@
mixin images
//- dialog: Change avatar image
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='changeAvatarImageDialog'
:visible.sync='changeAvatarImageDialogVisible'
:title='$t("dialog.change_content_image.avatar")'
width='850px')
div(v-if='changeAvatarImageDialogVisible' v-loading='changeAvatarImageDialogLoading')
input#AvatarImageUploadButton(
type='file'
accept='image/*'
@change='onFileChangeAvatarImage'
style='display: none')
span {{ $t('dialog.change_content_image.description') }}
br
el-button-group(style='padding-bottom: 10px; padding-top: 10px')
el-button(
type='default'
size='small'
@click='displayPreviousImages("Avatar", "Change")'
icon='el-icon-refresh') {{ $t('dialog.change_content_image.refresh') }}
el-button(type='default' size='small' @click='uploadAvatarImage' icon='el-icon-upload2') {{ $t('dialog.change_content_image.upload') }}
//- el-button(type="default" size="small" @click="deleteAvatarImage" icon="el-icon-delete") Delete Latest Image
br
div(
style='display: inline-block'
v-for='image in previousImagesTable'
:key='image.version'
v-if='image.file')
.x-change-image-item(
@click='setAvatarImage(image)'
style='cursor: pointer'
:class='{ "current-image": compareCurrentImage(image) }')
img.image(v-lazy='image.file.url')
//- dialog: Change world image
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='changeWorldImageDialog'
:visible.sync='changeWorldImageDialogVisible'
:title='$t("dialog.change_content_image.world")'
width='850px')
div(v-if='changeWorldImageDialogVisible' v-loading='changeWorldImageDialogLoading')
input#WorldImageUploadButton(
type='file'
accept='image/*'
@change='onFileChangeWorldImage'
style='display: none')
span {{ $t('dialog.change_content_image.description') }}
br
el-button-group(style='padding-bottom: 10px; padding-top: 10px')
el-button(
type='default'
size='small'
@click='displayPreviousImages("World", "Change")'
icon='el-icon-refresh') {{ $t('dialog.change_content_image.refresh') }}
el-button(type='default' size='small' @click='uploadWorldImage' icon='el-icon-upload2') {{ $t('dialog.change_content_image.upload') }}
//- el-button(type="default" size="small" @click="deleteWorldImage" icon="el-icon-delete") Delete Latest Image
br
div(
style='display: inline-block'
v-for='image in previousImagesTable'
:key='image.version'
v-if='image.file')
.x-change-image-item(
@click='setWorldImage(image)'
style='cursor: pointer'
:class='{ "current-image": compareCurrentImage(image) }')
img.image(v-lazy='image.file.url')
//- dialog: Display previous avatar/world images
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='previousImagesDialog'
:visible.sync='previousImagesDialogVisible'
:title='$t("dialog.previous_images.header")'
width='800px')
div(v-if='previousImagesDialogVisible')
div(
style='display: inline-block'
v-for='image in previousImagesTable'
:key='image.version'
v-if='image.file')
el-popover.x-change-image-item(placement='right' width='500px' trigger='click')
img.x-link(slot='reference' v-lazy='image.file.url')
img.x-link(
v-lazy='image.file.url'
style='width: 500px; height: 375px'
@click='showFullscreenImageDialog(image.file.url)')
//- dialog: gallery select
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='gallerySelectDialog'
:visible.sync='gallerySelectDialog.visible'
:title='$t("dialog.gallery_select.header")'
width='100%')
div(v-if='gallerySelectDialog.visible')
span(slot='label') {{ $t('dialog.gallery_select.gallery') }}
span(style='color: #909399; font-size: 12px; margin-left: 5px') {{ galleryTable.length }}/64
br
input#GalleryUploadButton(type='file' accept='image/*' @change='onFileChangeGallery' style='display: none')
el-button-group
el-button(type='default' size='small' @click='selectImageGallerySelect("", "")' icon='el-icon-close') {{ $t('dialog.gallery_select.none') }}
el-button(type='default' size='small' @click='refreshGalleryTable' icon='el-icon-refresh') {{ $t('dialog.gallery_select.refresh') }}
el-button(
type='default'
size='small'
@click='displayGalleryUpload'
icon='el-icon-upload2'
:disabled='!API.currentUser.$isVRCPlus') {{ $t('dialog.gallery_select.upload') }}
br
.x-friend-item(
v-if='image.versions && image.versions.length > 0'
v-for='image in galleryTable'
:key='image.id'
style='display: inline-block; margin-top: 10px; width: unset; cursor: default')
.vrcplus-icon(
v-if='image.versions[image.versions.length - 1].file.url'
@click='selectImageGallerySelect(image.versions[image.versions.length - 1].file.url, image.id)')
img.avatar(v-lazy='image.versions[image.versions.length - 1].file.url')
//- dialog: full screen image
el-dialog.x-dialog(
ref='fullscreenImageDialog'
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
:visible.sync='fullscreenImageDialog.visible'
top='1vh'
width='97vw')
div(style='margin: 0 0 5px 5px')
el-button(@click='copyImageUrl(fullscreenImageDialog.imageUrl)' size='mini' icon='el-icon-s-order' circle)
el-button(
type='default'
size='mini'
icon='el-icon-download'
circle
@click='downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)'
style='margin-left: 5px')
img(v-lazy='fullscreenImageDialog.imageUrl' style='width: 100%; height: 85vh; object-fit: contain')

View File

@@ -1,345 +0,0 @@
mixin invites
//- dialog: invite
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='inviteDialog'
:visible.sync='inviteDialog.visible'
:title='$t("dialog.invite.header")'
width='500px')
div(v-if='inviteDialog.visible' v-loading='inviteDialog.loading')
location(:location='inviteDialog.worldId' :link='false')
br
el-button(
size='mini'
v-text='$t("dialog.invite.add_self")'
@click='addSelfToInvite'
style='margin-top: 10px')
el-button(
size='mini'
v-text='$t("dialog.invite.add_friends_in_instance")'
@click='addFriendsInInstanceToInvite'
:disabled='inviteDialog.friendsInInstance.length === 0'
style='margin-top: 10px')
el-button(
size='mini'
v-text='$t("dialog.invite.add_favorite_friends")'
@click='addFavoriteFriendsToInvite'
:disabled='vipFriends.length === 0'
style='margin-top: 10px')
el-select(
v-model='inviteDialog.userIds'
multiple
clearable
:placeholder='$t("dialog.invite.select_placeholder")'
filterable
:disabled='inviteDialog.loading'
style='width: 100%; margin-top: 15px')
el-option-group(v-if='API.currentUser' :label='$t("side_panel.me")')
el-option.x-friend-item(
:label='API.currentUser.displayName'
:value='API.currentUser.id'
style='height: auto')
.avatar(:class='userStatusClass(API.currentUser)')
img(v-lazy='userImage(API.currentUser)')
.detail
span.name(v-text='API.currentUser.displayName')
el-option-group(
v-if='inviteDialog.friendsInInstance.length'
:label='$t("dialog.invite.friends_in_instance")')
el-option.x-friend-item(
v-for='friend in inviteDialog.friendsInInstance'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='vipFriends.length' :label='$t("side_panel.favorite")')
el-option.x-friend-item(
v-for='friend in vipFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='onlineFriends.length' :label='$t("side_panel.online")')
el-option.x-friend-item(
v-for='friend in onlineFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
el-option-group(v-if='activeFriends.length' :label='$t("side_panel.active")')
el-option.x-friend-item(
v-for='friend in activeFriends'
:key='friend.id'
:label='friend.name'
:value='friend.id'
style='height: auto')
template(v-if='friend.ref')
.avatar
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-text='friend.ref.displayName' :style='{ color: friend.ref.$userColour }')
span(v-else v-text='friend.id')
template(#footer)
el-button(
size='small'
:disabled='inviteDialog.loading || !inviteDialog.userIds.length'
@click='showSendInviteDialog()') {{ $t('dialog.invite.invite_with_message') }}
el-button(
type='primary'
size='small'
:disabled='inviteDialog.loading || !inviteDialog.userIds.length'
@click='sendInvite()') {{ $t('dialog.invite.invite') }}
//- dialog: Edit And Send Invite Response Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='editAndSendInviteResponseDialog'
:visible.sync='editAndSendInviteResponseDialog.visible'
:title='$t("dialog.edit_send_invite_response_message.header")'
width='400px')
div(style='font-size: 12px')
span {{ $t('dialog.edit_send_invite_response_message.description') }}
el-input(
type='textarea'
v-model='editAndSendInviteResponseDialog.newMessage'
size='mini'
maxlength='64'
show-word-limit
:autosize='{ minRows: 2, maxRows: 5 }'
placeholder=''
style='margin-top: 10px')
template(#footer)
el-button(type='small' @click='cancelEditAndSendInviteResponse') {{ $t('dialog.edit_send_invite_response_message.cancel') }}
el-button(type='primary' size='small' @click='saveEditAndSendInviteResponse') {{ $t('dialog.edit_send_invite_response_message.send') }}
//- dialog Table: Send Invite Response Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteResponseDialog'
:visible.sync='sendInviteResponseDialogVisible'
:title='$t("dialog.invite_response_message.header")'
width='800px')
template(v-if='API.currentUser.$isVRCPlus')
input.inviteImageUploadButton(type='file' accept='image/*' @change='inviteImageUpload')
data-tables(
v-if='sendInviteResponseDialogVisible'
v-bind='inviteResponseMessageTable'
@row-click='showSendInviteResponseConfirmDialog'
style='margin-top: 10px; cursor: pointer')
el-table-column(:label='$t("table.profile.invite_messages.slot")' prop='slot' sortable='custom' width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='70' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditAndSendInviteResponseDialog("response", scope.row)')
template(#footer)
el-button(type='small' @click='cancelSendInviteResponse') {{ $t('dialog.invite_response_message.cancel') }}
el-button(type='small' @click='API.refreshInviteMessageTableData("response")') {{ $t('dialog.invite_response_message.refresh') }}
//- dialog Table: Send Invite Request Response Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteRequestResponseDialog'
:visible.sync='sendInviteRequestResponseDialogVisible'
:title='$t("dialog.invite_request_response_message.header")'
width='800px')
template(v-if='API.currentUser.$isVRCPlus')
input.inviteImageUploadButton(type='file' accept='image/*' @change='inviteImageUpload')
data-tables(
v-if='sendInviteRequestResponseDialogVisible'
v-bind='inviteRequestResponseMessageTable'
@row-click='showSendInviteResponseConfirmDialog'
style='margin-top: 10px; cursor: pointer')
el-table-column(:label='$t("table.profile.invite_messages.slot")' prop='slot' sortable='custom' width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='70' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditAndSendInviteResponseDialog("requestResponse", scope.row)')
template(#footer)
el-button(type='small' @click='cancelSendInviteRequestResponse') {{ $t('dialog.invite_request_response_message.cancel') }}
el-button(type='small' @click='API.refreshInviteMessageTableData("requestResponse")') {{ $t('dialog.invite_request_response_message.refresh') }}
//- dialog: Send Invite Response Message Confirm
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteResponseConfirmDialog'
:visible.sync='sendInviteResponseConfirmDialog.visible'
:title='$t("dialog.invite_response_message.header")'
width='400px')
div(style='font-size: 12px')
span {{ $t('dialog.invite_response_message.confirmation') }}
template(#footer)
el-button(type='small' @click='cancelInviteResponseConfirm') {{ $t('dialog.invite_response_message.cancel') }}
el-button(type='primary' size='small' @click='sendInviteResponseConfirm') {{ $t('dialog.invite_response_message.confirm') }}
//- dialog Table: Send Invite Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteDialog'
:visible.sync='sendInviteDialogVisible'
:title='$t("dialog.invite_message.header")'
width='800px')
template(v-if='API.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")
//- img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover")
//- img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)")
//- el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }}
//- template(v-else)
//- el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }}
input.inviteImageUploadButton(type='file' accept='image/*' @change='inviteImageUpload')
data-tables(
v-if='sendInviteDialogVisible'
v-bind='inviteMessageTable'
@row-click='showSendInviteConfirmDialog'
style='margin-top: 10px; cursor: pointer')
el-table-column(:label='$t("table.profile.invite_messages.slot")' prop='slot' sortable='custom' width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='70' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditAndSendInviteDialog("message", scope.row)')
template(#footer)
el-button(type='small' @click='cancelSendInvite') {{ $t('dialog.invite_message.cancel') }}
el-button(type='small' @click='API.refreshInviteMessageTableData("message")') {{ $t('dialog.invite_message.refresh') }}
//- dialog Table: Send Invite Request Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteRequestDialog'
:visible.sync='sendInviteRequestDialogVisible'
:title='$t("dialog.invite_request_message.header")'
width='800px')
template(v-if='API.currentUser.$isVRCPlus')
input.inviteImageUploadButton(type='file' accept='image/*' @change='inviteImageUpload')
data-tables(
v-if='sendInviteRequestDialogVisible'
v-bind='inviteRequestMessageTable'
@row-click='showSendInviteConfirmDialog'
style='margin-top: 10px; cursor: pointer')
el-table-column(:label='$t("table.profile.invite_messages.slot")' prop='slot' sortable='custom' width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='70' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditAndSendInviteDialog("request", scope.row)')
template(#footer)
el-button(type='small' @click='cancelSendInviteRequest') {{ $t('dialog.invite_request_message.cancel') }}
el-button(type='small' @click='API.refreshInviteMessageTableData("request")') {{ $t('dialog.invite_request_message.refresh') }}
//- dialog: Send Invite Message Confirm
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='sendInviteConfirmDialog'
:visible.sync='sendInviteConfirmDialog.visible'
:title='$t("dialog.invite_message.header")'
width='400px')
div(style='font-size: 12px')
span {{ $t('dialog.invite_message.confirmation') }}
template(#footer)
el-button(type='small' @click='cancelInviteConfirm') {{ $t('dialog.invite_message.cancel') }}
el-button(type='primary' size='small' @click='sendInviteConfirm') {{ $t('dialog.invite_message.confirm') }}
//- dialog: Edit And Send Invite Message
el-dialog.x-dialog(
:before-close='beforeDialogClose'
@mousedown.native='dialogMouseDown'
@mouseup.native='dialogMouseUp'
ref='editAndSendInviteDialog'
:visible.sync='editAndSendInviteDialog.visible'
:title='$t("dialog.edit_send_invite_message.header")'
width='400px')
div(style='font-size: 12px')
span {{ $t('dialog.edit_send_invite_message.description') }}
el-input(
type='textarea'
v-model='editAndSendInviteDialog.newMessage'
size='mini'
maxlength='64'
show-word-limit
:autosize='{ minRows: 2, maxRows: 5 }'
placeholder=''
style='margin-top: 10px')
template(#footer)
el-button(type='small' @click='cancelEditAndSendInvite') {{ $t('dialog.edit_send_invite_message.cancel') }}
el-button(type='primary' size='small' @click='saveEditAndSendInvite') {{ $t('dialog.edit_send_invite_message.send') }}

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +0,0 @@
mixin loginPage
.x-login-container(v-if='!API.isLoggedIn' v-loading='loginForm.loading')
.x-login
div(style='position: fixed; top: 0; left: 0; margin: 5px')
el-tooltip(placement='top' :content='$t("view.login.updater")' :disabled='hideTooltips')
el-button(type='default' @click='showVRCXUpdateDialog' size='mini' icon='el-icon-download' circle)
el-tooltip(placement='top' :content='$t("view.login.proxy_settings")' :disabled='hideTooltips')
el-button(
type='default'
@click='promptProxySettings'
size='mini'
icon='el-icon-connection'
style='margin-left: 5px'
circle)
.x-login-form-container
div
h2(style='font-weight: bold; text-align: center; margin: 0') {{ $t('view.login.login') }}
el-form(
ref='loginForm'
:model='loginForm'
:rules='loginForm.rules'
@submit.native.prevent='login()')
el-form-item(:label='$t("view.login.field.username")' prop='username' required)
el-input(
v-model='loginForm.username'
name='username'
:placeholder='$t("view.login.field.username")'
clearable)
el-form-item(
:label='$t("view.login.field.password")'
prop='password'
required
style='margin-top: 10px')
el-input(
type='password'
v-model='loginForm.password'
name='password'
:placeholder='$t("view.login.field.password")'
clearable
show-password)
el-checkbox(v-model='loginForm.saveCredentials' style='margin-top: 15px') {{ $t('view.login.field.saveCredentials') }}
el-checkbox(
v-model='enableCustomEndpoint'
@change='toggleCustomEndpoint'
style='margin-top: 10px') {{ $t('view.login.field.devEndpoint') }}
el-form-item(
v-if='enableCustomEndpoint'
:label='$t("view.login.field.endpoint")'
prop='endpoint'
style='margin-top: 10px')
el-input(
v-model='loginForm.endpoint'
name='endpoint'
:placeholder='API.endpointDomainVrchat'
clearable)
el-form-item(
v-if='enableCustomEndpoint'
:label='$t("view.login.field.websocket")'
prop='endpoint'
style='margin-top: 10px')
el-input(
v-model='loginForm.websocket'
name='websocket'
:placeholder='API.websocketDomainVrchat'
clearable)
el-form-item(style='margin-top: 15px')
el-button(native-type='submit' type='primary' style='width: 100%') {{ $t('view.login.login') }}
el-button(
type='primary'
@click='openExternalLink("https://vrchat.com/register")'
style='width: 100%') {{ $t('view.login.register') }}
hr.x-vertical-divider(v-if='Object.keys(loginForm.savedCredentials).length !== 0')/
div(v-if='Object.keys(loginForm.savedCredentials).length !== 0')
h2(style='font-weight: bold; text-align: center; margin: 0') {{ $t('view.login.savedAccounts') }}
.x-scroll-wrapper(style='margin-top: 10px')
.x-saved-account-list
.x-friend-item(
v-for='user in loginForm.savedCredentials'
:key='user.user.id'
@click='relogin(user)')
.avatar
img(v-lazy='userImage(user.user)')
.detail
span.name(v-text='user.user.displayName')
span.extra(v-text='user.user.username')
span.extra(v-text='user.loginParmas.endpoint')
el-button(
type='default'
@click.stop='deleteSavedLogin(user.user.id)'
size='mini'
icon='el-icon-delete'
style='margin-left: 10px'
circle)
.x-legal-notice-container
div(style='text-align: center; font-size: 12px')
p #[a.x-link(@click='openExternalLink("https://vrchat.com/home/password")') {{ $t('view.login.forgotPassword') }}]
p &copy; 2019-2025 #[a.x-link(@click='openExternalLink("https://github.com/pypy-vrc")') pypy] &amp; #[a.x-link(@click='openExternalLink("https://github.com/Natsumi-sama")') Natsumi]
p {{ $t('view.settings.general.legal_notice.info') }}
p {{ $t('view.settings.general.legal_notice.disclaimer1') }}
p {{ $t('view.settings.general.legal_notice.disclaimer2') }}

View File

@@ -1,195 +0,0 @@
mixin feedTab
.x-container(v-show='menuActiveIndex === "feed"')
div(style='margin: 0 0 10px; display: flex; align-items: center')
div(style='flex: none; margin-right: 10px; display: flex; align-items: center')
el-tooltip(
placement='bottom'
:content='$t("view.feed.favorites_only_tooltip")'
:disabled='hideTooltips')
el-switch(v-model='feedTable.vip' @change='feedTableLookup' active-color='#13ce66')
el-select(
v-model='feedTable.filter'
@change='feedTableLookup'
multiple
clearable
style='flex: 1; height: 40px'
:placeholder='$t("view.feed.filter_placeholder")')
el-option(
v-for='type in ["GPS", "Online", "Offline", "Status", "Avatar", "Bio"]'
:key='type'
:label='$t("view.feed.filters." + type)'
:value='type')
el-input(
v-model='feedTable.search'
:placeholder='$t("view.feed.search_placeholder")'
@keyup.native.13='feedTableLookup'
@change='feedTableLookup'
clearable
style='flex: none; width: 150px; margin: 0 10px')
data-tables(v-bind='feedTable' v-loading='feedTable.loading' lazy)
el-table-column(type='expand' width='20')
template(#default='scope')
div(style='position: relative; font-size: 14px')
template(v-if='scope.row.type === "GPS"')
location(
v-if='scope.row.previousLocation'
:location='scope.row.previousLocation'
style='display: inline-block')
el-tag(type='info' effect='plain' size='mini' style='margin-left: 5px') {{ timeToText(scope.row.time) }}
br
span(style='margin-right: 5px')
i.el-icon-right
location(
v-if='scope.row.location'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
template(v-else-if='scope.row.type === "Offline"')
template(v-if='scope.row.location')
location(
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
el-tag(type='info' effect='plain' size='mini' style='margin-left: 5px') {{ timeToText(scope.row.time) }}
template(v-else-if='scope.row.type === "Online"')
location(
v-if='scope.row.location'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
template(v-else-if='scope.row.type === "Avatar"')
div(style='display: flex; align-items: center')
el-popover(placement='right' width='500px' trigger='click')
div(
slot='reference'
style='display: inline-block; vertical-align: top; width: 160px')
template(v-if='scope.row.previousCurrentAvatarThumbnailImageUrl')
img.x-link(
v-lazy='scope.row.previousCurrentAvatarThumbnailImageUrl'
style='flex: none; width: 160px; height: 120px; border-radius: 4px')
br
avatar-info(
:imageurl='scope.row.previousCurrentAvatarThumbnailImageUrl'
:userid='scope.row.userId'
:hintownerid='scope.row.previousOwnerId'
:hintavatarname='scope.row.previousAvatarName'
:avatartags='scope.row.previousCurrentAvatarTags')
img.x-link(
v-lazy='scope.row.previousCurrentAvatarImageUrl'
style='width: 500px; height: 375px'
@click='showFullscreenImageDialog(scope.row.previousCurrentAvatarImageUrl)')
span(style='position: relative; margin: 0 10px')
i.el-icon-right
el-popover(placement='right' width='500px' trigger='click')
div(
slot='reference'
style='display: inline-block; vertical-align: top; width: 160px')
template(v-if='scope.row.currentAvatarThumbnailImageUrl')
img.x-link(
v-lazy='scope.row.currentAvatarThumbnailImageUrl'
style='flex: none; width: 160px; height: 120px; border-radius: 4px')
br
avatar-info(
:imageurl='scope.row.currentAvatarThumbnailImageUrl'
:userid='scope.row.userId'
:hintownerid='scope.row.ownerId'
:hintavatarname='scope.row.avatarName'
:avatartags='scope.row.currentAvatarTags')
img.x-link(
v-lazy='scope.row.currentAvatarImageUrl'
style='width: 500px; height: 375px'
@click='showFullscreenImageDialog(scope.row.currentAvatarImageUrl)')
template(v-else-if='scope.row.type === "Status"')
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.previousStatus === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.previousStatus === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.previousStatus === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.previousStatus === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.previousStatus)')
span(v-text='scope.row.previousStatusDescription' style='margin-left: 5px')
br
span
i.el-icon-right
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.status === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.status === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.status === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.status === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.status)' style='margin: 0 5px')
span(v-text='scope.row.statusDescription')
template(v-else-if='scope.row.type === "Bio"')
pre(
v-html='formatDifference(scope.row.previousBio, scope.row.bio)'
style='font-family: inherit; font-size: 12px; white-space: pre-wrap; line-height: 25px; line-height: 22px')
el-table-column(:label='$t("table.feed.date")' prop='created_at' sortable='custom' width='120')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.feed.type")' prop='type' width='80')
template(#default='scope')
span.x-link(v-text='$t("view.feed.filters." + scope.row.type)')
el-table-column(:label='$t("table.feed.user")' prop='displayName' width='180')
template(#default='scope')
span.x-link(
v-text='scope.row.displayName'
@click='showUserDialog(scope.row.userId)'
style='padding-right: 10px')
el-table-column(:label='$t("table.feed.detail")')
template(#default='scope')
template(v-if='scope.row.type === "GPS"')
location(
v-if='scope.row.location'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
template(v-else-if='scope.row.type === "Offline" || scope.row.type === "Online"')
location(
v-if='scope.row.location'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
template(v-else-if='scope.row.type === "Status"')
template(v-if='scope.row.statusDescription === scope.row.previousStatusDescription')
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.previousStatus === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.previousStatus === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.previousStatus === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.previousStatus === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.previousStatus)')
span(style='margin: 0 5px')
i.el-icon-right
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.status === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.status === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.status === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.status === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.status)')
template(v-else)
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.status === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.status === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.status === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.status === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.status)' style='margin-right: 3px')
span(v-text='scope.row.statusDescription')
template(v-else-if='scope.row.type === "Avatar"')
avatar-info(
:imageurl='scope.row.currentAvatarImageUrl'
:userid='scope.row.userId'
:hintownerid='scope.row.ownerId'
:hintavatarname='scope.row.avatarName'
:avatartags='scope.row.currentAvatarTags')
template(v-else-if='scope.row.type === "Bio"')
span(v-text='scope.row.bio')

View File

@@ -1,54 +0,0 @@
mixin friendLogTab
.x-container(v-if='menuActiveIndex === "friendLog"')
data-tables(v-bind='friendLogTable' ref='friendLogTableRef')
template(#tool)
div(style='margin: 0 0 10px; display: flex; align-items: center')
el-select(
v-model='friendLogTable.filters[0].value'
@change='saveTableFilters'
multiple
clearable
style='flex: 1'
:placeholder='$t("view.friend_log.filter_placeholder")')
el-option(
v-for='type in ["Friend", "Unfriend", "FriendRequest", "CancelFriendRequest", "DisplayName", "TrustLevel"]'
:key='type'
:label='$t("view.friend_log.filters." + type)'
:value='type')
el-input(
v-model='friendLogTable.filters[1].value'
:placeholder='$t("view.friend_log.search_placeholder")'
style='flex: none; width: 150px; margin-left: 10px')
el-table-column(:label='$t("table.friendLog.date")' prop='created_at' sortable='custom' width='200')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.friendLog.type")' prop='type' width='150')
template(#default='scope')
span(v-text='$t("view.friend_log.filters." + scope.row.type)')
el-table-column(:label='$t("table.friendLog.user")' prop='displayName')
template(#default='scope')
span(v-if='scope.row.type === "DisplayName"') {{ scope.row.previousDisplayName }} #[i.el-icon-right]&nbsp;
span.x-link(
v-text='scope.row.displayName || scope.row.userId'
@click='showUserDialog(scope.row.userId)'
style='padding-right: 10px')
template(v-if='scope.row.type === "TrustLevel"')
span ({{ scope.row.previousTrustLevel }} #[i.el-icon-right] {{ scope.row.trustLevel }})
el-table-column(:label='$t("table.friendLog.action")' width='80' align='right')
template(#default='scope')
el-button(
v-if='shiftHeld'
style='color: #f56c6c'
type='text'
icon='el-icon-close'
size='mini'
@click='deleteFriendLog(scope.row)')
el-button(
v-else
type='text'
icon='el-icon-delete'
size='mini'
@click='deleteFriendLogPrompt(scope.row)')

View File

@@ -1,118 +0,0 @@
mixin gameLogTab
.x-container(v-show='menuActiveIndex === "gameLog"')
data-tables(v-bind='gameLogTable' v-loading='gameLogTable.loading')
template(#tool)
div(style='margin: 0 0 10px; display: flex; align-items: center')
div(style='flex: none; margin-right: 10px; display: flex; align-items: center')
el-tooltip(
placement='bottom'
:content='$t("view.feed.favorites_only_tooltip")'
:disabled='hideTooltips')
el-switch(v-model='gameLogTable.vip' @change='gameLogTableLookup' active-color='#13ce66')
el-select(
v-model='gameLogTable.filter'
@change='gameLogTableLookup'
multiple
clearable
style='flex: 1'
:placeholder='$t("view.game_log.filter_placeholder")')
el-option(
v-for='type in ["Location", "OnPlayerJoined", "OnPlayerLeft", "VideoPlay", "Event", "External", "StringLoad", "ImageLoad"]'
:key='type'
:label='$t("view.game_log.filters." + type)'
:value='type')
el-input(
v-model='gameLogTable.search'
:placeholder='$t("view.game_log.search_placeholder")'
@keyup.native.13='gameLogTableLookup'
@change='gameLogTableLookup'
clearable
style='flex: none; width: 150px; margin: 0 10px')
el-table-column(:label='$t("table.gameLog.date")' prop='created_at' sortable='custom' width='120')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.gameLog.type")' prop='type' width='120')
template(#default='scope')
el-tooltip(placement='right' :open-delay='500' :disabled='hideTooltips')
template(#content)
span {{ $t('view.game_log.filters.' + scope.row.type) }}
span.x-link(
v-if='scope.row.location && scope.row.type !== "Location"'
v-text='$t("view.game_log.filters." + scope.row.type)'
@click='showWorldDialog(scope.row.location)')
span(v-else v-text='$t("view.game_log.filters." + scope.row.type)')
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')
span ⭐
el-tooltip(v-else placement='top' content='Friend')
span 💚
el-table-column(:label='$t("table.gameLog.user")' prop='displayName' width='180')
template(#default='scope')
span.x-link(
v-if='scope.row.displayName'
v-text='scope.row.displayName'
@click='lookupUser(scope.row)'
style='padding-right: 10px')
el-table-column(:label='$t("table.gameLog.detail")' prop='data')
template(#default='scope')
location(
v-if='scope.row.type === "Location"'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
location(
v-else-if='scope.row.type === "PortalSpawn"'
:location='scope.row.instanceId'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName')
template(v-else-if='scope.row.type === "Event"')
span(v-text='scope.row.data')
template(v-else-if='scope.row.type === "External"')
span(v-text='scope.row.message')
template(v-else-if='scope.row.type === "VideoPlay"')
span(v-if='scope.row.videoId' style='margin-right: 5px') {{ scope.row.videoId }}:
span(v-if='scope.row.videoId === "LSMedia"' v-text='scope.row.videoName')
span.x-link(
v-else-if='scope.row.videoName'
@click='openExternalLink(scope.row.videoUrl)'
v-text='scope.row.videoName')
span.x-link(v-else @click='openExternalLink(scope.row.videoUrl)' v-text='scope.row.videoUrl')
template(v-else-if='scope.row.type === "ImageLoad"')
span.x-link(@click='openExternalLink(scope.row.resourceUrl)' v-text='scope.row.resourceUrl')
template(v-else-if='scope.row.type === "StringLoad"')
span.x-link(@click='openExternalLink(scope.row.resourceUrl)' v-text='scope.row.resourceUrl')
template(
v-else-if='scope.row.type === "Notification" || scope.row.type === "OnPlayerJoined" || scope.row.type === "OnPlayerLeft"')
span.x-link(v-else v-text='scope.row.data')
el-table-column(:label='$t("table.gameLog.action")' width='80' align='right')
template(#default='scope')
template(
v-if='scope.row.type !== "OnPlayerJoined" && scope.row.type !== "OnPlayerLeft" && scope.row.type !== "Location" && scope.row.type !== "PortalSpawn"')
el-button(
v-if='shiftHeld'
style='color: #f56c6c'
type='text'
icon='el-icon-close'
size='mini'
@click='deleteGameLogEntry(scope.row)')
el-button(
v-else
type='text'
icon='el-icon-delete'
size='mini'
@click='deleteGameLogEntryPrompt(scope.row)')
el-tooltip(
placement='top'
:content='$t("dialog.previous_instances.info")'
:disabled='hideTooltips')
el-button(
v-if='scope.row.type === "Location"'
type='text'
icon='el-icon-s-data'
size='mini'
@click='showPreviousInstancesInfoDialog(scope.row.location)')

View File

@@ -1,261 +0,0 @@
mixin notificationsTab
.x-container(v-if='menuActiveIndex === "notification"' v-loading='API.isNotificationsLoading')
data-tables.notification-table(v-bind='notificationTable' ref='notificationTableRef')
template(#tool)
div(style='margin: 0 0 10px; display: flex; align-items: center')
el-select(
v-model='notificationTable.filters[0].value'
@change='saveTableFilters'
multiple
clearable
style='flex: 1'
:placeholder='$t("view.notification.filter_placeholder")')
el-option(
v-for='type in ["requestInvite", "invite", "requestInviteResponse", "inviteResponse", "friendRequest", "ignoredFriendRequest", "message", "boop", "groupChange", "group.announcement", "group.informative", "group.invite", "group.joinRequest", "group.transfer", "group.queueReady", "moderation.warning.group", "moderation.report.closed", "instance.closed"]'
:key='type'
:label='$t("view.notification.filters." + type)'
:value='type')
el-input(
v-model='notificationTable.filters[1].value'
:placeholder='$t("view.notification.search_placeholder")'
style='flex: none; width: 150px; margin: 0 10px')
el-tooltip(
placement='bottom'
:content='$t("view.notification.refresh_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
:loading='API.isNotificationsLoading'
@click='API.refreshNotifications()'
icon='el-icon-refresh'
circle
style='flex: none')
el-table-column(:label='$t("table.notification.date")' prop='created_at' sortable='custom' width='120')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.notification.type")' prop='type' width='180')
template(#default='scope')
span.x-link(
v-if='scope.row.type === "invite"'
v-text='$t("view.notification.filters." + scope.row.type)'
@click='showWorldDialog(scope.row.details.worldId)')
el-tooltip(
v-else-if='scope.row.type === "group.queueReady" || scope.row.type === "instance.closed"'
placement='top')
template(#content)
location(
v-if='scope.row.location'
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName'
:link='false')
span.x-link(
v-text='$t("view.notification.filters." + scope.row.type)'
@click='showWorldDialog(scope.row.location)')
el-tooltip(
v-else-if='scope.row.link'
placement='top'
:content='scope.row.linkText'
:disabled='hideTooltips')
span.x-link(
v-text='$t("view.notification.filters." + scope.row.type)'
@click='openNotificationLink(scope.row.link)')
span(v-else v-text='$t("view.notification.filters." + scope.row.type)')
el-table-column(:label='$t("table.notification.user_group")' prop='senderUsername' width='150')
template(#default='scope')
template(v-if='scope.row.type === "groupChange"')
span.x-link(v-text='scope.row.senderUsername' @click='showGroupDialog(scope.row.senderUserId)')
template(v-else-if='scope.row.senderUserId')
span.x-link(v-text='scope.row.senderUsername' @click='showUserDialog(scope.row.senderUserId)')
template(v-else-if='scope.row.link && scope.row.data?.groupName')
span.x-link(v-text='scope.row.data?.groupName' @click='openNotificationLink(scope.row.link)')
template(v-else-if='scope.row.link')
span.x-link(v-text='scope.row.linkText' @click='openNotificationLink(scope.row.link)')
el-table-column(:label='$t("table.notification.photo")' width='100' prop='photo')
template(#default='scope')
template(v-if='scope.row.details && scope.row.details.imageUrl')
el-popover(placement='right' width='500px' trigger='click')
img.x-link(
slot='reference'
:src='getSmallThumbnailUrl(scope.row.details.imageUrl)'
style='flex: none; height: 50px; border-radius: 4px')
img.x-link(
v-lazy='scope.row.details.imageUrl'
style='width: 500px'
@click='showFullscreenImageDialog(scope.row.details.imageUrl)')
template(v-else-if='scope.row.imageUrl')
el-popover(placement='right' width='500px' trigger='click')
img.x-link(
slot='reference'
:src='getSmallThumbnailUrl(scope.row.imageUrl)'
style='flex: none; height: 50px; border-radius: 4px')
img.x-link(
v-lazy='scope.row.imageUrl'
style='width: 500px'
@click='showFullscreenImageDialog(scope.row.imageUrl)')
el-table-column(:label='$t("table.notification.message")' prop='message')
template(#default='scope')
span.x-link(v-if='scope.row.type === "invite"' style='display: flex')
location(
v-if='scope.row.details'
:location='scope.row.details.worldId'
:hint='scope.row.details.worldName'
:grouphint='scope.row.details.groupName'
:link='true')
br
el-tooltip(
v-if='scope.row.message && scope.row.message !== `This is a generated invite to ${scope.row.details?.worldName}`'
placement='top')
template(#content)
pre.extra(
style='display: inline-block; vertical-align: top; font-family: inherit; font-size: 12px; white-space: pre-wrap; margin: 0') {{ scope.row.message || '-' }}
div(v-text='scope.row.message')
span(
v-else-if='scope.row.details && scope.row.details.inviteMessage'
v-text='scope.row.details.inviteMessage')
span(
v-else-if='scope.row.details && scope.row.details.requestMessage'
v-text='scope.row.details.requestMessage')
span(
v-else-if='scope.row.details && scope.row.details.responseMessage'
v-text='scope.row.details.responseMessage')
el-table-column(:label='$t("table.notification.action")' width='100' align='right')
template(#default='scope')
template(v-if='scope.row.senderUserId !== API.currentUser.id && !scope.row.$isExpired')
template(v-if='scope.row.type === "friendRequest"')
el-tooltip(placement='top' content='Accept' :disabled='hideTooltips')
el-button(
type='text'
icon='el-icon-check'
style='color: #67c23a'
size='mini'
@click='acceptFriendRequestNotification(scope.row)')
template(v-else-if='scope.row.type === "invite"')
el-tooltip(placement='top' content='Decline with message' :disabled='hideTooltips')
el-button(
type='text'
icon='el-icon-chat-line-square'
size='mini'
@click='showSendInviteResponseDialog(scope.row)')
template(v-else-if='scope.row.type === "requestInvite"')
template(
v-if='lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)')
el-tooltip(placement='top' content='Invite' :disabled='hideTooltips')
el-button(
type='text'
icon='el-icon-check'
style='color: #67c23a'
size='mini'
@click='acceptRequestInvite(scope.row)')
el-tooltip(placement='top' content='Decline with message' :disabled='hideTooltips')
el-button(
type='text'
icon='el-icon-chat-line-square'
size='mini'
style='margin-left: 5px'
@click='showSendInviteRequestResponseDialog(scope.row)')
template(v-if='scope.row.responses')
template(v-for='response in scope.row.responses')
el-tooltip(placement='top' :content='response.text' :disabled='hideTooltips')
el-button(
v-if='response.icon === "check"'
type='text'
icon='el-icon-check'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
el-button(
v-else-if='response.icon === "cancel"'
type='text'
icon='el-icon-close'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
el-button(
v-else-if='response.icon === "ban"'
type='text'
icon='el-icon-circle-close'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
el-button(
v-else-if='response.icon === "bell-slash"'
type='text'
icon='el-icon-bell'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
el-button(
v-else-if='response.icon === "reply" && scope.row.type === "boop"'
type='text'
icon='el-icon-chat-line-square'
size='mini'
style='margin-left: 5px'
@click='showSendBoopDialog(scope.row.senderUserId)')
el-button(
v-else-if='response.icon === "reply"'
type='text'
icon='el-icon-chat-line-square'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
el-button(
v-else
type='text'
icon='el-icon-collection-tag'
size='mini'
style='margin-left: 5px'
@click='sendNotificationResponse(scope.row.id, scope.row.responses, response.type)')
template(
v-if='scope.row.type !== "requestInviteResponse" && scope.row.type !== "inviteResponse" && scope.row.type !== "message" && scope.row.type !== "boop" && scope.row.type !== "groupChange" && !scope.row.type.includes("group.") && !scope.row.type.includes("moderation.") && !scope.row.type.includes("instance.")')
el-tooltip(placement='top' content='Decline' :disabled='hideTooltips')
el-button(
v-if='shiftHeld'
style='color: #f56c6c; margin-left: 5px'
type='text'
icon='el-icon-close'
size='mini'
@click='hideNotification(scope.row)')
el-button(
v-else
type='text'
icon='el-icon-close'
size='mini'
style='margin-left: 5px'
@click='hideNotificationPrompt(scope.row)')
template(v-if='scope.row.type === "group.queueReady"')
el-tooltip(placement='top' content='Delete log' :disabled='hideTooltips')
el-button(
v-if='shiftHeld'
style='color: #f56c6c; margin-left: 5px'
type='text'
icon='el-icon-close'
size='mini'
@click='deleteNotificationLog(scope.row)')
el-button(
v-else
type='text'
icon='el-icon-delete'
size='mini'
style='margin-left: 5px'
@click='deleteNotificationLogPrompt(scope.row)')
template(
v-if='scope.row.type !== "friendRequest" && scope.row.type !== "ignoredFriendRequest" && !scope.row.type.includes("group.") && !scope.row.type.includes("moderation.")')
el-tooltip(placement='top' content='Delete log' :disabled='hideTooltips')
el-button(
v-if='shiftHeld'
style='color: #f56c6c; margin-left: 5px'
type='text'
icon='el-icon-close'
size='mini'
@click='deleteNotificationLog(scope.row)')
el-button(
v-else
type='text'
icon='el-icon-delete'
size='mini'
style='margin-left: 5px'
@click='deleteNotificationLogPrompt(scope.row)')

View File

@@ -1,479 +0,0 @@
mixin playerListTab
.x-container(v-show='menuActiveIndex === "playerList"' style='padding-top: 5px')
div(style='display: flex; flex-direction: column; height: 100%')
div(v-if='currentInstanceWorld.ref.id' style='display: flex')
el-popover(placement='right' width='500px' trigger='click' style='height: 120px')
img.x-link(
slot='reference'
v-lazy='currentInstanceWorld.ref.thumbnailImageUrl'
style='flex: none; width: 160px; height: 120px; border-radius: 4px')
img.x-link(
v-lazy='currentInstanceWorld.ref.imageUrl'
style='width: 500px; height: 375px'
@click='showFullscreenImageDialog(currentInstanceWorld.ref.imageUrl)')
div(style='margin-left: 10px; display: flex; flex-direction: column; min-width: 320px; width: 100%')
div
span.x-link(
@click='showWorldDialog(currentInstanceWorld.ref.id)'
style='font-weight: bold; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1')
| #[i.el-icon-s-home(v-show='API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === currentInstanceWorld.ref.id' style='margin-right: 5px')] {{ currentInstanceWorld.ref.name }}
div
span.x-link.x-grey(
v-text='currentInstanceWorld.ref.authorName'
@click='showUserDialog(currentInstanceWorld.ref.authorId)'
style='font-family: monospace')
div(style='margin-top: 5px')
el-tag(
v-if='currentInstanceWorld.ref.$isLabs'
type='primary'
effect='plain'
size='mini'
style='margin-right: 5px') {{ $t('dialog.world.tags.labs') }}
el-tag(
v-else-if='currentInstanceWorld.ref.releaseStatus === "public"'
type='success'
effect='plain'
size='mini'
style='margin-right: 5px') {{ $t('dialog.world.tags.public') }}
el-tag(
v-else-if='currentInstanceWorld.ref.releaseStatus === "private"'
type='danger'
effect='plain'
size='mini'
style='margin-right: 5px') {{ $t('dialog.world.tags.private') }}
el-tag.x-tag-platform-pc(
v-if='currentInstanceWorld.isPC'
type='info'
effect='plain'
size='mini'
style='margin-right: 5px') PC
span.x-grey(
v-if='currentInstanceWorld.bundleSizes["standalonewindows"]'
style='margin-left: 5px; border-left: inherit; padding-left: 5px') {{ currentInstanceWorld.bundleSizes['standalonewindows'].fileSize }}
el-tag.x-tag-platform-quest(
v-if='currentInstanceWorld.isQuest'
type='info'
effect='plain'
size='mini'
style='margin-right: 5px') Android
span.x-grey(
v-if='currentInstanceWorld.bundleSizes["android"]'
style='margin-left: 5px; border-left: inherit; padding-left: 5px') {{ currentInstanceWorld.bundleSizes['android'].fileSize }}
el-tag.x-tag-platform-ios(
v-if='currentInstanceWorld.isIOS'
type='info'
effect='plain'
size='mini'
style='margin-right: 5px') iOS
span.x-grey(
v-if='currentInstanceWorld.bundleSizes["ios"]'
style='margin-left: 5px; border-left: inherit; padding-left: 5px') {{ currentInstanceWorld.bundleSizes['ios'].fileSize }}
el-tag(
v-if='currentInstanceWorld.avatarScalingDisabled'
type='warning'
effect='plain'
size='mini'
style='margin-right: 5px; margin-top: 5px') {{ $t('dialog.world.tags.avatar_scaling_disabled') }}
el-tag(
v-if='currentInstanceWorld.inCache'
type='info'
effect='plain'
size='mini'
style='margin-right: 5px')
span {{ currentInstanceWorld.cacheSize }} {{ $t('dialog.world.tags.cache') }}
div(style='margin-top: 5px')
location-world(
:locationobject='currentInstanceLocation'
:currentuserid='API.currentUser.id'
@show-launch-dialog='showLaunchDialog')
span(v-if='lastLocation.playerList.size > 0' style='margin-left: 5px')
| {{ lastLocation.playerList.size }}
| #[template(v-if='lastLocation.friendList.size > 0') ({{ lastLocation.friendList.size }})]
| &nbsp;&horbar; #[timer(v-if='lastLocation.date' :epoch='lastLocation.date')]
div(style='margin-top: 5px')
span(
v-show='currentInstanceWorld.ref.name !== currentInstanceWorld.ref.description'
v-text='currentInstanceWorld.ref.description'
:style='{ fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitBoxOrient: "vertical", WebkitLineClamp: currentInstanceWorldDescriptionExpanded ? "none" : "2" }')
div(style='display: flex; justify-content: end')
el-button(
v-if='currentInstanceWorld.ref.description.length > 50 && !currentInstanceWorldDescriptionExpanded'
type='text'
size='mini'
@click='currentInstanceWorldDescriptionExpanded = true') {{ !currentInstanceWorldDescriptionExpanded && 'Show more' }}
div(style='display: flex; flex-direction: column; margin-left: 20px')
.x-friend-item(style='cursor: default')
.detail
span.name {{ $t('dialog.world.info.capacity') }}
span.extra {{ currentInstanceWorld.ref.recommendedCapacity | commaNumber }} ({{ currentInstanceWorld.ref.capacity | commaNumber }})
.x-friend-item(style='cursor: default')
.detail
span.name {{ $t('dialog.world.info.last_updated') }}
span.extra {{ currentInstanceWorld.lastUpdated | formatDate('long') }}
.x-friend-item(style='cursor: default')
.detail
span.name {{ $t('dialog.world.info.created_at') }}
span.extra {{ currentInstanceWorld.ref.created_at | formatDate('long') }}
.photon-event-table(v-if='photonLoggingEnabled')
div(style='position: absolute; width: 600px; margin-left: 215px; z-index: 1')
el-select(
v-model='photonEventTableTypeFilter'
@change='photonEventTableFilterChange'
multiple
clearable
collapse-tags
style='flex: 1; width: 220px'
:placeholder='$t("view.player_list.photon.filter_placeholder")')
el-option(
v-for='type in photonEventTableTypeFilterList'
:key='type'
:label='type'
:value='type')
el-input(
v-model='photonEventTableFilter'
@input='photonEventTableFilterChange'
:placeholder='$t("view.player_list.photon.search_placeholder")'
clearable
style='width: 150px; margin-left: 10px')
el-button(@click='showChatboxBlacklistDialog' style='margin-left: 10px') {{ $t('view.player_list.photon.chatbox_blacklist') }}
el-tooltip(
placement='bottom'
:content='$t("view.player_list.photon.status_tooltip")'
:disabled='hideTooltips')
div(
style='display: inline-block; margin-left: 15px; font-size: 14px; vertical-align: text-top; margin-top: 1px')
span(v-if='ipcEnabled && !photonEventIcon') 🟢
span(v-else-if='ipcEnabled') ⚪
span(v-else) 🔴
el-tabs(type='card')
el-tab-pane(:label='$t("view.player_list.photon.current")')
data-tables(v-bind='photonEventTable' style='margin-bottom: 10px')
el-table-column(:label='$t("table.playerList.date")' prop='created_at' width='120')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.playerList.user")' prop='photonId' width='160')
template(#default='scope')
span.x-link(
v-text='scope.row.displayName'
@click='showUserFromPhotonId(scope.row.photonId)'
style='padding-right: 10px')
el-table-column(:label='$t("table.playerList.type")' prop='type' width='140')
el-table-column(:label='$t("table.playerList.detail")' prop='text')
template(#default='scope')
template(v-if='scope.row.type === "ChangeAvatar"')
span.x-link(
v-text='scope.row.avatar.name'
@click='showAvatarDialog(scope.row.avatar.id)')
| &nbsp;
span(v-if='!scope.row.inCache' style='color: #aaa') #[i.el-icon-download]&nbsp;
span.avatar-info-public(v-if='scope.row.avatar.releaseStatus === "public"') {{ $t('dialog.avatar.labels.public') }}
span.avatar-info-own(v-else-if='scope.row.avatar.releaseStatus === "private"') {{ $t('dialog.avatar.labels.private') }}
template(
v-if='scope.row.avatar.description && scope.row.avatar.name !== scope.row.avatar.description')
|
| - {{ scope.row.avatar.description }}
template(v-else-if='scope.row.type === "ChangeStatus"')
template(v-if='scope.row.status !== scope.row.previousStatus')
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.previousStatus === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.previousStatus === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.previousStatus === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.previousStatus === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.previousStatus)')
span
i.el-icon-right
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.status === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.status === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.status === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.status === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(
:class='statusClass(scope.row.status)'
style='margin-right: 5px')
span(
v-if='scope.row.statusDescription !== scope.row.previousStatusDescription'
v-text='scope.row.statusDescription')
template(v-else-if='scope.row.type === "ChangeGroup"')
span.x-link(
v-if='scope.row.previousGroupName'
v-text='scope.row.previousGroupName'
@click='showGroupDialog(scope.row.previousGroupId)'
style='margin-right: 5px')
span.x-link(
v-else
v-text='scope.row.previousGroupId'
@click='showGroupDialog(scope.row.previousGroupId)'
style='margin-right: 5px')
span
i.el-icon-right
span.x-link(
v-if='scope.row.groupName'
v-text='scope.row.groupName'
@click='showGroupDialog(scope.row.groupId)'
style='margin-left: 5px')
span.x-link(
v-else
v-text='scope.row.groupId'
@click='showGroupDialog(scope.row.groupId)'
style='margin-left: 5px')
span.x-link(
v-else-if='scope.row.type === "PortalSpawn"'
@click='showWorldDialog(scope.row.location, scope.row.shortName)')
location(
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName'
:link='false')
span(v-else-if='scope.row.type === "ChatBoxMessage"' v-text='scope.row.text')
span(v-else-if='scope.row.type === "OnPlayerJoined"')
span(v-if='scope.row.platform === "Desktop"' style='color: #409eff') Desktop&nbsp;
span(v-else-if='scope.row.platform === "VR"' style='color: #409eff') VR&nbsp;
span(v-else-if='scope.row.platform === "Quest"' style='color: #67c23a') Android&nbsp;
span.x-link(
v-text='scope.row.avatar.name'
@click='showAvatarDialog(scope.row.avatar.id)')
| &nbsp;
span(v-if='!scope.row.inCache' style='color: #aaa') #[i.el-icon-download]&nbsp;
span.avatar-info-public(v-if='scope.row.avatar.releaseStatus === "public"') {{ $t('dialog.avatar.labels.public') }}
span.avatar-info-own(v-else-if='scope.row.avatar.releaseStatus === "private"') {{ $t('dialog.avatar.labels.private') }}
span(v-else-if='scope.row.type === "SpawnEmoji"')
span(v-if='scope.row.imageUrl')
el-tooltip(placement='right')
template(#content)
img.friends-list-avatar(
v-lazy='scope.row.imageUrl'
style='height: 500px; cursor: pointer'
@click='showFullscreenImageDialog(scope.row.imageUrl)')
span(v-text='scope.row.fileId')
span(v-else v-text='scope.row.text')
span(
v-else-if='scope.row.color === "yellow"'
v-text='scope.row.text'
style='color: yellow')
span(v-else v-text='scope.row.text')
el-tab-pane(:label='$t("view.player_list.photon.previous")')
data-tables(v-bind='photonEventTablePrevious' style='margin-bottom: 10px')
el-table-column(:label='$t("table.playerList.date")' prop='created_at' width='120')
template(#default='scope')
el-tooltip(placement='right')
template(#content)
span {{ scope.row.created_at | formatDate('long') }}
span {{ scope.row.created_at | formatDate('short') }}
el-table-column(:label='$t("table.playerList.user")' prop='photonId' width='160')
template(#default='scope')
span.x-link(
v-text='scope.row.displayName'
@click='lookupUser(scope.row)'
style='padding-right: 10px')
el-table-column(:label='$t("table.playerList.type")' prop='type' width='140')
el-table-column(:label='$t("table.playerList.detail")' prop='text')
template(#default='scope')
template(v-if='scope.row.type === "ChangeAvatar"')
span.x-link(
v-text='scope.row.avatar.name'
@click='showAvatarDialog(scope.row.avatar.id)')
| &nbsp;
span(v-if='!scope.row.inCache' style='color: #aaa') #[i.el-icon-download]&nbsp;
span.avatar-info-public(v-if='scope.row.avatar.releaseStatus === "public"') {{ $t('dialog.avatar.labels.public') }}
span.avatar-info-own(v-else-if='scope.row.avatar.releaseStatus === "private"') {{ $t('dialog.avatar.labels.private') }}
template(
v-if='scope.row.avatar.description && scope.row.avatar.name !== scope.row.avatar.description')
|
| - {{ scope.row.avatar.description }}
template(v-else-if='scope.row.type === "ChangeStatus"')
template(v-if='scope.row.status !== scope.row.previousStatus')
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.previousStatus === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.previousStatus === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.previousStatus === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.previousStatus === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(:class='statusClass(scope.row.previousStatus)')
span
i.el-icon-right
el-tooltip(placement='top')
template(#content)
span(v-if='scope.row.status === "active"') {{ $t('dialog.user.status.active') }}
span(v-else-if='scope.row.status === "join me"') {{ $t('dialog.user.status.join_me') }}
span(v-else-if='scope.row.status === "ask me"') {{ $t('dialog.user.status.ask_me') }}
span(v-else-if='scope.row.status === "busy"') {{ $t('dialog.user.status.busy') }}
span(v-else) {{ $t('dialog.user.status.offline') }}
i.x-user-status(
:class='statusClass(scope.row.status)'
style='margin-right: 5px')
span(
v-if='scope.row.statusDescription !== scope.row.previousStatusDescription'
v-text='scope.row.statusDescription')
template(v-else-if='scope.row.type === "ChangeGroup"')
span.x-link(
v-if='scope.row.previousGroupName'
v-text='scope.row.previousGroupName'
@click='showGroupDialog(scope.row.previousGroupId)'
style='margin-right: 5px')
span.x-link(
v-else
v-text='scope.row.previousGroupId'
@click='showGroupDialog(scope.row.previousGroupId)'
style='margin-right: 5px')
span
i.el-icon-right
span.x-link(
v-if='scope.row.groupName'
v-text='scope.row.groupName'
@click='showGroupDialog(scope.row.groupId)'
style='margin-left: 5px')
span.x-link(
v-else
v-text='scope.row.groupId'
@click='showGroupDialog(scope.row.groupId)'
style='margin-left: 5px')
span.x-link(
v-else-if='scope.row.type === "PortalSpawn"'
@click='showWorldDialog(scope.row.location, scope.row.shortName)')
location(
:location='scope.row.location'
:hint='scope.row.worldName'
:grouphint='scope.row.groupName'
:link='false')
span(v-else-if='scope.row.type === "ChatBoxMessage"' v-text='scope.row.text')
span(v-else-if='scope.row.type === "OnPlayerJoined"')
span(v-if='scope.row.platform === "Desktop"' style='color: #409eff') Desktop&nbsp;
span(v-else-if='scope.row.platform === "VR"' style='color: #409eff') VR&nbsp;
span(v-else-if='scope.row.platform === "Quest"' style='color: #67c23a') Android&nbsp;
span.x-link(
v-text='scope.row.avatar.name'
@click='showAvatarDialog(scope.row.avatar.id)')
| &nbsp;
span(v-if='!scope.row.inCache' style='color: #aaa') #[i.el-icon-download]&nbsp;
span.avatar-info-public(v-if='scope.row.avatar.releaseStatus === "public"') {{ $t('dialog.avatar.labels.public') }}
span.avatar-info-own(v-else-if='scope.row.avatar.releaseStatus === "private"') {{ $t('dialog.avatar.labels.private') }}
span(v-else-if='scope.row.type === "SpawnEmoji"')
span(v-if='scope.row.imageUrl')
el-tooltip(placement='right')
template(#content)
img.friends-list-avatar(
v-lazy='scope.row.imageUrl'
style='height: 500px; cursor: pointer'
@click='showFullscreenImageDialog(scope.row.imageUrl)')
span(v-text='scope.row.fileId')
span(v-else v-text='scope.row.text')
span(
v-else-if='scope.row.color === "yellow"'
v-text='scope.row.text'
style='color: yellow')
span(v-else v-text='scope.row.text')
.current-instance-table
data-tables(
v-bind='currentInstanceUserList'
@row-click='selectCurrentInstanceRow'
style='margin-top: 10px; cursor: pointer')
el-table-column(:label='$t("table.playerList.avatar")' width='70' prop='photo')
template(#default='scope')
template(v-if='userImage(scope.row.ref)')
el-popover(placement='right' height='500px' trigger='hover')
img.friends-list-avatar(slot='reference' v-lazy='userImage(scope.row.ref)')
img.friends-list-avatar(
v-lazy='userImageFull(scope.row.ref)'
style='height: 500px; cursor: pointer'
@click='showFullscreenImageDialog(userImageFull(scope.row.ref))')
el-table-column(:label='$t("table.playerList.timer")' width='80' prop='timer' sortable)
template(#default='scope')
timer(:epoch='scope.row.timer')
el-table-column(
v-if='photonLoggingEnabled'
:label='$t("table.playerList.photonId")'
width='110'
prop='photonId'
sortable)
template(#default='scope')
template(v-if='chatboxUserBlacklist.has(scope.row.ref.id)')
el-tooltip(placement='left' content='Unblock chatbox messages')
el-button(
type='text'
icon='el-icon-turn-off-microphone'
size='mini'
style='color: red; margin-right: 5px'
@click.stop='deleteChatboxUserBlacklist(scope.row.ref.id)')
template(v-else)
el-tooltip(placement='left' content='Block chatbox messages')
el-button(
type='text'
icon='el-icon-microphone'
size='mini'
style='margin-right: 5px'
@click.stop='addChatboxUserBlacklist(scope.row.ref)')
span(v-text='scope.row.photonId')
el-table-column(:label='$t("table.playerList.icon")' prop='isMaster' width='70' align='center')
template(#default='scope')
el-tooltip(v-if='scope.row.isMaster' placement='left' content='Instance Master')
span 👑
el-tooltip(v-if='scope.row.isModerator' placement='left' content='Moderator')
span ⚔️
el-tooltip(v-if='scope.row.isFriend' placement='left' content='Friend')
span 💚
el-tooltip(v-if='scope.row.timeoutTime' placement='left' content='Timeout')
span(style='color: red') 🔴{{ scope.row.timeoutTime }}s
el-table-column(:label='$t("table.playerList.platform")' prop='inVRMode' width='80')
template(#default='scope')
template(v-if='scope.row.ref.last_platform')
span(v-if='scope.row.ref.last_platform === "standalonewindows"' style='color: #409eff') PC
span(v-else-if='scope.row.ref.last_platform === "android"' style='color: #67c23a') A
span(v-else-if='scope.row.ref.last_platform === "ios"' style='color: #c7c7ce') iOS
span(v-else) {{ scope.row.ref.last_platform }}
template(v-if='scope.row.inVRMode !== null')
span(v-if='scope.row.inVRMode') VR
span(
v-else-if='scope.row.ref.last_platform === "android" || scope.row.ref.last_platform === "ios"') M
span(v-else) D
el-table-column(
:label='$t("table.playerList.displayName")'
min-width='140'
prop='displayName'
sortable='custom')
template(#default='scope')
span(
v-if='randomUserColours'
v-text='scope.row.ref.displayName'
:style='{ color: scope.row.ref.$userColour }')
span(v-else v-text='scope.row.ref.displayName')
el-table-column(:label='$t("table.playerList.status")' min-width='180' prop='ref.status')
template(#default='scope')
template(v-if='scope.row.ref.status')
i.x-user-status(:class='statusClass(scope.row.ref.status)' style='margin-right: 3px')
span(v-text='scope.row.ref.statusDescription')
//- el-table-column(label="Group" min-width="180" prop="groupOnNameplate" sortable)
//- template(v-once #default="scope")
//- span(v-text="scope.row.groupOnNameplate")
el-table-column(
:label='$t("table.playerList.rank")'
width='110'
prop='$trustSortNum'
sortable='custom')
template(#default='scope')
span.name(v-text='scope.row.ref.$trustLevel' :class='scope.row.ref.$trustClass')
el-table-column(:label='$t("table.playerList.language")' width='100' prop='ref.$languages')
template(#default='scope')
el-tooltip(v-for='item in scope.row.ref.$languages' :key='item.key' placement='top')
template(#content)
span {{ item.value }} ({{ item.key }})
span.flags(
:class='languageClass(item.key)'
style='display: inline-block; margin-right: 5px')
el-table-column(:label='$t("table.playerList.bioLink")' width='100' prop='ref.bioLinks')
template(#default='scope')
div(style='display: flex; align-items: center')
el-tooltip(v-if='link' v-for='(link, index) in scope.row.ref.bioLinks' :key='index')
template(#content)
span(v-text='link')
img(
:src='getFaviconUrl(link)'
style='width: 16px; height: 16px; vertical-align: middle; margin-right: 5px; cursor: pointer'
@click.stop='openExternalLink(link)')

View File

@@ -1,366 +0,0 @@
mixin profileTab
.x-container(v-if='menuActiveIndex === "profile"')
.options-container(style='margin-top: 0')
span.header {{ $t('view.profile.profile.header') }}
.x-friend-list(style='margin-top: 10px')
.x-friend-item(@click='showUserDialog(API.currentUser.id)')
.avatar
img(v-lazy='userImage(API.currentUser, true)')
.detail
span.name(v-text='API.currentUser.displayName')
span.extra(v-text='API.currentUser.username')
.x-friend-item(style='cursor: default')
.detail
span.name {{ $t('view.profile.profile.last_activity') }}
span.extra {{ API.currentUser.last_activity | formatDate('long') }}
.x-friend-item(style='cursor: default')
.detail
span.name {{ $t('view.profile.profile.two_factor') }}
span.extra {{ API.currentUser.twoFactorAuthEnabled ? $t('view.profile.profile.two_factor_enabled') : $t('view.profile.profile.two_factor_disabled') }}
.x-friend-item(@click='getVRChatCredits()')
.detail
span.name {{ $t('view.profile.profile.vrchat_credits') }}
span.extra {{ API.currentUser.$vrchatcredits ?? $t('view.profile.profile.refresh') }}
div(style='margin-top: 10px')
el-button(
size='small'
type='danger'
plain
icon='el-icon-switch-button'
@click='logout()'
style='margin-left: 0; margin-right: 5px; margin-top: 10px') {{ $t('view.profile.profile.logout') }}
el-button(
size='small'
icon='el-icon-picture-outline'
@click='showGalleryDialog()'
style='margin-left: 0; margin-right: 5px; margin-top: 10px') {{ $t('view.profile.profile.manage_gallery_icon') }}
el-button(
size='small'
icon='el-icon-chat-dot-round'
@click='showDiscordNamesDialog()'
style='margin-left: 0; margin-right: 5px; margin-top: 10px') {{ $t('view.profile.profile.discord_names') }}
el-button(
size='small'
icon='el-icon-printer'
@click='showExportFriendsListDialog()'
style='margin-left: 0; margin-right: 5px; margin-top: 10px') {{ $t('view.profile.profile.export_friend_list') }}
el-button(
size='small'
icon='el-icon-user'
@click='showExportAvatarsListDialog()'
style='margin-left: 0; margin-right: 5px; margin-top: 10px') {{ $t('view.profile.profile.export_own_avatars') }}
.options-container
span.header {{ $t('view.profile.game_info.header') }}
.x-friend-list(style='margin-top: 10px')
.x-friend-item
.detail(@click='API.getVisits()')
span.name {{ $t('view.profile.game_info.online_users') }}
span.extra(v-if='visits') {{ $t('view.profile.game_info.user_online', { count: visits }) }}
span.extra(v-else) {{ $t('view.profile.game_info.refresh') }}
.options-container
.header-bar
span.header {{ $t('view.profile.vrc_sdk_downloads.header') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='API.getConfig()'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
.x-friend-list(style='margin-top: 10px')
.x-friend-item(v-for='(link, item) in API.cachedConfig.downloadUrls' :key='item' placement='top')
.detail(@click='openExternalLink(link)')
span.name(v-text='item')
span.extra(v-text='link')
.options-container
span.header {{ $t('view.profile.direct_access.header') }}
div(style='margin-top: 10px')
el-button-group
el-button(size='small' @click='promptUsernameDialog()') {{ $t('view.profile.direct_access.username') }}
el-button(size='small' @click='promptUserIdDialog()') {{ $t('view.profile.direct_access.user_id') }}
el-button(size='small' @click='promptWorldDialog()') {{ $t('view.profile.direct_access.world_instance') }}
el-button(size='small' @click='promptAvatarDialog()') {{ $t('view.profile.direct_access.avatar') }}
.options-container
.header-bar
span.header {{ $t('view.profile.invite_messages') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='inviteMessageTable.visible = true; refreshInviteMessageTable("message")'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='inviteMessageTable.visible = false'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
data-tables(v-if='inviteMessageTable.visible' v-bind='inviteMessageTable' style='margin-top: 10px')
el-table-column(
:label='$t("table.profile.invite_messages.slot")'
prop='slot'
sortable='custom'
width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='60' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditInviteMessageDialog("message", scope.row)')
.options-container
.header-bar
span.header {{ $t('view.profile.invite_response_messages') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='inviteResponseMessageTable.visible = true; refreshInviteMessageTable("response")'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='inviteResponseMessageTable.visible = false'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
data-tables(
v-if='inviteResponseMessageTable.visible'
v-bind='inviteResponseMessageTable'
style='margin-top: 10px')
el-table-column(
:label='$t("table.profile.invite_messages.slot")'
prop='slot'
sortable='custom'
width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='60' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditInviteMessageDialog("response", scope.row)')
.options-container
.header-bar
span.header {{ $t('view.profile.invite_request_messages') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='inviteRequestMessageTable.visible = true; refreshInviteMessageTable("request")'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='inviteRequestMessageTable.visible = false'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
data-tables(
v-if='inviteRequestMessageTable.visible'
v-bind='inviteRequestMessageTable'
style='margin-top: 10px')
el-table-column(
:label='$t("table.profile.invite_messages.slot")'
prop='slot'
sortable='custom'
width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='60' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditInviteMessageDialog("request", scope.row)')
.options-container
.header-bar
span.header {{ $t('view.profile.invite_request_response_messages') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='inviteRequestResponseMessageTable.visible = true; refreshInviteMessageTable("requestResponse")'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='inviteRequestResponseMessageTable.visible = false'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
data-tables(
v-if='inviteRequestResponseMessageTable.visible'
v-bind='inviteRequestResponseMessageTable'
style='margin-top: 10px')
el-table-column(
:label='$t("table.profile.invite_messages.slot")'
prop='slot'
sortable='custom'
width='70')
el-table-column(:label='$t("table.profile.invite_messages.message")' prop='message')
el-table-column(
:label='$t("table.profile.invite_messages.cool_down")'
prop='updatedAt'
sortable='custom'
width='110'
align='right')
template(#default='scope')
countdown-timer(:datetime='scope.row.updatedAt' :hours='1')
el-table-column(:label='$t("table.profile.invite_messages.action")' width='60' align='right')
template(#default='scope')
el-button(
type='text'
icon='el-icon-edit'
size='mini'
@click='showEditInviteMessageDialog("requestResponse", scope.row)')
.options-container
span.header {{ $t('view.profile.past_display_names') }}
data-tables(v-bind='pastDisplayNameTable' style='margin-top: 10px')
el-table-column(
:label='$t("table.profile.previous_display_name.date")'
prop='updated_at'
sortable='custom')
template(#default='scope')
span {{ scope.row.updated_at | formatDate('long') }}
el-table-column(:label='$t("table.profile.previous_display_name.name")' prop='displayName')
.options-container
.header-bar
span.header {{ $t('view.profile.config_json') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='refreshConfigTreeData()'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='configTreeData = []'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tree(v-if='configTreeData.length > 0' :data='configTreeData' style='margin-top: 10px; font-size: 12px')
template(#default='scope')
span
span(v-text='scope.data.key' style='font-weight: bold; margin-right: 5px')
span(v-if='!scope.data.children' v-text='scope.data.value')
.options-container
.header-bar
span.header {{ $t('view.profile.current_user_json') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='refreshCurrentUserTreeData()'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='currentUserTreeData = []'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tree(
v-if='currentUserTreeData.length > 0'
:data='currentUserTreeData'
style='margin-top: 10px; font-size: 12px')
template(#default='scope')
span
span(v-text='scope.data.key' style='font-weight: bold; margin-right: 5px')
span(v-if='!scope.data.children' v-text='scope.data.value')
.options-container
.header-bar
span.header {{ $t('view.profile.feedback') }}
el-tooltip(placement='top' :content='$t("view.profile.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='getCurrentUserFeedback()'
size='mini'
icon='el-icon-refresh'
circle
style='margin-left: 5px')
el-tooltip(
placement='top'
:content='$t("view.profile.clear_results_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
@click='currentUserFeedbackData = []'
size='mini'
icon='el-icon-delete'
circle
style='margin-left: 5px')
el-tree(
v-if='currentUserFeedbackData.length > 0'
:data='currentUserFeedbackData'
style='margin-top: 10px; font-size: 12px')
template(#default='scope')
span
span(v-text='scope.data.key' style='font-weight: bold; margin-right: 5px')
span(v-if='!scope.data.children' v-text='scope.data.value')

View File

@@ -1,205 +0,0 @@
mixin searchTab
.x-container(v-show='menuActiveIndex === "search"')
div(style='margin: 0 0 10px; display: flex; align-items: center')
el-input(
v-model='searchText'
:placeholder='$t("view.search.search_placeholder")'
@keyup.native.13='search()'
style='flex: 1')
el-tooltip(placement='bottom' :content='$t("view.search.clear_results_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='clearSearch()'
icon='el-icon-delete'
circle
style='flex: none; margin-left: 10px')
el-tabs(ref='searchTab' type='card' style='margin-top: 15px' @tab-click='searchText = ""')
el-tab-pane(
:label='$t("view.search.user.header")'
v-loading='isSearchUserLoading'
style='min-height: 60px')
el-checkbox(v-model='searchUserByBio' style='margin-left: 10px') {{ $t('view.search.user.search_by_bio') }}
el-checkbox(v-model='searchUserSortByLastLoggedIn' style='margin-left: 10px') {{ $t('view.search.user.sort_by_last_logged_in') }}
.x-friend-list(style='min-height: 500px')
.x-friend-item(v-for='user in searchUserResults' :key='user.id' @click='showUserDialog(user.id)')
template
.avatar
img(v-lazy='userImage(user, true)')
.detail
span.name(v-text='user.displayName')
span.extra(
v-if='randomUserColours'
v-text='user.$trustLevel'
:class='user.$trustClass')
span.extra(v-else v-text='user.$trustLevel' :style='{ color: user.$userColour }')
el-button-group(style='margin-top: 15px' v-if='searchUserResults.length')
el-button(
:disabled='!searchUserParams.offset'
@click='moreSearchUser(-1)'
icon='el-icon-back'
size='small') {{ $t('view.search.prev_page') }}
el-button(
:disabled='searchUserResults.length < 10'
@click='moreSearchUser(1)'
icon='el-icon-right'
size='small') {{ $t('view.search.next_page') }}
el-tab-pane(
:label='$t("view.search.world.header")'
v-loading='isSearchWorldLoading'
style='min-height: 60px')
el-dropdown(
@command='(row) => searchWorld(row)'
size='small'
trigger='click'
style='margin-bottom: 15px')
el-button(size='small') {{ $t('view.search.world.category') }} #[i.el-icon-arrow-down.el-icon--right]
el-dropdown-menu(#default='dropdown')
el-dropdown-item(
v-for='row in API.cachedConfig.dynamicWorldRows'
:key='row.index'
v-text='row.name'
:command='row')
el-checkbox(v-model='searchWorldLabs' style='margin-left: 10px') {{ $t('view.search.world.community_lab') }}
.x-friend-list(style='min-height: 500px')
.x-friend-item(
v-for='world in searchWorldResults'
:key='world.id'
@click='showWorldDialog(world.id)')
template
.avatar
img(v-lazy='world.thumbnailImageUrl')
.detail
span.name(v-text='world.name')
span.extra(v-if='world.occupants') {{ world.authorName }} ({{ world.occupants }})
span.extra(v-else v-text='world.authorName')
el-button-group(style='margin-top: 15px' v-if='searchWorldResults.length')
el-button(
:disabled='!searchWorldParams.offset'
@click='moreSearchWorld(-1)'
icon='el-icon-back'
size='small') {{ $t('view.search.prev_page') }}
el-button(
:disabled='searchWorldResults.length < 10'
@click='moreSearchWorld(1)'
icon='el-icon-right'
size='small') {{ $t('view.search.next_page') }}
el-tab-pane(
:label='$t("view.search.avatar.header")'
v-loading='isSearchAvatarLoading'
style='min-height: 60px')
div(style='display: flex; align-items: center; justify-content: space-between')
div(style='display: flex; align-items: center')
el-dropdown(
v-if='avatarRemoteDatabaseProviderList.length > 1'
trigger='click'
@click.native.stop
size='mini'
style='margin-right: 5px')
el-button(size='small') {{ $t('view.search.avatar.search_provider') }} #[i.el-icon-arrow-down.el-icon--right]
el-dropdown-menu(#default='dropdown')
el-dropdown-item(
v-for='provider in avatarRemoteDatabaseProviderList'
:key='provider'
@click.native='setAvatarProvider(provider)') #[i.el-icon-check.el-icon--left(v-if='provider === avatarRemoteDatabaseProvider')] {{ provider }}
el-tooltip(
placement='bottom'
:content='$t("view.search.avatar.refresh_tooltip")'
:disabled='hideTooltips')
el-button(
type='default'
:loading='userDialog.isAvatarsLoading'
@click='refreshUserDialogAvatars()'
size='mini'
icon='el-icon-refresh'
circle)
span(style='font-size: 14px; margin-left: 5px; margin-right: 5px') {{ $t('view.search.avatar.result_count', { count: searchAvatarResults.length }) }}
div(style='display: flex; align-items: center')
el-radio-group(
v-model='searchAvatarFilter'
size='mini'
style='margin: 5px; display: block'
@change='searchAvatar')
el-radio(label='all') {{ $t('view.search.avatar.all') }}
el-radio(label='public') {{ $t('view.search.avatar.public') }}
el-radio(label='private') {{ $t('view.search.avatar.private') }}
el-divider(direction='vertical')
el-radio-group(
v-model='searchAvatarFilterRemote'
size='mini'
style='margin: 5px; display: block'
@change='searchAvatar')
el-radio(label='all') {{ $t('view.search.avatar.all') }}
el-radio(label='local') {{ $t('view.search.avatar.local') }}
el-radio(label='remote' :disabled='!avatarRemoteDatabase') {{ $t('view.search.avatar.remote') }}
div(style='display: flex; justify-content: end')
el-radio-group(
:disabled='searchAvatarFilterRemote !== "local"'
v-model='searchAvatarSort'
size='mini'
style='margin: 5px; display: block'
@change='searchAvatar')
el-radio(label='name') {{ $t('view.search.avatar.sort_name') }}
el-radio(label='update') {{ $t('view.search.avatar.sort_update') }}
el-radio(label='created') {{ $t('view.search.avatar.sort_created') }}
.x-friend-list(style='margin-top: 20px; min-height: 500px')
.x-friend-item(
v-for='avatar in searchAvatarPage'
:key='avatar.id'
@click='showAvatarDialog(avatar.id)')
template
.avatar
img(v-if='avatar.thumbnailImageUrl' v-lazy='avatar.thumbnailImageUrl')
img(v-else-if='avatar.imageUrl' v-lazy='avatar.imageUrl')
.detail
span.name(v-text='avatar.name')
span.extra(
v-text='avatar.releaseStatus'
v-if='avatar.releaseStatus === "public"'
style='color: #67c23a')
span.extra(
v-text='avatar.releaseStatus'
v-else-if='avatar.releaseStatus === "private"'
style='color: #f56c6c')
span.extra(v-text='avatar.releaseStatus' v-else)
span.extra(v-text='avatar.authorName')
el-button-group(style='margin-top: 15px' v-if='searchAvatarPage.length')
el-button(
:disabled='!searchAvatarPageNum'
@click='moreSearchAvatar(-1)'
icon='el-icon-back'
size='small') {{ $t('view.search.prev_page') }}
el-button(
:disabled='searchAvatarResults.length < 10 || (searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length'
@click='moreSearchAvatar(1)'
icon='el-icon-right'
size='small') {{ $t('view.search.next_page') }}
el-tab-pane(
:label='$t("view.search.group.header")'
v-loading='isSearchGroupLoading'
style='min-height: 60px')
.x-friend-list(style='min-height: 500px')
.x-friend-item(
v-for='group in searchGroupResults'
:key='group.id'
@click='showGroupDialog(group.id)')
template
.avatar
img(v-lazy='getSmallThumbnailUrl(group.iconUrl)')
.detail
span.name
span(v-text='group.name')
span(style='margin-left: 5px; font-weight: normal') ({{ group.memberCount }})
span(
style='margin-left: 5px; color: #909399; font-weight: normal; font-family: monospace; font-size: 12px') {{ group.shortCode }}.{{ group.discriminator }}
span.extra(v-text='group.description')
el-button-group(style='margin-top: 15px' v-if='searchGroupResults.length')
el-button(
:disabled='!searchGroupParams.offset'
@click='moreSearchGroup(-1)'
icon='el-icon-back'
size='small') {{ $t('view.search.prev_page') }}
el-button(
:disabled='searchGroupResults.length < 10'
@click='moreSearchGroup(1)'
icon='el-icon-right'
size='small') {{ $t('view.search.next_page') }}

View File

@@ -116,6 +116,7 @@
<script>
import dayjs from 'dayjs';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import utils from '../../../classes/utils';
import configRepository from '../../../service/config';
@@ -379,7 +380,7 @@
const timeString = utils.timeToText(param.data, true);
const color = param.color;
const name = param.name;
const location = utils.parseLocation(instanceData.location);
const location = parseLocation(instanceData.location);
return `
<div style="display: flex; align-items: center;">

View File

@@ -1,11 +1,5 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
:visible.sync="isDialogVisible"
:title="$t('dialog.avatar_export.header')"
width="650px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.avatar_export.header')" width="650px">
<el-checkbox-group
v-model="exportSelectedOptions"
style="margin-bottom: 10px"
@@ -82,13 +76,13 @@
readonly
style="margin-top: 15px"
@click.native="handleCopyAvatarExportData"></el-input>
</el-dialog>
</safe-dialog>
</template>
<script>
export default {
name: 'AvatarExportDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
inject: ['API'],
props: {
avatarExportDialogVisible: Boolean,
favoriteAvatars: Array,

View File

@@ -1,12 +1,9 @@
<template>
<el-dialog
<safe-dialog
ref="avatarImportDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.avatar_import.header')"
width="650px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ $t('dialog.avatar_import.description') }}</div>
<div style="display: flex; align-items: center">
@@ -171,7 +168,7 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -180,16 +177,7 @@
export default {
name: 'AvatarImportDialog',
inject: [
'API',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'adjustDialogZ',
'showFullscreenImageDialog',
'showUserDialog',
'showAvatarDialog'
],
inject: ['API', 'adjustDialogZ', 'showFullscreenImageDialog', 'showUserDialog', 'showAvatarDialog'],
props: {
getLocalAvatarFavoriteGroupLength: Function,
localAvatarFavoriteGroups: Array,

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="isDialogVisible"
class="x-dialog"
:title="$t('dialog.friend_export.header')"
width="650px"
destroy-on-close
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
destroy-on-close>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<span v-if="friendExportFavoriteGroup">
@@ -42,13 +39,13 @@
readonly
style="margin-top: 15px"
@click.native="handleCopyFriendExportData"></el-input>
</el-dialog>
</safe-dialog>
</template>
<script>
export default {
name: 'FriendExportDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
inject: ['API'],
props: {
friendExportDialogVisible: Boolean,
favoriteFriends: Array

View File

@@ -1,12 +1,9 @@
<template>
<el-dialog
<safe-dialog
ref="friendImportDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.friend_import.header')"
width="650px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ $t('dialog.friend_import.description') }}</div>
<div style="display: flex; align-items: center">
@@ -122,7 +119,7 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -131,17 +128,7 @@
export default {
name: 'FriendImportDialog',
inject: [
'API',
'userImage',
'userImageFull',
'showFullscreenImageDialog',
'showUserDialog',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'adjustDialogZ'
],
inject: ['API', 'userImage', 'userImageFull', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ'],
props: {
friendImportDialogVisible: {
type: Boolean,

View File

@@ -1,11 +1,5 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
:visible.sync="isDialogVisible"
:title="$t('dialog.world_export.header')"
width="650px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.world_export.header')" width="650px">
<el-checkbox-group
v-model="exportSelectedOptions"
style="margin-bottom: 10px"
@@ -84,13 +78,13 @@
readonly
style="margin-top: 15px"
@click.native="handleCopyWorldExportData"></el-input>
</el-dialog>
</safe-dialog>
</template>
<script>
export default {
name: 'WorldExportDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
inject: ['API'],
props: {
favoriteWorlds: Array,
worldExportDialogVisible: Boolean,

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="worldImportDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.world_import.header')"
width="650px"
top="10vh"
class="x-dialog"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
class="x-dialog">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ $t('dialog.world_import.description') }}</div>
<div style="display: flex; align-items: center">
@@ -176,7 +173,7 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -185,16 +182,7 @@
export default {
name: 'WorldImportDialog',
inject: [
'API',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showFullscreenImageDialog',
'showUserDialog',
'adjustDialogZ',
'showWorldDialog'
],
inject: ['API', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ', 'showWorldDialog'],
props: {
worldImportDialogVisible: Boolean,
worldImportDialogInput: String,

475
src/views/Feed/Feed.vue Normal file
View File

@@ -0,0 +1,475 @@
<template>
<div v-show="menuActiveIndex === 'feed'" class="x-container feed">
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip
placement="bottom"
:content="t('view.feed.favorites_only_tooltip')"
:disabled="hideTooltips">
<el-switch v-model="feedTable.vip" active-color="#13ce66" @change="feedTableLookup"></el-switch>
</el-tooltip>
</div>
<el-select
v-model="feedTable.filter"
multiple
clearable
style="flex: 1; height: 40px"
:placeholder="t('view.feed.filter_placeholder')"
@change="feedTableLookup">
<el-option
v-for="type in ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio']"
:key="type"
:label="t('view.feed.filters.' + type)"
:value="type"></el-option>
</el-select>
<el-input
v-model="feedTable.search"
:placeholder="t('view.feed.search_placeholder')"
clearable
style="flex: none; width: 150px; margin: 0 10px"
@keyup.native.13="feedTableLookup"
@change="feedTableLookup"></el-input>
</div>
<data-tables v-loading="feedTable.loading" v-bind="feedTable" lazy>
<el-table-column type="expand" width="20">
<template #default="scope">
<div style="position: relative; font-size: 14px">
<template v-if="scope.row.type === 'GPS'">
<location
v-if="scope.row.previousLocation"
:location="scope.row.previousLocation"
style="display: inline-block"></location>
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
<br />
<span style="margin-right: 5px">
<i class="el-icon-right"></i>
</span>
<location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
</template>
<template v-else-if="scope.row.type === 'Offline'">
<template v-if="scope.row.location">
<location
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
</template>
</template>
<template v-else-if="scope.row.type === 'Online'">
<location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
</template>
<template v-else-if="scope.row.type === 'Avatar'">
<div style="display: flex; align-items: center">
<el-popover placement="right" width="500px" trigger="click">
<div
slot="reference"
style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.previousCurrentAvatarThumbnailImageUrl">
<img
v-lazy="scope.row.previousCurrentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
<br />
<avatar-info
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.previousOwnerId"
:hintavatarname="scope.row.previousAvatarName"
:avatartags="scope.row.previousCurrentAvatarTags"></avatar-info>
</template>
</div>
<img
v-lazy="scope.row.previousCurrentAvatarImageUrl"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(scope.row.previousCurrentAvatarImageUrl)" />
</el-popover>
<span style="position: relative; margin: 0 10px">
<i class="el-icon-right"></i>
</span>
<el-popover placement="right" width="500px" trigger="click">
<div
slot="reference"
style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.currentAvatarThumbnailImageUrl">
<img
v-lazy="scope.row.currentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
<br />
<avatar-info
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags"></avatar-info>
</template>
</div>
<img
v-lazy="scope.row.currentAvatarImageUrl"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(scope.row.currentAvatarImageUrl)" />
</el-popover>
</div>
</template>
<template v-else-if="scope.row.type === 'Status'">
<el-tooltip placement="top">
<template #content>
<span v-if="scope.row.previousStatus === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.previousStatus === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
</el-tooltip>
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
<br />
<span>
<i class="el-icon-right"></i>
</span>
<el-tooltip placement="top">
<template #content>
<span v-if="scope.row.status === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.status === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.status === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.status === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i
class="x-user-status"
:class="statusClass(scope.row.status)"
style="margin: 0 5px"></i>
</el-tooltip>
<span v-text="scope.row.statusDescription"></span>
</template>
<template v-else-if="scope.row.type === 'Bio'">
<pre
style="
font-family: inherit;
font-size: 12px;
white-space: pre-wrap;
line-height: 25px;
line-height: 22px;
"
v-html="formatDifference(scope.row.previousBio, scope.row.bio)"></pre>
</template>
</div>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.date')" prop="created_at" sortable="custom" width="120">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
<span>{{ scope.row.created_at | formatDate('long') }}</span>
</template>
<span>{{ scope.row.created_at | formatDate('short') }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.type')" prop="type" width="80">
<template #default="scope">
<span class="x-link" v-text="t('view.feed.filters.' + scope.row.type)"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.user')" prop="displayName" width="180">
<template #default="scope">
<span
class="x-link"
style="padding-right: 10px"
@click="showUserDialog(scope.row.userId)"
v-text="scope.row.displayName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.feed.detail')">
<template #default="scope">
<template v-if="scope.row.type === 'GPS'">
<location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
</template>
<template v-else-if="scope.row.type === 'Offline' || scope.row.type === 'Online'">
<location
v-if="scope.row.location"
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
</template>
<template v-else-if="scope.row.type === 'Status'">
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
<el-tooltip placement="top">
<template #content>
<span v-if="scope.row.previousStatus === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.previousStatus === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
</el-tooltip>
<span style="margin: 0 5px">
<i class="el-icon-right"></i>
</span>
<el-tooltip placement="top">
<template #content>
<span v-if="scope.row.status === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.status === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.status === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.status === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i class="x-user-status" :class="statusClass(scope.row.status)"></i>
</el-tooltip>
</template>
<template v-else>
<el-tooltip placement="top">
<template #content>
<span v-if="scope.row.status === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.status === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.status === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.status === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i
class="x-user-status"
:class="statusClass(scope.row.status)"
style="margin-right: 3px"></i>
</el-tooltip>
<span v-text="scope.row.statusDescription"></span>
</template>
</template>
<template v-else-if="scope.row.type === 'Avatar'">
<avatar-info
:imageurl="scope.row.currentAvatarImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags"></avatar-info>
</template>
<template v-else-if="scope.row.type === 'Bio'">
<span v-text="scope.row.bio"></span>
</template>
</template>
</el-table-column>
</data-tables>
</div>
</template>
<script>
export default {
name: 'FeedTab'
};
</script>
<script setup>
import { inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../classes/utils';
import Location from '../../components/Location.vue';
const { t } = useI18n();
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const statusClass = inject('statusClass');
const showUserDialog = inject('showUserDialog');
defineProps({
menuActiveIndex: {
type: String,
default: 'feed'
},
hideTooltips: {
type: Boolean,
default: false
},
feedTable: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['feedTableLookup']);
/**
* Function that format the differences between two strings with HTML tags
* markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined.
* @param {*} s1
* @param {*} s2
* @param {*} markerStartTag
* @param {*} markerEndTag
* @returns An array that contains both the string 1 and string 2, which the differences are formatted with HTML tags
*/
//function getWordDifferences
function formatDifference(
oldString,
newString,
markerAddition = '<span class="x-text-added">{{text}}</span>',
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
) {
[oldString, newString] = [oldString, newString].map((s) =>
s
.replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/"/g, '&quot;')
.replaceAll(/'/g, '&#039;')
.replaceAll(/\n/g, '<br>')
);
const oldWords = oldString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
const newWords = newString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
function findLongestMatch(oldStart, oldEnd, newStart, newEnd) {
let bestOldStart = oldStart;
let bestNewStart = newStart;
let bestSize = 0;
const lookup = new Map();
for (let i = oldStart; i < oldEnd; i++) {
const word = oldWords[i];
if (!lookup.has(word)) lookup.set(word, []);
lookup.get(word).push(i);
}
for (let j = newStart; j < newEnd; j++) {
const word = newWords[j];
if (!lookup.has(word)) continue;
for (const i of lookup.get(word)) {
let size = 0;
while (i + size < oldEnd && j + size < newEnd && oldWords[i + size] === newWords[j + size]) {
size++;
}
if (size > bestSize) {
bestOldStart = i;
bestNewStart = j;
bestSize = size;
}
}
}
return {
oldStart: bestOldStart,
newStart: bestNewStart,
size: bestSize
};
}
function buildDiff(oldStart, oldEnd, newStart, newEnd) {
const result = [];
const match = findLongestMatch(oldStart, oldEnd, newStart, newEnd);
if (match.size > 0) {
// Handle differences before the match
if (oldStart < match.oldStart || newStart < match.newStart) {
result.push(...buildDiff(oldStart, match.oldStart, newStart, match.newStart));
}
// Add the matched words
result.push(oldWords.slice(match.oldStart, match.oldStart + match.size).join(' '));
// Handle differences after the match
if (match.oldStart + match.size < oldEnd || match.newStart + match.size < newEnd) {
result.push(...buildDiff(match.oldStart + match.size, oldEnd, match.newStart + match.size, newEnd));
}
} else {
function build(words, start, end, pattern) {
let r = [];
let ts = words
.slice(start, end)
.filter((w) => w.length > 0)
.join(' ')
.split('<br>');
for (let i = 0; i < ts.length; i++) {
if (i > 0) r.push('<br>');
if (ts[i].length < 1) continue;
r.push(pattern.replace('{{text}}', ts[i]));
}
return r;
}
// Add deletions
if (oldStart < oldEnd) result.push(...build(oldWords, oldStart, oldEnd, markerDeletion));
// Add insertions
if (newStart < newEnd) result.push(...build(newWords, newStart, newEnd, markerAddition));
}
return result;
}
return buildDiff(0, oldWords.length, 0, newWords.length)
.join(' ')
.replace(/<br>[ ]+<br>/g, '<br><br>')
.replace(/<br> /g, '<br>');
}
function feedTableLookup() {
emit('feedTableLookup');
}
function timeToText(time) {
return utils.timeToText(time);
}
</script>

View File

@@ -270,9 +270,11 @@
</template>
<script>
import removeConfusables, { removeWhitespace } from '../../service/confusables';
import utils from '../../classes/utils';
import { friendRequest, userRequest } from '../../api';
import utils from '../../classes/utils';
import { languageClass as _languageClass } from '../../composables/user/utils';
import removeConfusables, { removeWhitespace } from '../../service/confusables';
import { getFaviconUrl as _getFaviconUrl } from '../../composables/shared/utils';
export default {
name: 'FriendListTab',
@@ -282,8 +284,7 @@
'showFullscreenImageDialog',
'showUserDialog',
'statusClass',
'openExternalLink',
'languageClass'
'openExternalLink'
],
props: {
friends: {
@@ -322,7 +323,7 @@
friendsListLoading: false,
friendsListLoadingProgress: '',
friendsListSearchFilterVIP: false,
// emm
// TODO
friendsListBulkUnfriendForceUpdate: 0
};
},
@@ -336,6 +337,9 @@
}
},
methods: {
languageClass(key) {
return _languageClass(key);
},
friendsListSearchChange() {
this.friendsListLoading = true;
let query = '';
@@ -505,7 +509,7 @@
return utils.timeToText(val);
},
getFaviconUrl(link) {
return utils.getFaviconUrl(link);
return _getFaviconUrl(link);
}
}
};

View File

@@ -0,0 +1,140 @@
<template>
<div v-if="menuActiveIndex === 'friendLog'" class="x-container">
<data-tables v-bind="friendLogTable">
<template #tool>
<div style="margin: 0 0 10px; display: flex; align-items: center">
<el-select
v-model="friendLogTable.filters[0].value"
multiple
clearable
style="flex: 1"
:placeholder="t('view.friend_log.filter_placeholder')"
@change="saveTableFilters">
<el-option
v-for="type in [
'Friend',
'Unfriend',
'FriendRequest',
'CancelFriendRequest',
'DisplayName',
'TrustLevel'
]"
:key="type"
:label="t('view.friend_log.filters.' + type)"
:value="type" />
</el-select>
<el-input
v-model="friendLogTable.filters[1].value"
:placeholder="t('view.friend_log.search_placeholder')"
style="flex: none; width: 150px; margin-left: 10px" />
</div>
</template>
<el-table-column :label="t('table.friendLog.date')" prop="created_at" sortable="custom" width="200">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
<span>{{ scope.row.created_at | formatDate('long') }}</span>
</template>
<span>{{ scope.row.created_at | formatDate('short') }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('table.friendLog.type')" prop="type" width="150">
<template #default="scope">
<span v-text="t('view.friend_log.filters.' + scope.row.type)"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.friendLog.user')" prop="displayName">
<template #default="scope">
<span v-if="scope.row.type === 'DisplayName'">
{{ scope.row.previousDisplayName }} <i class="el-icon-right"></i>&nbsp;
</span>
<span
class="x-link"
style="padding-right: 10px"
@click="showUserDialog(scope.row.userId)"
v-text="scope.row.displayName || scope.row.userId"></span>
<template v-if="scope.row.type === 'TrustLevel'">
<span>
({{ scope.row.previousTrustLevel }} <i class="el-icon-right"></i>
{{ scope.row.trustLevel }})</span
>
</template>
</template>
</el-table-column>
<el-table-column :label="t('table.friendLog.action')" width="80" align="right">
<template #default="scope">
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
@click="deleteFriendLog(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-delete"
size="mini"
@click="deleteFriendLogPrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</div>
</template>
<script>
export default {
name: 'FriendLogTab'
};
</script>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../classes/utils';
import configRepository from '../../service/config';
import database from '../../service/database';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $confirm } = proxy;
const showUserDialog = inject('showUserDialog');
const props = defineProps({
menuActiveIndex: {
type: String,
default: ''
},
friendLogTable: {
type: Object,
default: () => ({})
},
shiftHeld: { type: Boolean, default: false }
});
function saveTableFilters() {
configRepository.setString('VRCX_friendLogTableFilters', JSON.stringify(props.friendLogTable.filters[0].value));
}
function deleteFriendLogPrompt(row) {
$confirm('Continue? Delete Log', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
deleteFriendLog(row);
}
}
});
}
function deleteFriendLog(row) {
utils.removeFromArray(props.friendLogTable.data, row);
database.deleteFriendLogHistory(row.rowId);
}
</script>

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