Files
VRCX/src/views/Favorites/dialogs/FriendImportDialog.vue
pa f4f78bb5ec refactor: app.js (#1291)
* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

* refactor favorite handling to use favorite store methods and clean up event emissions

* refactor moderation handling to use dedicated functions for player moderation events

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

* Fix friend in same instance on orange showing twice in friends list

* Add back in broken friend state repair methods

* add types

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
2025-07-14 15:00:08 +12:00

296 lines
12 KiB
Vue

<template>
<safe-dialog
ref="friendImportDialogRef"
:visible.sync="isVisible"
:title="t('dialog.friend_import.header')"
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.friend_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="friendImportDialog.progress">
{{ t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
{{ friendImportDialog.progressTotal }}
<i class="el-icon-loading" style="margin: 0 5px"></i>
</div>
<el-button v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport">
{{ t('dialog.friend_import.cancel') }}
</el-button>
<el-button v-else size="small" :disabled="!friendImportDialog.input" @click="processFriendImportList">
{{ t('dialog.friend_import.process_list') }}
</el-button>
</div>
</div>
<el-input
v-model="friendImportDialog.input"
type="textarea"
size="mini"
rows="10"
resize="none"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<span v-if="friendImportDialog.friendImportFavoriteGroup">
{{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{
friendImportDialog.friendImportFavoriteGroup.count
}}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }})
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<span v-else
>{{ t('dialog.friend_import.select_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i
></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteFriendGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="selectFriendImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
<span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px">
{{ friendImportTable.data.length }} /
{{
friendImportDialog.friendImportFavoriteGroup.capacity -
friendImportDialog.friendImportFavoriteGroup.count
}}
</span>
</div>
<div>
<el-button size="small" :disabled="friendImportTable.data.length === 0" @click="clearFriendImportTable">
{{ t('dialog.friend_import.clear_table') }}
</el-button>
<el-button
size="small"
type="primary"
style="margin: 5px"
:disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup"
@click="importFriendImportTable">
{{ t('dialog.friend_import.import') }}
</el-button>
</div>
</div>
<span v-if="friendImportDialog.importProgress" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
{{ t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
friendImportDialog.importProgressTotal
}}
</span>
<br />
<template v-if="friendImportDialog.errors">
<el-button size="small" @click="friendImportDialog.errors = ''">
{{ t('dialog.friend_import.clear_errors') }}
</el-button>
<h2 style="font-weight: bold; margin: 5px 0">{{ t('dialog.friend_import.errors') }}</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="friendImportDialog.errors"></pre>
</template>
<data-tables v-loading="friendImportDialog.loading" v-bind="friendImportTable" style="margin-top: 10px">
<el-table-column :label="t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
<template slot-scope="scope">
<el-popover placement="right" height="500px" trigger="hover">
<template slot="reference">
<img class="friends-list-avatar" :src="userImage(scope.row)" />
</template>
<img
class="friends-list-avatar"
:src="userImageFull(scope.row)"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row))" />
</el-popover>
</template>
</el-table-column>
<el-table-column :label="t('table.import.name')" prop="displayName">
<template slot-scope="scope">
<span class="x-link" :title="scope.row.displayName" @click="showUserDialog(scope.row.id)">
{{ scope.row.displayName }}
</span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.action')" width="90" align="right">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-close" size="mini" @click="deleteItemFriendImport(scope.row)">
</el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { favoriteRequest, userRequest } from '../../../api';
import { adjustDialogZ, removeFromArray, userImage, userImageFull } from '../../../shared/utils';
import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const emit = defineEmits(['update:friendImportDialogInput']);
const { showUserDialog } = useUserStore();
const { favoriteFriendGroups, friendImportDialogInput, friendImportDialogVisible } =
storeToRefs(useFavoriteStore());
const { showFullscreenImageDialog } = useGalleryStore();
const friendImportDialog = ref({
loading: false,
progress: 0,
progressTotal: 0,
input: '',
userIdList: new Set(),
errors: '',
friendImportFavoriteGroup: null,
importProgress: 0,
importProgressTotal: 0
});
const friendImportTable = ref({
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table'
});
const friendImportDialogRef = ref(null);
const isVisible = computed({
get() {
return friendImportDialogVisible.value;
},
set(value) {
friendImportDialogVisible.value = value;
}
});
watch(
() => friendImportDialogVisible.value,
(value) => {
if (value) {
adjustDialogZ(friendImportDialogRef.value.$el);
clearFriendImportTable();
resetFriendImport();
if (friendImportDialogInput.value) {
friendImportDialog.value.input = friendImportDialogInput.value;
processFriendImportList();
emit('update:friendImportDialogInput', '');
}
}
}
);
function cancelFriendImport() {
friendImportDialog.value.loading = false;
}
function deleteItemFriendImport(ref) {
removeFromArray(friendImportTable.value.data, ref);
friendImportDialog.value.userIdList.delete(ref.id);
}
function clearFriendImportTable() {
friendImportTable.value.data = [];
friendImportDialog.value.userIdList = new Set();
}
function selectFriendImportGroup(group) {
friendImportDialog.value.friendImportFavoriteGroup = group;
}
async function importFriendImportTable() {
const D = friendImportDialog.value;
D.loading = true;
if (!D.friendImportFavoriteGroup) {
return;
}
const data = [...friendImportTable.value.data].reverse();
D.importProgressTotal = data.length;
let ref = '';
try {
for (let i = data.length - 1; i >= 0; i--) {
if (!D.loading || !isVisible.value) {
break;
}
ref = data[i];
await addFavoriteUser(ref, D.friendImportFavoriteGroup, false);
removeFromArray(friendImportTable.value.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;
}
}
function addFavoriteUser(ref, group, message) {
return favoriteRequest
.addFavorite({
type: 'friend',
favoriteId: ref.id,
tags: group.name
})
.then((args) => {
if (message) {
proxy.$message({
message: 'Friend added to favorites',
type: 'success'
});
}
return args;
});
}
async function processFriendImportList() {
const D = friendImportDialog.value;
D.loading = true;
const regexFriendId = /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
let match = [];
const 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;
const data = Array.from(userIdList);
for (let i = 0; i < data.length; ++i) {
if (!isVisible.value) {
resetFriendImport();
}
if (!D.loading || !isVisible.value) {
break;
}
const userId = data[i];
if (!D.userIdList.has(userId)) {
try {
const args = await userRequest.getUser({
userId
});
friendImportTable.value.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;
}
function resetFriendImport() {
friendImportDialog.value.input = '';
friendImportDialog.value.errors = '';
}
</script>