diff --git a/html/src/app.js b/html/src/app.js index e3634819..86004eea 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -15116,13 +15116,21 @@ speechSynthesis.getVoices(); }; $app.methods.addFavoriteAvatar = function (ref, group) { - API.addFavorite({ + return API.addFavorite({ type: 'avatar', favoriteId: ref.id, tags: group.name }); }; + $app.methods.addFavoriteUser = function (ref, group) { + return API.addFavorite({ + type: 'friend', + favoriteId: ref.id, + tags: group.name + }); + }; + $app.methods.moveFavorite = function (ref, group, type) { API.deleteFavorite({ objectId: ref.id @@ -19175,6 +19183,9 @@ speechSynthesis.getVoices(); }; $app.methods.userImage = function (user) { + if (typeof user === 'undefined') { + return ''; + } if (this.displayVRCPlusIconsAsAvatar && user.userIcon) { return user.userIcon; } @@ -20217,6 +20228,374 @@ speechSynthesis.getVoices(); this.updateWorldExportDialog(); }; + // App: avatar favorite import + + $app.data.avatarImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + avatarIdList: new Set(), + errors: '', + avatarImportFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 + }; + + $app.data.avatarImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' + }; + + $app.methods.showAvatarImportDialog = function () { + this.$nextTick(() => adjustDialogZ(this.$refs.avatarImportDialog.$el)); + var D = this.avatarImportDialog; + this.resetAvatarImport(); + D.visible = true; + }; + + $app.methods.processAvatarImportList = async function () { + var D = this.avatarImportDialog; + D.loading = true; + var regexAvatarId = + /avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var avatarIdList = new Set(); + while ((match = regexAvatarId.exec(D.input)) !== null) { + avatarIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = avatarIdList.size; + var data = Array.from(avatarIdList); + for (var i = 0; i < data.length; ++i) { + if (!D.visible) { + this.resetAvatarImport(); + } + if (!D.loading || !D.visible) { + break; + } + var avatarId = data[i]; + if (!D.avatarIdList.has(avatarId)) { + try { + var args = await API.getAvatar({ + avatarId + }); + this.avatarImportTable.data.push(args.ref); + D.avatarIdList.add(avatarId); + } catch (err) { + D.errors = D.errors.concat( + `AvatarId: ${avatarId}\n${err}\n\n` + ); + } + } + D.progress++; + if (D.progress === avatarIdList.size) { + D.progress = 0; + } + } + D.loading = false; + }; + + $app.methods.deleteItemAvatarImport = function (ref) { + var D = this.avatarImportDialog; + removeFromArray(this.avatarImportTable.data, ref); + D.avatarIdList.delete(ref.id); + }; + + $app.methods.resetAvatarImport = function () { + var D = this.avatarImportDialog; + D.input = ''; + D.errors = ''; + }; + + $app.methods.clearAvatarImportTable = function () { + var D = this.avatarImportDialog; + this.avatarImportTable.data = []; + D.avatarIdList = new Set(); + }; + + $app.methods.selectAvatarImportGroup = function (group) { + var D = this.avatarImportDialog; + D.avatarImportFavoriteGroup = group; + }; + + $app.methods.cancelAvatarImport = function () { + var D = this.avatarImportDialog; + D.loading = false; + }; + + $app.methods.importAvatarImportTable = async function () { + var D = this.avatarImportDialog; + D.loading = true; + if (!D.avatarImportFavoriteGroup) { + return; + } + var data = [...this.avatarImportTable.data].reverse(); + D.importProgressTotal = data.length; + try { + for (var i = data.length - 1; i >= 0; i--) { + if (!D.loading || !D.visible) { + break; + } + var ref = data[i]; + await this.addFavoriteAvatar(ref, D.avatarImportFavoriteGroup); + removeFromArray(this.avatarImportTable.data, ref); + D.avatarIdList.delete(ref.id); + D.importProgress++; + } + } catch (err) { + D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`; + } finally { + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; + } + }; + + API.$on('LOGIN', function () { + $app.clearAvatarImportTable(); + $app.resetAvatarImport(); + $app.avatarImportDialog.visible = false; + $app.avatarImportFavoriteGroup = null; + + $app.avatarExportDialogVisible = false; + $app.avatarExportFavoriteGroup = null; + }); + + // App: avatar favorite export + + $app.data.avatarExportDialogRef = {}; + $app.data.avatarExportDialogVisible = false; + $app.data.avatarExportContent = ''; + $app.data.avatarExportFavoriteGroup = null; + + $app.methods.showAvatarExportDialog = function () { + this.$nextTick(() => + adjustDialogZ(this.$refs.avatarExportDialogRef.$el) + ); + this.avatarExportFavoriteGroup = null; + this.updateAvatarExportDialog(); + this.avatarExportDialogVisible = true; + }; + + $app.methods.updateAvatarExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + var lines = ['AvatarID,Name']; + API.favoriteAvatarGroups.forEach((group) => { + if ( + !this.avatarExportFavoriteGroup || + this.avatarExportFavoriteGroup === group + ) { + $app.favoriteAvatars.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); + } + }); + this.avatarExportContent = lines.join('\n'); + }; + + $app.methods.selectAvatarExportGroup = function (group) { + this.avatarExportFavoriteGroup = group; + this.updateAvatarExportDialog(); + }; + + // App: friend favorite import + + $app.data.friendImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + userIdList: new Set(), + errors: '', + friendImportFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 + }; + + $app.data.friendImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' + }; + + $app.methods.showFriendImportDialog = function () { + this.$nextTick(() => adjustDialogZ(this.$refs.friendImportDialog.$el)); + var D = this.friendImportDialog; + this.resetFriendImport(); + D.visible = true; + }; + + $app.methods.processFriendImportList = async function () { + var D = this.friendImportDialog; + D.loading = true; + var regexFriendId = + /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var userIdList = new Set(); + while ((match = regexFriendId.exec(D.input)) !== null) { + userIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = userIdList.size; + var data = Array.from(userIdList); + for (var i = 0; i < data.length; ++i) { + if (!D.visible) { + this.resetFriendImport(); + } + if (!D.loading || !D.visible) { + break; + } + var userId = data[i]; + if (!D.userIdList.has(userId)) { + try { + var args = await API.getUser({ + userId + }); + this.friendImportTable.data.push(args.ref); + D.userIdList.add(userId); + } catch (err) { + D.errors = D.errors.concat(`UserId: ${userId}\n${err}\n\n`); + } + } + D.progress++; + if (D.progress === userIdList.size) { + D.progress = 0; + } + } + D.loading = false; + }; + + $app.methods.deleteItemFriendImport = function (ref) { + var D = this.friendImportDialog; + removeFromArray(this.friendImportTable.data, ref); + D.userIdList.delete(ref.id); + }; + + $app.methods.resetFriendImport = function () { + var D = this.friendImportDialog; + D.input = ''; + D.errors = ''; + }; + + $app.methods.clearFriendImportTable = function () { + var D = this.friendImportDialog; + this.friendImportTable.data = []; + D.userIdList = new Set(); + }; + + $app.methods.selectFriendImportGroup = function (group) { + var D = this.friendImportDialog; + D.friendImportFavoriteGroup = group; + }; + + $app.methods.cancelFriendImport = function () { + var D = this.friendImportDialog; + D.loading = false; + }; + + $app.methods.importFriendImportTable = async function () { + var D = this.friendImportDialog; + D.loading = true; + if (!D.friendImportFavoriteGroup) { + return; + } + var data = [...this.friendImportTable.data].reverse(); + D.importProgressTotal = data.length; + try { + for (var i = data.length - 1; i >= 0; i--) { + if (!D.loading || !D.visible) { + break; + } + var ref = data[i]; + await this.addFavoriteUser(ref, D.friendImportFavoriteGroup); + removeFromArray(this.friendImportTable.data, ref); + D.userIdList.delete(ref.id); + D.importProgress++; + } + } catch (err) { + D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`; + } finally { + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; + } + }; + + API.$on('LOGIN', function () { + $app.clearFriendImportTable(); + $app.resetFriendImport(); + $app.friendImportDialog.visible = false; + $app.friendImportFavoriteGroup = null; + + $app.friendExportDialogVisible = false; + $app.friendExportFavoriteGroup = null; + }); + + // App: friend favorite export + + $app.data.friendExportDialogRef = {}; + $app.data.friendExportDialogVisible = false; + $app.data.friendExportContent = ''; + $app.data.friendExportFavoriteGroup = null; + + $app.methods.showFriendExportDialog = function () { + this.$nextTick(() => + adjustDialogZ(this.$refs.friendExportDialogRef.$el) + ); + this.friendExportFavoriteGroup = null; + this.updateFriendExportDialog(); + this.friendExportDialogVisible = true; + }; + + $app.methods.updateFriendExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + var lines = ['UserID,Name']; + API.favoriteFriendGroups.forEach((group) => { + if ( + !this.friendExportFavoriteGroup || + this.friendExportFavoriteGroup === group + ) { + $app.favoriteFriends.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); + } + }); + this.friendExportContent = lines.join('\n'); + }; + + $app.methods.selectFriendExportGroup = function (group) { + this.friendExportFavoriteGroup = group; + this.updateFriendExportDialog(); + }; + // App: user dialog notes API.saveNote = function (params) { diff --git a/html/src/index.pug b/html/src/index.pug index 69a001f2..6fed2fe4 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -506,6 +506,8 @@ html el-tabs(type="card" v-loading="API.isFavoriteLoading") el-tab-pane(label="Friends") el-collapse(style="border:0") + el-button(size="small" @click="showFriendExportDialog") Export + el-button(size="small" @click="showFriendImportDialog") Import el-collapse-item(v-for="group in API.favoriteFriendGroups" :key="group.name") template(slot="title") span(v-text="group.displayName" style="font-weight:bold;font-size:14px;margin-left:10px") @@ -576,6 +578,8 @@ html el-button(type="text" icon="el-icon-close" size="mini" @click.stop="deleteFavorite(favorite.id)" style="margin-left:5px") el-tab-pane(label="Avatars") el-collapse(style="border:0") + el-button(size="small" @click="showAvatarExportDialog") Export + el-button(size="small" @click="showAvatarImportDialog") Import el-collapse-item(v-for="group in API.favoriteAvatarGroups" :key="group.name") template(slot="title") span(v-text="group.displayName" style="font-weight:bold;font-size:14px;margin-left:10px") @@ -885,7 +889,7 @@ html span.header Friends List div(style="float:right;font-size:13px") span Load missing entries - el-tooltip(placement="top" style="margin-left:5px" content="This spams the API a little so use it sparingly") + el-tooltip(placement="top" style="margin-left:5px" content="This takes a lot of API requests so use it sparingly") i.el-icon-warning template(v-if="friendsListLoading") span(v-text="friendsListLoadingProgress" style="margin-left:5px") @@ -3040,6 +3044,116 @@ html template(v-once #default="scope") el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemWorldImport(scope.row)") + //- dialog: export avatar list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarExportDialogRef" :visible.sync="avatarExportDialogVisible" title="Avatar Favorites Export" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="avatarExportFavoriteGroup") {{ avatarExportFavoriteGroup.displayName }} ({{ avatarExportFavoriteGroup.count }}/{{ avatarExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(null)") All Favorites + template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + br + el-input(type="textarea" v-if="avatarExportDialogVisible" v-model="avatarExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: Avatar import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarImportDialog" :visible.sync="avatarImportDialog.visible" title="Avatar Favorites Import" width="650px") + div(style="font-size:12px") + | Enter a list of avatar IDs + el-input(type="textarea" v-model="avatarImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processAvatarImportList" :disabled="!avatarImportDialog.input") Process List + span(v-if="avatarImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] Progress: {{ avatarImportDialog.progress }}/{{ avatarImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportDialog.avatarImportFavoriteGroup.displayName }} ({{ avatarImportDialog.avatarImportFavoriteGroup.count }}/{{ avatarImportDialog.avatarImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-button(size="small" @click="importAvatarImportTable" style="margin:5px" :disabled="avatarImportTable.data.length === 0 || !avatarImportDialog.avatarImportFavoriteGroup") Import Avatars + el-button(v-if="avatarImportDialog.loading" size="small" @click="cancelAvatarImport" style="margin-top:10px") Cancel + span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportTable.data.length }} / {{ avatarImportDialog.avatarImportFavoriteGroup.capacity - avatarImportDialog.avatarImportFavoriteGroup.count }} + span(v-if="avatarImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] Import Progress: {{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearAvatarImportTable") Clear Table + template(v-if="avatarImportDialog.errors") + el-button(size="small" @click="avatarImportDialog.errors = ''" style="margin-left:5px") Clear Errors + h2(style="font-weight:bold;margin:0") Errors: + pre(v-text="avatarImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="avatarImportDialog.visible" v-bind="avatarImportTable" v-loading="avatarImportDialog.loading" style="margin-top:10px") + el-table-column(label="Image" width="70" prop="thumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") + img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="openExternalLink(scope.row.imageUrl)") + el-table-column(label="Name" prop="name") + template(v-once #default="scope") + span.x-link(v-text="scope.row.name" @click="showAvatarDialog(scope.row.id)") + el-table-column(label="Author" width="120" prop="authorName") + template(v-once #default="scope") + span.x-link(v-text="scope.row.authorName" @click="showUserDialog(scope.row.authorId)") + el-table-column(label="Status" width="70" prop="releaseStatus") + template(v-once #default="scope") + span(v-text="scope.row.releaseStatus" v-if="scope.row.releaseStatus === 'public'" style="color:#67c23a") + span(v-text="scope.row.releaseStatus" v-else-if="scope.row.releaseStatus === 'private'" style="color:#f56c6c") + span(v-text="scope.row.releaseStatus" v-else) + el-table-column(label="Action" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemAvatarImport(scope.row)") + + //- dialog: export friend list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendExportDialogRef" :visible.sync="friendExportDialogVisible" title="Friend Favorites Export" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="friendExportFavoriteGroup") {{ friendExportFavoriteGroup.displayName }} ({{ friendExportFavoriteGroup.count }}/{{ friendExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(null)") All Favorites + template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + br + el-input(type="textarea" v-if="friendExportDialogVisible" v-model="friendExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: Friend import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendImportDialog" :visible.sync="friendImportDialog.visible" title="Friend Favorites Import" width="650px") + div(style="font-size:12px") + | Enter a list of user IDs + el-input(type="textarea" v-model="friendImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processFriendImportList" :disabled="!friendImportDialog.input") Process List + span(v-if="friendImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] Progress: {{ friendImportDialog.progress }}/{{ friendImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{ friendImportDialog.friendImportFavoriteGroup.count }}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-button(size="small" @click="importFriendImportTable" style="margin:5px" :disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup") Import Friends + el-button(v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport" style="margin-top:10px") Cancel + span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportTable.data.length }} / {{ friendImportDialog.friendImportFavoriteGroup.capacity - friendImportDialog.friendImportFavoriteGroup.count }} + span(v-if="friendImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] Import Progress: {{ friendImportDialog.importProgress }}/{{ friendImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearFriendImportTable") Clear Table + template(v-if="friendImportDialog.errors") + el-button(size="small" @click="friendImportDialog.errors = ''" style="margin-left:5px") Clear Errors + h2(style="font-weight:bold;margin:0") Errors: + pre(v-text="friendImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="friendImportDialog.visible" v-bind="friendImportTable" v-loading="friendImportDialog.loading" style="margin-top:10px") + el-table-column(label="Image" width="70" prop="currentAvatarThumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row))") + el-table-column(label="Name" prop="displayName") + template(v-once #default="scope") + span.x-link(v-text="scope.row.displayName" @click="showUserDialog(scope.row.id)") + el-table-column(label="Action" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemFriendImport(scope.row)") + //- dialog: Note export dialog el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="noteExportDialog" :visible.sync="noteExportDialog.visible" title="Note Export" width="1000px") div(style="font-size:12px")