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, json,
params params
}; };
window.API.$emit('FRIEND:REQUEST:CANCEL', args); // window.API.$emit('FRIEND:REQUEST:CANCEL', args);
return args; return args;
}); });
}, },

View File

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

View File

@@ -19,7 +19,7 @@ const miscReq = {
json, json,
params params
}; };
window.API.$emit('NOTE', args); // window.API.$emit('NOTE', args);
return args; return args;
}); });
}, },
@@ -46,7 +46,7 @@ const miscReq = {
json, json,
params params
}; };
window.API.$emit('FEEDBACK:REPORT:USER', args); // window.API.$emit('FEEDBACK:REPORT:USER', args);
return args; return args;
}); });
}, },
@@ -81,7 +81,7 @@ const miscReq = {
const args = { const args = {
json json
}; };
window.API.$emit('VRCCREDITS', args); // window.API.$emit('VRCCREDITS', args);
return args; return args;
}); });
}, },
@@ -170,10 +170,43 @@ const miscReq = {
json, json,
params 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; 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; export default miscReq;

View File

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

View File

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

View File

@@ -126,7 +126,33 @@ const userReq = {
json, json,
params 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; return args;
}); });
} }

View File

@@ -56,7 +56,7 @@ const vrcPlusImageReq = {
json, json,
printId printId
}; };
window.API.$emit('PRINT:DELETE', args); // window.API.$emit('PRINT:DELETE', args);
return 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 doctype html
#x-app.x-app(@dragenter.prevent @dragover.prevent @drop.prevent) #x-app.x-app(@dragenter.prevent @dragover.prevent @drop.prevent)
//- login LoginPage(v-if="!API.isLoggedIn" v-bind="loginPageBind" v-on="loginPageEvent")
include ./mixins/loginPage.pug
+loginPage VRCXUpdateDialog(v-bind="vrcxUpdateDialogBind" v-on="vrcxUpdateDialogEvent")
//- menu //- menu
.x-menu-container .x-menu-container
@@ -23,47 +23,30 @@ doctype html
circle circle
style='font-size: 14px; height: 50px; width: 50px') 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 ### //- ### Tabs ###
template(v-if='API.isLoggedIn') template(v-if='API.isLoggedIn')
//- feed FeedTab(v-bind='feedTabBind' v-on='feedTabEvent')
include ./mixins/tabs/feed.pug
+feedTab
//- gameLog GameLogTab(v-bind='gameLogTabBind' v-on='gameLogTabEvent')
include ./mixins/tabs/gameLog.pug
+gameLogTab
//- playerList PlayerListTab(v-bind='playerListTabBind' v-on='playerListTabEvent')
include ./mixins/tabs/playerList.pug
+playerListTab
//- search SearchTab(v-bind='searchTabBind' v-on='searchTabEvent')
include ./mixins/tabs/search.pug
+searchTab
FavoritesTab(v-bind='favoritesTabBind' v-on='favoritesTabEvent') FavoritesTab(v-bind='favoritesTabBind' v-on='favoritesTabEvent')
//- friendLog FriendLogTab(v-bind='friendLogTabBind')
include ./mixins/tabs/friendLog.pug
+friendLogTab
//- moderation
ModerationTab(v-bind='moderationTabBind') ModerationTab(v-bind='moderationTabBind')
//- notification NotificationTab(v-bind='notificationTabBind' v-on='notificationTabEvent')
include ./mixins/tabs/notifications.pug
+notificationsTab
//- profile ProfileTab(v-bind='profileTabBind' v-on='profileTabEvent')
include ./mixins/tabs/profile.pug
+profileTab
//- friends list
FriendListTab(v-bind='friendsListTabBind' v-on='friendsListTabEvent') FriendListTab(v-bind='friendsListTabBind' v-on='friendsListTabEvent')
//- charts
KeepAlive KeepAlive
ChartsTab(v-if='menuActiveIndex === "charts"' v-bind='chartsTabBind' v-on='chartsTabEvent') ChartsTab(v-if='menuActiveIndex === "charts"' v-bind='chartsTabBind' v-on='chartsTabEvent')
@@ -73,89 +56,8 @@ doctype html
SideBar(v-bind='sideBarTabBind' v-on='sideBarTabEvent') SideBar(v-bind='sideBarTabBind' v-on='sideBarTabEvent')
//- ## Dialogs ## -\\ //- ## Dialogs ## -\\
include ./mixins/dialogs/userDialog.pug include ./mixins/dialogs/dialogs.pug
+userDialog +dialogs
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")
//- 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") //- 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; display: flex;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden auto; overflow: hidden;
cursor: default; cursor: default;
} }

View File

@@ -590,6 +590,10 @@ input[type='number'],
.el-table table tr td:first-child { .el-table table tr td:first-child {
border-left: none; 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 .el-table__body-wrapper table tr:last-child th,
.el-table table tr:last-child td, .el-table table tr:last-child td,
.el-table tr, .el-table tr,

View File

@@ -239,13 +239,12 @@ export default class extends baseClass {
API.websocketDomain = API.websocketDomainVrchat; API.websocketDomain = API.websocketDomainVrchat;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.loginForm.loading = true;
if (this.enablePrimaryPassword) { if (this.enablePrimaryPassword) {
this.checkPrimaryPassword(loginParmas) this.checkPrimaryPassword(loginParmas)
.then((pwd) => { .then((pwd) => {
this.loginForm.loading = true;
return API.getConfig() return API.getConfig()
.catch((err) => { .catch((err) => {
this.loginForm.loading = false;
reject(err); reject(err);
}) })
.then(() => { .then(() => {
@@ -257,12 +256,10 @@ export default class extends baseClass {
websocket: loginParmas.websocket websocket: loginParmas.websocket
}) })
.catch((err2) => { .catch((err2) => {
this.loginForm.loading = false;
// API.logout(); // API.logout();
reject(err2); reject(err2);
}) })
.then(() => { .then(() => {
this.loginForm.loading = false;
resolve(); resolve();
}); });
}); });
@@ -277,7 +274,6 @@ export default class extends baseClass {
} else { } else {
API.getConfig() API.getConfig()
.catch((err) => { .catch((err) => {
this.loginForm.loading = false;
reject(err); reject(err);
}) })
.then(() => { .then(() => {
@@ -288,17 +284,15 @@ export default class extends baseClass {
websocket: loginParmas.websocket websocket: loginParmas.websocket
}) })
.catch((err2) => { .catch((err2) => {
this.loginForm.loading = false;
API.logout(); API.logout();
reject(err2); reject(err2);
}) })
.then(() => { .then(() => {
this.loginForm.loading = false;
resolve(); resolve();
}); });
}); });
} }
}); }).finally(() => (this.loginForm.loading = false));
}, },
async deleteSavedLogin(userId) { async deleteSavedLogin(userId) {
@@ -325,100 +319,90 @@ export default class extends baseClass {
async login() { async login() {
await webApiService.clearCookies(); await webApiService.clearCookies();
this.$refs.loginForm.validate((valid) => { if (!this.loginForm.loading) {
if (valid && !this.loginForm.loading) { this.loginForm.loading = true;
this.loginForm.loading = true; if (this.loginForm.endpoint) {
if (this.loginForm.endpoint) { API.endpointDomain = this.loginForm.endpoint;
API.endpointDomain = this.loginForm.endpoint; API.websocketDomain = this.loginForm.websocket;
API.websocketDomain = this.loginForm.websocket; } else {
} else { API.endpointDomain = API.endpointDomainVrchat;
API.endpointDomain = API.endpointDomainVrchat; API.websocketDomain = API.websocketDomainVrchat;
API.websocketDomain = API.websocketDomainVrchat; }
} API.getConfig()
API.getConfig() .catch((err) => {
.catch((err) => { this.loginForm.loading = false;
this.loginForm.loading = false; throw err;
throw err; })
}) .then((args) => {
.then((args) => { if (
if ( this.loginForm.saveCredentials &&
this.loginForm.saveCredentials && this.enablePrimaryPassword
this.enablePrimaryPassword ) {
) { $app.$prompt(
$app.$prompt( $t('prompt.primary_password.description'),
$t('prompt.primary_password.description'), $t('prompt.primary_password.header'),
$t('prompt.primary_password.header'), {
{ inputType: 'password',
inputType: 'password', inputPattern: /[\s\S]{1,32}/
inputPattern: /[\s\S]{1,32}/ }
} )
) .then(({ value }) => {
.then(({ value }) => { let saveCredential =
let saveCredential = this.loginForm.savedCredentials[
this.loginForm.savedCredentials[ Object.keys(
Object.keys( this.loginForm.savedCredentials
this.loginForm )[0]
.savedCredentials ];
)[0] security
]; .decrypt(
security saveCredential.loginParmas.password,
.decrypt( value
saveCredential.loginParmas )
.password, .then(() => {
value security
) .encrypt(
.then(() => { this.loginForm.password,
security value
.encrypt( )
this.loginForm.password, .then((pwd) => {
value API.login({
) username:
.then((pwd) => { this.loginForm
API.login({ .username,
username: password:
this.loginForm this.loginForm
.username, .password,
password: endpoint:
this.loginForm this.loginForm
.password, .endpoint,
endpoint: websocket:
this.loginForm this.loginForm
.endpoint, .websocket,
websocket: saveCredentials:
this.loginForm this.loginForm
.websocket, .saveCredentials,
saveCredentials: cipher: pwd
this.loginForm
.saveCredentials,
cipher: pwd
}).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
})
.then(() => {
this.$refs.loginForm.resetFields();
}) })
.finally(() => { .finally(() => {
this.loginForm.loading = false; this.loginForm.loading = false;
}); });
return args; 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() { 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 { export default class extends baseClass {
constructor(_app, _API, _t) { constructor(_app, _API, _t) {
@@ -83,8 +84,8 @@ export default class extends baseClass {
args.ref = this.applyCurrentUser(json); args.ref = this.applyCurrentUser(json);
// when isGameRunning use gameLog instead of API // when isGameRunning use gameLog instead of API
var $location = $app.parseLocation($app.lastLocation.location); var $location = parseLocation($app.lastLocation.location);
var $travelingLocation = $app.parseLocation( var $travelingLocation = parseLocation(
$app.lastLocationDestination $app.lastLocationDestination
); );
var location = $app.lastLocation.location; var location = $app.lastLocation.location;
@@ -94,12 +95,12 @@ export default class extends baseClass {
var travelingToWorld = $travelingLocation.worldId; var travelingToWorld = $travelingLocation.worldId;
var travelingToInstance = $travelingLocation.instanceId; var travelingToInstance = $travelingLocation.instanceId;
if (!$app.isGameRunning && json.presence) { if (!$app.isGameRunning && json.presence) {
if ($utils.isRealInstance(json.presence.world)) { if (isRealInstance(json.presence.world)) {
location = `${json.presence.world}:${json.presence.instance}`; location = `${json.presence.world}:${json.presence.instance}`;
} else { } else {
location = json.presence.world; location = json.presence.world;
} }
if ($utils.isRealInstance(json.presence.travelingToWorld)) { if (isRealInstance(json.presence.travelingToWorld)) {
travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`; travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`;
} else { } else {
travelingToLocation = json.presence.travelingToWorld; travelingToLocation = json.presence.travelingToWorld;
@@ -175,7 +176,7 @@ export default class extends baseClass {
} }
Object.assign(ref, json); Object.assign(ref, json);
if (ref.homeLocation !== ref.$homeLocation.tag) { if (ref.homeLocation !== ref.$homeLocation.tag) {
ref.$homeLocation = $app.parseLocation(ref.homeLocation); ref.$homeLocation = parseLocation(ref.homeLocation);
// apply home location name to user dialog // apply home location name to user dialog
if ( if (
$app.userDialog.visible && $app.userDialog.visible &&
@@ -295,13 +296,12 @@ export default class extends baseClass {
$languages: [], $languages: [],
$locationTag: '', $locationTag: '',
$travelingToLocation: '', $travelingToLocation: '',
$vrchatcredits: null,
...json ...json
}; };
if ($app.isGameRunning) { if ($app.isGameRunning) {
ref.$previousAvatarSwapTime = Date.now(); ref.$previousAvatarSwapTime = Date.now();
} }
ref.$homeLocation = $app.parseLocation(ref.homeLocation); ref.$homeLocation = parseLocation(ref.homeLocation);
ref.$isVRCPlus = ref.tags.includes('system_supporter'); ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref); this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref); this.applyUserLanguage(ref);
@@ -316,32 +316,6 @@ export default class extends baseClass {
} }
return ref; 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 = {}; _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 { 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 { export default class extends baseClass {
constructor(_app, _API, _t) { constructor(_app, _API, _t) {
@@ -36,7 +38,7 @@ export default class extends baseClass {
var L = this.lastLocation$; var L = this.lastLocation$;
if (currentLocation !== this.lastLocation$.tag) { if (currentLocation !== this.lastLocation$.tag) {
Discord.SetTimestamps(timeStamp, 0); Discord.SetTimestamps(timeStamp, 0);
L = $app.parseLocation(currentLocation); L = parseLocation(currentLocation);
L.worldName = ''; L.worldName = '';
L.thumbnailImageUrl = ''; L.thumbnailImageUrl = '';
L.worldCapacity = 0; L.worldCapacity = 0;
@@ -76,7 +78,7 @@ export default class extends baseClass {
} }
switch (L.accessType) { switch (L.accessType) {
case 'public': case 'public':
L.joinUrl = $utils.getLaunchURL(L); L.joinUrl = getLaunchURL(L);
L.accessName = `Public #${L.instanceName} (${platform})`; L.accessName = `Public #${L.instanceName} (${platform})`;
break; break;
case 'invite+': case 'invite+':

View File

@@ -1,8 +1,9 @@
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
import { parseLocation } from '../composables/instance/utils';
import gameLogService from '../service/gamelog.js'; import gameLogService from '../service/gamelog.js';
import configRepository from '../service/config.js'; import configRepository from '../service/config.js';
import database from '../service/database.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 { userRequest } from '../api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -80,7 +81,7 @@ export default class extends baseClass {
this.lastLocation.location, this.lastLocation.location,
gameLog.dt gameLog.dt
); );
var worldName = this.replaceBioSymbols(gameLog.worldName); var worldName = $utils.replaceBioSymbols(gameLog.worldName);
if (this.isGameRunning) { if (this.isGameRunning) {
this.lastLocationReset(gameLog.dt); this.lastLocationReset(gameLog.dt);
this.clearNowPlaying(); this.clearNowPlaying();
@@ -100,7 +101,7 @@ export default class extends baseClass {
this.applyGroupDialogInstances(); this.applyGroupDialogInstances();
} }
this.addInstanceJoinHistory(gameLog.location, gameLog.dt); this.addInstanceJoinHistory(gameLog.location, gameLog.dt);
var L = $utils.parseLocation(gameLog.location); var L = parseLocation(gameLog.location);
var entry = { var entry = {
created_at: gameLog.dt, created_at: gameLog.dt,
type: 'Location', type: 'Location',
@@ -789,7 +790,7 @@ export default class extends baseClass {
var videoPos = Number(data[1]); var videoPos = Number(data[1]);
var videoLength = Number(data[2]); var videoLength = Number(data[2]);
var displayName = data[3]; var displayName = data[3];
var videoName = this.replaceBioSymbols(data[4]); var videoName = $utils.replaceBioSymbols(data[4]);
var videoUrl = videoName; var videoUrl = videoName;
var videoId = 'LSMedia'; var videoId = 'LSMedia';
if (videoUrl === this.nowPlaying.url) { if (videoUrl === this.nowPlaying.url) {
@@ -981,29 +982,6 @@ export default class extends baseClass {
this.addGameLogEntry(gameLog, this.lastLocation.location); 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) { gameLogSearch(row) {
var value = this.gameLogTable.search.toUpperCase(); var value = this.gameLogTable.search.toUpperCase();
if (!value) { if (!value) {

View File

@@ -1,8 +1,14 @@
import * as workerTimers from 'worker-timers'; 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 configRepository from '../service/config.js';
import database from '../service/database.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 { instanceRequest, userRequest } from '../api';
import {
photonEmojis,
photonEventType
} from '../composables/shared/constants/photon.js';
export default class extends baseClass { export default class extends baseClass {
constructor(_app, _API, _t) { 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: '', photonEventTableFilter: '',
photonEventTableTypeFilter: [], photonEventTableTypeFilter: [],
photonEventTableTypeOverlayFilter: [], photonEventTableTypeOverlayFilter: [],
@@ -894,7 +795,7 @@ export default class extends baseClass {
var imageUrl = ''; var imageUrl = '';
if (type === 0) { if (type === 0) {
var emojiId = data.Parameters[245][2]; var emojiId = data.Parameters[245][2];
emojiName = this.photonEmojis[emojiId]; emojiName = photonEmojis[emojiId];
} else if (type === 1) { } else if (type === 1) {
emojiName = 'Custom'; emojiName = 'Custom';
var fileId = data.Parameters[245][1]; var fileId = data.Parameters[245][1];
@@ -982,7 +883,7 @@ export default class extends baseClass {
if (this.debugPhotonLogging) { if (this.debugPhotonLogging) {
var displayName = this.getDisplayNameFromPhotonId(senderId); var displayName = this.getDisplayNameFromPhotonId(senderId);
var feed = `RPC ${displayName} ${ var feed = `RPC ${displayName} ${
this.photonEventType[eventData.EventType] photonEventType[eventData.EventType]
}${eventName}`; }${eventName}`;
console.log('VrcRpc:', feed); console.log('VrcRpc:', feed);
} }
@@ -1026,7 +927,7 @@ export default class extends baseClass {
shortName shortName
}); });
var location = instance.json.location; var location = instance.json.location;
var L = $utils.parseLocation(location); var L = parseLocation(location);
var groupName = ''; var groupName = '';
if (L.groupId) { if (L.groupId) {
groupName = await this.getGroupName(L.groupId); groupName = await this.getGroupName(L.groupId);
@@ -1040,14 +941,14 @@ export default class extends baseClass {
// if (shortName === newShortName) { // if (shortName === newShortName) {
// portalType = 'Unlocked'; // portalType = 'Unlocked';
// } // }
var displayLocation = this.displayLocation( var _displayLocation = displayLocation(
location, location,
worldName, worldName,
groupName groupName
); );
this.addEntryPhotonEvent({ this.addEntryPhotonEvent({
photonId: this.getPhotonIdFromUserId(userId), photonId: this.getPhotonIdFromUserId(userId),
text: `PortalSpawn to ${displayLocation}`, text: `PortalSpawn to ${_displayLocation}`,
type: 'PortalSpawn', type: 'PortalSpawn',
shortName, shortName,
location, location,
@@ -1210,10 +1111,10 @@ export default class extends baseClass {
type: 'ChangeStatus', type: 'ChangeStatus',
status: photonUser.status, status: photonUser.status,
previousStatus: ref.status, previousStatus: ref.status,
statusDescription: this.replaceBioSymbols( statusDescription: $utils.replaceBioSymbols(
photonUser.statusDescription photonUser.statusDescription
), ),
previousStatusDescription: this.replaceBioSymbols( previousStatusDescription: $utils.replaceBioSymbols(
ref.statusDescription ref.statusDescription
), ),
created_at: Date.parse(gameLogDate) created_at: Date.parse(gameLogDate)
@@ -1227,8 +1128,8 @@ export default class extends baseClass {
return; return;
} }
var avatar = user.avatarDict; var avatar = user.avatarDict;
avatar.name = this.replaceBioSymbols(avatar.name); avatar.name = $utils.replaceBioSymbols(avatar.name);
avatar.description = this.replaceBioSymbols(avatar.description); avatar.description = $utils.replaceBioSymbols(avatar.description);
var platform = ''; var platform = '';
if (user.last_platform === 'android') { if (user.last_platform === 'android') {
platform = 'Android'; platform = 'Android';
@@ -1240,7 +1141,7 @@ export default class extends baseClass {
platform = 'Desktop'; platform = 'Desktop';
} }
this.photonUserSusieCheck(photonId, user, gameLogDate); this.photonUserSusieCheck(photonId, user, gameLogDate);
$utils.checkVRChatCache(avatar).then((cacheInfo) => { checkVRChatCache(avatar).then((cacheInfo) => {
var inCache = false; var inCache = false;
if (cacheInfo.Item1 > 0) { if (cacheInfo.Item1 > 0) {
inCache = true; inCache = true;
@@ -1410,9 +1311,11 @@ export default class extends baseClass {
oldAvatarId !== avatar.id && oldAvatarId !== avatar.id &&
photonId !== this.photonLobbyCurrentUser photonId !== this.photonLobbyCurrentUser
) { ) {
avatar.name = this.replaceBioSymbols(avatar.name); avatar.name = $utils.replaceBioSymbols(avatar.name);
avatar.description = this.replaceBioSymbols(avatar.description); avatar.description = $utils.replaceBioSymbols(
$utils.checkVRChatCache(avatar).then((cacheInfo) => { avatar.description
);
checkVRChatCache(avatar).then((cacheInfo) => {
var inCache = false; var inCache = false;
if (cacheInfo.Item1 > 0) { if (cacheInfo.Item1 > 0) {
inCache = true; inCache = true;

View File

@@ -7,6 +7,7 @@ import {
instanceRequest, instanceRequest,
groupRequest groupRequest
} from '../api'; } from '../api';
import $utils from './utils';
export default class extends baseClass { export default class extends baseClass {
constructor(_app, _API, _t) { constructor(_app, _API, _t) {
@@ -171,8 +172,8 @@ export default class extends baseClass {
var D = $app.groupDialog; var D = $app.groupDialog;
if (D.id === args.params.groupId) { if (D.id === args.params.groupId) {
for (var post of args.posts) { for (var post of args.posts) {
post.title = $app.replaceBioSymbols(post.title); post.title = $utils.replaceBioSymbols(post.title);
post.text = $app.replaceBioSymbols(post.text); post.text = $utils.replaceBioSymbols(post.text);
} }
if (args.posts.length > 0) { if (args.posts.length > 0) {
D.announcement = args.posts[0]; D.announcement = args.posts[0];
@@ -189,8 +190,8 @@ export default class extends baseClass {
} }
var newPost = args.json; var newPost = args.json;
newPost.title = $app.replaceBioSymbols(newPost.title); newPost.title = $utils.replaceBioSymbols(newPost.title);
newPost.text = $app.replaceBioSymbols(newPost.text); newPost.text = $utils.replaceBioSymbols(newPost.text);
var hasPost = false; var hasPost = false;
// update existing post // update existing post
for (var post of D.posts) { for (var post of D.posts) {
@@ -275,9 +276,9 @@ export default class extends baseClass {
API.applyGroup = function (json) { API.applyGroup = function (json) {
var ref = this.cachedGroups.get(json.id); var ref = this.cachedGroups.get(json.id);
json.rules = $app.replaceBioSymbols(json.rules); json.rules = $utils.replaceBioSymbols(json.rules);
json.name = $app.replaceBioSymbols(json.name); json.name = $utils.replaceBioSymbols(json.name);
json.description = $app.replaceBioSymbols(json.description); json.description = $utils.replaceBioSymbols(json.description);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = {
id: '', id: '',
@@ -912,9 +913,6 @@ export default class extends baseClass {
case 'Unsubscribe To Announcements': case 'Unsubscribe To Announcements':
this.setGroupSubscription(D.id, false); this.setGroupSubscription(D.id, false);
break; break;
case 'Invite To Group':
this.showInviteGroupDialog(D.id, '');
break;
} }
}, },
@@ -1050,17 +1048,6 @@ export default class extends baseClass {
this.userDialog.representedGroup = args.json; this.userDialog.representedGroup = args.json;
return args; 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; $app.languageDialog.languages = data;
}); });
API.$on('LOGOUT', function () {
$app.languageDialog.visible = false;
});
} }
_data = { _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: [], subsetOfLanguages: [],
languageDialog: { languageDialog: {
@@ -113,54 +37,5 @@ export default class extends baseClass {
} }
}; };
_methods = { _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;
}
};
} }

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) { async getUserMemo(userId) {
try { try {
return await database.getUserMemo(userId); 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() { promptOmniDirectDialog() {
this.$prompt( this.$prompt(
$t('prompt.direct_access_omni.description'), $t('prompt.direct_access_omni.description'),

View File

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

View File

@@ -1,8 +1,5 @@
import Noty from 'noty';
let echarts = null; let echarts = null;
// messy here, organize later
const _utils = { const _utils = {
removeFromArray(array, item) { removeFromArray(array, item) {
var { length } = array; var { length } = array;
@@ -14,7 +11,6 @@ const _utils = {
} }
return false; return false;
}, },
arraysMatch(a, b) { arraysMatch(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b)) {
return false; return false;
@@ -27,12 +23,10 @@ const _utils = {
) )
); );
}, },
escapeTag(tag) { escapeTag(tag) {
var s = String(tag); var s = String(tag);
return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`); return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
}, },
escapeTagRecursive(obj) { escapeTagRecursive(obj) {
if (typeof obj === 'string') { if (typeof obj === 'string') {
return this.escapeTag(obj); return this.escapeTag(obj);
@@ -44,7 +38,6 @@ const _utils = {
} }
return obj; return obj;
}, },
timeToText(sec, isNeedSeconds = false) { timeToText(sec, isNeedSeconds = false) {
let n = Number(sec); let n = Number(sec);
if (isNaN(n)) { if (isNaN(n)) {
@@ -72,7 +65,6 @@ const _utils = {
} }
return arr.join(' '); return arr.join(' ');
}, },
textToHex(text) { textToHex(text) {
var s = String(text); var s = String(text);
return s return s
@@ -80,7 +72,6 @@ const _utils = {
.map((c) => c.charCodeAt(0).toString(16)) .map((c) => c.charCodeAt(0).toString(16))
.join(' '); .join(' ');
}, },
commaNumber(num) { commaNumber(num) {
if (!num) { if (!num) {
return '0'; return '0';
@@ -88,182 +79,6 @@ const _utils = {
var s = String(Number(num)); var s = String(Number(num));
return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); 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) { buildTreeData(json) {
var node = []; var node = [];
for (var key in json) { for (var key in json) {
@@ -325,8 +140,6 @@ const _utils = {
}); });
return node; return node;
}, },
// app.js 4900ln
// descending // descending
compareByCreatedAt(a, b) { compareByCreatedAt(a, b) {
if ( if (
@@ -406,298 +219,69 @@ const _utils = {
} }
return false; return false;
}, },
convertFileUrlToImageUrl(url, resolution = 128) { compareByName(a, b) {
if (!url) { if (typeof a.name !== 'string' || typeof b.name !== 'string') {
return 0;
}
return a.name.localeCompare(b.name);
},
replaceBioSymbols(text) {
if (!text) {
return ''; return '';
} }
/** var symbolList = {
* 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 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 newText.replace(/ {1,}/g, ' ').trimRight();
return url;
}, },
replaceVrcPackageUrl(url) { // descending
if (!url) { compareByUpdatedAt(a, b) {
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) {
if ( if (
ref && typeof a.updated_at !== 'string' ||
ref.myMember && typeof b.updated_at !== 'string'
ref.myMember.permissions &&
(ref.myMember.permissions.includes('*') ||
ref.myMember.permissions.includes(permission))
) { ) {
return true; return 0;
} }
return false; var A = a.updated_at.toUpperCase();
}, var B = b.updated_at.toUpperCase();
if (A < B) {
compareUnityVersion(unitySortNumber) { return 1;
if (!window.API.cachedConfig.sdkUnityVersion) {
console.error('No cachedConfig.sdkUnityVersion');
return false;
} }
if (A > B) {
// 2022.3.6f1 2022 03 06 000 return -1;
// 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;
} }
var currentUnityVersion = array[0]; return 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 };
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,5 @@
<template> <template>
<el-dialog <safe-dialog ref="favoriteDialog" :visible.sync="isVisible" :title="$t('dialog.favorite.header')" width="300px">
ref="favoriteDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.favorite.header')"
width="300px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div v-loading="loading"> <div v-loading="loading">
<span style="display: block; text-align: center">{{ $t('dialog.favorite.vrchat_favorites') }}</span> <span style="display: block; text-align: center">{{ $t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key"> <template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
@@ -69,7 +62,7 @@
</el-button> </el-button>
</template> </template>
</div> </div>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
@@ -78,7 +71,7 @@
export default { export default {
name: 'ChooseFavoriteGroupDialog', name: 'ChooseFavoriteGroupDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'adjustDialogZ'], inject: ['API', 'adjustDialogZ'],
props: { props: {
favoriteDialog: { favoriteDialog: {
type: Object, type: Object,
@@ -186,7 +179,7 @@
this.$emit('remove-local-avatar-favorite', ...args); this.$emit('remove-local-avatar-favorite', ...args);
}, },
deleteFavoriteNoConfirm(...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> <template>
<el-dialog <safe-dialog
ref="groupDialogRef" ref="groupDialogRef"
:before-close="beforeDialogClose"
:visible.sync="groupDialog.visible" :visible.sync="groupDialog.visible"
:show-close="false" :show-close="false"
width="770px" width="770px"
top="10vh" top="10vh"
class="x-dialog x-group-dialog" class="x-dialog x-group-dialog">
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div class="group-banner-image"> <div class="group-banner-image">
<el-popover placement="right" width="500px" trigger="click"> <el-popover placement="right" width="500px" trigger="click">
<img <img
@@ -1151,10 +1148,7 @@
</el-tabs> </el-tabs>
</div> </div>
<!--Nested--> <!--Nested-->
<GroupPostEditDialog <GroupPostEditDialog :dialog-data.sync="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
:gallery-select-dialog="gallerySelectDialog"
:dialog-data.sync="groupPostEditDialog"
@clear-image-gallery-select="clearImageGallerySelect" />
<GroupMemberModerationDialog <GroupMemberModerationDialog
:group-dialog="groupDialog" :group-dialog="groupDialog"
:is-group-members-loading.sync="isGroupMembersLoading" :is-group-members-loading.sync="isGroupMembersLoading"
@@ -1167,25 +1161,32 @@
@load-all-group-members="loadAllGroupMembers" @load-all-group-members="loadAllGroupMembers"
@set-group-member-filter="setGroupMemberFilter" @set-group-member-filter="setGroupMemberFilter"
@set-group-member-sort-order="setGroupMemberSortOrder" /> @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> </template>
<script setup> <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 { 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 * 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 API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showFullscreenImageDialog = inject('showFullscreenImageDialog'); const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const languageClass = inject('languageClass');
const showUserDialog = inject('showUserDialog'); const showUserDialog = inject('showUserDialog');
const userStatusClass = inject('userStatusClass'); const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage'); const userImage = inject('userImage');
@@ -1222,28 +1223,33 @@
type: Object, type: Object,
required: true required: true
}, },
gallerySelectDialog: {
type: Object,
default: () => ({})
},
randomUserColours: { randomUserColours: {
type: Boolean, type: Boolean,
default: true default: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
offlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
} }
}); });
const emit = defineEmits([ const emit = defineEmits([
'update:group-dialog', 'update:group-dialog',
'update:gallery-select-dialog', 'groupDialogCommand',
'update:group-member-moderation',
'group-dialog-command',
'update:group-dialog',
'get-group-dialog-group', 'get-group-dialog-group',
'get-group-dialog-group-members', 'updateGroupPostSearch'
'refresh-instance-player-count',
'update-group-post-search',
'set-group-member-sort-order',
'clear-image-gallery-select'
]); ]);
const groupDialogRef = ref(null); const groupDialogRef = ref(null);
@@ -1254,6 +1260,10 @@
const groupDialogGalleryCurrentName = ref('0'); const groupDialogGalleryCurrentName = ref('0');
const groupDialogTabCurrentName = ref('0'); const groupDialogTabCurrentName = ref('0');
const isGroupGalleryLoading = ref(false); const isGroupGalleryLoading = ref(false);
const selectedGalleryFile = ref({
selectedFileId: '',
selectedImageUrl: ''
});
const groupPostEditDialog = reactive({ const groupPostEditDialog = reactive({
visible: false, visible: false,
groupRef: {}, groupRef: {},
@@ -1273,6 +1283,16 @@
auditLogTypes: [] auditLogTypes: []
}); });
const inviteGroupDialog = ref({
visible: false,
loading: false,
groupId: '',
groupName: '',
userId: '',
userIds: [],
userObject: {}
});
let loadMoreGroupMembersParams = {}; let loadMoreGroupMembersParams = {};
watch( watch(
@@ -1293,8 +1313,15 @@
} }
); );
function getFaviconUrl(link) { function showInviteGroupDialog(groupId, userId) {
return utils.getFaviconUrl(link); 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) { function setGroupRepresentation(groupId) {
@@ -1439,9 +1466,7 @@
} }
}); });
} }
function copyToClipboard(text) {
utils.copyToClipboard(text);
}
function groupGalleryStatus(gallery) { function groupGalleryStatus(gallery) {
const style = {}; const style = {};
if (!gallery.membersOnly) { if (!gallery.membersOnly) {
@@ -1455,6 +1480,10 @@
} }
function groupDialogCommand(command) { function groupDialogCommand(command) {
const D = props.groupDialog;
if (D.visible === false) {
return;
}
switch (command) { switch (command) {
case 'Share': case 'Share':
copyToClipboard(props.groupDialog.ref.$url); copyToClipboard(props.groupDialog.ref.$url);
@@ -1465,8 +1494,11 @@
case 'Moderation Tools': case 'Moderation Tools':
showGroupMemberModerationDialog(props.groupDialog.id); showGroupMemberModerationDialog(props.groupDialog.id);
break; break;
case 'Invite To Group':
showInviteGroupDialog(D.id, '');
break;
default: default:
emit('group-dialog-command', command); emit('groupDialogCommand', command);
} }
} }
@@ -1481,7 +1513,7 @@
D.auditLogTypes = []; D.auditLogTypes = [];
API.getCachedGroup({ groupId }).then((args) => { API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref; D.groupRef = args.ref;
if (utils.hasGroupPermission(D.groupRef, 'group-audit-view')) { if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => { groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
// API.$on('GROUP:AUDITLOGTYPES', function (args) { // API.$on('GROUP:AUDITLOGTYPES', function (args) {
if (groupMemberModeration.id !== args.params.groupId) { if (groupMemberModeration.id !== args.params.groupId) {
@@ -1569,18 +1601,21 @@
D.roleIds = []; D.roleIds = [];
D.postId = ''; D.postId = '';
D.groupId = groupId; D.groupId = groupId;
emit('update:gallery-select-dialog', { ...D, selectedFileId: '', selectedImageUrl: '' }); selectedGalleryFile.value = {
selectedFileId: '',
selectedImageUrl: ''
};
if (post) { if (post) {
D.title = post.title; D.title = post.title;
D.text = post.text; D.text = post.text;
D.visibility = post.visibility; D.visibility = post.visibility;
D.roleIds = post.roleIds; D.roleIds = post.roleIds;
D.postId = post.id; D.postId = post.id;
emit('update:gallery-select-dialog', { selectedGalleryFile.value = {
...D,
selectedFileId: post.imageId, selectedFileId: post.imageId,
selectedImageUrl: post.imageUrl selectedImageUrl: post.imageUrl
}); };
} }
API.getCachedGroup({ groupId }).then((args) => { API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref; D.groupRef = args.ref;
@@ -1763,9 +1798,6 @@
await getGroupDialogGroupMembers(); await getGroupDialogGroupMembers();
} }
function hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
}
function updateGroupDialogData(obj) { function updateGroupDialogData(obj) {
// Be careful with the deep merge // Be careful with the deep merge
emit('update:group-dialog', obj); emit('update:group-dialog', obj);
@@ -1773,16 +1805,7 @@
function getGroupDialogGroup(groupId) { function getGroupDialogGroup(groupId) {
emit('get-group-dialog-group', groupId); emit('get-group-dialog-group', groupId);
} }
function refreshInstancePlayerCount(tag) {
emit('refresh-instance-player-count', tag);
}
function updateGroupPostSearch() { function updateGroupPostSearch() {
emit('update-group-post-search'); emit('updateGroupPostSearch');
}
function downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
}
function clearImageGallerySelect() {
emit('clear-image-gallery-select');
} }
</script> </script>

View File

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

View File

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

View File

@@ -1,13 +1,9 @@
<template> <template>
<el-dialog <safe-dialog
ref="groupPostEditDialog"
:before-close="beforeDialogClose"
:visible.sync="groupPostEditDialog.visible" :visible.sync="groupPostEditDialog.visible"
:title="$t('dialog.group_post_edit.header')" :title="$t('dialog.group_post_edit.header')"
width="650px" width="650px"
append-to-body append-to-body>
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div v-if="groupPostEditDialog.visible"> <div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3> <h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px"> <el-form :model="groupPostEditDialog" label-width="150px">
@@ -107,30 +103,39 @@
{{ $t('dialog.group_post_edit.create_post') }} {{ $t('dialog.group_post_edit.create_post') }}
</el-button> </el-button>
</template> </template>
</el-dialog> <GallerySelectDialog
:gallery-select-dialog="gallerySelectDialog"
:gallery-table="galleryTable"
@refresh-gallery-table="refreshGalleryTable" />
</safe-dialog>
</template> </template>
<script> <script>
import { groupRequest } from '../../../api'; import { groupRequest, vrcPlusIconRequest } from '../../../api';
import GallerySelectDialog from './GallerySelectDialog.vue';
export default { export default {
name: 'GroupPostEditDialog', name: 'GroupPostEditDialog',
inject: [ components: {
'beforeDialogClose', GallerySelectDialog
'showFullscreenImageDialog', },
'dialogMouseDown', inject: ['showFullscreenImageDialog'],
'dialogMouseUp',
'showGallerySelectDialog'
],
props: { props: {
dialogData: { dialogData: {
type: Object, type: Object,
required: true required: true
}, },
gallerySelectDialog: { selectedGalleryFile: { type: Object, default: () => ({}) }
type: Object, },
required: true data() {
} return {
gallerySelectDialog: {
visible: false,
selectedFileId: '',
selectedImageUrl: ''
},
galleryTable: []
};
}, },
computed: { computed: {
groupPostEditDialog: { groupPostEditDialog: {
@@ -143,6 +148,22 @@
} }
}, },
methods: { 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() { editGroupPost() {
const D = this.groupPostEditDialog; const D = this.groupPostEditDialog;
if (!D.groupId || !D.postId) { if (!D.groupId || !D.postId) {
@@ -193,7 +214,9 @@
D.visible = false; D.visible = false;
}, },
clearImageGallerySelect() { 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> <template>
<el-dialog <safe-dialog
ref="inviteGroupDialog" ref="inviteGroupDialog"
:visible.sync="inviteGroupDialog.visible" :visible.sync="inviteGroupDialog.visible"
:before-close="beforeDialogClose"
:title="$t('dialog.invite_to_group.header')" :title="$t('dialog.invite_to_group.header')"
width="450px" width="450px"
@mousedown.native="dialogMouseDown" append-to-body>
@mouseup.native="dialogMouseUp">
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading"> <div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ $t('dialog.invite_to_group.description') }}</span> <span>{{ $t('dialog.invite_to_group.description') }}</span>
<br /> <br />
@@ -165,24 +163,16 @@
Invite Invite
</el-button> </el-button>
</template> </template>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
import { groupRequest, userRequest } from '../../../api'; import { groupRequest, userRequest } from '../../api';
import utils from '../../../classes/utils'; import { hasGroupPermission } from '../../composables/group/utils';
export default { export default {
name: 'InviteGroupDialog', name: 'InviteGroupDialog',
inject: [ inject: ['API', 'userStatusClass', 'userImage', 'adjustDialogZ'],
'API',
'dialogMouseDown',
'dialogMouseUp',
'beforeDialogClose',
'userStatusClass',
'userImage',
'adjustDialogZ'
],
props: { props: {
dialogData: { dialogData: {
type: Object, type: Object,
@@ -256,7 +246,7 @@
groupRequest groupRequest
.getGroup({ groupId }) .getGroup({ groupId })
.then((args) => { .then((args) => {
if (utils.hasGroupPermission(args.ref, 'group-invites-manage')) { if (hasGroupPermission(args.ref, 'group-invites-manage')) {
return args; return args;
} }
// not allowed to invite // not allowed to invite

View File

@@ -1,12 +1,5 @@
<template> <template>
<el-dialog <safe-dialog ref="launchDialog" :visible.sync="isVisible" :title="$t('dialog.launch.header')" width="450px">
ref="launchDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.launch.header')"
width="450px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<el-form :model="launchDialog" label-width="100px"> <el-form :model="launchDialog" label-width="100px">
<el-form-item :label="$t('dialog.launch.url')"> <el-form-item :label="$t('dialog.launch.url')">
<el-input <el-input
@@ -84,30 +77,58 @@
{{ $t('dialog.launch.launch') }} {{ $t('dialog.launch.launch') }}
</el-button> </el-button>
</template> </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> </template>
<script> <script>
import utils from '../classes/utils'; import { instanceRequest, worldRequest } from '../../api';
import configRepository from '../service/config'; import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { instanceRequest } from '../api'; import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default { export default {
name: 'LaunchDialog', name: 'LaunchDialog',
inject: [ components: { InviteDialog },
'beforeDialogClose', inject: ['showPreviousInstancesInfoDialog', 'adjustDialogZ'],
'dialogMouseDown',
'dialogMouseUp',
'showPreviousInstancesInfoDialog',
'showInviteDialog',
'adjustDialogZ'
],
props: { props: {
hideTooltips: Boolean, hideTooltips: Boolean,
launchDialogData: { type: Object, required: true }, launchDialogData: { type: Object, required: true },
checkCanInvite: { checkCanInvite: {
type: Function, type: Function,
required: true 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() { data() {
@@ -121,6 +142,14 @@
shortName: '', shortName: '',
shortUrl: '', shortUrl: '',
secureOrShortName: '' secureOrShortName: ''
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
} }
}; };
}, },
@@ -147,8 +176,37 @@
this.getConfig(); this.getConfig();
}, },
methods: { 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) { launchGame(location, shortName, desktop) {
this.$emit('launch-game', location, shortName, desktop); this.$emit('launchGame', location, shortName, desktop);
this.isVisible = false; this.isVisible = false;
}, },
getConfig() { getConfig() {
@@ -159,7 +217,7 @@
}, },
async initLaunchDialog() { async initLaunchDialog() {
const { tag, shortName } = this.launchDialogData; const { tag, shortName } = this.launchDialogData;
if (!utils.isRealInstance(tag)) { if (!isRealInstance(tag)) {
return; return;
} }
this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el)); this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el));
@@ -168,7 +226,7 @@
D.secureOrShortName = shortName; D.secureOrShortName = shortName;
D.shortUrl = ''; D.shortUrl = '';
D.shortName = shortName; D.shortName = shortName;
const L = utils.parseLocation(tag); const L = parseLocation(tag);
L.shortName = shortName; L.shortName = shortName;
if (shortName) { if (shortName) {
D.shortUrl = `https://vrch.at/${shortName}`; D.shortUrl = `https://vrch.at/${shortName}`;
@@ -178,7 +236,7 @@
} else { } else {
D.location = L.worldId; D.location = L.worldId;
} }
D.url = utils.getLaunchURL(L); D.url = getLaunchURL(L);
if (!shortName) { if (!shortName) {
const res = await instanceRequest.getInstanceShortName({ const res = await instanceRequest.getInstanceShortName({
worldId: L.worldId, worldId: L.worldId,
@@ -193,14 +251,14 @@
if (resLocation === this.launchDialog.tag) { if (resLocation === this.launchDialog.tag) {
const resShortName = res.json.shortName; const resShortName = res.json.shortName;
const secureOrShortName = res.json.shortName || res.json.secureName; const secureOrShortName = res.json.shortName || res.json.secureName;
const parsedL = utils.parseLocation(resLocation); const parsedL = parseLocation(resLocation);
parsedL.shortName = resShortName; parsedL.shortName = resShortName;
this.launchDialog.shortName = resShortName; this.launchDialog.shortName = resShortName;
this.launchDialog.secureOrShortName = secureOrShortName; this.launchDialog.secureOrShortName = secureOrShortName;
if (resShortName) { if (resShortName) {
this.launchDialog.shortUrl = `https://vrch.at/${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> <template>
<el-dialog <safe-dialog
ref="newInstanceDialog" ref="newInstanceDialog"
:before-close="beforeDialogClose"
:visible.sync="newInstanceDialog.visible" :visible.sync="newInstanceDialog.visible"
:title="$t('dialog.new_instance.header')" :title="$t('dialog.new_instance.header')"
width="650px" width="650px"
append-to-body append-to-body>
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick"> <el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
<el-tab-pane :label="$t('dialog.new_instance.normal')"> <el-tab-pane :label="$t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px"> <el-form :model="newInstanceDialog" label-width="150px">
@@ -486,27 +483,30 @@
>{{ $t('dialog.new_instance.launch') }}</el-button >{{ $t('dialog.new_instance.launch') }}</el-button
> >
</template> </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> </template>
<script> <script>
import { groupRequest, instanceRequest } from '../../api'; import { groupRequest, instanceRequest, worldRequest } from '../../api';
import utils from '../../classes/utils'; 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 configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default { export default {
name: 'NewInstanceDialog', name: 'NewInstanceDialog',
inject: [ components: { InviteDialog },
'API', inject: ['API', 'userImage', 'userStatusClass', 'showLaunchDialog', 'adjustDialogZ'],
'userImage',
'userStatusClass',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showInviteDialog',
'showLaunchDialog',
'adjustDialogZ'
],
props: { props: {
vipFriends: { vipFriends: {
type: Array, type: Array,
@@ -535,6 +535,18 @@
newInstanceDialogLocationTag: { newInstanceDialogLocationTag: {
type: String, type: String,
required: true required: true
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
} }
}, },
data() { data() {
@@ -566,6 +578,14 @@
groupRef: {}, groupRef: {},
contentSettings: this.instanceContentSettings, contentSettings: this.instanceContentSettings,
selectedContentSettings: [] selectedContentSettings: []
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
} }
}; };
}, },
@@ -578,13 +598,42 @@
this.initializeNewInstanceDialog(); this.initializeNewInstanceDialog();
}, },
methods: { 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) { initNewInstanceDialog(tag) {
if (!utils.isRealInstance(tag)) { if (!isRealInstance(tag)) {
return; return;
} }
this.$nextTick(() => this.adjustDialogZ(this.$refs.newInstanceDialog.$el)); this.$nextTick(() => this.adjustDialogZ(this.$refs.newInstanceDialog.$el));
const D = this.newInstanceDialog; const D = this.newInstanceDialog;
const L = utils.parseLocation(tag); const L = parseLocation(tag);
if (D.worldId === L.worldId) { if (D.worldId === L.worldId) {
// reopening dialog, keep last open instance // reopening dialog, keep last open instance
D.visible = true; D.visible = true;
@@ -682,16 +731,16 @@
} else { } else {
D.location = D.worldId; D.location = D.worldId;
} }
const L = utils.parseLocation(D.location); const L = parseLocation(D.location);
if (noChanges) { if (noChanges) {
L.shortName = D.shortName; L.shortName = D.shortName;
} else { } else {
D.shortName = ''; D.shortName = '';
} }
D.url = utils.getLaunchURL(L); D.url = getLaunchURL(L);
}, },
selfInvite(location) { selfInvite(location) {
const L = utils.parseLocation(location); const L = parseLocation(location);
if (!L.isRealInstance) { if (!L.isRealInstance) {
return; return;
} }
@@ -831,7 +880,7 @@
this.saveNewInstanceDialog(); this.saveNewInstanceDialog();
}, },
async copyInstanceUrl(location) { async copyInstanceUrl(location) {
const L = utils.parseLocation(location); const L = parseLocation(location);
const args = await instanceRequest.getInstanceShortName({ const args = await instanceRequest.getInstanceShortName({
worldId: L.worldId, worldId: L.worldId,
instanceId: L.instanceId instanceId: L.instanceId
@@ -870,7 +919,7 @@
} }
}, },
hasGroupPermission(ref, permission) { 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> <template>
<el-dialog <safe-dialog
ref="dialog" ref="dialog"
:before-close="beforeDialogClose"
:visible="visible" :visible="visible"
:title="$t('dialog.previous_instances.info')" :title="$t('dialog.previous_instances.info')"
width="800px" width="800px"
:fullscreen="fullscreen" :fullscreen="fullscreen"
destroy-on-close destroy-on-close
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp"
@close="$emit('update:visible', false)"> @close="$emit('update:visible', false)">
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<location :location="location.tag" style="font-size: 14px"></location> <location :location="location.tag" style="font-size: 14px"></location>
@@ -57,13 +54,14 @@
</template> </template>
</el-table-column> </el-table-column>
</data-tables> </data-tables>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
import utils from '../../../classes/utils';
import database from '../../../service/database';
import dayjs from 'dayjs'; 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'; import Location from '../../Location.vue';
export default { export default {
@@ -71,7 +69,7 @@
components: { components: {
Location Location
}, },
inject: ['adjustDialogZ', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'], inject: ['adjustDialogZ'],
props: { props: {
visible: { visible: {
type: Boolean, type: Boolean,
@@ -144,7 +142,7 @@
init() { init() {
this.adjustDialogZ(this.$refs.dialog.$el); this.adjustDialogZ(this.$refs.dialog.$el);
this.loading = true; this.loading = true;
this.location = utils.parseLocation(this.instanceId); this.location = parseLocation(this.instanceId);
}, },
refreshPreviousInstancesInfoTable() { refreshPreviousInstancesInfoTable() {
database.getPlayersFromInstance(this.location.tag).then((data) => { database.getPlayersFromInstance(this.location.tag).then((data) => {

View File

@@ -1,13 +1,10 @@
<template> <template>
<el-dialog <safe-dialog
ref="previousInstancesWorldDialog" ref="previousInstancesWorldDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible" :visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')" :title="$t('dialog.previous_instances.header')"
width="1000px" width="1000px"
append-to-body append-to-body>
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span> <span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
<el-input <el-input
@@ -66,24 +63,17 @@
</template> </template>
</el-table-column> </el-table-column>
</data-tables> </data-tables>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
import utils from '../../../classes/utils'; import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database'; import database from '../../../service/database';
export default { export default {
name: 'PreviousInstancesWorldDialog', name: 'PreviousInstancesWorldDialog',
inject: [ inject: ['API', 'showLaunchDialog', 'showPreviousInstancesInfoDialog', 'adjustDialogZ'],
'API',
'showLaunchDialog',
'showPreviousInstancesInfoDialog',
'adjustDialogZ',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp'
],
props: { props: {
previousInstancesWorldDialog: { previousInstancesWorldDialog: {
type: Object, type: Object,
@@ -149,7 +139,7 @@
database.getpreviousInstancesByWorldId(D.worldRef).then((data) => { database.getpreviousInstancesByWorldId(D.worldRef).then((data) => {
const array = []; const array = [];
for (const ref of data.values()) { for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location); ref.$location = parseLocation(ref.location);
if (ref.time > 0) { if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time); ref.timer = utils.timeToText(ref.time);
} else { } 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> <template>
<el-dialog <safe-dialog
ref="previousInstancesUserDialog" ref="previousInstancesUserDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible" :visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')" :title="$t('dialog.previous_instances.header')"
width="1000px" width="1000px"
@mousedown.native="dialogMouseDown" append-to-body>
@mouseup.native="dialogMouseUp">
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span> <span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span>
<el-input <el-input
@@ -68,11 +66,12 @@
</template> </template>
</el-table-column> </el-table-column>
</data-tables> </data-tables>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
import utils from '../../../classes/utils'; import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database'; import database from '../../../service/database';
import Location from '../../Location.vue'; import Location from '../../Location.vue';
@@ -81,14 +80,7 @@
components: { components: {
Location Location
}, },
inject: [ inject: ['adjustDialogZ', 'showLaunchDialog', 'showPreviousInstancesInfoDialog'],
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'adjustDialogZ',
'showLaunchDialog',
'showPreviousInstancesInfoDialog'
],
props: { props: {
previousInstancesUserDialog: { previousInstancesUserDialog: {
type: Object, type: Object,
@@ -176,7 +168,7 @@
database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => { database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => {
const array = []; const array = [];
for (const ref of data.values()) { for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location); ref.$location = parseLocation(ref.location);
if (ref.time > 0) { if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time); ref.timer = utils.timeToText(ref.time);
} else { } 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> <template>
<el-dialog <safe-dialog
ref="VRCXUpdateDialogRef" ref="VRCXUpdateDialogRef"
class="x-dialog" class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="VRCXUpdateDialog.visible" :visible.sync="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')" :title="t('dialog.vrcx_updater.header')"
width="400px" width="400px">
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px"> <div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
<template v-if="updateInProgress"> <template v-if="updateInProgress">
<el-progress :percentage="updateProgress" :format="updateProgressText"></el-progress> <el-progress :percentage="updateProgress" :format="updateProgressText"></el-progress>
@@ -62,7 +59,7 @@
{{ t('dialog.vrcx_updater.install') }} {{ t('dialog.vrcx_updater.install') }}
</el-button> </el-button>
</template> </template>
</el-dialog> </safe-dialog>
</template> </template>
<script setup> <script setup>
@@ -71,9 +68,6 @@
import { useI18n } from 'vue-i18n-bridge'; import { useI18n } from 'vue-i18n-bridge';
const { t } = useI18n(); const { t } = useI18n();
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const adjustDialogZ = inject('adjustDialogZ'); const adjustDialogZ = inject('adjustDialogZ');
const props = defineProps({ 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> <template>
<el-dialog <safe-dialog
:before-close="beforeDialogClose"
:visible.sync="isVisible" :visible.sync="isVisible"
:title="$t('dialog.set_world_tags.header')" :title="$t('dialog.set_world_tags.header')"
width="400px" width="400px"
destroy-on-close destroy-on-close
append-to-body append-to-body>
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled"> <el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled">
{{ $t('dialog.set_world_tags.avatar_scaling_disabled') }} {{ $t('dialog.set_world_tags.avatar_scaling_disabled') }}
</el-checkbox> </el-checkbox>
@@ -80,7 +77,7 @@
</el-button> </el-button>
</div> </div>
</template> </template>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
@@ -88,7 +85,7 @@
export default { export default {
name: 'SetWorldTagsDialog', name: 'SetWorldTagsDialog',
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'showWorldDialog'], inject: ['showWorldDialog'],
props: { props: {
oldTags: { oldTags: {
type: Array, type: Array,

View File

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

View File

@@ -1,13 +1,10 @@
<template> <template>
<el-dialog <safe-dialog
ref="worldDialog" ref="worldDialog"
:before-close="beforeDialogClose"
class="x-dialog x-world-dialog" class="x-dialog x-world-dialog"
:visible.sync="isDialogVisible" :visible.sync="isDialogVisible"
:show-close="false" :show-close="false"
width="770px" width="770px">
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div v-loading="worldDialog.loading"> <div v-loading="worldDialog.loading">
<div style="display: flex"> <div style="display: flex">
<el-popover placement="right" width="500px" trigger="click"> <el-popover placement="right" width="500px" trigger="click">
@@ -760,22 +757,49 @@
:offline-friends="offlineFriends" :offline-friends="offlineFriends"
:active-friends="activeFriends" :active-friends="activeFriends"
:online-friends="onlineFriends" :online-friends="onlineFriends"
:vip-friends="vipFriends" /> :vip-friends="vipFriends"
</el-dialog> :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> </template>
<script> <script>
import { favoriteRequest, imageRequest, miscRequest, userRequest, worldRequest } from '../../../api';
import utils from '../../../classes/utils'; 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 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 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 { export default {
name: 'WorldDialog', name: 'WorldDialog',
components: { SetWorldTagsDialog, WorldAllowedDomainsDialog, PreviousInstancesWorldDialog, NewInstanceDialog }, components: {
PreviousImagesDialog,
SetWorldTagsDialog,
WorldAllowedDomainsDialog,
PreviousInstancesWorldDialog,
NewInstanceDialog,
ChangeWorldImageDialog
},
inject: [ inject: [
'API', 'API',
'showUserDialog', 'showUserDialog',
@@ -785,10 +809,6 @@
'showPreviousInstancesInfoDialog', 'showPreviousInstancesInfoDialog',
'showLaunchDialog', 'showLaunchDialog',
'showFullscreenImageDialog', 'showFullscreenImageDialog',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'displayPreviousImages',
'showWorldDialog', 'showWorldDialog',
'showFavoriteDialog', 'showFavoriteDialog',
'openExternalLink' 'openExternalLink'
@@ -808,6 +828,8 @@
activeFriends: Array, activeFriends: Array,
onlineFriends: Array, onlineFriends: Array,
vipFriends: Array, vipFriends: Array,
inviteMessageTable: Object,
uploadImage: String,
// TODO: Remove // TODO: Remove
updateInstanceInfo: Number updateInstanceInfo: Number
@@ -826,7 +848,11 @@
openFlg: false, openFlg: false,
worldRef: {} worldRef: {}
}, },
newInstanceDialogLocationTag: '' newInstanceDialogLocationTag: '',
changeWorldImageDialogVisible: false,
previousImagesFileId: '',
previousImagesDialogVisible: false,
previousImagesTable: []
}; };
}, },
computed: { computed: {
@@ -907,6 +933,51 @@
} }
}, },
methods: { 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) { showNewInstanceDialog(tag) {
// trigger watcher // trigger watcher
this.newInstanceDialogLocationTag = ''; this.newInstanceDialogLocationTag = '';
@@ -946,26 +1017,30 @@
}); });
break; break;
case 'Make Home': case 'Make Home':
this.API.saveCurrentUser({ userRequest
homeLocation: D.id .saveCurrentUser({
}).then((args) => { homeLocation: D.id
this.$message({ })
message: 'Home world updated', .then((args) => {
type: 'success' this.$message({
message: 'Home world updated',
type: 'success'
});
return args;
}); });
return args;
});
break; break;
case 'Reset Home': case 'Reset Home':
this.API.saveCurrentUser({ userRequest
homeLocation: '' .saveCurrentUser({
}).then((args) => { homeLocation: ''
this.$message({ })
message: 'Home world has been reset', .then((args) => {
type: 'success' this.$message({
message: 'Home world has been reset',
type: 'success'
});
return args;
}); });
return args;
});
break; break;
case 'Publish': case 'Publish':
worldRequest worldRequest
@@ -1040,10 +1115,10 @@
this.openExternalLink(this.replaceVrcPackageUrl(this.worldDialog.ref.unityPackageUrl)); this.openExternalLink(this.replaceVrcPackageUrl(this.worldDialog.ref.unityPackageUrl));
break; break;
case 'Change Image': case 'Change Image':
this.displayPreviousImages('World', 'Change'); this.displayPreviousImages('Change');
break; break;
case 'Previous Images': case 'Previous Images':
this.displayPreviousImages('World', 'Display'); this.displayPreviousImages('Display');
break; break;
case 'Refresh': case 'Refresh':
this.showWorldDialog(D.id); this.showWorldDialog(D.id);
@@ -1055,12 +1130,15 @@
this.showFavoriteDialog('world', D.id); this.showFavoriteDialog('world', D.id);
break; break;
default: default:
this.$emit('world-dialog-command', command); this.$emit('worldDialogCommand', command);
break; break;
} }
}, },
replaceVrcPackageUrl(url) {
_replaceVrcPackageUrl(url);
},
refreshInstancePlayerCount(tag) { refreshInstancePlayerCount(tag) {
this.$emit('refresh-instance-player-count', tag); _refreshInstancePlayerCount(tag);
}, },
onWorldMemoChange() { onWorldMemoChange() {
const worldId = this.worldDialog.id; const worldId = this.worldDialog.id;
@@ -1087,7 +1165,7 @@
this.treeData = utils.buildTreeData(this.worldDialog.ref); this.treeData = utils.buildTreeData(this.worldDialog.ref);
}, },
downloadAndSaveJson(fileName, data) { downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data); _downloadAndSaveJson(fileName, data);
}, },
copyWorldId() { copyWorldId() {
navigator.clipboard 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 = [ const VRChatScreenshotResolutions = [
{ name: '1280x720 (720p)', width: 1280, height: 720 }, { name: '1280x720 (720p)', width: 1280, height: 720 },
{ name: '1920x1080 (1080p Default)', width: '', height: '' }, { name: '1920x1080 (1080p Default)', width: '', height: '' },
@@ -29,8 +13,4 @@ const VRChatCameraResolutions = [
{ name: '7680x4320 (8K)', width: 7680, height: 4320 } { name: '7680x4320 (8K)', width: 7680, height: 4320 }
]; ];
export { export { VRChatScreenshotResolutions, VRChatCameraResolutions };
getVRChatResolution,
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> <script>
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database'; import database from '../../../service/database';
import utils from '../../../classes/utils'; import utils from '../../../classes/utils';
import configRepository from '../../../service/config'; import configRepository from '../../../service/config';
@@ -379,7 +380,7 @@
const timeString = utils.timeToText(param.data, true); const timeString = utils.timeToText(param.data, true);
const color = param.color; const color = param.color;
const name = param.name; const name = param.name;
const location = utils.parseLocation(instanceData.location); const location = parseLocation(instanceData.location);
return ` return `
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,11 @@
<template> <template>
<el-dialog <safe-dialog
ref="worldImportDialog" ref="worldImportDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible" :visible.sync="isVisible"
:title="$t('dialog.world_import.header')" :title="$t('dialog.world_import.header')"
width="650px" width="650px"
top="10vh" top="10vh"
class="x-dialog" class="x-dialog">
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ $t('dialog.world_import.description') }}</div> <div style="font-size: 12px">{{ $t('dialog.world_import.description') }}</div>
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
@@ -176,7 +173,7 @@
</template> </template>
</el-table-column> </el-table-column>
</data-tables> </data-tables>
</el-dialog> </safe-dialog>
</template> </template>
<script> <script>
@@ -185,16 +182,7 @@
export default { export default {
name: 'WorldImportDialog', name: 'WorldImportDialog',
inject: [ inject: ['API', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ', 'showWorldDialog'],
'API',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showFullscreenImageDialog',
'showUserDialog',
'adjustDialogZ',
'showWorldDialog'
],
props: { props: {
worldImportDialogVisible: Boolean, worldImportDialogVisible: Boolean,
worldImportDialogInput: String, 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> </template>
<script> <script>
import removeConfusables, { removeWhitespace } from '../../service/confusables';
import utils from '../../classes/utils';
import { friendRequest, userRequest } from '../../api'; 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 { export default {
name: 'FriendListTab', name: 'FriendListTab',
@@ -282,8 +284,7 @@
'showFullscreenImageDialog', 'showFullscreenImageDialog',
'showUserDialog', 'showUserDialog',
'statusClass', 'statusClass',
'openExternalLink', 'openExternalLink'
'languageClass'
], ],
props: { props: {
friends: { friends: {
@@ -322,7 +323,7 @@
friendsListLoading: false, friendsListLoading: false,
friendsListLoadingProgress: '', friendsListLoadingProgress: '',
friendsListSearchFilterVIP: false, friendsListSearchFilterVIP: false,
// emm // TODO
friendsListBulkUnfriendForceUpdate: 0 friendsListBulkUnfriendForceUpdate: 0
}; };
}, },
@@ -336,6 +337,9 @@
} }
}, },
methods: { methods: {
languageClass(key) {
return _languageClass(key);
},
friendsListSearchChange() { friendsListSearchChange() {
this.friendsListLoading = true; this.friendsListLoading = true;
let query = ''; let query = '';
@@ -505,7 +509,7 @@
return utils.timeToText(val); return utils.timeToText(val);
}, },
getFaviconUrl(link) { 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