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>
This commit is contained in:
pa
2025-07-14 12:00:08 +09:00
committed by GitHub
parent 952fd77ed5
commit f4f78bb5ec
323 changed files with 47745 additions and 43326 deletions

View File

@@ -0,0 +1,73 @@
<template>
<div @click="confirm" class="avatar-info">
<span style="margin-right: 5px">{{ avatarName }}</span>
<span v-if="avatarType" :class="color" style="margin-right: 5px">{{ avatarType }}</span>
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useAvatarStore } from '../stores';
const avatarStore = useAvatarStore();
const props = defineProps({
imageurl: String,
userid: String,
hintownerid: String,
hintavatarname: String,
avatartags: Array
});
const avatarName = ref('');
const avatarType = ref('');
const avatarTags = ref('');
const color = ref('');
let ownerId = '';
const parse = async () => {
ownerId = '';
avatarName.value = '';
avatarType.value = '';
color.value = '';
avatarTags.value = '';
if (!props.imageurl) {
avatarName.value = '-';
} else if (props.hintownerid) {
avatarName.value = props.hintavatarname;
ownerId = props.hintownerid;
} else {
try {
const info = await avatarStore.getAvatarName(props.imageurl);
avatarName.value = info.avatarName;
ownerId = info.ownerId;
} catch {
console.error('Failed to fetch avatar name');
}
}
if (typeof props.userid === 'undefined' || !ownerId) {
color.value = '';
avatarType.value = '';
} else if (ownerId === props.userid) {
color.value = 'avatar-info-own';
avatarType.value = '(own)';
} else {
color.value = 'avatar-info-public';
avatarType.value = '(public)';
}
if (Array.isArray(props.avatartags)) {
avatarTags.value = props.avatartags.map((tag) => tag.replace('content_', '')).join(', ');
}
};
const confirm = () => {
if (!props.imageurl) return;
avatarStore.showAvatarAuthorDialog(props.userid, ownerId, props.imageurl);
};
watch([() => props.imageurl, () => props.userid, () => props.avatartags], parse, { immediate: true });
</script>

View File

@@ -0,0 +1,33 @@
<template>
<span>{{ text }}</span>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import * as workerTimers from 'worker-timers';
import { timeToText } from '../shared/utils';
const props = defineProps({
datetime: { type: String, default: '' },
hours: { type: Number, default: 1 }
});
const text = ref('');
function update() {
const epoch = new Date(props.datetime).getTime() + 1000 * 60 * 60 * props.hours - Date.now();
text.value = epoch >= 0 ? timeToText(epoch) : '-';
}
watch(() => props.datetime, update);
onMounted(() => {
update();
});
const timer = workerTimers.setInterval(update, 5000);
onBeforeUnmount(() => {
workerTimers.clearInterval(timer);
});
</script>

View File

@@ -0,0 +1,41 @@
<template>
<span @click="showUserDialog" class="x-link">{{ username }}</span>
</template>
<script setup>
import { ref, watch } from 'vue';
import { userRequest } from '../api';
import { useUserStore } from '../stores';
const userStore = useUserStore();
const props = defineProps({
userid: String,
location: String,
forceUpdateKey: Number,
hint: {
type: String,
default: ''
}
});
const username = ref(props.userid);
async function parse() {
username.value = props.userid;
if (props.hint) {
username.value = props.hint;
} else if (props.userid) {
const args = await userRequest.getCachedUser({ userId: props.userid });
if (args?.json?.displayName) {
username.value = args.json.displayName;
}
}
}
function showUserDialog() {
userStore.showUserDialog(props.userid);
}
watch([() => props.userid, () => props.location, () => props.forceUpdateKey], parse, { immediate: true });
</script>

View File

@@ -21,14 +21,12 @@
</span>
<template v-else-if="isGroupByInstance">
<i v-if="isFriendTraveling" class="el-icon el-icon-loading"></i>
<timer
<Timer
class="extra"
:epoch="epoch"
:style="
isFriendTraveling ? { display: 'inline-block', overflow: 'unset' } : undefined
"></timer>
:style="isFriendTraveling ? { display: 'inline-block', overflow: 'unset' } : undefined" />
</template>
<location
<Location
v-else
class="extra"
:location="friend.ref.location"
@@ -37,7 +35,7 @@
</template>
</div>
</template>
<template v-else-if="!friend.ref && !API.isRefreshFriendsLoading">
<template v-else-if="!friend.ref && !isRefreshFriendsLoading">
<span>{{ friend.name || friend.id }}</span>
<el-button
ttype="text"
@@ -62,38 +60,25 @@
</div>
</template>
<script>
import Location from './Location.vue';
<script setup>
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { userImage, userStatusClass } from '../shared/utils';
import { useAppearanceSettingsStore, useFriendStore } from '../stores';
export default {
name: 'FriendItem',
components: {
Location
},
inject: ['API', 'userImage', 'userStatusClass'],
props: {
friend: {
type: Object,
required: true
},
hideNicknames: {
type: Boolean,
default: false
},
isGroupByInstance: Boolean
},
computed: {
isFriendTraveling() {
return this.friend.ref.location === 'traveling';
},
isFriendActiveOrOffline() {
return this.friend.state === 'active' || this.friend.state === 'offline';
},
epoch() {
return this.isFriendTraveling ? this.friend.ref.$travelingToTime : this.friend.ref.$location_at;
}
}
};
const props = defineProps({
friend: { type: Object, required: true },
isGroupByInstance: Boolean
});
const { hideNicknames } = storeToRefs(useAppearanceSettingsStore());
const { isRefreshFriendsLoading } = storeToRefs(useFriendStore());
const isFriendTraveling = computed(() => props.friend.ref.location === 'traveling');
const isFriendActiveOrOffline = computed(() => props.friend.state === 'active' || props.friend.state === 'offline');
const epoch = computed(() =>
isFriendTraveling.value ? props.friend.ref.$travelingToTime : props.friend.ref.$location_at
);
</script>
<style scoped>

View File

@@ -0,0 +1,170 @@
<template>
<div style="display: inline-block; margin-left: 5px">
<el-tooltip v-if="state.isValidInstance" placement="bottom">
<template #content>
<div>
<span v-if="state.isClosed">Closed At: {{ formatDateFilter(state.closedAt, 'long') }}<br /></span>
<template v-if="state.canCloseInstance">
<el-button
:disabled="state.isClosed"
size="mini"
type="primary"
@click="closeInstance(props.location)">
{{ t('dialog.user.info.close_instance') }} </el-button
><br /><br />
</template>
<span><span style="color: #409eff">PC: </span>{{ state.platforms.standalonewindows }}</span
><br />
<span><span style="color: #67c23a">Android: </span>{{ state.platforms.android }}</span
><br />
<span>{{ t('dialog.user.info.instance_game_version') }} {{ state.gameServerVersion }}</span
><br />
<span v-if="state.queueEnabled">{{ t('dialog.user.info.instance_queuing_enabled') }}<br /></span>
<span v-if="state.disabledContentSettings"
>{{ t('dialog.user.info.instance_disabled_content') }} {{ state.disabledContentSettings }}<br
/></span>
<span v-if="state.userList.length">{{ t('dialog.user.info.instance_users') }}<br /></span>
<template v-for="user in state.userList">
<span
style="cursor: pointer; margin-right: 5px"
@click="showUserDialog(user.id)"
:key="user.id"
>{{ user.displayName }}</span
>
</template>
</div>
</template>
<i class="el-icon-caret-bottom"></i>
</el-tooltip>
<span v-if="state.occupants" style="margin-left: 5px">{{ state.occupants }}/{{ state.capacity }}</span>
<span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span>
<span v-if="state.isFull" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_full')
}}</span>
<span v-if="state.isHardClosed" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_hard_closed')
}}</span>
<span v-else-if="state.isClosed" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_closed')
}}</span>
<span v-if="state.queueSize" style="margin-left: 5px"
>{{ t('dialog.user.info.instance_queue') }} {{ state.queueSize }}</span
>
<span v-if="state.isAgeGated" style="margin-left: 5px; color: lightcoral">{{
t('dialog.user.info.instance_age_gated')
}}</span>
</div>
</template>
<script setup>
import { getCurrentInstance, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { miscRequest } from '../api';
import { formatDateFilter, hasGroupPermission } from '../shared/utils';
import { useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../stores';
const { t } = useI18n();
const locationStore = useLocationStore();
const userStore = useUserStore();
const groupStore = useGroupStore();
const instanceStore = useInstanceStore();
const props = defineProps({
location: String,
instance: Object,
friendcount: Number
});
const state = reactive({
isValidInstance: false,
isFull: false,
isClosed: false,
isHardClosed: false,
closedAt: '',
occupants: 0,
capacity: 0,
queueSize: 0,
queueEnabled: false,
platforms: [],
userList: [],
gameServerVersion: '',
canCloseInstance: false,
isAgeGated: false,
disabledContentSettings: ''
});
const { proxy } = getCurrentInstance();
function parse() {
Object.assign(state, {
isValidInstance: false,
isFull: false,
isClosed: false,
isHardClosed: false,
closedAt: '',
occupants: 0,
capacity: 0,
queueSize: 0,
queueEnabled: false,
platforms: [],
userList: [],
gameServerVersion: '',
canCloseInstance: false,
isAgeGated: false,
disabledContentSettings: ''
});
if (!props.location || !props.instance || Object.keys(props.instance).length === 0) return;
state.isValidInstance = true;
state.isFull = props.instance.hasCapacityForYou === false;
if (props.instance.closedAt) {
state.isClosed = true;
state.closedAt = props.instance.closedAt;
}
state.isHardClosed = props.instance.hardClose === true;
state.occupants = props.instance.userCount;
if (props.location === locationStore.lastLocation.location) {
state.occupants = locationStore.lastLocation.playerList.size;
}
state.capacity = props.instance.capacity;
state.gameServerVersion = props.instance.gameServerVersion;
state.queueSize = props.instance.queueSize;
if (props.instance.platforms) state.platforms = props.instance.platforms;
if (props.instance.users) state.userList = props.instance.users;
if (props.instance.ownerId === userStore.currentUser.id) {
state.canCloseInstance = true;
} else if (props.instance.ownerId?.startsWith('grp_')) {
const group = groupStore.cachedGroups.get(props.instance.ownerId);
state.canCloseInstance = hasGroupPermission(group, 'group-instance-moderate');
}
state.isAgeGated = props.instance.ageGate === true;
if (props.location?.includes('~ageGate')) state.isAgeGated = true;
if (props.instance.$disabledContentSettings?.length) {
state.disabledContentSettings = props.instance.$disabledContentSettings.join(', ');
}
}
watch([() => props.location, () => props.instance, () => props.friendcount], parse, { immediate: true });
function showUserDialog(userId) {
userStore.showUserDialog(userId);
}
function closeInstance(location) {
proxy.$confirm('Continue? Close Instance, nobody will be able to join', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: async (action) => {
if (action !== 'confirm') return;
const args = await miscRequest.closeInstance({ location, hardClose: false });
if (args.json) {
proxy.$message({ message: t('message.instance.closed'), type: 'success' });
instanceStore.applyInstance(args.json);
}
}
});
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<el-button v-show="isVisible" @click="confirmInvite" size="mini" icon="el-icon-message" circle />
</template>
<script setup>
import { computed, getCurrentInstance } from 'vue';
import { instanceRequest } from '../api';
import { checkCanInviteSelf, parseLocation } from '../shared/utils';
const props = defineProps({
location: String,
shortname: String
});
const { proxy } = getCurrentInstance();
const isVisible = computed(() => checkCanInviteSelf(props.location));
function confirmInvite() {
const L = parseLocation(props.location);
if (!L.isRealInstance) return;
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId,
shortName: props.shortname
})
.then((args) => {
proxy.$message({ message: 'Self invite sent', type: 'success' });
return args;
});
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<span v-if="lastJoin">
<el-tooltip placement="top" style="margin-left: 5px">
<template #content>
<span>{{ $t('dialog.user.info.last_join') }} <Timer :epoch="lastJoin" /></span>
</template>
<i class="el-icon el-icon-location-outline" style="display: inline-block" />
</el-tooltip>
</span>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useInstanceStore } from '../stores';
const { instanceJoinHistory } = storeToRefs(useInstanceStore());
const props = defineProps({
location: String,
currentlocation: String
});
const lastJoin = ref(null);
function parse() {
lastJoin.value = instanceJoinHistory.value.get(props.location);
}
watch(() => props.location, parse, { immediate: true });
watch(() => props.currentlocation, parse);
</script>

32
src/components/Launch.vue Normal file
View File

@@ -0,0 +1,32 @@
<template>
<el-tooltip
v-show="isVisible"
placement="top"
:content="t('dialog.user.info.launch_invite_tooltip')"
:disabled="hideTooltips">
<el-button @click="confirm" size="mini" icon="el-icon-switch-button" circle />
</el-tooltip>
</template>
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { checkCanInviteSelf } from '../shared/utils';
import { useLaunchStore } from '../stores';
const launchStore = useLaunchStore();
const { t } = useI18n();
const props = defineProps({
location: String,
hideTooltips: Boolean
});
const isVisible = computed(() => {
return checkCanInviteSelf(props.location);
});
function confirm() {
launchStore.showLaunchDialog(props.location);
}
</script>

View File

@@ -15,147 +15,140 @@
</div>
</template>
<script>
import { parseLocation } from '../composables/instance/utils';
<script setup>
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { getGroupName, getWorldName, parseLocation } from '../shared/utils';
import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Location',
inject: {
// prevent randomly error
// not good idea, it's temporary
API: { default: window.API },
getWorldName: { default: window.$app?.getWorldName },
getGroupName: { default: window.$app?.getGroupName },
showWorldDialog: { default: window.$app?.showWorldDialog },
showGroupDialog: { default: window.$app?.showGroupDialog }
const { cachedWorlds } = storeToRefs(useWorldStore());
const { showWorldDialog } = useWorldStore();
const { showGroupDialog } = useGroupStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { verifyShortName } = useSearchStore();
const props = defineProps({
location: String,
traveling: String,
hint: {
type: String,
default: ''
},
props: {
location: String,
traveling: String,
hint: {
type: String,
default: ''
},
grouphint: {
type: String,
default: ''
},
link: {
type: Boolean,
default: true
},
isOpenPreviousInstanceInfoDialog: Boolean
grouphint: {
type: String,
default: ''
},
data() {
return {
text: '',
region: this.region,
strict: this.strict,
isTraveling: this.isTraveling,
groupName: this.groupName
};
link: {
type: Boolean,
default: true
},
watch: {
location() {
this.parse();
isOpenPreviousInstanceInfoDialog: Boolean
});
const text = ref('');
const region = ref('');
const strict = ref(false);
const isTraveling = ref(false);
const groupName = ref('');
watch(
() => props.location,
() => {
parse();
}
);
parse();
function parse() {
isTraveling.value = false;
groupName.value = '';
let instanceId = props.location;
if (typeof props.traveling !== 'undefined' && props.location === 'traveling') {
instanceId = props.traveling;
isTraveling.value = true;
}
const L = parseLocation(instanceId);
if (L.isOffline) {
text.value = 'Offline';
} else if (L.isPrivate) {
text.value = 'Private';
} else if (L.isTraveling) {
text.value = 'Traveling';
} else if (typeof props.hint === 'string' && props.hint !== '') {
if (L.instanceId) {
text.value = `${props.hint} #${L.instanceName} ${L.accessTypeName}`;
} else {
text.value = props.hint;
}
},
created() {
this.parse();
},
methods: {
parse() {
if (!this.API) return;
this.isTraveling = false;
this.groupName = '';
let instanceId = this.location;
if (typeof this.traveling !== 'undefined' && this.location === 'traveling') {
instanceId = this.traveling;
this.isTraveling = true;
}
const L = parseLocation(instanceId);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
this.text = 'Private';
} else if (L.isTraveling) {
this.text = 'Traveling';
} else if (typeof this.hint === 'string' && this.hint !== '') {
if (L.instanceId) {
this.text = `${this.hint} #${L.instanceName} ${L.accessTypeName}`;
} else {
this.text = this.hint;
}
} else if (L.worldId) {
var ref = this.API.cachedWorlds.get(L.worldId);
if (typeof ref === 'undefined') {
this.getWorldName(L.worldId).then((worldName) => {
if (L.tag === instanceId) {
if (L.instanceId) {
this.text = `${worldName} #${L.instanceName} ${L.accessTypeName}`;
} else {
this.text = worldName;
}
}
});
} else if (L.instanceId) {
this.text = `${ref.name} #${L.instanceName} ${L.accessTypeName}`;
} else {
this.text = ref.name;
}
}
if (this.grouphint) {
this.groupName = this.grouphint;
} else if (L.groupId) {
this.groupName = L.groupId;
this.getGroupName(instanceId).then((groupName) => {
if (L.tag === instanceId) {
this.groupName = groupName;
} else if (L.worldId) {
const ref = cachedWorlds.value.get(L.worldId);
if (typeof ref === 'undefined') {
getWorldName(L.worldId).then((worldName) => {
if (L.tag === instanceId) {
if (L.instanceId) {
text.value = `${worldName} #${L.instanceName} ${L.accessTypeName}`;
} else {
text.value = worldName;
}
});
}
this.region = '';
if (!L.isOffline && !L.isPrivate && !L.isTraveling) {
this.region = L.region;
if (!L.region && L.instanceId) {
this.region = 'us';
}
}
this.strict = L.strict;
},
handleShowWorldDialog() {
if (this.link) {
let instanceId = this.location;
if (this.traveling && this.location === 'traveling') {
instanceId = this.traveling;
}
if (!instanceId && this.hint.length === 8) {
// shortName
this.API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint);
return;
}
if (this.isOpenPreviousInstanceInfoDialog) {
this.$emit('open-previous-instance-info-dialog', instanceId);
} else {
this.showWorldDialog(instanceId);
}
}
},
handleShowGroupDialog() {
let location = this.location;
if (this.isTraveling) {
location = this.traveling;
}
if (!location || !this.link) {
return;
}
const L = parseLocation(location);
if (!L.groupId) {
return;
}
this.showGroupDialog(L.groupId);
});
} else if (L.instanceId) {
text.value = `${ref.name} #${L.instanceName} ${L.accessTypeName}`;
} else {
text.value = ref.name;
}
}
};
if (props.grouphint) {
groupName.value = props.grouphint;
} else if (L.groupId) {
groupName.value = L.groupId;
getGroupName(instanceId).then((name) => {
if (L.tag === instanceId) {
groupName.value = name;
}
});
}
region.value = '';
if (!L.isOffline && !L.isPrivate && !L.isTraveling) {
region.value = L.region;
if (!L.region && L.instanceId) {
region.value = 'us';
}
}
strict.value = L.strict;
}
function handleShowWorldDialog() {
if (props.link) {
let instanceId = props.location;
if (props.traveling && props.location === 'traveling') {
instanceId = props.traveling;
}
if (!instanceId && props.hint.length === 8) {
verifyShortName('', props.hint);
return;
}
if (props.isOpenPreviousInstanceInfoDialog) {
showPreviousInstancesInfoDialog(instanceId);
} else {
showWorldDialog(instanceId);
}
}
}
function handleShowGroupDialog() {
let location = props.location;
if (isTraveling.value) {
location = props.traveling;
}
if (!location || !props.link) {
return;
}
const L = parseLocation(location);
if (!L.groupId) {
return;
}
showGroupDialog(L.groupId);
}
</script>

View File

@@ -0,0 +1,78 @@
<template>
<span>
<span @click="showLaunchDialog" class="x-link">
<i v-if="isUnlocked" class="el-icon el-icon-unlock" style="display: inline-block; margin-right: 5px"></i>
<span>#{{ instanceName }} {{ accessTypeName }}</span>
</span>
<span v-if="groupName" @click="showGroupDialog" class="x-link">({{ groupName }})</span>
<span class="flags" :class="region" style="display: inline-block; margin-left: 5px"></span>
<i v-if="strict" class="el-icon el-icon-lock" style="display: inline-block; margin-left: 5px"></i>
</span>
</template>
<script setup>
import { ref, watch } from 'vue';
import { getGroupName, parseLocation } from '../shared/utils';
import { useGroupStore, useLaunchStore } from '../stores';
const launchStore = useLaunchStore();
const groupStore = useGroupStore();
const props = defineProps({
locationobject: Object,
currentuserid: String,
worlddialogshortname: String,
grouphint: {
type: String,
default: ''
}
});
const location = ref('');
const instanceName = ref('');
const accessTypeName = ref('');
const region = ref('us');
const shortName = ref('');
const isUnlocked = ref(false);
const strict = ref(false);
const groupName = ref('');
function parse() {
const locObj = props.locationobject;
location.value = locObj.tag;
instanceName.value = locObj.instanceName;
accessTypeName.value = locObj.accessTypeName;
strict.value = locObj.strict;
shortName.value = locObj.shortName;
isUnlocked.value =
(props.worlddialogshortname && locObj.shortName && props.worlddialogshortname === locObj.shortName) ||
props.currentuserid === locObj.userId;
region.value = locObj.region || 'us';
if (props.grouphint) {
groupName.value = props.grouphint;
} else if (locObj.groupId) {
groupName.value = locObj.groupId;
getGroupName(locObj.groupId).then((name) => {
groupName.value = name;
});
} else {
groupName.value = '';
}
}
watch(() => props.locationobject, parse, { immediate: true });
function showLaunchDialog() {
launchStore.showLaunchDialog(location.value, shortName.value);
}
function showGroupDialog() {
if (!location.value) return;
const L = parseLocation(location.value);
if (!L.groupId) return;
groupStore.showGroupDialog(L.groupId);
}
</script>

View File

@@ -1,104 +1,65 @@
<template>
<el-menu
ref="navRef"
collapse
@select="$emit('select', $event)"
:default-active="menuActiveIndex"
:collapse-transition="false">
<el-menu-item index="feed">
<i class="el-icon-news"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.feed') }}</span>
</template>
</el-menu-item>
<el-menu-item index="gameLog">
<i class="el-icon-s-data"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.game_log') }}</span>
</template>
</el-menu-item>
<el-menu-item index="playerList">
<i class="el-icon-tickets"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.player_list') }}</span>
</template>
</el-menu-item>
<el-menu-item index="search">
<i class="el-icon-search"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.search') }}</span>
</template>
</el-menu-item>
<el-menu-item index="favorite">
<i class="el-icon-star-off"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.favorites') }}</span>
</template>
</el-menu-item>
<el-menu-item index="friendLog">
<i class="el-icon-notebook-2"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.friend_log') }}</span>
</template>
</el-menu-item>
<el-menu-item index="moderation">
<i class="el-icon-finished"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.moderation') }}</span>
</template>
</el-menu-item>
<el-menu-item index="notification">
<i class="el-icon-bell"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.notification') }}</span>
</template>
</el-menu-item>
<el-menu-item index="friendList">
<i class="el-icon-s-management"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.friend_list') }}</span>
</template>
</el-menu-item>
<el-menu-item index="charts">
<i class="el-icon-data-analysis"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.charts') }}</span>
</template>
</el-menu-item>
<el-menu-item index="profile">
<i class="el-icon-user"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.profile') }}</span>
</template>
</el-menu-item>
<el-menu-item index="settings">
<i class="el-icon-s-tools"></i>
<template slot="title">
<span>{{ $t('nav_tooltip.settings') }}</span>
</template>
</el-menu-item>
</el-menu>
<div class="x-menu-container">
<div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog">
<el-progress
type="circle"
width="50"
stroke-width="3"
:percentage="updateProgress"
:format="updateProgressText"></el-progress>
</div>
<div v-else-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
<el-button
type="default"
size="mini"
icon="el-icon-download"
circle
style="font-size: 14px; height: 50px; width: 50px"
@click="showVRCXUpdateDialog" />
</div>
<el-menu
ref="navRef"
collapse
:default-active="menuActiveIndex"
:collapse-transition="false"
@select="selectMenu">
<el-menu-item
v-for="item in navItems"
:key="item.index"
:index="item.index"
:class="{ notify: notifiedMenus.includes(item.index) }">
<i :class="item.icon" />
<template #title>
<span>{{ $t(item.tooltip) }}</span>
</template>
</el-menu-item>
</el-menu>
</div>
</template>
<script>
export default {
name: 'NavMenu',
props: {
menuActiveIndex: {
type: String,
default: 'feed'
}
}
};
<script setup>
import { storeToRefs } from 'pinia';
import { useUiStore, useVRCXUpdaterStore } from '../stores';
const navItems = [
{ index: 'feed', icon: 'el-icon-news', tooltip: 'nav_tooltip.feed' },
{ index: 'gameLog', icon: 'el-icon-s-data', tooltip: 'nav_tooltip.game_log' },
{ index: 'playerList', icon: 'el-icon-tickets', tooltip: 'nav_tooltip.player_list' },
{ index: 'search', icon: 'el-icon-search', tooltip: 'nav_tooltip.search' },
{ index: 'favorite', icon: 'el-icon-star-off', tooltip: 'nav_tooltip.favorites' },
{ index: 'friendLog', icon: 'el-icon-notebook-2', tooltip: 'nav_tooltip.friend_log' },
{ index: 'moderation', icon: 'el-icon-finished', tooltip: 'nav_tooltip.moderation' },
{ index: 'notification', icon: 'el-icon-bell', tooltip: 'nav_tooltip.notification' },
{ index: 'friendList', icon: 'el-icon-s-management', tooltip: 'nav_tooltip.friend_list' },
{ index: 'charts', icon: 'el-icon-data-analysis', tooltip: 'nav_tooltip.charts' },
{ index: 'profile', icon: 'el-icon-user', tooltip: 'nav_tooltip.profile' },
{ index: 'settings', icon: 'el-icon-s-tools', tooltip: 'nav_tooltip.settings' }
];
const VRCXUpdaterStore = useVRCXUpdaterStore();
const { pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress } = storeToRefs(VRCXUpdaterStore);
const { showVRCXUpdateDialog, updateProgressText } = VRCXUpdaterStore;
const uiStore = useUiStore();
const { menuActiveIndex, notifiedMenus } = storeToRefs(uiStore);
const { selectMenu } = uiStore;
</script>

View File

@@ -11,35 +11,23 @@
</div>
</template>
<script>
export default {
name: 'SimpleSwitch',
props: {
label: {
type: String
},
value: {
type: Boolean
},
tooltip: {
type: String
},
disabled: {
type: Boolean
},
longLabel: {
type: Boolean
}
},
methods: {
change(event) {
this.$emit('change', event);
}
}
};
<script setup>
defineProps({
label: String,
value: Boolean,
tooltip: String,
disabled: Boolean,
longLabel: Boolean
});
const emit = defineEmits(['change']);
function change(event) {
emit('change', event);
}
</script>
<style lang="scss">
<style scoped>
.simple-switch {
font-size: 12px;
margin-top: 5px;

29
src/components/Timer.vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<span>{{ text }}</span>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { timeToText } from '../shared/utils';
const props = defineProps({
epoch: {
type: Number,
required: true
}
});
const now = ref(Date.now());
const text = computed(() => {
return props.epoch ? timeToText(now.value - props.epoch) : '-';
});
let timerId = null;
onMounted(() => {
timerId = setInterval(() => {
now.value = Date.now();
}, 5000);
});
onBeforeUnmount(() => {
clearInterval(timerId);
});
</script>

View File

@@ -261,7 +261,7 @@
type="default"
icon="el-icon-check"
circle
:disabled="API.currentUser.currentAvatar === avatarDialog.id"
:disabled="currentUser.currentAvatar === avatarDialog.id"
style="margin-left: 5px"
@click="selectAvatar(avatarDialog.id)"></el-button>
</el-tooltip>
@@ -299,12 +299,12 @@
>{{ t('dialog.avatar.actions.select_fallback') }}</el-dropdown-item
>
<el-dropdown-item
v-if="avatarDialog.ref.authorId !== API.currentUser.id"
v-if="avatarDialog.ref.authorId !== currentUser.id"
icon="el-icon-picture-outline"
command="Previous Images"
>{{ t('dialog.avatar.actions.show_previous_images') }}</el-dropdown-item
>
<template v-if="avatarDialog.ref.authorId === API.currentUser.id">
<template v-if="avatarDialog.ref.authorId === currentUser.id">
<el-dropdown-item
v-if="avatarDialog.ref.releaseStatus === 'public'"
icon="el-icon-user-solid"
@@ -367,7 +367,7 @@
<el-tab-pane :label="t('dialog.avatar.info.header')">
<div class="x-friend-list" style="max-height: unset">
<div
v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === API.currentUser.id"
v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === currentUser.id"
style="width: 100%">
<span class="name">{{ t('dialog.avatar.info.gallery') }}</span>
<input
@@ -377,8 +377,8 @@
style="display: none"
@change="onFileChangeAvatarGallery" />
<el-button
v-if="avatarDialog.ref.authorId === API.currentUser.id"
v-loading="avatarDialog.galleryLoading"
v-if="avatarDialog.ref.authorId === currentUser.id"
:disabled="!!avatarDialog.galleryLoading"
size="small"
icon="el-icon-upload2"
style="margin-left: 5px"
@@ -396,7 +396,7 @@
style="width: 100%; height: 100%; object-fit: contain"
@click="showFullscreenImageDialog(imageUrl)" />
<div
v-if="avatarDialog.ref.authorId === API.currentUser.id"
v-if="avatarDialog.ref.authorId === currentUser.id"
style="position: absolute; bottom: 5px; left: 33.3%">
<el-button
size="mini"
@@ -497,16 +497,18 @@
<div class="x-friend-item" style="cursor: default">
<div class="detail">
<span class="name">{{ t('dialog.avatar.info.created_at') }}</span>
<span class="extra">{{ avatarDialog.ref.created_at | formatDate('long') }}</span>
<span class="extra">{{ formatDateFilter(avatarDialog.ref.created_at, 'long') }}</span>
</div>
</div>
<div class="x-friend-item" style="cursor: default">
<div class="detail">
<span class="name">{{ t('dialog.avatar.info.last_updated') }}</span>
<span v-if="avatarDialog.lastUpdated" class="extra">{{
avatarDialog.lastUpdated | formatDate('long')
formatDateFilter(avatarDialog.lastUpdated, 'long')
}}</span>
<span v-else class="extra">{{
formatDateFilter(avatarDialog.ref.updated_at, 'long')
}}</span>
<span v-else class="extra">{{ avatarDialog.ref.updated_at | formatDate('long') }}</span>
</div>
</div>
<div class="x-friend-item" style="cursor: default">
@@ -596,71 +598,68 @@
<SetAvatarStylesDialog :set-avatar-styles-dialog="setAvatarStylesDialog" />
<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" />
<PreviousImagesDialog />
</safe-dialog>
</template>
<script setup>
import { computed, getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { avatarModerationRequest, avatarRequest, favoriteRequest, imageRequest, miscRequest } from '../../../api';
import utils from '../../../classes/utils';
import { compareUnityVersion, storeAvatarImage } from '../../../composables/avatar/utils';
import { database } from '../../../service/database';
import {
adjustDialogZ,
buildTreeData,
commaNumber,
compareUnityVersion,
copyToClipboard,
downloadAndSaveJson,
extractFileId,
extractFileVersion,
replaceVrcPackageUrl
} from '../../../composables/shared/utils';
import database from '../../../service/database';
openExternalLink,
openFolderGeneric,
replaceVrcPackageUrl,
storeAvatarImage,
timeToText,
moveArrayItem,
formatDateFilter
} from '../../../shared/utils';
import {
useAppearanceSettingsStore,
useAvatarStore,
useFavoriteStore,
useGalleryStore,
useGameStore,
useUserStore
} from '../../../stores';
import PreviousImagesDialog from '../PreviousImagesDialog.vue';
import ChangeAvatarImageDialog from './ChangeAvatarImageDialog.vue';
import SetAvatarStylesDialog from './SetAvatarStylesDialog.vue';
import SetAvatarTagsDialog from './SetAvatarTagsDialog.vue';
const API = inject('API');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const showUserDialog = inject('showUserDialog');
const showAvatarDialog = inject('showAvatarDialog');
const showFavoriteDialog = inject('showFavoriteDialog');
const openExternalLink = inject('openExternalLink');
const adjustDialogZ = inject('adjustDialogZ');
const getImageUrlFromImageId = inject('getImageUrlFromImageId');
const getAvatarGallery = inject('getAvatarGallery');
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog, sortUserDialogAvatars } = useUserStore();
const { userDialog, currentUser } = storeToRefs(useUserStore());
const { avatarDialog, cachedAvatarModerations, cachedAvatars, cachedAvatarNames } = storeToRefs(useAvatarStore());
const { showAvatarDialog, getAvatarGallery, applyAvatarModeration, applyAvatar } = useAvatarStore();
const { showFavoriteDialog } = useFavoriteStore();
const { isGameRunning } = storeToRefs(useGameStore());
const { deleteVRChatCache } = useGameStore();
const { previousImagesDialogVisible, previousImagesTable } = storeToRefs(useGalleryStore());
const { showFullscreenImageDialog, checkPreviousImageAvailable } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const { $message, $confirm, $prompt } = instance.proxy;
const emit = defineEmits(['openFolderGeneric', 'deleteVRChatCache', 'openPreviousImagesDialog']);
const props = defineProps({
avatarDialog: {
type: Object,
required: true
},
hideTooltips: {
type: Boolean,
default: false
},
isGameRunning: {
type: Boolean,
default: false
}
});
defineEmits(['openPreviousImagesDialog']);
const avatarDialogRef = ref(null);
const changeAvatarImageDialogVisible = ref(false);
const previousImagesFileId = ref('');
const previousImagesDialogVisible = ref(false);
const previousImagesTable = ref([]);
const treeData = ref([]);
const timeSpent = ref(0);
@@ -695,7 +694,7 @@
});
const avatarDialogPlatform = computed(() => {
const { ref } = props.avatarDialog;
const { ref } = avatarDialog.value;
const platforms = [];
if (ref.unityPackages) {
for (const unityPackage of ref.unityPackages) {
@@ -721,11 +720,11 @@
});
watch(
() => props.avatarDialog.loading,
() => avatarDialog.value.loading,
(newVal) => {
if (newVal) {
nextTick(() => {
const D = props.avatarDialog;
const D = avatarDialog.value;
if (D.visible) {
adjustDialogZ(avatarDialogRef.value.$el);
}
@@ -735,6 +734,10 @@
}
);
function getImageUrlFromImageId(imageId) {
return `https://api.vrchat.cloud/api/1/file/${imageId}/1/`;
}
function handleDialogOpen() {
fileAnalysis.value = {};
memo.value = '';
@@ -744,19 +747,19 @@
}
function getAvatarTimeSpent() {
const D = props.avatarDialog;
const D = avatarDialog.value;
database.getAvatarTimeSpent(D.id).then((aviTime) => {
if (D.id === aviTime.avatarId) {
timeSpent.value = aviTime.timeSpent;
if (D.id === API.currentUser.currentAvatar && API.currentUser.$previousAvatarSwapTime) {
timeSpent.value += Date.now() - API.currentUser.$previousAvatarSwapTime;
if (D.id === currentUser.value.currentAvatar && currentUser.value.$previousAvatarSwapTime) {
timeSpent.value += Date.now() - currentUser.value.$previousAvatarSwapTime;
}
}
});
}
function getAvatarMemo() {
const D = props.avatarDialog;
const D = avatarDialog.value;
database.getAvatarMemoDB(D.id).then((res) => {
if (D.id === res.avatarId) {
memo.value = res.memo;
@@ -764,20 +767,8 @@
});
}
function openFolderGeneric(path) {
emit('openFolderGeneric', path);
}
function deleteVRChatCache(ref) {
emit('deleteVRChatCache', ref);
}
function commaNumber(num) {
return utils.commaNumber(num);
}
function avatarDialogCommand(command) {
const D = props.avatarDialog;
const D = avatarDialog.value;
switch (command) {
case 'Refresh':
showAvatarDialog(D.id);
@@ -804,7 +795,7 @@
showSetAvatarStylesDialog(D.id);
break;
case 'Download Unity Package':
openExternalLink(replaceVrcPackageUrl(props.avatarDialog.ref.unityPackageUrl));
openExternalLink(replaceVrcPackageUrl(avatarDialog.value.ref.unityPackageUrl));
break;
case 'Add Favorite':
showFavoriteDialog('avatar', D.id);
@@ -845,7 +836,7 @@
})
.then((args) => {
// 'AVATAR-MODERATION';
args.ref = API.applyAvatarModeration(args.json);
applyAvatarModeration(args.json);
$message({
message: 'Avatar blocked',
type: 'success'
@@ -860,9 +851,8 @@
targetAvatarId: D.id
})
.then((args) => {
// 'AVATAR-MODERATION:DELETE';
API.cachedAvatarModerations.delete(args.params.targetAvatarId);
const D = props.avatarDialog;
cachedAvatarModerations.value.delete(args.params.targetAvatarId);
const D = avatarDialog.value;
if (
args.params.avatarModerationType === 'block' &&
D.id === args.params.targetAvatarId
@@ -878,6 +868,7 @@
releaseStatus: 'public'
})
.then((args) => {
applyAvatar(args.json);
$message({
message: 'Avatar updated to public',
type: 'success'
@@ -892,6 +883,7 @@
releaseStatus: 'private'
})
.then((args) => {
applyAvatar(args.json);
$message({
message: 'Avatar updated to private',
type: 'success'
@@ -905,6 +897,19 @@
avatarId: D.id
})
.then((args) => {
const { json } = args;
cachedAvatars.value.delete(json._id);
if (userDialog.value.id === json.authorId) {
const map = new Map();
for (const ref of cachedAvatars.value.values()) {
if (ref.authorId === json.authorId) {
map.set(ref.id, ref);
}
}
const array = Array.from(map.values());
sortUserDialogAvatars(array);
}
$message({
message: 'Avatar deleted',
type: 'success'
@@ -973,7 +978,7 @@
function displayPreviousImages(command) {
previousImagesTable.value = [];
previousImagesFileId.value = '';
const { imageUrl } = props.avatarDialog.ref;
const { imageUrl } = avatarDialog.value.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
return;
@@ -988,7 +993,7 @@
changeAvatarImageDialogVisible.value = true;
}
imageRequest.getAvatarImages(params).then((args) => {
storeAvatarImage(args);
storeAvatarImage(args, cachedAvatarNames.value);
previousImagesFileId.value = args.json.id;
const images = [];
@@ -1001,23 +1006,6 @@
});
}
async function checkPreviousImageAvailable(images) {
previousImagesTable.value = [];
for (const image of images) {
if (image.file && image.file.url) {
const response = await fetch(image.file.url, {
method: 'HEAD',
redirect: 'follow'
}).catch((error) => {
console.log(error);
});
if (response.status === 200) {
previousImagesTable.value.push(image);
}
}
}
}
function selectAvatar(id) {
avatarRequest
.selectAvatar({
@@ -1047,6 +1035,7 @@
description: instance.inputValue
})
.then((args) => {
applyAvatar(args.json);
$message({
message: t('prompt.change_avatar_description.message.success'),
type: 'success'
@@ -1073,6 +1062,7 @@
name: instance.inputValue
})
.then((args) => {
applyAvatar(args.json);
$message({
message: t('prompt.rename_avatar.message.success'),
type: 'success'
@@ -1087,12 +1077,12 @@
function onAvatarMemoChange() {
if (memo.value) {
database.setAvatarMemo({
avatarId: props.avatarDialog.id,
avatarId: avatarDialog.value.id,
editedAt: new Date().toJSON(),
memo: memo.value
});
} else {
database.deleteAvatarMemo(props.avatarDialog.id);
database.deleteAvatarMemo(avatarDialog.value.id);
}
}
@@ -1104,17 +1094,13 @@
copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
}
function timeToText(time) {
return utils.timeToText(time);
}
function refreshAvatarDialogTreeData() {
treeData.value = utils.buildTreeData(props.avatarDialog.ref);
treeData.value = buildTreeData(avatarDialog.value.ref);
}
function getAvatarFileAnalysis() {
let unityPackage;
const D = props.avatarDialog;
const D = avatarDialog.value;
const avatarId = D.ref.id;
let assetUrl = '';
let variant = 'security';
@@ -1154,8 +1140,7 @@
return;
}
miscRequest.getFileAnalysis({ fileId, version, variant, avatarId }).then((args) => {
// API.$on('FILE:ANALYSIS', function (args) {
if (!props.avatarDialog.visible || props.avatarDialog.id !== args.params.avatarId) {
if (!avatarDialog.value.visible || avatarDialog.value.id !== args.params.avatarId) {
return;
}
const ref = args.json;
@@ -1169,9 +1154,8 @@
ref._totalTextureUsage = `${(ref.avatarStats.totalTextureUsage / 1048576).toFixed(2)} MB`;
}
fileAnalysis.value = utils.buildTreeData(args.json);
fileAnalysis.value = buildTreeData(args.json);
});
// });
}
function showSetAvatarTagsDialog(avatarId) {
@@ -1187,7 +1171,7 @@
D.contentViolence = false;
D.contentAdult = false;
D.contentSex = false;
const oldTags = props.avatarDialog.ref.tags;
const oldTags = avatarDialog.value.ref.tags;
oldTags.forEach((tag) => {
switch (tag) {
case 'content_horror':
@@ -1212,8 +1196,8 @@
break;
}
});
for (const ref of API.cachedAvatars.values()) {
if (ref.authorId === API.currentUser.id) {
for (const ref of cachedAvatars.value.values()) {
if (ref.authorId === currentUser.value.id) {
ref.$selected = false;
ref.$tagString = '';
if (avatarId === ref.id) {
@@ -1245,12 +1229,12 @@
const D = setAvatarStylesDialog;
D.visible = true;
D.loading = true;
D.avatarId = props.avatarDialog.id;
D.primaryStyle = props.avatarDialog.ref.styles?.primary || '';
D.secondaryStyle = props.avatarDialog.ref.styles?.secondary || '';
D.avatarId = avatarDialog.value.id;
D.primaryStyle = avatarDialog.value.ref.styles?.primary || '';
D.secondaryStyle = avatarDialog.value.ref.styles?.secondary || '';
D.initialPrimaryStyle = D.primaryStyle;
D.initialSecondaryStyle = D.secondaryStyle;
D.initialTags = props.avatarDialog.ref.tags;
D.initialTags = avatarDialog.value.ref.tags;
D.authorTags = '';
for (const tag of D.initialTags) {
if (tag.startsWith('author_tag_')) {
@@ -1298,43 +1282,31 @@
}
const r = new FileReader();
r.onload = function () {
props.avatarDialog.galleryLoading = true;
avatarDialog.value.galleryLoading = true;
const base64Body = btoa(r.result);
avatarRequest
.uploadAvatarGalleryImage(base64Body, props.avatarDialog.id)
.uploadAvatarGalleryImage(base64Body, avatarDialog.value.id)
.then((args) => {
$message({
message: t('message.avatar_gallery.uploaded'),
type: 'success'
});
console.log(args);
props.avatarDialog.galleryImages = getAvatarGallery(props.avatarDialog.id);
avatarDialog.value.galleryImages = getAvatarGallery(avatarDialog.value.id);
return args;
})
.finally(() => {
props.avatarDialog.galleryLoading = false;
avatarDialog.value.galleryLoading = false;
});
};
r.readAsBinaryString(files[0]);
clearFile();
}
function deleteAvatarGalleryImage(imageUrl) {
const fileId = extractFileId(imageUrl);
miscRequest.deleteFile(fileId).then((args) => {
$message({
message: t('message.avatar_gallery.deleted'),
type: 'success'
});
props.avatarDialog.galleryImages = getAvatarGallery(props.avatarDialog.id);
return args;
});
}
function reorderAvatarGalleryImage(imageUrl, direction) {
const fileId = extractFileId(imageUrl);
let fileIds = [];
props.avatarDialog.ref.gallery.forEach((item) => {
avatarDialog.value.ref.gallery.forEach((item) => {
fileIds.push(extractFileId(item.id));
});
const index = fileIds.indexOf(fileId);
@@ -1360,16 +1332,28 @@
return;
}
if (direction === -1) {
utils.moveArrayItem(fileIds, index, index - 1);
moveArrayItem(fileIds, index, index - 1);
} else {
utils.moveArrayItem(fileIds, index, index + 1);
moveArrayItem(fileIds, index, index + 1);
}
avatarRequest.setAvatarGalleryOrder(fileIds).then((args) => {
$message({
message: t('message.avatar_gallery.reordered'),
type: 'success'
});
props.avatarDialog.galleryImages = getAvatarGallery(props.avatarDialog.id);
avatarDialog.value.galleryImages = getAvatarGallery(avatarDialog.value.id);
return args;
});
}
function deleteAvatarGalleryImage(imageUrl) {
const fileId = extractFileId(imageUrl);
miscRequest.deleteFile(fileId).then((args) => {
$message({
message: t('message.avatar_gallery.deleted'),
type: 'success'
});
getAvatarGallery(avatarDialog.value.id);
return args;
});
}

View File

@@ -42,32 +42,29 @@
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { extractFileId } from '../../../composables/shared/utils';
import webApiService from '../../../service/webapi';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useAvatarStore, useGalleryStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const { avatarDialog } = storeToRefs(useAvatarStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const { applyAvatar } = useAvatarStore();
const props = defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
avatarDialog: {
type: Object,
default: () => ({})
},
previousImagesFileId: {
type: String,
default: ''
@@ -121,7 +118,7 @@
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.avatarDialog.visible || props.avatarDialog.loading) {
if (!files.length || !avatarDialog.value.visible || avatarDialog.value.loading) {
clearFile();
return;
}
@@ -156,8 +153,8 @@
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const avatarId = props.avatarDialog.id;
const { imageUrl } = props.avatarDialog.ref;
const avatarId = avatarDialog.value.id;
const { imageUrl } = avatarDialog.value.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
@@ -206,7 +203,6 @@
}
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 = {
@@ -218,7 +214,6 @@
}
async function avatarImageFileStart(args) {
// API.$on('AVATARIMAGE:FILESTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
@@ -242,7 +237,7 @@
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
API.$throw('Avatar image upload failed', json, params.url);
$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
@@ -252,7 +247,6 @@
}
async function avatarImageFileAWS(args) {
// API.$on('AVATARIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -263,7 +257,6 @@
}
async function avatarImageFileFinish(args) {
// API.$on('AVATARIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -274,7 +267,6 @@
}
async function avatarImageSigStart(args) {
// API.$on('AVATARIMAGE:SIGSTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
@@ -298,7 +290,7 @@
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
API.$throw('Avatar image upload failed', json, params.url);
$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
@@ -308,7 +300,6 @@
}
async function avatarImageSigAWS(args) {
// API.$on('AVATARIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -319,18 +310,17 @@
}
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`
imageUrl: `${AppGlobal.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setAvatarImage(parmas);
return avatarImageSet(res);
}
async function avatarImageSet(args) {
// API.$on('AVATARIMAGE:SET')
applyAvatar(args.json);
changeAvatarImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
@@ -339,7 +329,7 @@
});
refresh();
} else {
API.$throw(0, 'Avatar image change failed', args.params.imageUrl);
$throw(0, 'Avatar image change failed', args.params.imageUrl);
}
}
@@ -352,42 +342,22 @@
function setAvatarImage(image) {
changeAvatarImageDialogLoading.value = true;
const parmas = {
id: props.avatarDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
id: avatarDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest.setAvatarImage(parmas).finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
imageRequest
.setAvatarImage(parmas)
.then((args) => applyAvatar(args.json))
.finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
return (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
props.avatarDialog.ref.imageUrl
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
avatarDialog.value.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

@@ -64,13 +64,16 @@
import { watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { arraysMatch } from '../../../shared/utils';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { applyAvatar } = useAvatarStore();
const props = defineProps({
setAvatarStylesDialog: {
type: Object,
@@ -125,7 +128,7 @@
if (
props.setAvatarStylesDialog.initialPrimaryStyle === props.setAvatarStylesDialog.primaryStyle &&
props.setAvatarStylesDialog.initialSecondaryStyle === props.setAvatarStylesDialog.secondaryStyle &&
utils.arraysMatch(props.setAvatarStylesDialog.initialTags, tags)
arraysMatch(props.setAvatarStylesDialog.initialTags, tags)
) {
props.setAvatarStylesDialog.visible = false;
return;
@@ -139,7 +142,8 @@
};
avatarRequest
.saveAvatar(params)
.then(() => {
.then((args) => {
applyAvatar(args.json);
$message.success(t('dialog.set_avatar_styles.save_success'));
props.setAvatarStylesDialog.visible = false;
})

View File

@@ -94,12 +94,12 @@
</template>
<script setup>
import { inject, watch, getCurrentInstance } from 'vue';
import { getCurrentInstance, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const showAvatarDialog = inject('showAvatarDialog');
const { showAvatarDialog, applyAvatar } = useAvatarStore();
const { t } = useI18n();
const instance = getCurrentInstance();
@@ -220,10 +220,11 @@
tags.push(tag);
}
}
await avatarRequest.saveAvatar({
const args = await avatarRequest.saveAvatar({
id: ref.id,
tags
});
applyAvatar(args.json);
D.selectedCount--;
}
} catch (err) {
@@ -270,15 +271,6 @@
}
}
}
// useless
// $app.data.avatarContentTags = [
// 'content_horror',
// 'content_gore',
// 'content_violence',
// 'content_adult',
// 'content_sex'
// ];
</script>
<style scoped></style>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog ref="favoriteDialog" :visible.sync="isVisible" :title="$t('dialog.favorite.header')" width="300px">
<safe-dialog ref="favoriteDialogRef" :visible.sync="isVisible" :title="t('dialog.favorite.header')" width="300px">
<div v-loading="loading">
<span style="display: block; text-align: center">{{ $t('dialog.favorite.vrchat_favorites') }}</span>
<span style="display: block; text-align: center">{{ t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
<el-button
style="display: block; width: 100%; margin: 10px 0"
@@ -22,7 +22,7 @@
</template>
</div>
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ $t('dialog.favorite.local_favorites') }}</span>
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
<template v-for="group in localWorldFavoriteGroups">
<el-button
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
@@ -42,7 +42,7 @@
</template>
</div>
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ $t('dialog.favorite.local_avatar_favorites') }}</span>
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
<template v-for="group in localAvatarFavoriteGroups">
<el-button
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
@@ -65,122 +65,88 @@
</safe-dialog>
</template>
<script>
import { favoriteRequest } from '../../api';
<script setup>
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { computed, nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { favoriteRequest } from '../../api';
import { adjustDialogZ } from '../../shared/utils';
import { useFavoriteStore, useUserStore } from '../../stores';
export default {
name: 'ChooseFavoriteGroupDialog',
inject: ['API', 'adjustDialogZ'],
props: {
favoriteDialog: {
type: Object,
default: () => ({
visible: false,
type: '',
objectId: '',
currentGroup: {}
})
},
localWorldFavoriteGroups: {
type: Array,
default: () => []
},
localAvatarFavoriteGroups: {
type: Array,
default: () => []
},
hasLocalWorldFavorite: {
type: Function,
default: () => () => false
},
getLocalWorldFavoriteGroupLength: {
type: Function,
default: () => () => 0
},
hasLocalAvatarFavorite: {
type: Function,
default: () => () => false
},
getLocalAvatarFavoriteGroupLength: {
type: Function,
default: () => () => 0
}
},
data() {
return {
groups: [],
loading: false
};
},
computed: {
isVisible: {
get() {
return this.favoriteDialog.visible;
},
set(value) {
this.$emit('update:favorite-dialog', { ...this.favoriteDialog, visible: value });
}
},
isLocalUserVrcplusSupporter() {
return this.API.currentUser.$isVRCPlus;
}
},
watch: {
'favoriteDialog.visible'(value) {
if (value) {
this.initFavoriteDialog();
this.$nextTick(() => {
this.adjustDialogZ(this.$refs.favoriteDialog.$el);
});
}
}
},
methods: {
initFavoriteDialog() {
if (this.favoriteDialog.type === 'friend') {
this.groups = this.API.favoriteFriendGroups;
} else if (this.favoriteDialog.type === 'world') {
this.groups = this.API.favoriteWorldGroups;
} else if (this.favoriteDialog.type === 'avatar') {
this.groups = this.API.favoriteAvatarGroups;
}
},
addFavorite(group) {
const D = this.favoriteDialog;
this.loading = true;
favoriteRequest
.addFavorite({
type: D.type,
favoriteId: D.objectId,
tags: group.name
})
.then(() => {
this.isVisible = false;
new Noty({
type: 'success',
text: 'Favorite added'
}).show();
})
.finally(() => {
this.loading = false;
});
},
addLocalWorldFavorite(...args) {
this.$emit('addLocalWorldFavorite', ...args);
},
removeLocalWorldFavorite(...args) {
this.$emit('removeLocalWorldFavorite', ...args);
},
addLocalAvatarFavorite(...args) {
this.$emit('addLocalAvatarFavorite', ...args);
},
removeLocalAvatarFavorite(...args) {
this.$emit('removeLocalAvatarFavorite', ...args);
},
deleteFavoriteNoConfirm(...args) {
this.$emit('deleteFavoriteNoConfirm', ...args);
const { t } = useI18n();
const favoriteStore = useFavoriteStore();
const {
favoriteFriendGroups,
favoriteAvatarGroups,
favoriteWorldGroups,
localAvatarFavoriteGroups,
favoriteDialog,
localWorldFavoriteGroups
} = storeToRefs(favoriteStore);
const {
getLocalWorldFavoriteGroupLength,
addLocalWorldFavorite,
hasLocalWorldFavorite,
hasLocalAvatarFavorite,
addLocalAvatarFavorite,
getLocalAvatarFavoriteGroupLength,
removeLocalAvatarFavorite,
removeLocalWorldFavorite,
deleteFavoriteNoConfirm
} = favoriteStore;
const { currentUser } = storeToRefs(useUserStore());
const favoriteDialogRef = ref(null);
const groups = ref([]);
const loading = ref(false);
const isVisible = computed({
get: () => favoriteDialog.value.visible,
set: (v) => {
favoriteDialog.value.visible = v;
}
});
const isLocalUserVrcplusSupporter = computed(() => currentUser.value.$isVRCPlus);
watch(
() => favoriteDialog.value.visible,
async (value) => {
if (value) {
initFavoriteDialog();
await nextTick();
adjustDialogZ(favoriteDialogRef.value.$el);
}
}
};
);
function initFavoriteDialog() {
if (favoriteDialog.value.type === 'friend') {
groups.value = favoriteFriendGroups.value;
} else if (favoriteDialog.value.type === 'world') {
groups.value = favoriteWorldGroups.value;
} else if (favoriteDialog.value.type === 'avatar') {
groups.value = favoriteAvatarGroups.value;
}
}
function addFavorite(group) {
const D = favoriteDialog.value;
loading.value = true;
favoriteRequest
.addFavorite({
type: D.type,
favoriteId: D.objectId,
tags: group.name
})
.then(() => {
isVisible.value = false;
new Noty({ type: 'success', text: 'favorite added!' }).show();
})
.finally(() => {
loading.value = false;
});
}
</script>

View File

@@ -1,11 +1,5 @@
<template>
<safe-dialog
ref="fullscreenImageDialog"
class="x-dialog"
:visible.sync="fullscreenImageDialog.visible"
append-to-body
top="1vh"
width="97vw">
<safe-dialog class="x-dialog" :visible.sync="fullscreenImageDialog.visible" append-to-body top="1vh" width="97vw">
<div>
<div style="margin: 0 0 5px 5px">
<el-button
@@ -29,21 +23,16 @@
</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';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { copyToClipboard, escapeTag, extractFileId } from '../../shared/utils';
import { useGalleryStore } from '../../stores';
const { proxy } = getCurrentInstance();
const { $message } = proxy;
defineProps({
fullscreenImageDialog: {
type: Object,
default: () => ({})
}
});
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
function copyImageUrl(imageUrl) {
copyToClipboard(imageUrl, 'ImageUrl copied to clipboard');
@@ -84,7 +73,7 @@
} catch {
new Noty({
type: 'error',
text: utils.escapeTag(`Failed to download image. ${url}`)
text: escapeTag(`Failed to download image. ${url}`)
}).show();
}
}

View File

@@ -33,7 +33,7 @@
size="small"
@click="displayGalleryUpload"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus">
:disabled="!currentUser.$isVRCPlus">
{{ t('dialog.gallery_icons.upload') }}
</el-button>
<el-button
@@ -41,7 +41,7 @@
size="small"
@click="setProfilePicOverride('')"
icon="el-icon-close"
:disabled="!API.currentUser.profilePicOverride">
:disabled="!currentUser.profilePicOverride">
{{ t('dialog.gallery_icons.clear') }}
</el-button>
</el-button-group>
@@ -102,7 +102,7 @@
size="small"
@click="displayVRCPlusIconUpload"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus">
:disabled="!currentUser.$isVRCPlus">
{{ t('dialog.gallery_icons.upload') }}
</el-button>
<el-button
@@ -110,7 +110,7 @@
size="small"
@click="setVRCPlusIcon('')"
icon="el-icon-close"
:disabled="!API.currentUser.userIcon">
:disabled="!currentUser.userIcon">
{{ t('dialog.gallery_icons.clear') }}
</el-button>
</el-button-group>
@@ -150,7 +150,7 @@
<span slot="label">
{{ t('dialog.gallery_icons.emojis') }}
<span style="color: #909399; font-size: 12px; margin-left: 5px">
{{ emojiTable.length }}/{{ API.cachedConfig?.maxUserEmoji }}
{{ emojiTable.length }}/{{ cachedConfig?.maxUserEmoji }}
</span>
</span>
<input
@@ -172,7 +172,7 @@
size="small"
@click="displayEmojiUpload"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus">
:disabled="!currentUser.$isVRCPlus">
{{ t('dialog.gallery_icons.upload') }}
</el-button>
</el-button-group>
@@ -237,7 +237,7 @@
@click="
showFullscreenImageDialog(
image.versions[image.versions.length - 1].file.url,
getEmojiName(image)
getEmojiFileName(image)
)
">
<template v-if="image.frames">
@@ -271,7 +271,7 @@
@click="
showFullscreenImageDialog(
image.versions[image.versions.length - 1].file.url,
getEmojiName(image)
getEmojiFileName(image)
)
"
size="mini"
@@ -292,7 +292,7 @@
<span slot="label">
{{ t('dialog.gallery_icons.stickers') }}
<span style="color: #909399; font-size: 12px; margin-left: 5px">
{{ stickerTable.length }}/{{ API.cachedConfig?.maxUserStickers }}
{{ stickerTable.length }}/{{ cachedConfig?.maxUserStickers }}
</span>
</span>
<input
@@ -313,7 +313,7 @@
size="small"
@click="displayStickerUpload"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus">
:disabled="!currentUser.$isVRCPlus">
{{ t('dialog.gallery_icons.upload') }}
</el-button>
</el-button-group>
@@ -373,7 +373,7 @@
size="small"
@click="displayPrintUpload"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus">
:disabled="!currentUser.$isVRCPlus">
{{ t('dialog.gallery_icons.upload') }}
</el-button>
</el-button-group>
@@ -405,25 +405,25 @@
<div style="margin-top: 5px; width: 208px">
<span class="x-ellipsis" v-if="image.note" v-text="image.note" style="display: block"></span>
<span v-else style="display: block">&nbsp;</span>
<location
<Location
class="x-ellipsis"
v-if="image.worldId"
:location="image.worldId"
:hint="image.worldName"
style="display: block"></location>
style="display: block" />
<span v-else style="display: block">&nbsp;</span>
<display-name
<DisplayName
class="x-ellipsis"
v-if="image.authorId"
:userid="image.authorId"
:hint="image.authorName"
style="color: #909399; font-family: monospace; display: block"></display-name>
style="color: #909399; font-family: monospace; display: block" />
<span v-else style="font-family: monospace; display: block">&nbsp;</span>
<span
class="x-ellipsis"
v-if="image.createdAt"
style="color: #909399; font-family: monospace; font-size: 11px; display: block">
{{ image.createdAt | formatDate('long') }}
{{ formatDateFilter(image.createdAt, 'long') }}
</span>
<span v-else style="display: block">&nbsp;</span>
</div>
@@ -479,7 +479,7 @@
<span
class="x-ellipsis"
style="color: #909399; font-family: monospace; font-size: 11px; display: block">
{{ item.created_at | formatDate('long') }}
{{ formatDateFilter(item.created_at, 'long') }}
</span>
<span v-text="item.itemType" style="display: block"></span>
</div>
@@ -500,95 +500,50 @@
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
import { extractFileId } from '../../composables/shared/utils';
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../composables/user/constants/emoji';
import { getPrintFileName, getEmojiFileName } from '../../composables/user/utils';
import Location from '../Location.vue';
import { AppGlobal } from '../../service/appConfig';
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
import { extractFileId, formatDateFilter, getEmojiFileName, getPrintFileName } from '../../shared/utils';
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useUserStore } from '../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const {
galleryTable,
galleryDialogVisible,
galleryDialogGalleryLoading,
galleryDialogIconsLoading,
galleryDialogEmojisLoading,
galleryDialogStickersLoading,
galleryDialogPrintsLoading,
galleryDialogInventoryLoading,
VRCPlusIconsTable,
printUploadNote,
printCropBorder,
stickerTable,
printTable,
emojiTable,
inventoryTable
} = storeToRefs(useGalleryStore());
const {
refreshGalleryTable,
refreshVRCPlusIconsTable,
refreshStickerTable,
refreshPrintTable,
refreshEmojiTable,
getInventory,
handleStickerAdd,
handleGalleryImageAdd
} = useGalleryStore();
const API = inject('API');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const props = defineProps({
galleryDialogVisible: {
type: Boolean,
required: true
},
galleryDialogGalleryLoading: {
type: Boolean,
required: true
},
galleryDialogIconsLoading: {
type: Boolean,
required: true
},
galleryDialogEmojisLoading: {
type: Boolean,
required: true
},
galleryDialogStickersLoading: {
type: Boolean,
required: true
},
galleryDialogPrintsLoading: {
type: Boolean,
required: true
},
galleryDialogInventoryLoading: {
type: Boolean,
required: true
},
galleryTable: {
type: Array,
required: true
},
// eslint-disable-next-line vue/prop-name-casing
VRCPlusIconsTable: {
type: Array,
required: true
},
emojiTable: {
type: Array,
required: true
},
stickerTable: {
type: Array,
required: true
},
printUploadNote: {
type: String,
required: true
},
printCropBorder: {
type: Boolean,
required: true
},
printTable: {
type: Array,
required: true
},
inventoryTable: {
type: Array,
required: true
}
});
const emit = defineEmits([
'refreshGalleryTable',
'refreshVRCPlusIconsTable',
'refreshStickerTable',
'refreshEmojiTable',
'refreshPrintTable',
'getInventory',
'closeGalleryDialog'
]);
const { currentUserInventory } = storeToRefs(useAdvancedSettingsStore());
const { showFullscreenImageDialog } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
const { cachedConfig } = storeToRefs(useAuthStore());
const emojiAnimFps = ref(15);
const emojiAnimFrameCount = ref(4);
@@ -596,12 +551,8 @@
const emojiAnimationStyle = ref('Stop');
const emojiAnimLoopPingPong = ref(false);
function refreshGalleryTable() {
emit('refreshGalleryTable');
}
function closeGalleryDialog() {
emit('closeGalleryDialog');
galleryDialogVisible.value = false;
}
function onFileChangeGallery(e) {
@@ -616,7 +567,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
proxy.$message({
message: t('message.file.too_large'),
type: 'error'
});
@@ -624,7 +575,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
proxy.$message({
message: t('message.file.not_image'),
type: 'error'
});
@@ -635,7 +586,8 @@
r.onload = function () {
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
$message({
handleGalleryImageAdd(args);
proxy.$message({
message: t('message.gallery.uploaded'),
type: 'success'
});
@@ -651,8 +603,8 @@
}
function setProfilePicOverride(fileId) {
if (!API.currentUser.$isVRCPlus) {
$message({
if (!currentUser.value.$isVRCPlus) {
proxy.$message({
message: 'VRCPlus required',
type: 'error'
});
@@ -660,9 +612,9 @@
}
let profilePicOverride = '';
if (fileId) {
profilePicOverride = `${API.endpointDomain}/file/${fileId}/1`;
profilePicOverride = `${AppGlobal.endpointDomain}/file/${fileId}/1`;
}
if (profilePicOverride === API.currentUser.profilePicOverride) {
if (profilePicOverride === currentUser.value.profilePicOverride) {
return;
}
userRequest
@@ -670,7 +622,7 @@
profilePicOverride
})
.then((args) => {
$message({
proxy.$message({
message: 'Profile picture changed',
type: 'success'
});
@@ -679,7 +631,7 @@
}
function compareCurrentProfilePic(fileId) {
const currentProfilePicOverride = extractFileId(API.currentUser.profilePicOverride);
const currentProfilePicOverride = extractFileId(currentUser.value.profilePicOverride);
if (fileId === currentProfilePicOverride) {
return true;
}
@@ -688,9 +640,7 @@
function deleteGalleryImage(fileId) {
miscRequest.deleteFile(fileId).then((args) => {
// API.$emit('GALLERYIMAGE:DELETE', args);
// API.$on('GALLERYIMAGE:DELETE')
const array = props.galleryTable;
const array = galleryTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (args.fileId === array[i].id) {
@@ -715,7 +665,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
proxy.$message({
message: t('message.file.too_large'),
type: 'error'
});
@@ -723,7 +673,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
proxy.$message({
message: t('message.file.not_image'),
type: 'error'
});
@@ -734,7 +684,10 @@
r.onload = function () {
const base64Body = btoa(r.result);
vrcPlusIconRequest.uploadVRCPlusIcon(base64Body).then((args) => {
$message({
if (Object.keys(VRCPlusIconsTable.value).length !== 0) {
VRCPlusIconsTable.value.unshift(args.json);
}
proxy.$message({
message: t('message.icon.uploaded'),
type: 'success'
});
@@ -745,17 +698,13 @@
clearFile();
}
function refreshVRCPlusIconsTable() {
emit('refreshVRCPlusIconsTable');
}
function displayVRCPlusIconUpload() {
document.getElementById('VRCPlusIconUploadButton').click();
}
function setVRCPlusIcon(fileId) {
if (!API.currentUser.$isVRCPlus) {
$message({
if (!currentUser.value.$isVRCPlus) {
proxy.$message({
message: 'VRCPlus required',
type: 'error'
});
@@ -763,9 +712,9 @@
}
let userIcon = '';
if (fileId) {
userIcon = `${API.endpointDomain}/file/${fileId}/1`;
userIcon = `${AppGlobal.endpointDomain}/file/${fileId}/1`;
}
if (userIcon === API.currentUser.userIcon) {
if (userIcon === currentUser.value.userIcon) {
return;
}
userRequest
@@ -773,7 +722,7 @@
userIcon
})
.then((args) => {
$message({
proxy.$message({
message: 'Icon changed',
type: 'success'
});
@@ -782,7 +731,7 @@
}
function compareCurrentVRCPlusIcon(userIcon) {
const currentUserIcon = extractFileId(API.currentUser.userIcon);
const currentUserIcon = extractFileId(currentUser.value.userIcon);
if (userIcon === currentUserIcon) {
return true;
}
@@ -791,9 +740,7 @@
function deleteVRCPlusIcon(fileId) {
miscRequest.deleteFile(fileId).then((args) => {
// API.$emit('VRCPLUSICON:DELETE', args);
// API.$on('VRCPLUSICON:DELETE')
const array = props.VRCPlusIconsTable;
const array = VRCPlusIconsTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (args.fileId === array[i].id) {
@@ -823,7 +770,7 @@
emojiAnimFps.value = parseInt(value.replace('fps', ''));
}
if (value.endsWith('loopStyle')) {
emojiAnimLoopPingPong.value = value.replace('loopStyle', '').toLowerCase() === 'pingpong';
emojiAnimLoopPingPong.value = value.replace('loopStyle', '').toLowerCase();
}
}
}
@@ -840,7 +787,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
proxy.$message({
message: t('message.file.too_large'),
type: 'error'
});
@@ -848,7 +795,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
proxy.$message({
message: t('message.file.not_image'),
type: 'error'
});
@@ -873,7 +820,10 @@
}
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadEmoji(base64Body, params).then((args) => {
$message({
if (Object.keys(emojiTable).length !== 0) {
emojiTable.unshift(args.json);
}
proxy.$message({
message: t('message.emoji.uploaded'),
type: 'success'
});
@@ -884,18 +834,10 @@
clearFile();
}
function refreshEmojiTable() {
emit('refreshEmojiTable');
}
function displayEmojiUpload() {
document.getElementById('EmojiUploadButton').click();
}
function getEmojiName(emoji) {
getEmojiFileName(emoji);
}
function generateEmojiStyle(url, fps, frameCount, loopStyle) {
let framesPerLine = 2;
if (frameCount > 4) framesPerLine = 4;
@@ -917,9 +859,7 @@
function deleteEmoji(fileId) {
miscRequest.deleteFile(fileId).then((args) => {
// API.$emit('EMOJI:DELETE', args);
// API.$on('EMOJI:DELETE')
const array = props.emojiTable;
const array = emojiTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (args.fileId === array[i].id) {
@@ -943,7 +883,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
proxy.$message({
message: t('message.file.too_large'),
type: 'error'
});
@@ -951,7 +891,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
proxy.$message({
message: t('message.file.not_image'),
type: 'error'
});
@@ -966,7 +906,8 @@
};
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadSticker(base64Body, params).then((args) => {
$message({
handleStickerAdd(args);
proxy.$message({
message: t('message.sticker.uploaded'),
type: 'success'
});
@@ -977,19 +918,13 @@
clearFile();
}
function refreshStickerTable() {
emit('refreshStickerTable');
}
function displayStickerUpload() {
document.getElementById('StickerUploadButton').click();
}
function deleteSticker(fileId) {
miscRequest.deleteFile(fileId).then((args) => {
// API.$emit('STICKER:DELETE', args);
// API.$on('STICKER:DELETE')
const array = props.stickerTable;
const array = stickerTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (args.fileId === array[i].id) {
@@ -1014,7 +949,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
proxy.$message({
message: t('message.file.too_large'),
type: 'error'
});
@@ -1022,7 +957,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
proxy.$message({
message: t('message.file.not_image'),
type: 'error'
});
@@ -1036,20 +971,19 @@
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
const timestamp = date.toISOString().slice(0, 19);
const params = {
note: props.printUploadNote,
note: printUploadNote.value,
// worldId: '',
timestamp
};
const base64Body = btoa(r.result);
const cropWhiteBorder = props.printCropBorder;
const cropWhiteBorder = printCropBorder.value;
vrcPlusImageRequest.uploadPrint(base64Body, cropWhiteBorder, params).then((args) => {
$message({
proxy.$message({
message: t('message.print.uploaded'),
type: 'success'
});
// API.$on('PRINT:ADD')
if (Object.keys(props.printTable).length !== 0) {
props.printTable.unshift(args.json);
if (Object.keys(printTable.value).length !== 0) {
printTable.value.unshift(args.json);
}
return args;
@@ -1059,22 +993,13 @@
clearFile();
}
function refreshPrintTable() {
emit('refreshPrintTable');
}
function getInventory() {
emit('getInventory');
}
function displayPrintUpload() {
document.getElementById('PrintUploadButton').click();
}
function deletePrint(printId) {
vrcPlusImageRequest.deletePrint(printId).then((args) => {
// API.$on('PRINT:DELETE');
const array = props.printTable;
const array = printTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (args.printId === array[i].id) {
@@ -1090,8 +1015,8 @@
const args = await inventoryRequest.consumeInventoryBundle({
inventoryId
});
API.currentUserInventory.delete(inventoryId);
const array = props.inventoryTable;
currentUserInventory.value.delete(inventoryId);
const array = inventoryTable.value;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (inventoryId === array[i].id) {

View File

@@ -26,7 +26,7 @@
type="default"
size="small"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus"
:disabled="!currentUser.$isVRCPlus"
@click="displayGalleryUpload"
>{{ t('dialog.gallery_select.upload') }}</el-button
>
@@ -50,30 +50,27 @@
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { vrcPlusImageRequest } from '../../../api';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const API = inject('API');
const { galleryTable } = storeToRefs(useGalleryStore());
const { refreshGalleryTable, handleGalleryImageAdd } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
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;
@@ -116,13 +113,13 @@
r.onload = function () {
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
handleGalleryImageAdd(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);
if (Object.keys(galleryTable.value).length !== 0) {
galleryTable.value.unshift(args.json);
}
return args;
});
@@ -130,7 +127,4 @@
r.readAsBinaryString(files[0]);
clearFile();
}
function refreshGalleryTable() {
emit('refreshGalleryTable');
}
</script>

View File

@@ -36,7 +36,7 @@
</el-popover>
<div style="flex: 1; display: flex; align-items: center; margin-left: 15px">
<div class="group-header" style="flex: 1">
<span v-if="groupDialog.ref.ownerId === API.currentUser.id" style="margin-right: 5px">👑</span>
<span v-if="groupDialog.ref.ownerId === currentUser.id" style="margin-right: 5px">👑</span>
<span class="dialog-title" style="margin-right: 5px" v-text="groupDialog.ref.name"></span>
<span
class="group-discriminator x-grey"
@@ -399,9 +399,9 @@
</span>
<div v-for="room in groupDialog.instances" :key="room.tag" style="width: 100%">
<div style="margin: 5px 0">
<location :location="room.tag" style="display: inline-block" />
<Location :location="room.tag" style="display: inline-block" />
<el-tooltip placement="top" content="Invite yourself" :disabled="hideTooltips">
<invite-yourself :location="room.tag" style="margin-left: 5px" />
<InviteYourself :location="room.tag" style="margin-left: 5px" />
</el-tooltip>
<el-tooltip placement="top" content="Refresh player count" :disabled="hideTooltips">
<el-button
@@ -411,12 +411,11 @@
circle
@click="refreshInstancePlayerCount(room.tag)" />
</el-tooltip>
<last-join :location="room.tag" :currentlocation="lastLocation.location" />
<instance-info
<LastJoin :location="room.tag" :currentlocation="lastLocation.location" />
<InstanceInfo
:location="room.tag"
:instance="room.ref"
:friendcount="room.friendCount"
:updateelement="updateInstanceInfo" />
:friendcount="room.friendCount" />
</div>
<div
v-if="room.users.length"
@@ -437,10 +436,10 @@
v-text="user.displayName" />
<span v-if="user.location === 'traveling'" class="extra">
<i class="el-icon-loading" style="margin-right: 5px" />
<timer :epoch="user.$travelingToTime" />
<Timer :epoch="user.$travelingToTime" />
</span>
<span v-else class="extra">
<timer :epoch="user.$location_at" />
<Timer :epoch="user.$location_at" />
</span>
</div>
</div>
@@ -494,13 +493,14 @@
<span>{{ t('dialog.group.posts.visibility') }}</span>
<br />
<template v-for="roleId in groupDialog.announcement.roleIds">
<template v-for="role in groupDialog.ref.roles"
><span
v-if="role.id === roleId"
:key="roleId + role.id"
v-text="role.name"
/></template>
<span
v-for="(role, rIndex) in groupDialog.ref.roles"
v-if="role.id === roleId"
:key="rIndex"
v-text="role.name" />
<span
:key="rIndex"
:key="roleId"
v-if="
groupDialog.announcement.roleIds.indexOf(roleId) <
groupDialog.announcement.roleIds.length - 1
@@ -511,18 +511,18 @@
</template>
<i class="el-icon-view" style="margin-right: 5px" />
</el-tooltip>
<display-name
<DisplayName
:userid="groupDialog.announcement.authorId"
style="margin-right: 5px" />
<span v-if="groupDialog.announcement.editorId" style="margin-right: 5px">
({{ t('dialog.group.posts.edited_by') }}
<display-name :userid="groupDialog.announcement.editorId" />)
<DisplayName :userid="groupDialog.announcement.editorId" />)
</span>
<el-tooltip placement="bottom">
<template #content>
<span
>{{ t('dialog.group.posts.created_at') }}
{{ groupDialog.announcement.createdAt | formatDate('long') }}</span
{{ formatDateFilter(groupDialog.announcement.createdAt, 'long') }}</span
>
<template
v-if="
@@ -532,11 +532,13 @@
<br />
<span
>{{ t('dialog.group.posts.edited_at') }}
{{ groupDialog.announcement.updatedAt | formatDate('long') }}</span
{{
formatDateFilter(groupDialog.announcement.updatedAt, 'long')
}}</span
>
</template>
</template>
<timer :epoch="Date.parse(groupDialog.announcement.updatedAt)" />
<Timer :epoch="Date.parse(groupDialog.announcement.updatedAt)" />
</el-tooltip>
<template v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')">
<el-tooltip
@@ -593,7 +595,7 @@
<div class="x-friend-item" style="cursor: default">
<div class="detail">
<span class="name">{{ t('dialog.group.info.created_at') }}</span>
<span class="extra">{{ groupDialog.ref.createdAt | formatDate('long') }}</span>
<span class="extra">{{ formatDateFilter(groupDialog.ref.createdAt, 'long') }}</span>
</div>
</div>
<div class="x-friend-item" style="cursor: default">
@@ -667,7 +669,7 @@
<div class="detail">
<span class="name">{{ t('dialog.group.info.joined_at') }}</span>
<span class="extra">{{
groupDialog.ref.myMember.joinedAt | formatDate('long')
formatDateFilter(groupDialog.ref.myMember.joinedAt, 'long')
}}</span>
</div>
</div>
@@ -688,18 +690,18 @@
<br />
<span v-if="role.updatedAt"
>{{ t('dialog.group.info.role_updated_at') }}
{{ role.updatedAt | formatDate('long') }}</span
{{ formatDateFilter(role.updatedAt, 'long') }}</span
>
<span v-else
>{{ t('dialog.group.info.role_created_at') }}
{{ role.createdAt | formatDate('long') }}</span
{{ formatDateFilter(role.createdAt, 'long') }}</span
>
<br />
<span>{{ t('dialog.group.info.role_permissions') }}</span>
<br />
<template v-for="(permission, pIndex) in role.permissions">
<span :key="pIndex">{{ permission }}</span>
<br />
<br :key="pIndex + permission" />
</template>
</template>
<span
@@ -776,38 +778,40 @@
<span>{{ t('dialog.group.posts.visibility') }}</span>
<br />
<template v-for="roleId in post.roleIds">
<span
v-for="(role, rIndex) in groupDialog.ref.roles"
v-if="role.id === roleId"
:key="rIndex"
v-text="role.name" />
<span v-if="post.roleIds.indexOf(roleId) < post.roleIds.length - 1"
>,&nbsp;</span
<template v-for="role in groupDialog.ref.roles"
><span
v-if="role.id === roleId"
:key="role.id + roleId"
v-text="role.name" />
</template>
<template
v-if="post.roleIds.indexOf(roleId) < post.roleIds.length - 1"
><span :key="roleId">,&nbsp;</span></template
>
</template>
</template>
<i class="el-icon-view" style="margin-right: 5px" />
</el-tooltip>
<display-name :userid="post.authorId" style="margin-right: 5px" />
<DisplayName :userid="post.authorId" style="margin-right: 5px" />
<span v-if="post.editorId" style="margin-right: 5px"
>({{ t('dialog.group.posts.edited_by') }}
<display-name :userid="post.editorId" />)</span
<DisplayName :userid="post.editorId" />)</span
>
<el-tooltip placement="bottom">
<template slot="content">
<span
>{{ t('dialog.group.posts.created_at') }}
{{ post.createdAt | formatDate('long') }}</span
{{ formatDateFilter(post.createdAt, 'long') }}</span
>
<template v-if="post.updatedAt !== post.createdAt">
<br />
<span
>{{ t('dialog.group.posts.edited_at') }}
{{ post.updatedAt | formatDate('long') }}</span
{{ formatDateFilter(post.updatedAt, 'long') }}</span
>
</template>
</template>
<timer :epoch="Date.parse(post.updatedAt)" />
<Timer :epoch="Date.parse(post.updatedAt)" />
</el-tooltip>
<template
v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')">
@@ -885,7 +889,7 @@
@click.native.stop>
<el-button size="mini">
<span
>{{ groupDialog.memberSortOrder.name }}
>{{ t(groupDialog.memberSortOrder.name) }}
<i class="el-icon-arrow-down el-icon--right"
/></span>
</el-button>
@@ -894,7 +898,7 @@
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click.native="setGroupMemberSortOrder(item)"
v-text="item.name" />
v-text="t(item.name)" />
</el-dropdown-menu>
</el-dropdown>
<span style="margin-right: 5px">{{ t('dialog.group.members.filter') }}</span>
@@ -906,7 +910,7 @@
@click.native.stop>
<el-button size="mini">
<span
>{{ groupDialog.memberFilter.name }}
>{{ t(groupDialog.memberFilter.name) }}
<i class="el-icon-arrow-down el-icon--right"
/></span>
</el-button>
@@ -915,7 +919,7 @@
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
v-text="item.name" />
v-text="t(item.name)" />
<el-dropdown-item
v-for="item in groupDialog.ref.roles"
v-if="!item.defaultRole"
@@ -984,13 +988,13 @@
</el-tooltip>
</template>
<template v-for="roleId in user.roleIds">
<span
v-for="(role, rIndex) in groupDialog.ref.roles"
v-if="role.id === roleId"
:key="rIndex"
v-text="role.name" />
<span v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
>,&nbsp;</span
<template v-for="role in groupDialog.ref.roles"
><span
v-if="role.id === roleId"
:key="role.id + roleId"
v-text="role.name" /></template
><template v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
><span :key="roleId">,&nbsp;</span></template
>
</template>
</span>
@@ -1048,19 +1052,18 @@
</el-tooltip>
</template>
<template v-for="roleId in user.roleIds">
<span
v-for="(role, rIndex) in groupDialog.ref.roles"
v-if="role.id === roleId"
:key="rIndex"
v-text="role.name" />
<span v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
>,&nbsp;</span
<template v-for="role in groupDialog.ref.roles"
><span
v-if="role.id === roleId"
:key="roleId + role"
v-text="role.name" /></template
><template v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
><span :key="roleId">&nbsp;</span></template
>
</template>
</span>
</div>
</li>
<!--FIXME: div in ul-->
<div
v-if="!isGroupMembersDone"
v-loading="isGroupMembersLoading"
@@ -1150,108 +1153,75 @@
<!--Nested-->
<GroupPostEditDialog :dialog-data.sync="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
<GroupMemberModerationDialog
:group-dialog="groupDialog"
:is-group-members-loading.sync="isGroupMembersLoading"
:group-dialog-filter-options="groupDialogFilterOptions"
:group-dialog-sorting-options="groupDialogSortingOptions"
:random-user-colours="randomUserColours"
:group-member-moderation="groupMemberModeration"
@close-dialog="closeMemberModerationDialog"
@group-members-search="groupMembersSearch"
@load-all-group-members="loadAllGroupMembers"
@set-group-member-filter="setGroupMemberFilter"
@set-group-member-sort-order="setGroupMemberSortOrder" />
<InviteGroupDialog
:dialog-data.sync="inviteGroupDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:offline-friends="offlineFriends"
:active-friends="activeFriends" />
<InviteGroupDialog />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
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 { $app } from '../../../app';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
import {
adjustDialogZ,
buildTreeData,
copyToClipboard,
downloadAndSaveJson,
getFaviconUrl,
hasGroupPermission,
languageClass,
openExternalLink,
refreshInstancePlayerCount,
removeFromArray,
userImage,
userStatusClass,
formatDateFilter
} from '../../../shared/utils';
import {
useAppearanceSettingsStore,
useGalleryStore,
useGroupStore,
useLocationStore,
useUserStore
} from '../../../stores';
import InviteGroupDialog from '../InviteGroupDialog.vue';
import GroupMemberModerationDialog from './GroupMemberModerationDialog.vue';
import GroupPostEditDialog from './GroupPostEditDialog.vue';
const API = inject('API');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const showUserDialog = inject('showUserDialog');
const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage');
const openExternalLink = inject('openExternalLink');
const adjustDialogZ = inject('adjustDialogZ');
const { t } = useI18n();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore();
const { currentUser } = storeToRefs(useUserStore());
const { groupDialog, inviteGroupDialog } = storeToRefs(useGroupStore());
const {
getGroupDialogGroup,
updateGroupPostSearch,
showGroupDialog,
leaveGroupPrompt,
setGroupVisibility,
applyGroupMember,
handleGroupMember,
handleGroupMemberProps
} = useGroupStore();
const { lastLocation } = storeToRefs(useLocationStore());
const { showFullscreenImageDialog } = useGalleryStore();
const instance = getCurrentInstance();
const $confirm = instance.proxy.$confirm;
const $message = instance.proxy.$message;
const props = defineProps({
groupDialog: {
type: Object,
required: true
},
hideTooltips: {
type: Boolean,
default: false
},
lastLocation: {
type: Object,
required: true
},
updateInstanceInfo: {
type: Number,
required: true
},
groupDialogSortingOptions: {
type: Object,
required: true
},
groupDialogFilterOptions: {
type: Object,
required: true
},
randomUserColours: {
type: Boolean,
default: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
offlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
}
});
const emit = defineEmits([
'update:group-dialog',
'groupDialogCommand',
'getGroupDialogGroup',
'updateGroupPostSearch'
]);
const groupDialogRef = ref(null);
const isGroupMembersDone = ref(false);
const isGroupMembersLoading = ref(false);
@@ -1283,20 +1253,10 @@
auditLogTypes: []
});
const inviteGroupDialog = ref({
visible: false,
loading: false,
groupId: '',
groupName: '',
userId: '',
userIds: [],
userObject: {}
});
let loadMoreGroupMembersParams = {};
watch(
() => props.groupDialog.loading,
() => groupDialog.value.loading,
(val) => {
if (val) {
nextTick(() => adjustDialogZ(groupDialogRef.value.$el));
@@ -1305,7 +1265,7 @@
);
watch(
() => props.groupDialog.isGetGroupDialogGroupLoading,
() => groupDialog.value.isGetGroupDialogGroupLoading,
(val) => {
if (val) {
getCurrentTabData();
@@ -1314,14 +1274,9 @@
);
function showInviteGroupDialog(groupId, userId) {
const D = inviteGroupDialog.value;
D.userIds = '';
D.groups = [];
D.groupId = groupId;
D.groupName = groupId;
D.userId = userId;
D.userObject = {};
D.visible = true;
inviteGroupDialog.value.groupId = groupId;
inviteGroupDialog.value.userId = userId;
inviteGroupDialog.value.visible = true;
}
function setGroupRepresentation(groupId) {
@@ -1360,7 +1315,7 @@
}
function groupMembersSearchDebounce() {
const D = props.groupDialog;
const D = groupDialog.value;
const search = D.memberSearch;
D.memberSearchResults = [];
if (!search || search.length < 3) {
@@ -1375,16 +1330,14 @@
offset: 0
})
.then((args) => {
// API.$on('GROUP:MEMBERS:SEARCH', function (args) {
for (const json of args.json.results) {
API.$emit('GROUP:MEMBER', {
handleGroupMember({
json,
params: {
groupId: args.params.groupId
}
});
}
// });
if (D.id === args.params.groupId) {
D.memberSearchResults = args.json.results;
}
@@ -1400,11 +1353,10 @@
isRepresenting: isSet
})
.then((args) => {
// API.$on('GROUP:SETREPRESENTATION', function (args) {
if (props.groupDialog.visible && props.groupDialog.id === groupId) {
if (groupDialog.value.visible && groupDialog.value.id === args.groupId) {
updateGroupDialogData({
...props.groupDialog,
ref: { ...props.groupDialog.ref, isRepresenting: args.params.isRepresenting }
...groupDialog.value,
ref: { ...groupDialog.value.ref, isRepresenting: args.params.isRepresenting }
});
getGroupDialogGroup(groupId);
}
@@ -1417,11 +1369,9 @@
groupId: id
})
.then((args) => {
// API.$on('GROUP:CANCELJOINREQUEST', function (args) {
if (props.groupDialog.visible && props.groupDialog.id === id) {
if (groupDialog.value.visible && groupDialog.value.id === id) {
getGroupDialogGroup(id);
}
// });
});
}
function confirmDeleteGroupPost(post) {
@@ -1437,8 +1387,7 @@
postId: post.id
})
.then((args) => {
// API.$on('GROUP:POST:DELETE', function (args) {
const D = props.groupDialog;
const D = groupDialog.value;
if (D.id !== args.params.groupId) {
return;
}
@@ -1447,7 +1396,7 @@
// remove existing post
for (const item of D.posts) {
if (item.id === postId) {
utils.removeFromArray(D.posts, item);
removeFromArray(D.posts, item);
break;
}
}
@@ -1460,7 +1409,6 @@
}
}
updateGroupPostSearch();
// });
});
}
}
@@ -1480,30 +1428,113 @@
}
function groupDialogCommand(command) {
const D = props.groupDialog;
const D = groupDialog.value;
if (D.visible === false) {
return;
}
switch (command) {
case 'Share':
copyToClipboard(props.groupDialog.ref.$url);
copyToClipboard(groupDialog.value.ref.$url);
break;
case 'Create Post':
showGroupPostEditDialog(props.groupDialog.id, null);
showGroupPostEditDialog(groupDialog.value.id, null);
break;
case 'Moderation Tools':
showGroupMemberModerationDialog(props.groupDialog.id);
showGroupMemberModerationDialog(groupDialog.value.id);
break;
case 'Invite To Group':
showInviteGroupDialog(D.id, '');
break;
default:
emit('groupDialogCommand', command);
case 'Refresh':
showGroupDialog(D.id);
break;
case 'Leave Group':
leaveGroupPrompt(D.id);
break;
case 'Block Group':
blockGroup(D.id);
break;
case 'Unblock Group':
unblockGroup(D.id);
break;
case 'Visibility Everyone':
setGroupVisibility(D.id, 'visible');
break;
case 'Visibility Friends':
setGroupVisibility(D.id, 'friends');
break;
case 'Visibility Hidden':
setGroupVisibility(D.id, 'hidden');
break;
case 'Subscribe To Announcements':
setGroupSubscription(D.id, true);
break;
case 'Unsubscribe To Announcements':
setGroupSubscription(D.id, false);
break;
}
}
function setGroupSubscription(groupId, subscribe) {
return groupRequest
.setGroupMemberProps(currentUser.value.id, groupId, {
isSubscribedToAnnouncements: subscribe
})
.then((args) => {
handleGroupMemberProps(args);
$app.$message({
message: 'Group subscription updated',
type: 'success'
});
return args;
});
}
function blockGroup(groupId) {
$app.$confirm('Are you sure you want to block this group?', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
groupRequest
.blockGroup({
groupId
})
.then((args) => {
if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) {
showGroupDialog(args.params.groupId);
}
});
}
}
});
}
function unblockGroup(groupId) {
$app.$confirm('Are you sure you want to unblock this group?', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
groupRequest
.unblockGroup({
groupId,
userId: currentUser.value.id
})
.then((args) => {
if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) {
showGroupDialog(args.params.groupId);
}
});
}
}
});
}
function showGroupMemberModerationDialog(groupId) {
if (groupId !== props.groupDialog.id) {
if (groupId !== groupDialog.value.id) {
return;
}
const D = groupMemberModeration;
@@ -1511,16 +1542,14 @@
D.groupRef = {};
D.auditLogTypes = [];
API.getCachedGroup({ groupId }).then((args) => {
groupRequest.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
// API.$on('GROUP:AUDITLOGTYPES', function (args) {
if (groupMemberModeration.id !== args.params.groupId) {
return;
}
groupMemberModeration.auditLogTypes = args.json;
// });
});
}
});
@@ -1535,16 +1564,14 @@
groupId: id
})
.then((args) => {
// API.$on('GROUP:JOIN', function (args) {
if (props.groupDialog.visible && props.groupDialog.id === id) {
if (groupDialog.value.visible && groupDialog.value.id === id) {
updateGroupDialogData({
...props.groupDialog,
...groupDialog.value,
inGroup: args.json.membershipStatus === 'member'
});
// props.groupDialog.inGroup = args.json.membershipStatus === 'member';
// groupDialog.value.inGroup = json.membershipStatus === 'member';
getGroupDialogGroup(id);
}
// });
if (args.json.membershipStatus === 'member') {
$message({
message: 'Group joined',
@@ -1595,14 +1622,14 @@
selectedImageUrl: post.imageUrl
};
}
API.getCachedGroup({ groupId }).then((args) => {
groupRequest.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
});
D.visible = true;
}
async function getGroupDialogGroupMembers() {
const D = props.groupDialog;
const D = groupDialog.value;
D.members = [];
isGroupMembersDone.value = false;
loadMoreGroupMembersParams = {
@@ -1620,12 +1647,12 @@
await groupRequest
.getGroupMember({
groupId: D.id,
userId: API.currentUser.id
userId: currentUser.value.id
})
.then((args) => {
args.ref = API.applyGroupMember(args.json);
args.ref = applyGroupMember(args.json);
if (args.json) {
args.json.user = API.currentUser;
args.json.user = currentUser.value;
if (D.memberFilter.id === null) {
// when flitered by role don't include self
D.members.push(args.json);
@@ -1641,7 +1668,7 @@
if (isGroupMembersDone.value || isGroupMembersLoading.value) {
return;
}
const D = props.groupDialog;
const D = groupDialog.value;
const params = loadMoreGroupMembersParams;
D.memberSearch = '';
isGroupMembersLoading.value = true;
@@ -1651,10 +1678,18 @@
isGroupMembersLoading.value = false;
})
.then((args) => {
for (const json of args.json) {
handleGroupMember({
json,
params: {
groupId: args.params.groupId
}
});
}
for (let i = 0; i < args.json.length; i++) {
const member = args.json[i];
if (member.userId === API.currentUser.id) {
if (D.members.length > 0 && D.members[0].userId === API.currentUser.id) {
if (member.userId === currentUser.value.id) {
if (D.members.length > 0 && D.members[0].userId === currentUser.value.id) {
// remove duplicate and keep sort order
D.members.splice(0, 1);
}
@@ -1685,12 +1720,12 @@
}
async function getGroupGalleries() {
updateGroupDialogData({ ...props.groupDialog, galleries: {} });
updateGroupDialogData({ ...groupDialog.value, galleries: {} });
groupDialogGalleryCurrentName.value = '0';
isGroupGalleryLoading.value = true;
for (let i = 0; i < props.groupDialog.ref.galleries.length; i++) {
const gallery = props.groupDialog.ref.galleries[i];
await getGroupGallery(props.groupDialog.id, gallery.id);
for (let i = 0; i < groupDialog.value.ref.galleries.length; i++) {
const gallery = groupDialog.value.ref.galleries[i];
await getGroupGallery(groupDialog.value.id, gallery.id);
}
isGroupGalleryLoading.value = false;
}
@@ -1707,16 +1742,14 @@
for (let i = 0; i < count; i++) {
const args = await groupRequest.getGroupGallery(params);
if (args) {
// API.$on('GROUP:GALLERY', function (args) {
for (const json of args.json) {
if (props.groupDialog.id === json.groupId) {
if (!props.groupDialog.galleries[json.galleryId]) {
props.groupDialog.galleries[json.galleryId] = [];
if (groupDialog.value.id === json.groupId) {
if (!groupDialog.value.galleries[json.galleryId]) {
groupDialog.value.galleries[json.galleryId] = [];
}
props.groupDialog.galleries[json.galleryId].push(json);
groupDialog.value.galleries[json.galleryId].push(json);
}
}
// });
}
params.offset += 100;
if (args.json.length < 100) {
@@ -1729,8 +1762,8 @@
}
function refreshGroupDialogTreeData() {
const D = props.groupDialog;
const treeData = utils.buildTreeData({
const D = groupDialog.value;
const treeData = buildTreeData({
group: D.ref,
posts: D.posts,
instances: D.instances,
@@ -1738,7 +1771,7 @@
galleries: D.galleries
});
updateGroupDialogData({
...props.groupDialog,
...groupDialog.value,
treeData
});
}
@@ -1748,19 +1781,19 @@
return;
}
await getGroupDialogGroupMembers();
while (props.groupDialog.visible && !isGroupMembersDone.value) {
while (groupDialog.value.visible && !isGroupMembersDone.value) {
isGroupMembersLoading.value = true;
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 1000);
});
isGroupMembersLoading.value = false;
await this.loadMoreGroupMembers();
await loadMoreGroupMembers();
}
}
async function setGroupMemberSortOrder(sortOrder) {
const D = props.groupDialog;
if (D.memberSortOrder === sortOrder) {
const D = groupDialog.value;
if (D.memberSortOrder.value === sortOrder) {
return;
}
D.memberSortOrder = sortOrder;
@@ -1768,8 +1801,8 @@
}
async function setGroupMemberFilter(filter) {
const D = props.groupDialog;
if (D.memberFilter === filter) {
const D = groupDialog.value;
if (D.memberFilter.value === filter) {
return;
}
D.memberFilter = filter;
@@ -1777,13 +1810,9 @@
}
function updateGroupDialogData(obj) {
// Be careful with the deep merge
emit('update:group-dialog', obj);
}
function getGroupDialogGroup(groupId) {
emit('getGroupDialogGroup', groupId);
}
function updateGroupPostSearch() {
emit('updateGroupPostSearch');
groupDialog.value = {
...groupDialog.value,
...obj
};
}
</script>

View File

@@ -18,7 +18,7 @@
icon="el-icon-refresh"
:loading="isGroupMembersLoading"
circle
@click="loadAllGroupMembers"></el-button>
@click="loadAllGroupMembers" />
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">
{{ groupMemberModerationTable.data.length }}/{{
groupMemberModeration.groupRef.memberCount
@@ -40,7 +40,7 @@
@click.native.stop>
<el-button size="mini">
<span
>{{ groupDialog.memberSortOrder.name }}
>{{ t(groupDialog.memberSortOrder.name) }}
<i class="el-icon-arrow-down el-icon--right"></i
></span>
</el-button>
@@ -49,7 +49,7 @@
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click.native="setGroupMemberSortOrder(item)">
{{ item.name }}
{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@@ -68,7 +68,7 @@
@click.native.stop>
<el-button size="mini">
<span
>{{ groupDialog.memberFilter.name }}
>{{ t(groupDialog.memberFilter.name) }}
<i class="el-icon-arrow-down el-icon--right"></i
></span>
</el-button>
@@ -77,7 +77,7 @@
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
v-text="item.name"></el-dropdown-item>
v-text="t(item.name)"></el-dropdown-item>
<el-dropdown-item
v-for="item in groupDialog.ref.roles"
v-if="!item.defaultRole"
@@ -147,12 +147,11 @@
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
<template slot-scope="scope">
<template v-for="(roleId, index) in scope.row.roleIds">
<span
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
v-if="role?.id === roleId"
:key="rIndex"
>{{ role.name
}}<span v-if="index < scope.row.roleIds.length - 1">, </span></span
<template v-for="(role, rIndex) in groupMemberModeration.groupRef.roles">
<span v-if="role?.id === roleId" :key="roleId + rIndex"
>{{ role.name
}}<span v-if="index < scope.row.roleIds.length - 1">, </span></span
></template
>
</template>
</template>
@@ -171,7 +170,7 @@
prop="joinedAt"
sortable>
<template slot-scope="scope">
<span>{{ scope.row.joinedAt | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.joinedAt, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -284,7 +283,7 @@
prop="joinedAt"
sortable>
<template slot-scope="scope">
<span>{{ scope.row.joinedAt | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.joinedAt, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -293,7 +292,7 @@
prop="bannedAt"
sortable>
<template slot-scope="scope">
<span>{{ scope.row.bannedAt | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.bannedAt, 'long') }}</span>
</template>
</el-table-column>
</data-tables>
@@ -629,7 +628,7 @@
prop="created_at"
sortable>
<template slot-scope="scope">
<span>{{ scope.row.created_at | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -656,7 +655,7 @@
:label="t('dialog.group_member_moderation.description')"
prop="description">
<template slot-scope="scope">
<location
<Location
v-if="scope.row?.targetId.startsWith('wrld_')"
:location="scope.row.targetId" />
<span v-text="scope.row.description"></span>
@@ -810,45 +809,165 @@
</template>
<script setup>
import Location from '../../Location.vue';
import { getCurrentInstance, inject, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { groupRequest, userRequest } from '../../../api';
import { useModerationTable, useSelectedUsers } from '../../../composables/group/useGroupMemberModeration';
import { hasGroupPermission } from '../../../composables/group/utils';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
import { hasGroupPermission, userImage, userImageFull, formatDateFilter } from '../../../shared/utils';
import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores';
import GroupMemberModerationExportDialog from './GroupMemberModerationExportDialog.vue';
const API = inject('API');
const showUserDialog = inject('showUserDialog');
const userImage = inject('userImage');
const userImageFull = inject('userImageFull');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore();
const { currentUser } = storeToRefs(useUserStore());
const { groupDialog } = storeToRefs(useGroupStore());
const { applyGroupMember, handleGroupMemberProps } = useGroupStore();
const { showFullscreenImageDialog } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const selectedUsers = reactive({});
const selectedUsersArray = ref([]);
function setSelectedUsers(usersId, user) {
if (!user) {
return;
}
selectedUsers[usersId] = user;
selectedUsersArray.value = Object.values(selectedUsers);
}
function deselectedUsers(userId, isAll = false) {
if (isAll) {
for (const id in selectedUsers) {
if (Object.prototype.hasOwnProperty.call(selectedUsers, id)) {
delete selectedUsers[id];
}
}
} else {
if (Object.prototype.hasOwnProperty.call(selectedUsers, userId)) {
delete selectedUsers[userId];
}
}
selectedUsersArray.value = Object.values(selectedUsers);
}
function groupMemberModerationTableSelectionChange(row) {
if (row.$selected && !selectedUsers[row.userId]) {
setSelectedUsers(row.userId, row);
} else if (!row.$selected && selectedUsers[row.userId]) {
deselectedUsers(row.userId);
}
}
const groupInvitesModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
const groupJoinRequestsModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
const groupBlockedModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
const groupLogsModerationTable = reactive({
data: [],
filters: [{ prop: ['description'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
const groupBansModerationTable = reactive({
data: [],
filters: [{ prop: ['$displayName'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
const groupMemberModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
});
async function initializePageSize() {
try {
const { tablePageSize } = storeToRefs(useAppearanceSettingsStore());
groupMemberModerationTable.pageSize = tablePageSize.value;
groupBansModerationTable.pageSize = tablePageSize.value;
groupLogsModerationTable.pageSize = tablePageSize.value;
groupInvitesModerationTable.pageSize = tablePageSize.value;
groupJoinRequestsModerationTable.pageSize = tablePageSize.value;
groupBlockedModerationTable.pageSize = tablePageSize.value;
} catch (error) {
console.error('Failed to initialize table page size:', error);
}
}
function deselectGroupMember(userId) {
const deselectInTable = (tableData) => {
if (userId) {
const row = tableData.find((item) => item.userId === userId);
if (row) {
row.$selected = false;
}
} else {
tableData.forEach((row) => {
if (row.$selected) {
row.$selected = false;
}
});
}
};
deselectInTable(groupMemberModerationTable.data);
deselectInTable(groupBansModerationTable.data);
deselectInTable(groupInvitesModerationTable.data);
deselectInTable(groupJoinRequestsModerationTable.data);
deselectInTable(groupBlockedModerationTable.data);
}
const props = defineProps({
isGroupMembersLoading: {
type: Boolean,
default: false
},
groupDialog: {
type: Object,
required: true
},
groupDialogSortingOptions: {
type: Object,
required: true
},
groupDialogFilterOptions: {
type: Object,
required: true
},
randomUserColours: {
type: Boolean,
default: false
},
groupMemberModeration: {
type: Object,
required: true
@@ -864,25 +983,6 @@
'group-members-search'
]);
const {
groupInvitesModerationTable,
groupJoinRequestsModerationTable,
groupBlockedModerationTable,
groupLogsModerationTable,
groupBansModerationTable,
groupMemberModerationTable,
initializePageSize,
deselectGroupMember
} = useModerationTable();
const {
selectedUsers,
selectedUsersArray,
groupMemberModerationTableSelectionChange,
deselectedUsers,
setSelectedUsers
} = useSelectedUsers();
const selectUserId = ref('');
const progressCurrent = ref(0);
const progressTotal = ref(0);
@@ -895,7 +995,7 @@
() => props.groupMemberModeration.visible,
(newVal) => {
if (newVal) {
if (props.groupMemberModeration.id !== props.groupDialog.id) {
if (props.groupMemberModeration.id !== groupDialog.value.id) {
return;
}
groupMemberModerationTable.data = [];
@@ -905,6 +1005,7 @@
groupBlockedModerationTable.data = [];
groupLogsModerationTable.data = [];
Object.assign(selectedUsers, {});
selectedUsersArray.value = [];
selectUserId.value = '';
selectedRoles.value = [];
note.value = '';
@@ -913,7 +1014,7 @@
);
watch(
() => props.groupDialog.members,
() => groupDialog.value.members,
(newVal) => {
if (newVal) {
setGroupMemberModerationTable(newVal);
@@ -923,7 +1024,7 @@
);
watch(
() => props.groupDialog.memberSearchResults,
() => groupDialog.value.memberSearchResults,
(newVal) => {
if (newVal) {
setGroupMemberModerationTable(newVal);
@@ -965,9 +1066,8 @@
}
function handleGroupMemberRoleChange(args) {
// 'GROUP:MEMBER:ROLE:CHANGE'
if (props.groupDialog.id === args.params.groupId) {
props.groupDialog.members.forEach((member) => {
if (groupDialog.value.id === args.params.groupId) {
groupDialog.value.members.forEach((member) => {
if (member.userId === args.params.userId) {
member.roleIds = args.json;
return true;
@@ -988,7 +1088,7 @@
}
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) {
if (user.userId === currentUser.value.id) {
continue;
}
console.log(`Deleting group invite ${user.userId} ${i + 1}/${memberCount}`);
@@ -1014,7 +1114,7 @@
}
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupInvites(D.id, groupInvitesModerationTable);
getAllGroupInvites(D.id);
}
function selectAllGroupMembers() {
@@ -1038,7 +1138,7 @@
continue;
}
args.json.forEach((json) => {
const ref = API.applyGroupMember(json);
const ref = applyGroupMember(json);
fetchedBans.push(ref);
});
if (args.json.length < params.n) {
@@ -1076,7 +1176,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Banning ${user.userId} ${i + 1}/${memberCount}`);
try {
await groupRequest.banGroupMember({ groupId: D.id, userId: user.userId });
@@ -1105,7 +1205,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Unbanning ${user.userId} ${i + 1}/${memberCount}`);
try {
await groupRequest.unbanGroupMember({ groupId: D.id, userId: user.userId });
@@ -1138,7 +1238,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Kicking ${user.userId} ${i + 1}/${memberCount}`);
try {
@@ -1158,6 +1258,7 @@
progressCurrent.value = 0;
progressTotal.value = 0;
deselectedUsers(null, true);
loadAllGroupMembers();
}
async function groupMembersSaveNote() {
@@ -1176,7 +1277,8 @@
}
console.log(`Setting note ${noteToSave} for ${user.userId} ${i + 1}/${memberCount}`);
try {
await groupRequest.setGroupMemberProps(user.userId, D.id, { managerNotes: noteToSave });
const args = await groupRequest.setGroupMemberProps(user.userId, D.id, { managerNotes: noteToSave });
handleGroupMemberProps(args);
} catch (err) {
console.error(err);
$message({
@@ -1334,7 +1436,7 @@
let member = {};
const memberArgs = await groupRequest.getGroupMember({ groupId: D.id, userId });
if (memberArgs && memberArgs.json) {
member = API.applyGroupMember(memberArgs.json);
member = applyGroupMember(memberArgs.json);
}
if (member && member.user) {
setSelectedUsers(member.userId, member);
@@ -1400,7 +1502,7 @@
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) {
if (user.userId === currentUser.value.id) {
continue;
}
@@ -1424,7 +1526,7 @@
}
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupInvitesAndJoinRequests();
getAllGroupInvitesAndJoinRequests(D.id);
deselectedUsers(null, true);
}
@@ -1438,7 +1540,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Blocking group join request from ${user.userId} ${i + 1}/${memberCount}`);
try {
@@ -1460,7 +1562,7 @@
}
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupInvitesAndJoinRequests();
getAllGroupInvitesAndJoinRequests(D.id);
deselectedUsers(null, true);
}
@@ -1475,7 +1577,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Rejecting group join request from ${user.userId} ${i + 1}/${memberCount}`);
try {
@@ -1497,7 +1599,7 @@
}
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupInvitesAndJoinRequests();
getAllGroupInvitesAndJoinRequests(D.id);
deselectedUsers(null, true);
}
@@ -1511,7 +1613,7 @@
if (!progressTotal.value) break;
const user = users[i];
progressCurrent.value = i + 1;
if (user.userId === API.currentUser.id) continue;
if (user.userId === currentUser.value.id) continue;
console.log(`Accepting group join request from ${user.userId} ${i + 1}/${memberCount}`);
try {
@@ -1533,7 +1635,7 @@
}
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupInvitesAndJoinRequests();
getAllGroupInvitesAndJoinRequests(D.id);
deselectedUsers(null, true);
}
@@ -1565,7 +1667,7 @@
? groupBlockedModerationTable
: groupJoinRequestsModerationTable;
for (const json of args.json) {
const ref = API.applyGroupMember(json);
const ref = applyGroupMember(json);
targetTable.data.push(ref);
}
params.offset += params.n;
@@ -1598,7 +1700,7 @@
? groupBlockedModerationTable
: groupJoinRequestsModerationTable;
for (const json of args.json) {
const ref = API.applyGroupMember(json);
const ref = applyGroupMember(json);
targetTable.data.push(ref);
}
params.offset += params.n;
@@ -1631,7 +1733,7 @@
}
for (const json of args.json) {
const ref = API.applyGroupMember(json);
const ref = applyGroupMember(json);
groupInvitesModerationTable.data.push(ref);
}
}
@@ -1649,7 +1751,7 @@
type: 'error'
});
} finally {
updateIsGroupMembersLoading(false); // Use emit
updateIsGroupMembersLoading(false);
}
}

View File

@@ -32,7 +32,7 @@
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { copyToClipboard } from '../../../composables/shared/utils';
import { copyToClipboard } from '../../../shared/utils';
const { t } = useI18n();

View File

@@ -1,16 +1,16 @@
<template>
<safe-dialog
:visible.sync="groupPostEditDialog.visible"
:title="$t('dialog.group_post_edit.header')"
:title="t('dialog.group_post_edit.header')"
width="650px"
append-to-body>
<div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
<el-form-item :label="$t('dialog.group_post_edit.title')">
<el-form-item :label="t('dialog.group_post_edit.title')">
<el-input v-model="groupPostEditDialog.title" size="mini"></el-input>
</el-form-item>
<el-form-item :label="$t('dialog.group_post_edit.message')">
<el-form-item :label="t('dialog.group_post_edit.message')">
<el-input
v-model="groupPostEditDialog.text"
type="textarea"
@@ -24,29 +24,27 @@
v-if="!groupPostEditDialog.postId"
v-model="groupPostEditDialog.sendNotification"
size="small">
{{ $t('dialog.group_post_edit.send_notification') }}
{{ t('dialog.group_post_edit.send_notification') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('dialog.group_post_edit.post_visibility')">
<el-form-item :label="t('dialog.group_post_edit.post_visibility')">
<el-radio-group v-model="groupPostEditDialog.visibility" size="small">
<el-radio label="public">
{{ $t('dialog.group_post_edit.visibility_public') }}
{{ t('dialog.group_post_edit.visibility_public') }}
</el-radio>
<el-radio label="group">
{{ $t('dialog.group_post_edit.visibility_group') }}
{{ t('dialog.group_post_edit.visibility_group') }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="groupPostEditDialog.visibility === 'group'"
:label="$t('dialog.new_instance.roles')">
<el-form-item v-if="groupPostEditDialog.visibility === 'group'" :label="t('dialog.new_instance.roles')">
<el-select
v-model="groupPostEditDialog.roleIds"
multiple
clearable
:placeholder="$t('dialog.new_instance.role_placeholder')"
:placeholder="t('dialog.new_instance.role_placeholder')"
style="width: 100%">
<el-option-group :label="$t('dialog.new_instance.role_placeholder')">
<el-option-group :label="t('dialog.new_instance.role_placeholder')">
<el-option
v-for="role in groupPostEditDialog.groupRef?.roles"
:key="role.id"
@@ -60,7 +58,7 @@
</el-option-group>
</el-select>
</el-form-item>
<el-form-item :label="$t('dialog.group_post_edit.image')">
<el-form-item :label="t('dialog.group_post_edit.image')">
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<el-popover placement="right" width="500px" trigger="click">
@@ -80,13 +78,13 @@
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />
</el-popover>
<el-button size="mini" style="vertical-align: top" @click="clearImageGallerySelect">
{{ $t('dialog.invite_message.clear_selected_image') }}
{{ t('dialog.invite_message.clear_selected_image') }}
</el-button>
</div>
</template>
<template v-else>
<el-button size="mini" style="margin-right: 5px" @click="showGallerySelectDialog">
{{ $t('dialog.invite_message.select_image') }}
{{ t('dialog.invite_message.select_image') }}
</el-button>
</template>
</el-form-item>
@@ -94,13 +92,13 @@
</div>
<template #footer>
<el-button size="small" @click="groupPostEditDialog.visible = false">
{{ $t('dialog.group_post_edit.cancel') }}
{{ t('dialog.group_post_edit.cancel') }}
</el-button>
<el-button v-if="groupPostEditDialog.postId" size="small" @click="editGroupPost">
{{ $t('dialog.group_post_edit.edit_post') }}
{{ t('dialog.group_post_edit.edit_post') }}
</el-button>
<el-button v-else size="small" @click="createGroupPost">
{{ $t('dialog.group_post_edit.create_post') }}
{{ t('dialog.group_post_edit.create_post') }}
</el-button>
</template>
<GallerySelectDialog
@@ -110,114 +108,115 @@
</safe-dialog>
</template>
<script>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { groupRequest, vrcPlusIconRequest } from '../../../api';
import { useGalleryStore, useGroupStore } from '../../../stores';
import GallerySelectDialog from './GallerySelectDialog.vue';
export default {
name: 'GroupPostEditDialog',
components: {
GallerySelectDialog
const props = defineProps({
dialogData: {
type: Object,
required: true
},
inject: ['showFullscreenImageDialog'],
props: {
dialogData: {
type: Object,
required: true
},
selectedGalleryFile: { type: Object, default: () => ({}) }
selectedGalleryFile: { type: Object, default: () => ({}) }
});
const emit = defineEmits(['update:dialogData']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { showFullscreenImageDialog, handleFilesList } = useGalleryStore();
const { handleGroupPost } = useGroupStore();
const gallerySelectDialog = ref({
visible: false,
selectedFileId: '',
selectedImageUrl: ''
});
const galleryTable = ref([]);
const groupPostEditDialog = computed({
get() {
return props.dialogData;
},
data() {
return {
gallerySelectDialog: {
visible: false,
selectedFileId: '',
selectedImageUrl: ''
},
galleryTable: []
};
},
computed: {
groupPostEditDialog: {
get() {
return this.dialogData;
},
set(value) {
this.$emit('update:dialog-data', value);
}
}
},
methods: {
showGallerySelectDialog() {
const D = this.gallerySelectDialog;
D.visible = true;
this.refreshGalleryTable();
},
async refreshGalleryTable() {
const params = {
n: 100,
tag: 'gallery'
};
const args = await vrcPlusIconRequest.getFileList(params);
// API.$on('FILES:LIST')
if (args.params.tag === 'gallery') {
this.galleryTable = args.json.reverse();
}
},
editGroupPost() {
const D = this.groupPostEditDialog;
if (!D.groupId || !D.postId) {
return;
}
const params = {
groupId: D.groupId,
postId: D.postId,
title: D.title,
text: D.text,
roleIds: D.roleIds,
visibility: D.visibility,
imageId: null
};
if (this.gallerySelectDialog.selectedFileId) {
params.imageId = this.gallerySelectDialog.selectedFileId;
}
groupRequest.editGroupPost(params).then((args) => {
this.$message({
message: 'Group post edited',
type: 'success'
});
return args;
});
D.visible = false;
},
createGroupPost() {
const D = this.groupPostEditDialog;
const params = {
groupId: D.groupId,
title: D.title,
text: D.text,
roleIds: D.roleIds,
visibility: D.visibility,
sendNotification: D.sendNotification,
imageId: null
};
if (this.gallerySelectDialog.selectedFileId) {
params.imageId = this.gallerySelectDialog.selectedFileId;
}
groupRequest.createGroupPost(params).then((args) => {
this.$message({
message: 'Group post created',
type: 'success'
});
return args;
});
D.visible = false;
},
clearImageGallerySelect() {
const D = this.gallerySelectDialog;
D.selectedFileId = '';
D.selectedImageUrl = '';
}
set(value) {
emit('update:dialogData', value);
}
};
});
function showGallerySelectDialog() {
const D = gallerySelectDialog.value;
D.visible = true;
refreshGalleryTable();
}
async function refreshGalleryTable() {
const params = {
n: 100,
tag: 'gallery'
};
const args = await vrcPlusIconRequest.getFileList(params);
handleFilesList(args);
if (args.params.tag === 'gallery') {
galleryTable.value = args.json.reverse();
}
}
function editGroupPost() {
const D = groupPostEditDialog.value;
if (!D.groupId || !D.postId) {
return;
}
const params = {
groupId: D.groupId,
postId: D.postId,
title: D.title,
text: D.text,
roleIds: D.roleIds,
visibility: D.visibility,
imageId: null
};
if (gallerySelectDialog.value.selectedFileId) {
params.imageId = gallerySelectDialog.value.selectedFileId;
}
groupRequest.editGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
message: 'Group post edited',
type: 'success'
});
return args;
});
D.visible = false;
}
function createGroupPost() {
const D = groupPostEditDialog.value;
const params = {
groupId: D.groupId,
title: D.title,
text: D.text,
roleIds: D.roleIds,
visibility: D.visibility,
sendNotification: D.sendNotification,
imageId: null
};
if (gallerySelectDialog.value.selectedFileId) {
params.imageId = gallerySelectDialog.value.selectedFileId;
}
groupRequest.createGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
message: 'Group post created',
type: 'success'
});
return args;
});
D.visible = false;
}
function clearImageGallerySelect() {
const D = gallerySelectDialog.value;
D.selectedFileId = '';
D.selectedImageUrl = '';
}
</script>

View File

@@ -32,17 +32,20 @@
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const clearInviteImageUpload = inject('clearInviteImageUpload');
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
editAndSendInviteDialog: {
@@ -57,9 +60,6 @@
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
@@ -85,7 +85,6 @@
throw err;
})
.then((args) => {
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
if (args.json[slot].message === I.messageSlot.message) {
$message({
message: "VRChat API didn't update message, try again",
@@ -103,7 +102,7 @@
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
if (receiverUserId === currentUser.value.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
@@ -112,7 +111,7 @@
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
} else if (uploadImage.value) {
notificationRequest
.sendInvitePhoto(
{
@@ -149,7 +148,7 @@
inviteLoop();
} else if (messageType === 'invite') {
I.params.messageSlot = slot;
if (props.uploadImage) {
if (uploadImage.value) {
notificationRequest
.sendInvitePhoto(I.params, I.userId)
.catch((err) => {
@@ -178,7 +177,7 @@
}
} else if (messageType === 'request') {
I.params.requestSlot = slot;
if (props.uploadImage) {
if (uploadImage.value) {
notificationRequest
.sendRequestInvitePhoto(I.params, I.userId)
.catch((err) => {

View File

@@ -6,7 +6,7 @@
width="500px"
append-to-body>
<div v-if="inviteDialog.visible" v-loading="inviteDialog.loading">
<location :location="inviteDialog.worldId" :link="false"></location>
<Location :location="inviteDialog.worldId" :link="false" />
<br />
<el-button size="mini" style="margin-top: 10px" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
@@ -34,17 +34,17 @@
filterable
:disabled="inviteDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="API.currentUser" :label="t('side_panel.me')">
<el-option-group v-if="currentUser" :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="API.currentUser.displayName"
:value="API.currentUser.id"
:label="currentUser.displayName"
:value="currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(API.currentUser)]">
<img v-lazy="userImage(API.currentUser)" />
<div :class="['avatar', userStatusClass(currentUser)]">
<img v-lazy="userImage(currentUser)" />
</div>
<div class="detail">
<span class="name">{{ API.currentUser.displayName }}</span>
<span class="name">{{ currentUser.displayName }}</span>
</div>
</el-option>
</el-option-group>
@@ -156,57 +156,35 @@
</template>
<SendInviteDialog
:send-invite-dialog-visible.sync="sendInviteDialogVisible"
:invite-message-table="inviteMessageTable"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, 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 { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation, userImage, userStatusClass } from '../../../shared/utils';
import { useFriendStore, useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import SendInviteDialog from './SendInviteDialog.vue';
const { vipFriends, onlineFriends, activeFriends } = storeToRefs(useFriendStore());
const { refreshInviteMessageTableData } = useInviteStore();
const { currentUser } = storeToRefs(useUserStore());
const { clearInviteImageUpload } = useGalleryStore();
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 clearInviteImageUpload = inject('clearInviteImageUpload');
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: ''
}
});
@@ -229,15 +207,15 @@
userId,
messageSlot: {}
};
inviteMessagesRequest.refreshInviteMessageTableData('message');
refreshInviteMessageTableData('message');
clearInviteImageUpload();
sendInviteDialogVisible.value = true;
}
function addSelfToInvite() {
const D = props.inviteDialog;
if (!D.userIds.includes(API.currentUser.id)) {
D.userIds.push(API.currentUser.id);
if (!D.userIds.includes(currentUser.value.id)) {
D.userIds.push(currentUser.value.id);
}
}
@@ -273,7 +251,7 @@
const inviteLoop = () => {
if (D.userIds.length > 0) {
const receiverUserId = D.userIds.shift();
if (receiverUserId === API.currentUser.id) {
if (receiverUserId === currentUser.value.id) {
// can't invite self!?
const L = parseLocation(D.worldId);
instanceRequest

View File

@@ -22,18 +22,21 @@
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const clearInviteImageUpload = inject('clearInviteImageUpload');
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
visible: {
@@ -48,9 +51,6 @@
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
@@ -69,7 +69,7 @@
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
if (receiverUserId === currentUser.value.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
@@ -78,7 +78,7 @@
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
} else if (uploadImage.value) {
notificationRequest
.sendInvitePhoto(
{
@@ -115,7 +115,7 @@
inviteLoop();
} else if (messageType === 'invite') {
D.params.messageSlot = slot;
if (props.uploadImage) {
if (uploadImage.value) {
notificationRequest
.sendInvitePhoto(D.params, D.userId)
.catch((err) => {
@@ -144,7 +144,7 @@
}
} else if (messageType === 'request') {
D.params.requestSlot = slot;
if (props.uploadImage) {
if (uploadImage.value) {
notificationRequest
.sendRequestInvitePhoto(D.params, D.userId)
.catch((err) => {

View File

@@ -6,7 +6,7 @@
width="800px"
append-to-body
@close="cancelSendInvite">
<template v-if="API.currentUser.$isVRCPlus">
<template v-if="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">-->
@@ -70,7 +70,7 @@
<el-button type="small" @click="cancelSendInvite">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('message')">
<el-button type="small" @click="refreshInviteMessageTableData('message')">
{{ t('dialog.invite_message.refresh') }}
</el-button>
</template>
@@ -78,37 +78,35 @@
: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 { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const inviteImageUpload = inject('inviteImageUpload');
const { refreshInviteMessageTableData } = useInviteStore();
const { inviteMessageTable } = storeToRefs(useInviteStore());
const { inviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
sendInviteDialogVisible: {
type: Boolean,
default: false
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
required: true
@@ -117,10 +115,6 @@
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});

View File

@@ -1,6 +1,6 @@
<template>
<safe-dialog
ref="inviteGroupDialog"
ref="inviteGroupDialogRef"
:visible.sync="inviteGroupDialog.visible"
:title="$t('dialog.invite_to_group.header')"
width="450px"
@@ -17,11 +17,11 @@
style="margin-top: 15px"
@change="isAllowedToInviteToGroup">
<el-option-group
v-if="API.currentUserGroups.size"
v-if="currentUserGroups.size"
:label="$t('dialog.invite_to_group.groups')"
style="width: 410px">
<el-option
v-for="group in API.currentUserGroups.values()"
v-for="group in currentUserGroups.values()"
:key="group.id"
:label="group.name"
:value="group.id"
@@ -166,132 +166,111 @@
</safe-dialog>
</template>
<script>
<script setup>
import { ref, watch, getCurrentInstance, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { groupRequest, userRequest } from '../../api';
import { hasGroupPermission } from '../../composables/group/utils';
import { adjustDialogZ, hasGroupPermission, userImage, userStatusClass } from '../../shared/utils';
import { useFriendStore, useGroupStore } from '../../stores';
export default {
name: 'InviteGroupDialog',
inject: ['API', 'userStatusClass', 'userImage', 'adjustDialogZ'],
props: {
dialogData: {
type: Object,
required: true
},
vipFriends: {
type: Array,
required: true
},
onlineFriends: {
type: Array,
required: true
},
activeFriends: {
type: Array,
required: true
},
offlineFriends: {
type: Array,
required: true
}
},
computed: {
inviteGroupDialog: {
get() {
return this.dialogData;
},
set(value) {
this.$emit('update:dialog-data', value);
}
}
},
watch: {
'dialogData.visible'(value) {
if (value) {
this.initDialog();
}
}
},
methods: {
initDialog() {
this.$nextTick(() => this.adjustDialogZ(this.$refs.inviteGroupDialog.$el));
const D = this.inviteGroupDialog;
if (D.groupId) {
this.API.getCachedGroup({
groupId: D.groupId
})
.then((args) => {
D.groupName = args.ref.name;
})
.catch(() => {
D.groupId = '';
});
this.isAllowedToInviteToGroup();
}
const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { currentUserGroups, inviteGroupDialog } = storeToRefs(useGroupStore());
const { applyGroup } = useGroupStore();
if (D.userId) {
userRequest.getCachedUser({ userId: D.userId }).then((args) => {
D.userObject = args.ref;
D.userIds = [D.userId];
});
}
},
isAllowedToInviteToGroup() {
const D = this.inviteGroupDialog;
const groupId = D.groupId;
if (!groupId) {
return;
}
this.inviteGroupDialog.loading = true;
groupRequest
.getGroup({ groupId })
.then((args) => {
if (hasGroupPermission(args.ref, 'group-invites-manage')) {
return args;
}
// not allowed to invite
this.inviteGroupDialog.groupId = '';
this.$message({
type: 'error',
message: 'You are not allowed to invite to this group'
});
return args;
})
.finally(() => {
this.inviteGroupDialog.loading = false;
});
},
sendGroupInvite() {
this.$confirm('Continue? Invite User(s) To Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
const D = this.inviteGroupDialog;
if (action !== 'confirm' || D.loading === true) {
return;
}
D.loading = true;
const inviteLoop = () => {
if (D.userIds.length === 0) {
D.loading = false;
return;
}
const receiverUserId = D.userIds.shift();
groupRequest
.sendGroupInvite({
groupId: D.groupId,
userId: receiverUserId
})
.then(inviteLoop)
.catch(() => {
D.loading = false;
});
};
inviteLoop();
}
});
const { proxy } = getCurrentInstance();
watch(
() => {
return inviteGroupDialog.value.visible;
},
(value) => {
if (value) {
initDialog();
}
}
};
);
const inviteGroupDialogRef = ref(null);
function initDialog() {
nextTick(() => adjustDialogZ(inviteGroupDialogRef.value.$el));
const D = inviteGroupDialog.value;
if (D.groupId) {
groupRequest
.getCachedGroup({
groupId: D.groupId
})
.then((args) => {
D.groupName = args.ref.name;
})
.catch(() => {
D.groupId = '';
});
isAllowedToInviteToGroup();
}
if (D.userId) {
userRequest.getCachedUser({ userId: D.userId }).then((args) => {
D.userObject = args.ref;
D.userIds = [D.userId];
});
}
}
function isAllowedToInviteToGroup() {
const D = inviteGroupDialog.value;
const groupId = D.groupId;
if (!groupId) {
return;
}
inviteGroupDialog.value.loading = true;
groupRequest
.getGroup({ groupId })
.then((args) => {
const ref = applyGroup(args.json);
if (hasGroupPermission(ref, 'group-invites-manage')) {
return args;
}
// not allowed to invite
inviteGroupDialog.value.groupId = '';
proxy.$message({
type: 'error',
message: 'You are not allowed to invite to this group'
});
return args;
})
.finally(() => {
inviteGroupDialog.value.loading = false;
});
}
function sendGroupInvite() {
proxy.$confirm('Continue? Invite User(s) To Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
const D = inviteGroupDialog.value;
if (action !== 'confirm' || D.loading === true) {
return;
}
D.loading = true;
const inviteLoop = () => {
if (D.userIds.length === 0) {
D.loading = false;
return;
}
const receiverUserId = D.userIds.shift();
groupRequest
.sendGroupInvite({
groupId: D.groupId,
userId: receiverUserId
})
.then(inviteLoop)
.catch(() => {
D.loading = false;
});
};
inviteLoop();
}
});
}
</script>

View File

@@ -1,13 +1,13 @@
<template>
<safe-dialog ref="launchDialog" :visible.sync="isVisible" :title="$t('dialog.launch.header')" width="450px">
<safe-dialog ref="launchDialogRef" :visible.sync="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-form :model="launchDialog" label-width="100px">
<el-form-item :label="$t('dialog.launch.url')">
<el-form-item :label="t('dialog.launch.url')">
<el-input
v-model="launchDialog.url"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
@@ -18,11 +18,8 @@
</el-form-item>
<el-form-item v-if="launchDialog.shortUrl">
<template slot="label">
<span>{{ $t('dialog.launch.short_url') }}</span>
<el-tooltip
placement="top"
style="margin-left: 5px"
:content="$t('dialog.launch.short_url_notice')">
<span>{{ t('dialog.launch.short_url') }}</span>
<el-tooltip placement="top" style="margin-left: 5px" :content="t('dialog.launch.short_url_notice')">
<i class="el-icon-warning" />
</el-tooltip>
</template>
@@ -31,7 +28,7 @@
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
@@ -40,13 +37,13 @@
@click="copyInstanceMessage(launchDialog.shortUrl)" />
</el-tooltip>
</el-form-item>
<el-form-item :label="$t('dialog.launch.location')">
<el-form-item :label="t('dialog.launch.location')">
<el-input
v-model="launchDialog.location"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
@@ -57,226 +54,196 @@
</el-form-item>
</el-form>
<el-checkbox v-model="launchDialog.desktop" style="float: left; margin-top: 5px" @change="saveLaunchDialog">
{{ $t('dialog.launch.start_as_desktop') }}
{{ t('dialog.launch.start_as_desktop') }}
</el-checkbox>
<template slot="footer">
<el-button size="small" @click="showPreviousInstancesInfoDialog(launchDialog.location)">
{{ $t('dialog.launch.info') }}
{{ t('dialog.launch.info') }}
</el-button>
<el-button
size="small"
:disabled="!checkCanInvite(launchDialog.location)"
@click="showInviteDialog(launchDialog.location)">
{{ $t('dialog.launch.invite') }}
{{ t('dialog.launch.invite') }}
</el-button>
<el-button
type="primary"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="launchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
{{ $t('dialog.launch.launch') }}
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
{{ t('dialog.launch.launch') }}
</el-button>
</template>
<InviteDialog
:invite-dialog="inviteDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:active-friends="activeFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script>
<script setup>
import { ref, computed, nextTick, watch, getCurrentInstance, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, worldRequest } from '../../api';
import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import { adjustDialogZ, checkCanInvite, getLaunchURL, isRealInstance, parseLocation } from '../../shared/utils';
import {
useAppearanceSettingsStore,
useFriendStore,
useInstanceStore,
useLaunchStore,
useLocationStore
} from '../../stores';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default {
name: 'LaunchDialog',
components: { InviteDialog },
inject: ['friends', 'showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
hideTooltips: Boolean,
launchDialogData: { type: Object, required: true },
checkCanInvite: {
type: Function,
required: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
}
},
data() {
return {
launchDialog: {
loading: false,
desktop: false,
tag: '',
location: '',
url: '',
shortName: '',
shortUrl: '',
secureOrShortName: ''
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
}
};
},
computed: {
isVisible: {
get() {
return this.launchDialogData.visible;
},
set(value) {
this.$emit('update:launch-dialog-data', { ...this.launchDialogData, visible: value });
}
}
},
watch: {
'launchDialogData.loading': {
handler() {
this.getConfig();
this.initLaunchDialog();
}
}
},
const { proxy } = getCurrentInstance();
const { t } = useI18n();
created() {
this.getConfig();
const { friends } = storeToRefs(useFriendStore());
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { lastLocation } = storeToRefs(useLocationStore());
const { launchGame } = useLaunchStore();
const { launchDialogData } = storeToRefs(useLaunchStore());
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const launchDialogRef = ref(null);
const launchDialog = ref({
loading: false,
desktop: false,
tag: '',
location: '',
url: '',
shortName: '',
shortUrl: '',
secureOrShortName: ''
});
const inviteDialog = ref({
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
});
const isVisible = computed({
get() {
return launchDialogData.value.visible;
},
methods: {
closeInviteDialog() {
this.inviteDialog.visible = false;
},
showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = this.inviteDialog;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = this.lastLocation.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = this.friends.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
},
launchGame(location, shortName, desktop) {
this.$emit('launchGame', location, shortName, desktop);
this.isVisible = false;
},
getConfig() {
configRepository.getBool('launchAsDesktop').then((value) => (this.launchDialog.desktop = value));
},
saveLaunchDialog() {
configRepository.setBool('launchAsDesktop', this.launchDialog.desktop);
},
async initLaunchDialog() {
const { tag, shortName } = this.launchDialogData;
if (!isRealInstance(tag)) {
return;
}
this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el));
const D = this.launchDialog;
D.tag = tag;
D.secureOrShortName = shortName;
D.shortUrl = '';
D.shortName = shortName;
const L = parseLocation(tag);
L.shortName = shortName;
if (shortName) {
D.shortUrl = `https://vrch.at/${shortName}`;
}
if (L.instanceId) {
D.location = `${L.worldId}:${L.instanceId}`;
} else {
D.location = L.worldId;
}
D.url = getLaunchURL(L);
if (!shortName) {
const res = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
});
// NOTE:
// splitting the 'INSTANCE:SHORTNAME' event and put code here
if (!res.json) {
return;
}
const resLocation = `${res.instance.worldId}:${res.instance.instanceId}`;
if (resLocation === this.launchDialog.tag) {
const resShortName = res.json.shortName;
const secureOrShortName = res.json.shortName || res.json.secureName;
const parsedL = parseLocation(resLocation);
parsedL.shortName = resShortName;
this.launchDialog.shortName = resShortName;
this.launchDialog.secureOrShortName = secureOrShortName;
if (resShortName) {
this.launchDialog.shortUrl = `https://vrch.at/${resShortName}`;
}
this.launchDialog.url = getLaunchURL(parsedL);
}
}
},
async copyInstanceMessage(input) {
try {
await navigator.clipboard.writeText(input);
this.$message({
message: 'Instance copied to clipboard',
type: 'success'
});
} catch (error) {
this.$message({
message: 'Instance copied failed',
type: 'error'
});
console.error(error.message);
}
set(value) {
launchDialogData.value.visible = value;
}
});
watch(
() => launchDialogData.value.loading,
(loading) => {
if (loading) {
getConfig();
initLaunchDialog();
}
}
};
);
getConfig();
function closeInviteDialog() {
inviteDialog.value.visible = false;
}
function showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = inviteDialog.value;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = lastLocation.value.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = friends.value.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
}
function handleLaunchGame(location, shortName, desktop) {
launchGame(location, shortName, desktop);
isVisible.value = false;
}
function getConfig() {
configRepository.getBool('launchAsDesktop').then((value) => (launchDialog.value.desktop = value));
}
function saveLaunchDialog() {
configRepository.setBool('launchAsDesktop', launchDialog.value.desktop);
}
async function initLaunchDialog() {
const { tag, shortName } = launchDialogData.value;
if (!isRealInstance(tag)) {
return;
}
nextTick(() => adjustDialogZ(launchDialogRef.value.$el));
const D = launchDialog.value;
D.tag = tag;
D.secureOrShortName = shortName;
D.shortUrl = '';
D.shortName = shortName;
const L = parseLocation(tag);
L.shortName = shortName;
if (shortName) {
D.shortUrl = `https://vrch.at/${shortName}`;
}
if (L.instanceId) {
D.location = `${L.worldId}:${L.instanceId}`;
} else {
D.location = L.worldId;
}
D.url = getLaunchURL(L);
if (!shortName) {
const res = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
});
if (!res.json) {
return;
}
const resLocation = `${res.instance.worldId}:${res.instance.instanceId}`;
if (resLocation === launchDialog.value.tag) {
const resShortName = res.json.shortName;
const secureOrShortName = res.json.shortName || res.json.secureName;
const parsedL = parseLocation(resLocation);
parsedL.shortName = resShortName;
launchDialog.value.shortName = resShortName;
launchDialog.value.secureOrShortName = secureOrShortName;
if (resShortName) {
launchDialog.value.shortUrl = `https://vrch.at/${resShortName}`;
}
launchDialog.value.url = getLaunchURL(parsedL);
}
}
}
async function copyInstanceMessage(input) {
try {
await navigator.clipboard.writeText(input);
proxy.$message({
message: 'Instance copied to clipboard',
type: 'success'
});
} catch (error) {
proxy.$message({
message: 'Instance copied failed',
type: 'error'
});
console.error(error.message);
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -26,26 +26,16 @@
</template>
<script setup>
import { inject } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore } from '../../stores';
const { t } = useI18n();
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
defineProps({
previousImagesDialogVisible: {
type: Boolean,
required: true
},
previousImagesTable: {
type: Array,
required: true
}
});
const emit = defineEmits(['update:previousImagesDialogVisible']);
const { previousImagesDialogVisible, previousImagesTable } = storeToRefs(useGalleryStore());
const { showFullscreenImageDialog } = useGalleryStore();
function closeDialog() {
emit('update:previousImagesDialogVisible', false);
previousImagesDialogVisible.value = false;
}
</script>

View File

@@ -1,14 +1,14 @@
<template>
<safe-dialog
ref="dialog"
:visible="visible"
ref="dialogRef"
:visible="previousInstancesInfoDialogVisible"
:title="$t('dialog.previous_instances.info')"
width="800px"
:fullscreen="fullscreen"
destroy-on-close
@close="$emit('update:visible', false)">
@close="closeDialog">
<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" />
<el-input
v-model="dataTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
@@ -20,9 +20,9 @@
<template slot-scope="scope">
<el-tooltip placement="left">
<template slot="content">
<span>{{ scope.row.created_at | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
<span>{{ scope.row.created_at | formatDate('short') }}</span>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</el-tooltip>
</template>
</el-table-column>
@@ -57,105 +57,85 @@
</safe-dialog>
</template>
<script>
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';
<script setup>
import { ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { database } from '../../../service/database';
import {
adjustDialogZ,
compareByCreatedAt,
parseLocation,
timeToText,
formatDateFilter
} from '../../../shared/utils';
import { useGameLogStore, useInstanceStore, useUserStore } from '../../../stores';
export default {
name: 'PreviousInstancesInfoDialog',
components: {
Location
},
inject: ['adjustDialogZ'],
props: {
visible: {
type: Boolean,
default: false
},
instanceId: { type: String, required: true },
gameLogIsFriend: { type: Function, required: true },
gameLogIsFavorite: { type: Function, required: true },
lookupUser: { type: Function, required: true },
isDarkMode: { type: Boolean, required: true }
},
data() {
return {
echarts: null,
echartsInstance: null,
loading: false,
location: {},
currentTab: 'table',
dataTable: {
data: [],
filters: [
{
prop: 'displayName',
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
},
fullscreen: false
};
},
computed: {
activityDetailData() {
return this.dataTable.data.map((item) => ({
displayName: item.displayName,
joinTime: dayjs(item.created_at),
leaveTime: dayjs(item.created_at).add(item.time, 'ms'),
time: item.time,
timer: item.timer
}));
const { lookupUser } = useUserStore();
const { previousInstancesInfoDialogVisible, previousInstancesInfoDialogInstanceId } =
storeToRefs(useInstanceStore());
const { gameLogIsFriend, gameLogIsFavorite } = useGameLogStore();
const dialogRef = ref(null);
const loading = ref(false);
const location = ref({});
const dataTable = ref({
data: [],
filters: [
{
prop: 'displayName',
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
watch: {
visible(value) {
if (value) {
this.$nextTick(() => {
this.init();
this.refreshPreviousInstancesInfoTable();
});
utils.loadEcharts().then((echarts) => {
this.echarts = echarts;
});
}
}
},
methods: {
init() {
this.adjustDialogZ(this.$refs.dialog.$el);
this.loading = true;
this.location = parseLocation(this.instanceId);
},
refreshPreviousInstancesInfoTable() {
database.getPlayersFromInstance(this.location.tag).then((data) => {
const array = [];
for (const entry of Array.from(data.values())) {
entry.timer = utils.timeToText(entry.time);
array.push(entry);
}
array.sort(utils.compareByCreatedAt);
this.dataTable.data = array;
this.loading = false;
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
});
const fullscreen = ref(false);
watch(
() => previousInstancesInfoDialogVisible.value,
(value) => {
if (value) {
nextTick(() => {
init();
refreshPreviousInstancesInfoTable();
});
}
}
};
);
function init() {
adjustDialogZ(dialogRef.value.$el);
loading.value = true;
location.value = parseLocation(previousInstancesInfoDialogInstanceId.value);
}
function refreshPreviousInstancesInfoTable() {
database.getPlayersFromInstance(location.value.tag).then((data) => {
const array = [];
for (const entry of Array.from(data.values())) {
entry.timer = timeToText(entry.time);
array.push(entry);
}
array.sort(compareByCreatedAt);
dataTable.value.data = array;
loading.value = false;
});
}
function closeDialog() {
previousInstancesInfoDialogVisible.value = false;
}
</script>

View File

@@ -2,45 +2,44 @@
<safe-dialog
ref="previousInstancesWorldDialog"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
<el-input
v-model="previousInstancesWorldDialogTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
</div>
<data-tables v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template slot-scope="scope">
<span>{{ scope.row.created_at | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.instance_name')" prop="name">
<el-table-column :label="t('table.previous_instances.instance_name')" prop="name">
<template slot-scope="scope">
<location-world
<LocationWorld
:locationobject="scope.row.$location"
:grouphint="scope.row.groupName"
:currentuserid="API.currentUser.id"
@show-launch-dialog="showLaunchDialog"></location-world>
:currentuserid="currentUser.id" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location">
<el-table-column :label="t('table.previous_instances.instance_creator')" prop="location">
<template slot-scope="scope">
<display-name
<DisplayName
:userid="scope.row.$location.userId"
:location="scope.row.$location.tag"
:force-update-key="previousInstancesWorldDialog.forceUpdate"></display-name>
:force-update-key="previousInstancesWorldDialog.forceUpdate" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
<el-table-column :label="t('table.previous_instances.time')" prop="time" width="100" sortable>
<template slot-scope="scope">
<span v-text="scope.row.timer"></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.action')" width="90" align="right">
<el-table-column :label="t('table.previous_instances.action')" width="90" align="right">
<template slot-scope="scope">
<el-button
type="text"
@@ -66,111 +65,106 @@
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
<script setup>
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { database } from '../../../service/database';
import {
adjustDialogZ,
compareByCreatedAt,
parseLocation,
removeFromArray,
timeToText,
formatDateFilter
} from '../../../shared/utils';
import { useInstanceStore, useUiStore, useUserStore } from '../../../stores';
export default {
name: 'PreviousInstancesWorldDialog',
inject: ['API', 'showLaunchDialog', 'showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
previousInstancesWorldDialog: {
type: Object,
required: true
},
shiftHeld: Boolean
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const props = defineProps({
previousInstancesWorldDialog: {
type: Object,
required: true
}
});
const emit = defineEmits(['update:previous-instances-world-dialog']);
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { currentUser } = storeToRefs(useUserStore());
const previousInstancesWorldDialogTable = reactive({
data: [],
filters: [{ prop: 'groupName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: { prop: 'created_at', order: 'descending' }
},
data() {
return {
previousInstancesWorldDialogTable: {
data: [],
filters: [
{
prop: 'groupName',
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
},
loading: false
};
},
computed: {
isVisible: {
get() {
return this.previousInstancesWorldDialog.visible;
},
set(value) {
this.$emit('update:previous-instances-world-dialog', {
...this.previousInstancesWorldDialog,
visible: value
});
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
});
const loading = ref(false);
const isVisible = computed({
get: () => props.previousInstancesWorldDialog.visible,
set: (value) => {
emit('update:previous-instances-world-dialog', {
...props.previousInstancesWorldDialog,
visible: value
});
}
});
function refreshPreviousInstancesWorldTable() {
loading.value = true;
const D = props.previousInstancesWorldDialog;
database.getPreviousInstancesByWorldId(D.worldRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = parseLocation(ref.location);
ref.timer = ref.time > 0 ? timeToText(ref.time) : '';
array.push(ref);
}
array.sort(compareByCreatedAt);
previousInstancesWorldDialogTable.data = array;
loading.value = false;
});
}
function deleteGameLogWorldInstance(row) {
database.deleteGameLogInstanceByInstanceId({ location: row.location });
removeFromArray(previousInstancesWorldDialogTable.data, row);
}
function deleteGameLogWorldInstancePrompt(row) {
proxy.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
deleteGameLogWorldInstance(row);
}
}
},
watch: {
'previousInstancesWorldDialog.openFlg'() {
if (this.previousInstancesWorldDialog.visible) {
this.$nextTick(() => {
this.adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el);
});
this.refreshPreviousInstancesWorldTable();
}
}
},
methods: {
refreshPreviousInstancesWorldTable() {
this.loading = true;
const D = this.previousInstancesWorldDialog;
database.getpreviousInstancesByWorldId(D.worldRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {
ref.timer = '';
}
array.push(ref);
}
array.sort(utils.compareByCreatedAt);
this.previousInstancesWorldDialogTable.data = array;
this.loading = false;
});
},
deleteGameLogWorldInstance(row) {
database.deleteGameLogInstanceByInstanceId({
location: row.location
});
utils.removeFromArray(this.previousInstancesWorldDialogTable.data, row);
},
});
}
deleteGameLogWorldInstancePrompt(row) {
this.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
this.deleteGameLogWorldInstance(row);
}
}
watch(
() => props.previousInstancesWorldDialog.openFlg,
() => {
if (props.previousInstancesWorldDialog.visible) {
nextTick(() => {
adjustDialogZ(proxy.$refs.previousInstancesWorldDialog.$el);
});
refreshPreviousInstancesWorldTable();
}
}
};
);
</script>

View File

@@ -20,7 +20,6 @@
<el-input
v-for="(link, index) in bioDialog.bioLinks"
:key="index"
v-model="bioDialog.bioLinks[index]"
:value="link"
size="small"
style="margin-top: 5px">
@@ -52,7 +51,7 @@
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { getFaviconUrl } from '../../../composables/shared/utils';
import { getFaviconUrl } from '../../../shared/utils';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;

View File

@@ -6,7 +6,7 @@
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">
<div v-for="item in currentUser.$languages" :key="item.key" style="margin: 6px 0">
<el-tag
size="small"
type="info"
@@ -23,9 +23,7 @@
</div>
<el-select
value=""
:disabled="
languageDialog.loading || (API.currentUser.$languages && API.currentUser.$languages.length === 3)
"
:disabled="languageDialog.loading || (currentUser.$languages && currentUser.$languages.length === 3)"
:placeholder="t('dialog.language.select_language')"
style="margin-top: 14px"
@change="addUserLanguage">
@@ -46,29 +44,21 @@
</template>
<script setup>
import { inject } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { languageClass } from '../../../composables/user/utils';
import { languageClass } from '../../../shared/utils';
import { useUserStore } from '../../../stores';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
languageDialog: {
type: Object,
required: true
}
});
const { languageDialog, currentUser } = storeToRefs(useUserStore());
function removeUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
const D = languageDialog.value;
D.loading = true;
userRequest
.removeUserTags({
@@ -83,7 +73,7 @@
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
const D = languageDialog.value;
D.loading = true;
userRequest
.addUserTags({

View File

@@ -15,22 +15,20 @@
<data-tables v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template slot-scope="scope">
<span>{{ scope.row.created_at | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.world')" prop="name" sortable>
<template slot-scope="scope">
<location
<Location
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"></location>
:grouphint="scope.row.groupName" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location" width="170">
<template slot-scope="scope">
<display-name
:userid="scope.row.$location.userId"
:location="scope.row.$location.tag"></display-name>
<DisplayName :userid="scope.row.$location.userId" :location="scope.row.$location.tag" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
@@ -69,138 +67,115 @@
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import Location from '../../Location.vue';
<script setup>
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { database } from '../../../service/database';
import {
adjustDialogZ,
compareByCreatedAt,
parseLocation,
removeFromArray,
timeToText,
formatDateFilter
} from '../../../shared/utils';
import { useInstanceStore, useLaunchStore, useUiStore } from '../../../stores';
export default {
name: 'PreviousInstancesUserDialog',
components: {
Location
},
inject: ['adjustDialogZ', 'showLaunchDialog', 'showPreviousInstancesInfoDialog'],
props: {
previousInstancesUserDialog: {
type: Object,
default: () => ({
visible: false,
userRef: {},
loading: false,
forceUpdate: 0,
previousInstances: [],
previousInstancesTable: {
data: [],
filters: [
{
prop: 'displayName',
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
height: '400px'
}
}
})
},
shiftHeld: {
type: Boolean,
default: false
}
},
data() {
return {
previousInstancesUserDialogTable: {
const props = defineProps({
previousInstancesUserDialog: {
type: Object,
default: () => ({
visible: false,
userRef: {},
loading: false,
forceUpdate: 0,
previousInstances: [],
previousInstancesTable: {
data: [],
filters: [
{
prop: 'worldName',
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
},
loading: false
};
},
computed: {
isVisible: {
get() {
return this.previousInstancesUserDialog.visible;
},
set(value) {
this.$emit('update:previous-instances-user-dialog', {
...this.previousInstancesUserDialog,
visible: value
});
filters: [{ prop: 'displayName', value: '' }],
tableProps: { stripe: true, size: 'mini', height: '400px' }
}
}
})
}
});
const emit = defineEmits(['update:previous-instances-user-dialog']);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const previousInstancesUserDialogTable = reactive({
data: [],
filters: [{ prop: 'worldName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: { prop: 'created_at', order: 'descending' }
},
watch: {
'previousInstancesUserDialog.openFlg'() {
if (this.previousInstancesUserDialog.visible) {
this.$nextTick(() => {
this.adjustDialogZ(this.$refs.previousInstancesUserDialog.$el);
});
this.refreshPreviousInstancesUserTable();
}
}
},
methods: {
refreshPreviousInstancesUserTable() {
this.loading = true;
database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {
ref.timer = '';
}
array.push(ref);
}
array.sort(utils.compareByCreatedAt);
this.previousInstancesUserDialogTable.data = array;
this.loading = false;
});
},
deleteGameLogUserInstance(row) {
database.deleteGameLogInstance({
id: this.previousInstancesUserDialog.userRef.id,
displayName: this.previousInstancesUserDialog.userRef.displayName,
location: row.location
});
utils.removeFromArray(this.previousInstancesUserDialogTable.data, row);
},
deleteGameLogUserInstancePrompt(row) {
this.$confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
this.deleteGameLogUserInstance(row);
}
}
pageSize: 10,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 25, 50, 100]
}
});
const { showLaunchDialog } = useLaunchStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = storeToRefs(useUiStore());
const isVisible = computed({
get: () => props.previousInstancesUserDialog.visible,
set: (value) => {
emit('update:previous-instances-user-dialog', {
...props.previousInstancesUserDialog,
visible: value
});
}
});
const refreshPreviousInstancesUserTable = async () => {
loading.value = true;
const data = await database.getPreviousInstancesByUserId(props.previousInstancesUserDialog.userRef);
const array = [];
for (const item of data.values()) {
item.$location = parseLocation(item.location);
item.timer = item.time > 0 ? timeToText(item.time) : '';
array.push(item);
}
array.sort(compareByCreatedAt);
previousInstancesUserDialogTable.data = array;
loading.value = false;
};
watch(
() => props.previousInstancesUserDialog.openFlg,
() => {
if (props.previousInstancesUserDialog.visible) {
nextTick(() => {
adjustDialogZ(proxy.$refs.previousInstancesUserDialog.$el);
});
refreshPreviousInstancesUserTable();
}
}
};
);
function deleteGameLogUserInstance(row) {
database.deleteGameLogInstance({
id: props.previousInstancesUserDialog.userRef.id,
displayName: props.previousInstancesUserDialog.userRef.displayName,
location: row.location
});
removeFromArray(previousInstancesUserDialogTable.data, row);
}
function deleteGameLogUserInstancePrompt(row) {
proxy.$confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') deleteGameLogUserInstance(row);
}
});
}
</script>

View File

@@ -6,7 +6,7 @@
width="800px"
append-to-body
@close="cancelSendInviteRequest">
<template v-if="API.currentUser.$isVRCPlus">
<template v-if="currentUser.$isVRCPlus">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
@@ -45,7 +45,7 @@
<el-button type="small" @click="cancelSendInviteRequest">{{
t('dialog.invite_request_message.cancel')
}}</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('request')">{{
<el-button type="small" @click="refreshInviteMessageTableData('request')">{{
t('dialog.invite_request_message.refresh')
}}</el-button>
</template>
@@ -53,37 +53,37 @@
: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 { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const inviteImageUpload = inject('inviteImageUpload');
const inviteStore = useInviteStore();
const { refreshInviteMessageTableData } = inviteStore;
const { inviteRequestMessageTable } = storeToRefs(inviteStore);
const galleryStore = useGalleryStore();
const { inviteImageUpload } = galleryStore;
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
sendInviteRequestDialogVisible: {
type: Boolean,
default: false
},
inviteRequestMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
default: () => ({})
@@ -92,10 +92,6 @@
type: Object,
require: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
@@ -120,7 +116,6 @@
visible: true
};
}
function cancelSendInviteRequest() {
emit('update:sendInviteRequestDialogVisible', false);
}

View File

@@ -34,7 +34,7 @@
<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">
<el-option v-if="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>
@@ -44,6 +44,7 @@
:placeholder="t('dialog.social_status.status_placeholder')"
maxlength="32"
show-word-limit
clearable
style="display: block; margin-top: 10px"></el-input>
</div>
@@ -56,13 +57,16 @@
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { useUserStore } from '../../../stores';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const API = inject('API');
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
socialStatusDialog: {

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
class="x-dialog"
:visible.sync="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')"
append-to-body
width="400px">
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
<template v-if="updateInProgress">
@@ -17,11 +18,10 @@
<span>{{ t('dialog.vrcx_updater.ready_for_update') }}</span>
</div>
<el-select
v-model="currentBranch"
v-model="branch"
style="display: inline-block; width: 150px; margin-right: 15px"
@change="loadBranchVersions">
<el-option v-for="branch in branches" :key="branch.name" :label="branch.name" :value="branch.name">
</el-option>
<el-option v-for="b in branches" :key="b.name" :label="b.name" :value="b.name"> </el-option>
</el-select>
<el-select v-model="VRCXUpdateDialog.release" style="display: inline-block; width: 150px">
<el-option
@@ -63,72 +63,32 @@
</template>
<script setup>
import { ref, computed, inject, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { branches } from '../../shared/constants';
import { adjustDialogZ } from '../../shared/utils';
import { useVRCXUpdaterStore } from '../../stores';
const VRCXUpdaterStore = useVRCXUpdaterStore();
const {
appVersion,
branch,
checkingForVRCXUpdate,
VRCXUpdateDialog,
pendingVRCXInstall,
updateInProgress,
updateProgress
} = storeToRefs(VRCXUpdaterStore);
const { installVRCXUpdate, loadBranchVersions, restartVRCX, updateProgressText, cancelUpdate } = VRCXUpdaterStore;
const { t } = useI18n();
const adjustDialogZ = inject('adjustDialogZ');
const props = defineProps({
// eslint-disable-next-line vue/prop-name-casing
VRCXUpdateDialog: {
type: Object,
required: true
},
appVersion: {
type: String,
required: true
},
checkingForVRCXUpdate: {
type: Boolean,
default: false
},
updateInProgress: {
type: Boolean,
default: false
},
updateProgress: {
type: Number,
default: 0
},
updateProgressText: {
type: Function,
default: () => ''
},
pendingVRCXInstall: {
type: String,
default: ''
},
branch: {
type: String,
default: ''
},
branches: {
type: Object,
default: () => {}
}
});
const VRCXUpdateDialogRef = ref(null);
const emit = defineEmits([
'loadBranchVersions',
'cancelUpdate',
'installVRCXUpdate',
'restartVRCX',
'update:branch'
]);
const currentBranch = computed({
get: () => props.branch,
set: (value) => {
emit('update:branch', value);
}
});
watch(
() => props.VRCXUpdateDialog,
() => VRCXUpdateDialog,
(newVal) => {
if (newVal.visible) {
nextTick(() => {
@@ -137,20 +97,4 @@
}
}
);
function loadBranchVersions(event) {
emit('loadBranchVersions', event);
}
function cancelUpdate() {
emit('cancelUpdate');
}
function installVRCXUpdate() {
emit('installVRCXUpdate');
}
function restartVRCX(isUpgrade) {
emit('restartVRCX', isUpgrade);
}
</script>

View File

@@ -25,12 +25,9 @@
<!-- 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 v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<div
v-if="image.file"
class="x-change-image-item"
style="cursor: pointer"
:class="{ 'current-image': compareCurrentImage(image) }"
@@ -43,35 +40,31 @@
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { extractFileId } from '../../../composables/shared/utils';
import webApiService from '../../../service/webapi';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useGalleryStore, useWorldStore } from '../../../stores';
const { t } = useI18n();
const API = inject('API');
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { worldDialog } = storeToRefs(useWorldStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const props = defineProps({
changeWorldImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
previousImagesFileId: {
type: String,
default: ''
},
worldDialog: {
type: Object,
default: () => ({})
}
});
@@ -126,7 +119,7 @@
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.worldDialog.visible || props.worldDialog.loading) {
if (!files.length || !worldDialog.value.visible || worldDialog.value.loading) {
clearFile();
return;
}
@@ -158,8 +151,8 @@
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 worldId = worldDialog.value.id;
const { imageUrl } = worldDialog.value.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
$message({
@@ -204,7 +197,6 @@
}
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 = {
@@ -216,7 +208,6 @@
}
async function worldImageFileStart(args) {
// API.$on('WORLDIMAGE:FILESTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
@@ -239,9 +230,8 @@
});
if (json.status !== 200) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
$throw('World image upload failed', json, params.url);
}
const args = {
json,
@@ -251,7 +241,6 @@
}
async function worldImageFileAWS(args) {
// API.$on('WORLDIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -262,7 +251,6 @@
}
async function worldImageFileFinish(args) {
// API.$on('WORLDIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -273,7 +261,6 @@
}
async function worldImageSigStart(args) {
// API.$on('WORLDIMAGE:SIGSTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
@@ -296,9 +283,8 @@
});
if (json.status !== 200) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
$throw('World image upload failed', json, params.url);
}
const args = {
json,
@@ -308,7 +294,6 @@
}
async function worldImageSigAWS(args) {
// API.$on('WORLDIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
@@ -318,11 +303,10 @@
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`
imageUrl: `${AppGlobal.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setWorldImage(parmas);
return worldImageSet(res);
@@ -337,7 +321,7 @@
});
refresh();
} else {
API.$throw(0, 'World image change failed', args.params.imageUrl);
$throw(0, 'World image change failed', args.params.imageUrl);
}
}
@@ -346,8 +330,8 @@
function setWorldImage(image) {
changeWorldImageDialogLoading.value = true;
const parmas = {
id: props.worldDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
id: worldDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setWorldImage(parmas)
@@ -360,35 +344,11 @@
function compareCurrentImage(image) {
if (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
// FIXME: old:avatarDialog -> new:worldDialog, is this correct?
props.worldDialog.ref.imageUrl
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
worldDialog.value.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,22 +1,22 @@
<template>
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.set_world_tags.header')"
:title="t('dialog.set_world_tags.header')"
width="400px"
destroy-on-close
append-to-body>
<el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled">
{{ $t('dialog.set_world_tags.avatar_scaling_disabled') }}
{{ t('dialog.set_world_tags.avatar_scaling_disabled') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.focusViewDisabled">
{{ $t('dialog.set_world_tags.focus_view_disabled') }}
{{ t('dialog.set_world_tags.focus_view_disabled') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.debugAllowed">
{{ $t('dialog.set_world_tags.enable_debugging') }}
{{ t('dialog.set_world_tags.enable_debugging') }}
</el-checkbox>
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.author_tags') }}<br /></div>
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.author_tags') }}<br /></div>
<el-input
v-model="setWorldTagsDialog.authorTags"
type="textarea"
@@ -25,281 +25,286 @@
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
style="margin-top: 10px"></el-input>
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.content_tags') }}<br /></div>
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.content_tags') }}<br /></div>
<el-checkbox v-model="setWorldTagsDialog.contentHorror">
{{ $t('dialog.set_world_tags.content_horror') }}
{{ t('dialog.set_world_tags.content_horror') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.contentGore">
{{ $t('dialog.set_world_tags.content_gore') }}
{{ t('dialog.set_world_tags.content_gore') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.contentViolence">
{{ $t('dialog.set_world_tags.content_violence') }}
{{ t('dialog.set_world_tags.content_violence') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.contentAdult">
{{ $t('dialog.set_world_tags.content_adult') }}
{{ t('dialog.set_world_tags.content_adult') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.contentSex">
{{ $t('dialog.set_world_tags.content_sex') }}
{{ t('dialog.set_world_tags.content_sex') }}
</el-checkbox>
<div style="font-size: 12px; margin-top: 10px">
{{ $t('dialog.set_world_tags.default_content_settings') }}<br />
{{ t('dialog.set_world_tags.default_content_settings') }}<br />
</div>
<el-checkbox v-model="setWorldTagsDialog.emoji">
{{ $t('dialog.new_instance.content_emoji') }}
{{ t('dialog.new_instance.content_emoji') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.stickers">
{{ $t('dialog.new_instance.content_stickers') }}
{{ t('dialog.new_instance.content_stickers') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.pedestals">
{{ $t('dialog.new_instance.content_pedestals') }}
{{ t('dialog.new_instance.content_pedestals') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.prints">
{{ $t('dialog.new_instance.content_prints') }}
{{ t('dialog.new_instance.content_prints') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.drones">
{{ $t('dialog.new_instance.content_drones') }}
{{ t('dialog.new_instance.content_drones') }}
</el-checkbox>
<br />
<el-checkbox v-model="setWorldTagsDialog.props">
{{ $t('dialog.new_instance.content_items') }}
{{ t('dialog.new_instance.content_items') }}
</el-checkbox>
<template #footer>
<div style="display: flex">
<el-button size="small" @click="setWorldTagsDialog.visible = false">
{{ $t('dialog.set_world_tags.cancel') }}
{{ t('dialog.set_world_tags.cancel') }}
</el-button>
<el-button type="primary" size="small" @click="saveSetWorldTagsDialog">
{{ $t('dialog.set_world_tags.save') }}
{{ t('dialog.set_world_tags.save') }}
</el-button>
</div>
</template>
</safe-dialog>
</template>
<script>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { worldRequest } from '../../../api';
import { useWorldStore } from '../../../stores';
export default {
name: 'SetWorldTagsDialog',
inject: ['showWorldDialog'],
props: {
oldTags: {
type: Array,
default: () => []
},
isSetWorldTagsDialogVisible: {
type: Boolean,
required: true
},
worldId: {
type: String,
required: true
},
isWorldDialogVisible: {
type: Boolean,
required: true
}
const props = defineProps({
oldTags: {
type: Array,
default: () => []
},
data() {
return {
setWorldTagsDialog: {
authorTags: [],
contentTags: [],
debugAllowed: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
contentHorror: false,
contentGore: false,
contentViolence: false,
contentAdult: false,
contentSex: false,
emoji: true,
stickers: true,
pedestals: true,
prints: true,
drones: true,
props: true
}
};
isSetWorldTagsDialogVisible: {
type: Boolean,
required: true
},
computed: {
isVisible: {
get() {
return this.isSetWorldTagsDialogVisible;
},
set(val) {
this.$emit('update:is-set-world-tags-dialog-visible', val);
}
}
worldId: {
type: String,
required: true
},
watch: {
isSetWorldTagsDialogVisible(val) {
if (val) {
this.showSetWorldTagsDialog();
}
}
isWorldDialogVisible: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:isSetWorldTagsDialogVisible']);
const { showWorldDialog } = useWorldStore();
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const setWorldTagsDialog = ref({
authorTags: '',
contentTags: '',
debugAllowed: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
contentHorror: false,
contentGore: false,
contentViolence: false,
contentAdult: false,
contentSex: false,
emoji: true,
stickers: true,
pedestals: true,
prints: true,
drones: true,
props: true
});
const isVisible = computed({
get() {
return props.isSetWorldTagsDialogVisible;
},
methods: {
showSetWorldTagsDialog() {
const D = this.setWorldTagsDialog;
D.visible = true;
D.debugAllowed = false;
D.avatarScalingDisabled = false;
D.focusViewDisabled = false;
D.contentHorror = false;
D.contentGore = false;
D.contentViolence = false;
D.contentAdult = false;
D.contentSex = false;
const authorTags = [];
const contentTags = [];
this.oldTags.forEach((tag) => {
if (tag.startsWith('author_tag_')) {
authorTags.unshift(tag.substring(11));
}
if (tag.startsWith('content_')) {
contentTags.unshift(tag.substring(8));
}
switch (tag) {
case 'content_horror':
D.contentHorror = true;
break;
case 'content_gore':
D.contentGore = true;
break;
case 'content_violence':
D.contentViolence = true;
break;
case 'content_adult':
D.contentAdult = true;
break;
case 'content_sex':
D.contentSex = true;
break;
case 'debug_allowed':
D.debugAllowed = true;
break;
case 'feature_avatar_scaling_disabled':
D.avatarScalingDisabled = true;
break;
case 'feature_focus_view_disabled':
D.focusViewDisabled = true;
break;
case 'feature_emoji_disabled':
D.emoji = false;
break;
case 'feature_stickers_disabled':
D.stickers = false;
break;
case 'feature_pedestals_disabled':
D.pedestals = false;
break;
case 'feature_prints_disabled':
D.prints = false;
break;
case 'feature_drones_disabled':
D.drones = false;
break;
case 'feature_props_disabled':
D.props = false;
break;
}
});
D.authorTags = authorTags.toString();
D.contentTags = contentTags.toString();
},
saveSetWorldTagsDialog() {
const D = this.setWorldTagsDialog;
const authorTags = D.authorTags.trim().split(',');
const contentTags = D.contentTags.trim().split(',');
const tags = [];
authorTags.forEach((tag) => {
if (tag) {
tags.unshift(`author_tag_${tag}`);
}
});
// add back custom tags
contentTags.forEach((tag) => {
switch (tag) {
case 'horror':
case 'gore':
case 'violence':
case 'adult':
case 'sex':
case '':
break;
default:
tags.unshift(`content_${tag}`);
break;
}
});
if (D.contentHorror) {
tags.unshift('content_horror');
}
if (D.contentGore) {
tags.unshift('content_gore');
}
if (D.contentViolence) {
tags.unshift('content_violence');
}
if (D.contentAdult) {
tags.unshift('content_adult');
}
if (D.contentSex) {
tags.unshift('content_sex');
}
if (D.debugAllowed) {
tags.unshift('debug_allowed');
}
if (D.avatarScalingDisabled) {
tags.unshift('feature_avatar_scaling_disabled');
}
if (D.focusViewDisabled) {
tags.unshift('feature_focus_view_disabled');
}
if (!D.emoji) {
tags.unshift('feature_emoji_disabled');
}
if (!D.stickers) {
tags.unshift('feature_stickers_disabled');
}
if (!D.pedestals) {
tags.unshift('feature_pedestals_disabled');
}
if (!D.prints) {
tags.unshift('feature_prints_disabled');
}
if (!D.drones) {
tags.unshift('feature_drones_disabled');
}
if (!D.props) {
tags.unshift('feature_props_disabled');
}
worldRequest
.saveWorld({
id: this.worldId,
tags
})
.then((args) => {
this.$message({
message: 'Tags updated',
type: 'success'
});
this.$emit('update:is-set-world-tags-dialog-visible', false);
if (this.isWorldDialogVisible) {
this.showWorldDialog(args.json.id);
}
return args;
});
set(val) {
emit('update:isSetWorldTagsDialogVisible', val);
}
});
watch(
() => props.isSetWorldTagsDialogVisible,
(val) => {
if (val) {
showSetWorldTagsDialog();
}
}
};
);
function showSetWorldTagsDialog() {
const D = setWorldTagsDialog.value;
D.debugAllowed = false;
D.avatarScalingDisabled = false;
D.focusViewDisabled = false;
D.contentHorror = false;
D.contentGore = false;
D.contentViolence = false;
D.contentAdult = false;
D.contentSex = false;
const authorTags = [];
const contentTags = [];
props.oldTags.forEach((tag) => {
if (tag.startsWith('author_tag_')) {
authorTags.unshift(tag.substring(11));
}
if (tag.startsWith('content_')) {
contentTags.unshift(tag.substring(8));
}
switch (tag) {
case 'content_horror':
D.contentHorror = true;
break;
case 'content_gore':
D.contentGore = true;
break;
case 'content_violence':
D.contentViolence = true;
break;
case 'content_adult':
D.contentAdult = true;
break;
case 'content_sex':
D.contentSex = true;
break;
case 'debug_allowed':
D.debugAllowed = true;
break;
case 'feature_avatar_scaling_disabled':
D.avatarScalingDisabled = true;
break;
case 'feature_focus_view_disabled':
D.focusViewDisabled = true;
break;
case 'feature_emoji_disabled':
D.emoji = false;
break;
case 'feature_stickers_disabled':
D.stickers = false;
break;
case 'feature_pedestals_disabled':
D.pedestals = false;
break;
case 'feature_prints_disabled':
D.prints = false;
break;
case 'feature_drones_disabled':
D.drones = false;
break;
case 'feature_props_disabled':
D.props = false;
break;
}
});
D.authorTags = authorTags.toString();
D.contentTags = contentTags.toString();
}
function saveSetWorldTagsDialog() {
const D = setWorldTagsDialog.value;
const authorTags = D.authorTags.trim().split(',');
const contentTags = D.contentTags.trim().split(',');
const tags = [];
authorTags.forEach((tag) => {
if (tag) {
tags.unshift(`author_tag_${tag}`);
}
});
// add back custom tags
contentTags.forEach((tag) => {
switch (tag) {
case 'horror':
case 'gore':
case 'violence':
case 'adult':
case 'sex':
case '':
break;
default:
tags.unshift(`content_${tag}`);
break;
}
});
if (D.contentHorror) {
tags.unshift('content_horror');
}
if (D.contentGore) {
tags.unshift('content_gore');
}
if (D.contentViolence) {
tags.unshift('content_violence');
}
if (D.contentAdult) {
tags.unshift('content_adult');
}
if (D.contentSex) {
tags.unshift('content_sex');
}
if (D.debugAllowed) {
tags.unshift('debug_allowed');
}
if (D.avatarScalingDisabled) {
tags.unshift('feature_avatar_scaling_disabled');
}
if (D.focusViewDisabled) {
tags.unshift('feature_focus_view_disabled');
}
if (!D.emoji) {
tags.unshift('feature_emoji_disabled');
}
if (!D.stickers) {
tags.unshift('feature_stickers_disabled');
}
if (!D.pedestals) {
tags.unshift('feature_pedestals_disabled');
}
if (!D.prints) {
tags.unshift('feature_prints_disabled');
}
if (!D.drones) {
tags.unshift('feature_drones_disabled');
}
if (!D.props) {
tags.unshift('feature_props_disabled');
}
worldRequest
.saveWorld({
id: props.worldId,
tags
})
.then((args) => {
proxy.$message({
message: 'Tags updated',
type: 'success'
});
emit('update:isSetWorldTagsDialogVisible', false);
if (props.isWorldDialogVisible) {
showWorldDialog(args.json.id);
}
return args;
});
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.allowed_video_player_domains.header')"
:title="t('dialog.allowed_video_player_domains.header')"
width="600px"
destroy-on-close
append-to-body>
@@ -10,13 +10,12 @@
v-for="(domain, index) in urlList"
:key="index"
v-model="urlList[index]"
:value="domain"
size="small"
style="margin-top: 5px">
<el-button slot="append" icon="el-icon-delete" @click="urlList.splice(index, 1)"></el-button>
</el-input>
<el-button size="mini" style="margin-top: 5px" @click="urlList.push('')">
{{ $t('dialog.allowed_video_player_domains.add_domain') }}
{{ t('dialog.allowed_video_player_domains.add_domain') }}
</el-button>
</div>
<template #footer>
@@ -25,65 +24,67 @@
size="small"
:disabled="!worldAllowedDomainsDialog.worldId"
@click="saveWorldAllowedDomains">
{{ $t('dialog.allowed_video_player_domains.save') }}
{{ t('dialog.allowed_video_player_domains.save') }}
</el-button>
</template>
</safe-dialog>
</template>
<script>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { worldRequest } from '../../../api';
export default {
name: 'WorldAllowedDomainsDialog',
props: {
worldAllowedDomainsDialog: {
type: Object,
required: true
}
const props = defineProps({
worldAllowedDomainsDialog: {
type: Object,
required: true
}
});
const emit = defineEmits(['update:worldAllowedDomainsDialog']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const urlList = ref([]);
const isVisible = computed({
get() {
return props.worldAllowedDomainsDialog.visible;
},
data() {
return {
urlList: []
};
},
computed: {
isVisible: {
get() {
return this.worldAllowedDomainsDialog.visible;
},
set(val) {
this.$emit('update:world-allowed-domains-dialog', {
...this.worldAllowedDomainsDialog,
visible: val
});
}
}
},
watch: {
'worldAllowedDomainsDialog.visible'(val) {
if (val) {
this.urlList = this.worldAllowedDomainsDialog.urlList;
}
}
},
methods: {
saveWorldAllowedDomains() {
const D = this.worldAllowedDomainsDialog;
worldRequest
.saveWorld({
id: D.worldId,
urlList: D.urlList
})
.then((args) => {
this.$message({
message: 'Allowed Video Player Domains updated',
type: 'success'
});
return args;
});
D.visible = false;
set(val) {
emit('update:worldAllowedDomainsDialog', {
...props.worldAllowedDomainsDialog,
visible: val
});
}
});
watch(
() => props.worldAllowedDomainsDialog.visible,
(val) => {
if (val) {
urlList.value = props.worldAllowedDomainsDialog.urlList;
}
}
};
);
function saveWorldAllowedDomains() {
const D = props.worldAllowedDomainsDialog;
worldRequest
.saveWorld({
id: D.worldId,
urlList: urlList.value
})
.then((args) => {
proxy.$message({
message: 'Allowed Video Player Domains updated',
type: 'success'
});
return args;
});
D.visible = false;
}
</script>

File diff suppressed because it is too large Load Diff