diff --git a/html/src/app.js b/html/src/app.js index c6217ccf..34a57ef4 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -15073,6 +15073,14 @@ speechSynthesis.getVoices(); }); }; + $app.methods.addFavoriteWorld = function (ref, group) { + return API.addFavorite({ + type: 'world', + favoriteId: ref.id, + tags: group.name + }); + }; + $app.methods.addFavoriteAvatar = function (ref, group) { API.addFavorite({ type: 'avatar', @@ -19988,6 +19996,181 @@ speechSynthesis.getVoices(); } }; + // App: world favorite import + + $app.data.worldImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + worldIdList: new Set(), + errors: '', + worldImportFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 + }; + + $app.data.worldImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' + }; + + $app.methods.showWorldImportDialog = function () { + this.$nextTick(() => adjustDialogZ(this.$refs.avatarDialog.$el)); + var D = this.worldImportDialog; + this.resetWorldImport(); + D.visible = true; + }; + + $app.methods.processWorldImportList = async function () { + var D = this.worldImportDialog; + D.loading = true; + var regexWorldId = + /wrld_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var worldIdList = new Set(); + while ((match = regexWorldId.exec(D.input)) !== null) { + worldIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = worldIdList.size; + var data = Array.from(worldIdList); + for (var i = 0; i < data.length; ++i) { + var worldId = data[i]; + if (!D.visible) { + this.resetWorldImport(); + return; + } + if (!D.worldIdList.has(worldId)) { + try { + var args = await API.getWorld({ + worldId + }); + this.worldImportTable.data.push(args.ref); + D.worldIdList.add(worldId); + } catch (err) { + D.errors = D.errors.concat( + `WorldId: ${worldId}\n${err}\n\n` + ); + } + } + D.progress++; + if (D.progress === worldIdList.size) { + D.progress = 0; + } + } + D.loading = false; + }; + + $app.methods.deleteItemWorldImport = function (ref) { + var D = this.worldImportDialog; + removeFromArray(this.worldImportTable.data, ref); + D.worldIdList.delete(ref.id); + }; + + $app.methods.resetWorldImport = function () { + var D = this.worldImportDialog; + D.input = ''; + D.errors = ''; + }; + + $app.methods.clearWorldImportTable = function () { + var D = this.worldImportDialog; + this.worldImportTable.data = []; + D.worldIdList = new Set(); + }; + + $app.methods.selectWorldImportGroup = function (group) { + var D = this.worldImportDialog; + D.worldImportFavoriteGroup = group; + }; + + $app.methods.importWorldImportTable = async function () { + var D = this.worldImportDialog; + D.loading = true; + if (!D.worldImportFavoriteGroup) { + return; + } + + var data = [...this.worldImportTable.data].reverse(); + D.importProgressTotal = data.length; + try { + for (var i = data.length - 1; i >= 0; i--) { + var ref = data[i]; + await this.addFavoriteWorld(ref, D.worldImportFavoriteGroup); + removeFromArray(this.worldImportTable.data, ref); + D.worldIdList.delete(ref.id); + D.importProgress++; + if (D.importProgress === data.length) { + D.importProgress = 0; + } + } + } catch (err) { + D.errors = `Name: ${ref.name}\nWorldId: ${ref.id}\n${err}\n\n`; + } finally { + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; + } + }; + + API.$on('LOGIN', function () { + $app.clearWorldImportTable(); + $app.resetWorldImport(); + $app.worldImportDialog.visible = false; + $app.worldImportFavoriteGroup = null; + + $app.worldExportDialogVisible = false; + $app.worldExportFavoriteGroup = null; + }); + + // App: world favorite export + + $app.data.worldExportDialogVisible = false; + $app.data.worldExportContent = ''; + $app.data.worldExportFavoriteGroup = null; + + $app.methods.showWorldExportDialog = function () { + this.worldExportFavoriteGroup = null; + this.updateWorldExportDialog(); + this.worldExportDialogVisible = true; + }; + + $app.methods.updateWorldExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + var lines = ['WorldID,Name']; + API.favoriteWorldGroups.forEach((group) => { + if ( + !this.worldExportFavoriteGroup || + this.worldExportFavoriteGroup === group + ) { + $app.favoriteWorlds.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); + } + }); + this.worldExportContent = lines.join('\n'); + }; + + $app.methods.selectWorldExportGroup = function (group) { + this.worldExportFavoriteGroup = group; + this.updateWorldExportDialog(); + }; + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/index.pug b/html/src/index.pug index f231d4af..768cd7ee 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -537,6 +537,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="Worlds") el-collapse(style="border:0") + el-button(size="small" @click="showWorldExportDialog") Export + el-button(size="small" @click="showWorldImportDialog") Import el-collapse-item(v-for="group in API.favoriteWorldGroups" :key="group.name") template(slot="title") span(v-text="group.displayName" style="font-weight:bold;font-size:14px;margin-left:10px") @@ -2962,6 +2964,66 @@ html template(v-once #default="scope") span(v-text="scope.row.count") + + //- dialog: export world list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="worldExportDialogVisible" title="World Favorites Export" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="worldExportFavoriteGroup") {{ worldExportFavoriteGroup.displayName }} ({{ worldExportFavoriteGroup.count }}/{{ worldExportFavoriteGroup.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="selectWorldExportGroup(null)") All Favorites + template(v-for="groupAPI in API.favoriteWorldGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + br + el-input(type="textarea" v-if="worldExportDialogVisible" v-model="worldExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: World import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="worldImportDialog.visible" title="World Favorites Import" width="650px") + div(style="font-size:12px") + | Enter a list of world IDs + el-input(type="textarea" v-model="worldImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processWorldImportList" :disabled="!worldImportDialog.input") Process List + span(v-if="worldImportDialog.progress" style="margin-top:10px") Progress: {{ worldImportDialog.progress }}/{{ worldImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportDialog.worldImportFavoriteGroup.displayName }} ({{ worldImportDialog.worldImportFavoriteGroup.count }}/{{ worldImportDialog.worldImportFavoriteGroup.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.favoriteWorldGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-button(size="small" @click="importWorldImportTable" style="margin:10px" :disabled="worldImportTable.data.length === 0 || !worldImportDialog.worldImportFavoriteGroup") Import Worlds + span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportTable.data.length }} / {{ worldImportDialog.worldImportFavoriteGroup.capacity - worldImportDialog.worldImportFavoriteGroup.count }} + span(v-if="worldImportDialog.importProgress" style="margin:10px") Import Progress: {{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearWorldImportTable") Clear Table + br + template(v-if="worldImportDialog.errors") + el-button(size="small" @click="worldImportDialog.errors = ''") Clear Errors + h2(style="font-weight:bold;margin:0") Errors: + pre(v-text="worldImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="worldImportDialog.visible" v-bind="worldImportTable" v-loading="worldImportDialog.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="showWorldDialog(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="deleteItemWorldImport(scope.row)") + //- dialog: open source software notice el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" title="Open Source Software Notice" width="650px") div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all")