mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
refactor: favorites tab
This commit is contained in:
@@ -47,11 +47,11 @@
|
|||||||
:key="entry.label"
|
:key="entry.label"
|
||||||
type="button"
|
type="button"
|
||||||
class="nav-menu-popover__menu-item"
|
class="nav-menu-popover__menu-item"
|
||||||
@click="handleSubmenuClick(entry.path, item.index)">
|
@click="handleSubmenuClick(entry, item.index)">
|
||||||
<span class="nav-menu-popover__menu-label"
|
<span class="nav-menu-popover__menu-label"
|
||||||
>{{ t(entry.label)
|
>{{ t(entry.label)
|
||||||
}}<span
|
}}<span
|
||||||
v-if="notifiedMenus.includes(entry.path.split('/').pop())"
|
v-if="notifiedMenus.includes(entry.routeName || entry.path.split('/').pop())"
|
||||||
class="nav-menu-popover__menu-label-dot"></span
|
class="nav-menu-popover__menu-label-dot"></span
|
||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -250,7 +250,25 @@
|
|||||||
{
|
{
|
||||||
index: 'favorites',
|
index: 'favorites',
|
||||||
icon: 'ri-star-line',
|
icon: 'ri-star-line',
|
||||||
tooltip: 'nav_tooltip.favorites'
|
tooltip: '',
|
||||||
|
title: 'nav_tooltip.favorites',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
label: 'view.favorite.friends.header',
|
||||||
|
path: '/favorites/friends',
|
||||||
|
routeName: 'favorite-friends'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'view.favorite.worlds.header',
|
||||||
|
path: '/favorites/worlds',
|
||||||
|
routeName: 'favorite-worlds'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'view.favorite.avatars.header',
|
||||||
|
path: '/favorites/avatars',
|
||||||
|
routeName: 'favorite-avatars'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 'social',
|
index: 'social',
|
||||||
@@ -258,9 +276,21 @@
|
|||||||
tooltip: '',
|
tooltip: '',
|
||||||
title: 'nav_tooltip.social',
|
title: 'nav_tooltip.social',
|
||||||
entries: [
|
entries: [
|
||||||
{ label: 'nav_tooltip.friend_log', path: '/social/friend-log' },
|
{
|
||||||
{ label: 'nav_tooltip.friend_list', path: '/social/friend-list' },
|
label: 'nav_tooltip.friend_log',
|
||||||
{ label: 'nav_tooltip.moderation', path: '/social/moderation' }
|
path: '/social/friend-log',
|
||||||
|
routeName: 'friend-log'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'nav_tooltip.friend_list',
|
||||||
|
path: '/social/friend-list',
|
||||||
|
routeName: 'friend-list'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'nav_tooltip.moderation',
|
||||||
|
path: '/social/moderation',
|
||||||
|
routeName: 'moderation'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -301,7 +331,7 @@
|
|||||||
storeToRefs(VRCXUpdaterStore);
|
storeToRefs(VRCXUpdaterStore);
|
||||||
const { showVRCXUpdateDialog, updateProgressText, showChangeLogDialog } = VRCXUpdaterStore;
|
const { showVRCXUpdateDialog, updateProgressText, showChangeLogDialog } = VRCXUpdaterStore;
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const { notifiedMenus } = storeToRefs(uiStore);
|
const { notifiedMenus, lastVisitedSocialRoute, lastVisitedFavoritesRoute } = storeToRefs(uiStore);
|
||||||
const { directAccessPaste } = useSearchStore();
|
const { directAccessPaste } = useSearchStore();
|
||||||
const { sentryErrorReporting } = storeToRefs(useAdvancedSettingsStore());
|
const { sentryErrorReporting } = storeToRefs(useAdvancedSettingsStore());
|
||||||
const { setSentryErrorReporting } = useAdvancedSettingsStore();
|
const { setSentryErrorReporting } = useAdvancedSettingsStore();
|
||||||
@@ -358,11 +388,16 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmenuClick = (path, index) => {
|
const handleSubmenuClick = (entry, index) => {
|
||||||
if (path) {
|
if (!entry) {
|
||||||
router.push(path);
|
return;
|
||||||
navMenuRef.value?.updateActiveIndex(index);
|
|
||||||
}
|
}
|
||||||
|
if (entry.routeName) {
|
||||||
|
router.push({ name: entry.routeName });
|
||||||
|
} else if (entry.path) {
|
||||||
|
router.push(entry.path);
|
||||||
|
}
|
||||||
|
navMenuRef.value?.updateActiveIndex(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubMenuBeforeEnter = () => {
|
const handleSubMenuBeforeEnter = () => {
|
||||||
@@ -372,10 +407,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRouteChange = (index) => {
|
const handleRouteChange = (index) => {
|
||||||
|
let targetName = index;
|
||||||
if (index === 'social') {
|
if (index === 'social') {
|
||||||
index = 'friend-log';
|
targetName = lastVisitedSocialRoute.value || 'friend-log';
|
||||||
|
} else if (index === 'favorites') {
|
||||||
|
targetName = lastVisitedFavoritesRoute.value || 'favorite-friends';
|
||||||
}
|
}
|
||||||
router.push({ name: index });
|
router.push({ name: targetName });
|
||||||
navMenuRef.value?.updateActiveIndex(index);
|
navMenuRef.value?.updateActiveIndex(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
import Charts from './../views/Charts/Charts.vue';
|
import Charts from './../views/Charts/Charts.vue';
|
||||||
import Favorites from './../views/Favorites/Favorites.vue';
|
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
|
||||||
|
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
|
||||||
|
import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue';
|
||||||
import Feed from './../views/Feed/Feed.vue';
|
import Feed from './../views/Feed/Feed.vue';
|
||||||
import FriendList from './../views/FriendList/FriendList.vue';
|
import FriendList from './../views/FriendList/FriendList.vue';
|
||||||
import FriendLocation from './../views/FriendLocation/FriendLocation.vue';
|
import FriendLocation from './../views/FriendLocation/FriendLocation.vue';
|
||||||
@@ -24,7 +26,21 @@ const routes = [
|
|||||||
{ path: '/game-log', name: 'game-log', component: GameLog },
|
{ path: '/game-log', name: 'game-log', component: GameLog },
|
||||||
{ path: '/player-list', name: 'player-list', component: PlayerList },
|
{ path: '/player-list', name: 'player-list', component: PlayerList },
|
||||||
{ path: '/search', name: 'search', component: Search },
|
{ path: '/search', name: 'search', component: Search },
|
||||||
{ path: '/favorites', name: 'favorites', component: Favorites },
|
{
|
||||||
|
path: '/favorites/friends',
|
||||||
|
name: 'favorite-friends',
|
||||||
|
component: FavoritesFriend
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/favorites/worlds',
|
||||||
|
name: 'favorite-worlds',
|
||||||
|
component: FavoritesWorld
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/favorites/avatars',
|
||||||
|
name: 'favorite-avatars',
|
||||||
|
component: FavoritesAvatar
|
||||||
|
},
|
||||||
{ path: '/social/friend-log', name: 'friend-log', component: FriendLog },
|
{ path: '/social/friend-log', name: 'friend-log', component: FriendLog },
|
||||||
{ path: '/social/moderation', name: 'moderation', component: Moderation },
|
{ path: '/social/moderation', name: 'moderation', component: Moderation },
|
||||||
{ path: '/notification', name: 'notification', component: Notification },
|
{ path: '/notification', name: 'notification', component: Notification },
|
||||||
|
|||||||
@@ -56,4 +56,16 @@ function moveArrayItem(array, fromIndex, toIndex) {
|
|||||||
array.splice(toIndex, 0, item);
|
array.splice(toIndex, 0, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { removeFromArray, arraysMatch, moveArrayItem };
|
function replaceReactiveObject(target, source) {
|
||||||
|
for (const key in target) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
||||||
|
delete target[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in source) {
|
||||||
|
target[key] = source[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { removeFromArray, arraysMatch, moveArrayItem, replaceReactiveObject };
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { ElMessage } from 'element-plus';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { compareByName, removeFromArray } from '../shared/utils';
|
import {
|
||||||
|
compareByName,
|
||||||
|
removeFromArray,
|
||||||
|
replaceReactiveObject
|
||||||
|
} from '../shared/utils';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { favoriteRequest } from '../api';
|
import { favoriteRequest } from '../api';
|
||||||
import { processBulk } from '../service/request';
|
import { processBulk } from '../service/request';
|
||||||
@@ -34,8 +38,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
|
|
||||||
const cachedFavorites = reactive(new Map());
|
const cachedFavorites = reactive(new Map());
|
||||||
|
|
||||||
const currentFavoriteTab = ref('friend');
|
|
||||||
|
|
||||||
const cachedFavoriteGroups = ref({});
|
const cachedFavoriteGroups = ref({});
|
||||||
|
|
||||||
const isFavoriteGroupLoading = ref(false);
|
const isFavoriteGroupLoading = ref(false);
|
||||||
@@ -72,11 +74,13 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
|
|
||||||
const friendImportDialogVisible = ref(false);
|
const friendImportDialogVisible = ref(false);
|
||||||
|
|
||||||
const localWorldFavorites = ref({});
|
const localWorldFavorites = reactive({});
|
||||||
|
|
||||||
const localAvatarFavorites = ref({});
|
const localAvatarFavorites = reactive({});
|
||||||
|
|
||||||
const editFavoritesMode = ref(false);
|
const selectedFavoriteFriends = ref([]);
|
||||||
|
const selectedFavoriteWorlds = ref([]);
|
||||||
|
const selectedFavoriteAvatars = ref([]);
|
||||||
|
|
||||||
const favoriteDialog = ref({
|
const favoriteDialog = ref({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -113,22 +117,46 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
return sorted;
|
return sorted;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
favoriteFriends,
|
||||||
|
(list) => {
|
||||||
|
syncFavoriteSelection(list, selectedFavoriteFriends);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
favoriteWorlds,
|
||||||
|
(list) => {
|
||||||
|
syncFavoriteSelection(list, selectedFavoriteWorlds);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
favoriteAvatars,
|
||||||
|
(list) => {
|
||||||
|
syncFavoriteSelection(list, selectedFavoriteAvatars);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
const localAvatarFavoriteGroups = computed(() =>
|
const localAvatarFavoriteGroups = computed(() =>
|
||||||
Object.keys(localAvatarFavorites.value).sort()
|
Object.keys(localAvatarFavorites).sort()
|
||||||
);
|
);
|
||||||
|
|
||||||
const localWorldFavoriteGroups = computed(() =>
|
const localWorldFavoriteGroups = computed(() =>
|
||||||
Object.keys(localWorldFavorites.value).sort()
|
Object.keys(localWorldFavorites).sort()
|
||||||
);
|
);
|
||||||
|
|
||||||
const localWorldFavoritesList = computed(() =>
|
const localWorldFavoritesList = computed(() =>
|
||||||
Object.values(localWorldFavorites.value)
|
Object.values(localWorldFavorites)
|
||||||
.flat()
|
.flat()
|
||||||
.map((fav) => fav.id)
|
.map((fav) => fav.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const localAvatarFavoritesList = computed(() =>
|
const localAvatarFavoritesList = computed(() =>
|
||||||
Object.values(localAvatarFavorites.value)
|
Object.values(localAvatarFavorites)
|
||||||
.flat()
|
.flat()
|
||||||
.map((fav) => fav.id)
|
.map((fav) => fav.id)
|
||||||
);
|
);
|
||||||
@@ -147,7 +175,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const localWorldFavGroupLength = computed(() => (group) => {
|
const localWorldFavGroupLength = computed(() => (group) => {
|
||||||
const favoriteGroup = localWorldFavorites.value[group];
|
const favoriteGroup = localWorldFavorites[group];
|
||||||
if (!favoriteGroup) {
|
if (!favoriteGroup) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -155,13 +183,27 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const localAvatarFavGroupLength = computed(() => (group) => {
|
const localAvatarFavGroupLength = computed(() => (group) => {
|
||||||
const favoriteGroup = localAvatarFavorites.value[group];
|
const favoriteGroup = localAvatarFavorites[group];
|
||||||
if (!favoriteGroup) {
|
if (!favoriteGroup) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return favoriteGroup.length;
|
return favoriteGroup.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function syncFavoriteSelection(list, selectionRef) {
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
selectionRef.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const availableIds = new Set(list.map((item) => item.id));
|
||||||
|
const filtered = selectionRef.value.filter((id) =>
|
||||||
|
availableIds.has(id)
|
||||||
|
);
|
||||||
|
if (filtered.length !== selectionRef.value.length) {
|
||||||
|
selectionRef.value = filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => watchState.isLoggedIn,
|
() => watchState.isLoggedIn,
|
||||||
(isLoggedIn) => {
|
(isLoggedIn) => {
|
||||||
@@ -177,7 +219,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
state.favoriteFriends_ = [];
|
state.favoriteFriends_ = [];
|
||||||
state.favoriteWorlds_ = [];
|
state.favoriteWorlds_ = [];
|
||||||
state.favoriteAvatars_ = [];
|
state.favoriteAvatars_ = [];
|
||||||
localAvatarFavorites.value = {};
|
replaceReactiveObject(localWorldFavorites, {});
|
||||||
|
replaceReactiveObject(localAvatarFavorites, {});
|
||||||
|
selectedFavoriteFriends.value = [];
|
||||||
|
selectedFavoriteWorlds.value = [];
|
||||||
|
selectedFavoriteAvatars.value = [];
|
||||||
favoriteDialog.value.visible = false;
|
favoriteDialog.value.visible = false;
|
||||||
worldImportDialogVisible.value = false;
|
worldImportDialogVisible.value = false;
|
||||||
avatarImportDialogVisible.value = false;
|
avatarImportDialogVisible.value = false;
|
||||||
@@ -353,8 +399,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
type,
|
type,
|
||||||
groupKey: favorite.$groupKey,
|
groupKey: favorite.$groupKey,
|
||||||
ref: null,
|
ref: null,
|
||||||
name: '',
|
name: ''
|
||||||
$selected: false
|
|
||||||
};
|
};
|
||||||
if (type === 'friend') {
|
if (type === 'friend') {
|
||||||
ref = userStore.cachedUsers.get(objectId);
|
ref = userStore.cachedUsers.get(objectId);
|
||||||
@@ -807,19 +852,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearBulkFavoriteSelection() {
|
|
||||||
let ctx;
|
|
||||||
for (ctx of state.favoriteFriends_) {
|
|
||||||
ctx.$selected = false;
|
|
||||||
}
|
|
||||||
for (ctx of state.favoriteWorlds_) {
|
|
||||||
ctx.$selected = false;
|
|
||||||
}
|
|
||||||
for (ctx of state.favoriteAvatars_) {
|
|
||||||
ctx.$selected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWorldImportDialog() {
|
function showWorldImportDialog() {
|
||||||
worldImportDialogVisible.value = true;
|
worldImportDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -845,11 +877,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
if (typeof ref === 'undefined') {
|
if (typeof ref === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!localWorldFavorites.value[group]) {
|
if (!localWorldFavorites[group]) {
|
||||||
localWorldFavorites.value[group] = [];
|
localWorldFavorites[group] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
localWorldFavorites.value[group].unshift(ref);
|
localWorldFavorites[group].unshift(ref);
|
||||||
database.addWorldToCache(ref);
|
database.addWorldToCache(ref);
|
||||||
database.addWorldToFavorites(worldId, group);
|
database.addWorldToFavorites(worldId, group);
|
||||||
if (
|
if (
|
||||||
@@ -876,7 +908,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function hasLocalWorldFavorite(worldId, group) {
|
function hasLocalWorldFavorite(worldId, group) {
|
||||||
const favoriteGroup = localWorldFavorites.value[group];
|
const favoriteGroup = localWorldFavorites[group];
|
||||||
if (!favoriteGroup) {
|
if (!favoriteGroup) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -901,10 +933,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
if (typeof ref === 'undefined') {
|
if (typeof ref === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!localAvatarFavorites.value[group]) {
|
if (!localAvatarFavorites[group]) {
|
||||||
localAvatarFavorites.value[group] = [];
|
localAvatarFavorites[group] = [];
|
||||||
}
|
}
|
||||||
localAvatarFavorites.value[group].unshift(ref);
|
localAvatarFavorites[group].unshift(ref);
|
||||||
database.addAvatarToCache(ref);
|
database.addAvatarToCache(ref);
|
||||||
database.addAvatarToFavorites(avatarId, group);
|
database.addAvatarToFavorites(avatarId, group);
|
||||||
if (
|
if (
|
||||||
@@ -931,7 +963,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function hasLocalAvatarFavorite(avatarId, group) {
|
function hasLocalAvatarFavorite(avatarId, group) {
|
||||||
const favoriteGroup = localAvatarFavorites.value[group];
|
const favoriteGroup = localAvatarFavorites[group];
|
||||||
if (!favoriteGroup) {
|
if (!favoriteGroup) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -981,25 +1013,21 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
let i;
|
let i;
|
||||||
// remove from cache if no longer in favorites
|
// remove from cache if no longer in favorites
|
||||||
const avatarIdRemoveList = new Set();
|
const avatarIdRemoveList = new Set();
|
||||||
const favoriteGroup = localAvatarFavorites.value[group];
|
const favoriteGroup = localAvatarFavorites[group];
|
||||||
for (i = 0; i < favoriteGroup.length; ++i) {
|
for (i = 0; i < favoriteGroup.length; ++i) {
|
||||||
avatarIdRemoveList.add(favoriteGroup[i].id);
|
avatarIdRemoveList.add(favoriteGroup[i].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete localAvatarFavorites.value[group];
|
delete localAvatarFavorites[group];
|
||||||
database.deleteAvatarFavoriteGroup(group);
|
database.deleteAvatarFavoriteGroup(group);
|
||||||
|
|
||||||
for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
||||||
const groupName = localAvatarFavoriteGroups.value[i];
|
const groupName = localAvatarFavoriteGroups.value[i];
|
||||||
if (!localAvatarFavorites.value[groupName]) {
|
if (!localAvatarFavorites[groupName]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (
|
for (let j = 0; j < localAvatarFavorites[groupName].length; ++j) {
|
||||||
let j = 0;
|
const avatarId = localAvatarFavorites[groupName][j].id;
|
||||||
j < localAvatarFavorites.value[groupName].length;
|
|
||||||
++j
|
|
||||||
) {
|
|
||||||
const avatarId = localAvatarFavorites.value[groupName][j].id;
|
|
||||||
if (avatarIdRemoveList.has(avatarId)) {
|
if (avatarIdRemoveList.has(avatarId)) {
|
||||||
avatarIdRemoveList.delete(avatarId);
|
avatarIdRemoveList.delete(avatarId);
|
||||||
break;
|
break;
|
||||||
@@ -1016,19 +1044,15 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
++i
|
++i
|
||||||
) {
|
) {
|
||||||
const groupName = localAvatarFavoriteGroups.value[i];
|
const groupName = localAvatarFavoriteGroups.value[i];
|
||||||
if (
|
if (!localAvatarFavorites[groupName] || group === groupName) {
|
||||||
!localAvatarFavorites.value[groupName] ||
|
|
||||||
group === groupName
|
|
||||||
) {
|
|
||||||
continue loop;
|
continue loop;
|
||||||
}
|
}
|
||||||
for (
|
for (
|
||||||
let j = 0;
|
let j = 0;
|
||||||
j < localAvatarFavorites.value[groupName].length;
|
j < localAvatarFavorites[groupName].length;
|
||||||
++j
|
++j
|
||||||
) {
|
) {
|
||||||
const avatarId =
|
const avatarId = localAvatarFavorites[groupName][j].id;
|
||||||
localAvatarFavorites.value[groupName][j].id;
|
|
||||||
if (id === avatarId) {
|
if (id === avatarId) {
|
||||||
avatarInFavorites = true;
|
avatarInFavorites = true;
|
||||||
break loop;
|
break loop;
|
||||||
@@ -1047,8 +1071,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
if (!appearanceSettingsStore.sortFavorites) {
|
if (!appearanceSettingsStore.sortFavorites) {
|
||||||
for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
||||||
const group = localAvatarFavoriteGroups.value[i];
|
const group = localAvatarFavoriteGroups.value[i];
|
||||||
if (localAvatarFavorites.value[group]) {
|
if (localAvatarFavorites[group]) {
|
||||||
localAvatarFavorites.value[group].sort(compareByName);
|
localAvatarFavorites[group].sort(compareByName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1069,9 +1093,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localAvatarFavorites.value[newName] = localAvatarFavorites.value[group];
|
localAvatarFavorites[newName] = localAvatarFavorites[group];
|
||||||
|
|
||||||
delete localAvatarFavorites.value[group];
|
delete localAvatarFavorites[group];
|
||||||
database.renameAvatarFavoriteGroup(newName, group);
|
database.renameAvatarFavoriteGroup(newName, group);
|
||||||
sortLocalAvatarFavorites();
|
sortLocalAvatarFavorites();
|
||||||
}
|
}
|
||||||
@@ -1090,8 +1114,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!localAvatarFavorites.value[group]) {
|
if (!localAvatarFavorites[group]) {
|
||||||
localAvatarFavorites.value[group] = [];
|
localAvatarFavorites[group] = [];
|
||||||
}
|
}
|
||||||
sortLocalAvatarFavorites();
|
sortLocalAvatarFavorites();
|
||||||
}
|
}
|
||||||
@@ -1138,7 +1162,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
groupsArr = ['Favorites'];
|
groupsArr = ['Favorites'];
|
||||||
}
|
}
|
||||||
|
|
||||||
localAvatarFavorites.value = localFavorites;
|
replaceReactiveObject(localAvatarFavorites, localFavorites);
|
||||||
|
|
||||||
sortLocalAvatarFavorites();
|
sortLocalAvatarFavorites();
|
||||||
}
|
}
|
||||||
@@ -1150,7 +1174,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
*/
|
*/
|
||||||
function removeLocalAvatarFavorite(avatarId, group) {
|
function removeLocalAvatarFavorite(avatarId, group) {
|
||||||
let i;
|
let i;
|
||||||
const favoriteGroup = localAvatarFavorites.value[group];
|
const favoriteGroup = localAvatarFavorites[group];
|
||||||
for (i = 0; i < favoriteGroup.length; ++i) {
|
for (i = 0; i < favoriteGroup.length; ++i) {
|
||||||
if (favoriteGroup[i].id === avatarId) {
|
if (favoriteGroup[i].id === avatarId) {
|
||||||
favoriteGroup.splice(i, 1);
|
favoriteGroup.splice(i, 1);
|
||||||
@@ -1161,15 +1185,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
let avatarInFavorites = false;
|
let avatarInFavorites = false;
|
||||||
for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
||||||
const groupName = localAvatarFavoriteGroups.value[i];
|
const groupName = localAvatarFavoriteGroups.value[i];
|
||||||
if (!localAvatarFavorites.value[groupName] || group === groupName) {
|
if (!localAvatarFavorites[groupName] || group === groupName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (
|
for (let j = 0; j < localAvatarFavorites[groupName].length; ++j) {
|
||||||
let j = 0;
|
const id = localAvatarFavorites[groupName][j].id;
|
||||||
j < localAvatarFavorites.value[groupName].length;
|
|
||||||
++j
|
|
||||||
) {
|
|
||||||
const id = localAvatarFavorites.value[groupName][j].id;
|
|
||||||
if (id === avatarId) {
|
if (id === avatarId) {
|
||||||
avatarInFavorites = true;
|
avatarInFavorites = true;
|
||||||
break;
|
break;
|
||||||
@@ -1208,25 +1228,21 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
let i;
|
let i;
|
||||||
// remove from cache if no longer in favorites
|
// remove from cache if no longer in favorites
|
||||||
const worldIdRemoveList = new Set();
|
const worldIdRemoveList = new Set();
|
||||||
const favoriteGroup = localWorldFavorites.value[group];
|
const favoriteGroup = localWorldFavorites[group];
|
||||||
for (i = 0; i < favoriteGroup.length; ++i) {
|
for (i = 0; i < favoriteGroup.length; ++i) {
|
||||||
worldIdRemoveList.add(favoriteGroup[i].id);
|
worldIdRemoveList.add(favoriteGroup[i].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete localWorldFavorites.value[group];
|
delete localWorldFavorites[group];
|
||||||
database.deleteWorldFavoriteGroup(group);
|
database.deleteWorldFavoriteGroup(group);
|
||||||
|
|
||||||
for (i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
for (i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
||||||
const groupName = localWorldFavoriteGroups.value[i];
|
const groupName = localWorldFavoriteGroups.value[i];
|
||||||
if (!localWorldFavorites.value[groupName]) {
|
if (!localWorldFavorites[groupName]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (
|
for (let j = 0; j < localWorldFavorites[groupName].length; ++j) {
|
||||||
let j = 0;
|
const worldId = localWorldFavorites[groupName][j].id;
|
||||||
j < localWorldFavorites.value[groupName].length;
|
|
||||||
++j
|
|
||||||
) {
|
|
||||||
const worldId = localWorldFavorites.value[groupName][j].id;
|
|
||||||
if (worldIdRemoveList.has(worldId)) {
|
if (worldIdRemoveList.has(worldId)) {
|
||||||
worldIdRemoveList.delete(worldId);
|
worldIdRemoveList.delete(worldId);
|
||||||
break;
|
break;
|
||||||
@@ -1243,8 +1259,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
if (!appearanceSettingsStore.sortFavorites) {
|
if (!appearanceSettingsStore.sortFavorites) {
|
||||||
for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
||||||
const group = localWorldFavoriteGroups.value[i];
|
const group = localWorldFavoriteGroups.value[i];
|
||||||
if (localWorldFavorites.value[group]) {
|
if (localWorldFavorites[group]) {
|
||||||
localWorldFavorites.value[group].sort(compareByName);
|
localWorldFavorites[group].sort(compareByName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1265,9 +1281,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localWorldFavorites.value[newName] = localWorldFavorites.value[group];
|
localWorldFavorites[newName] = localWorldFavorites[group];
|
||||||
|
|
||||||
delete localWorldFavorites.value[group];
|
delete localWorldFavorites[group];
|
||||||
database.renameWorldFavoriteGroup(newName, group);
|
database.renameWorldFavoriteGroup(newName, group);
|
||||||
sortLocalWorldFavorites();
|
sortLocalWorldFavorites();
|
||||||
}
|
}
|
||||||
@@ -1279,7 +1295,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
*/
|
*/
|
||||||
function removeLocalWorldFavorite(worldId, group) {
|
function removeLocalWorldFavorite(worldId, group) {
|
||||||
let i;
|
let i;
|
||||||
const favoriteGroup = localWorldFavorites.value[group];
|
const favoriteGroup = localWorldFavorites[group];
|
||||||
for (i = 0; i < favoriteGroup.length; ++i) {
|
for (i = 0; i < favoriteGroup.length; ++i) {
|
||||||
if (favoriteGroup[i].id === worldId) {
|
if (favoriteGroup[i].id === worldId) {
|
||||||
favoriteGroup.splice(i, 1);
|
favoriteGroup.splice(i, 1);
|
||||||
@@ -1290,15 +1306,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
let worldInFavorites = false;
|
let worldInFavorites = false;
|
||||||
for (i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
for (i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
||||||
const groupName = localWorldFavoriteGroups.value[i];
|
const groupName = localWorldFavoriteGroups.value[i];
|
||||||
if (!localWorldFavorites.value[groupName] || group === groupName) {
|
if (!localWorldFavorites[groupName] || group === groupName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (
|
for (let j = 0; j < localWorldFavorites[groupName].length; ++j) {
|
||||||
let j = 0;
|
const id = localWorldFavorites[groupName][j].id;
|
||||||
j < localWorldFavorites.value[groupName].length;
|
|
||||||
++j
|
|
||||||
) {
|
|
||||||
const id = localWorldFavorites.value[groupName][j].id;
|
|
||||||
if (id === worldId) {
|
if (id === worldId) {
|
||||||
worldInFavorites = true;
|
worldInFavorites = true;
|
||||||
break;
|
break;
|
||||||
@@ -1369,7 +1381,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
groupsArr = ['Favorites'];
|
groupsArr = ['Favorites'];
|
||||||
}
|
}
|
||||||
|
|
||||||
localWorldFavorites.value = localFavorites;
|
replaceReactiveObject(localWorldFavorites, localFavorites);
|
||||||
|
|
||||||
sortLocalWorldFavorites();
|
sortLocalWorldFavorites();
|
||||||
}
|
}
|
||||||
@@ -1388,11 +1400,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!localWorldFavorites.value[group]) {
|
if (!localWorldFavorites[group]) {
|
||||||
localWorldFavorites.value[group] = [];
|
localWorldFavorites[group] = [];
|
||||||
}
|
|
||||||
if (!localWorldFavoriteGroups.value.includes(group)) {
|
|
||||||
localWorldFavoriteGroups.value.push(group);
|
|
||||||
}
|
}
|
||||||
sortLocalWorldFavorites();
|
sortLocalWorldFavorites();
|
||||||
}
|
}
|
||||||
@@ -1466,10 +1475,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
|
|
||||||
localWorldFavoriteGroups,
|
localWorldFavoriteGroups,
|
||||||
groupedByGroupKeyFavoriteFriends,
|
groupedByGroupKeyFavoriteFriends,
|
||||||
currentFavoriteTab,
|
selectedFavoriteFriends,
|
||||||
|
selectedFavoriteWorlds,
|
||||||
|
selectedFavoriteAvatars,
|
||||||
localWorldFavGroupLength,
|
localWorldFavGroupLength,
|
||||||
localAvatarFavGroupLength,
|
localAvatarFavGroupLength,
|
||||||
editFavoritesMode,
|
|
||||||
|
|
||||||
initFavorites,
|
initFavorites,
|
||||||
applyFavorite,
|
applyFavorite,
|
||||||
@@ -1477,7 +1487,6 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
refreshFavorites,
|
refreshFavorites,
|
||||||
applyFavoriteGroup,
|
applyFavoriteGroup,
|
||||||
refreshFavoriteAvatars,
|
refreshFavoriteAvatars,
|
||||||
clearBulkFavoriteSelection,
|
|
||||||
showWorldImportDialog,
|
showWorldImportDialog,
|
||||||
showAvatarImportDialog,
|
showAvatarImportDialog,
|
||||||
showFriendImportDialog,
|
showFriendImportDialog,
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ export const useUiStore = defineStore('Ui', () => {
|
|||||||
const notifiedMenus = ref([]);
|
const notifiedMenus = ref([]);
|
||||||
const shiftHeld = ref(false);
|
const shiftHeld = ref(false);
|
||||||
const trayIconNotify = ref(false);
|
const trayIconNotify = ref(false);
|
||||||
|
const socialRouteNames = ['friend-log', 'friend-list', 'moderation'];
|
||||||
|
const favoriteRouteNames = [
|
||||||
|
'favorite-friends',
|
||||||
|
'favorite-worlds',
|
||||||
|
'favorite-avatars'
|
||||||
|
];
|
||||||
|
const lastVisitedSocialRoute = ref(socialRouteNames[0]);
|
||||||
|
const lastVisitedFavoritesRoute = ref(favoriteRouteNames[0]);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => watchState.isLoggedIn,
|
() => watchState.isLoggedIn,
|
||||||
@@ -50,10 +58,16 @@ export const useUiStore = defineStore('Ui', () => {
|
|||||||
() => router.currentRoute.value.name,
|
() => router.currentRoute.value.name,
|
||||||
(routeName) => {
|
(routeName) => {
|
||||||
if (routeName) {
|
if (routeName) {
|
||||||
removeNotify(routeName);
|
const name = String(routeName);
|
||||||
if (routeName === 'notification') {
|
removeNotify(name);
|
||||||
|
if (name === 'notification') {
|
||||||
notificationStore.unseenNotifications = [];
|
notificationStore.unseenNotifications = [];
|
||||||
}
|
}
|
||||||
|
if (socialRouteNames.includes(name)) {
|
||||||
|
lastVisitedSocialRoute.value = name;
|
||||||
|
} else if (favoriteRouteNames.includes(name)) {
|
||||||
|
lastVisitedFavoritesRoute.value = name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -82,6 +96,8 @@ export const useUiStore = defineStore('Ui', () => {
|
|||||||
return {
|
return {
|
||||||
notifiedMenus,
|
notifiedMenus,
|
||||||
shiftHeld,
|
shiftHeld,
|
||||||
|
lastVisitedSocialRoute,
|
||||||
|
lastVisitedFavoritesRoute,
|
||||||
|
|
||||||
notifyMenu,
|
notifyMenu,
|
||||||
removeNotify
|
removeNotify
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="x-container">
|
|
||||||
<div class="header">
|
|
||||||
<div v-if="editFavoritesMode" style="display: inline-block; margin-right: 10px">
|
|
||||||
<el-button size="small" @click="clearBulkFavoriteSelection">{{ t('view.favorite.clear') }}</el-button>
|
|
||||||
<el-button size="small" @click="handleBulkCopyFavoriteSelection">{{
|
|
||||||
t('view.favorite.copy')
|
|
||||||
}}</el-button>
|
|
||||||
<el-button size="small" @click="showBulkUnfavoriteSelectionConfirm">{{
|
|
||||||
t('view.favorite.bulk_unfavorite')
|
|
||||||
}}</el-button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; margin-right: 10px">
|
|
||||||
<span class="name">{{ t('view.favorite.edit_mode') }}</span>
|
|
||||||
<el-switch v-model="editFavoritesMode" style="margin-left: 5px"></el-switch>
|
|
||||||
</div>
|
|
||||||
<el-tooltip placement="bottom" :content="t('view.favorite.refresh_favorites_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
type="default"
|
|
||||||
:loading="isFavoriteLoading"
|
|
||||||
size="small"
|
|
||||||
:icon="Refresh"
|
|
||||||
circle
|
|
||||||
@click="
|
|
||||||
refreshFavorites();
|
|
||||||
getLocalWorldFavorites();
|
|
||||||
"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<el-tabs v-model="currentFavoriteTab" v-loading="isFavoriteLoading" type="card" style="height: 100%">
|
|
||||||
<el-tab-pane name="friend" :label="t('view.favorite.friends.header')">
|
|
||||||
<FavoritesFriendTab @change-favorite-group-name="changeFavoriteGroupName" />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane name="world" :label="t('view.favorite.worlds.header')" lazy>
|
|
||||||
<FavoritesWorldTab @change-favorite-group-name="changeFavoriteGroupName" />
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane name="avatar" :label="t('view.favorite.avatars.header')" lazy>
|
|
||||||
<FavoritesAvatarTab @change-favorite-group-name="changeFavoriteGroupName" />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { Refresh } from '@element-plus/icons-vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { favoriteRequest } from '../../api';
|
|
||||||
import { useFavoriteStore } from '../../stores';
|
|
||||||
|
|
||||||
import FavoritesAvatarTab from './components/FavoritesAvatarTab.vue';
|
|
||||||
import FavoritesFriendTab from './components/FavoritesFriendTab.vue';
|
|
||||||
import FavoritesWorldTab from './components/FavoritesWorldTab.vue';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const {
|
|
||||||
favoriteFriends,
|
|
||||||
favoriteWorlds,
|
|
||||||
favoriteAvatars,
|
|
||||||
isFavoriteLoading,
|
|
||||||
avatarImportDialogInput,
|
|
||||||
worldImportDialogInput,
|
|
||||||
friendImportDialogInput,
|
|
||||||
currentFavoriteTab,
|
|
||||||
editFavoritesMode
|
|
||||||
} = storeToRefs(useFavoriteStore());
|
|
||||||
const {
|
|
||||||
refreshFavorites,
|
|
||||||
refreshFavoriteGroups,
|
|
||||||
clearBulkFavoriteSelection,
|
|
||||||
getLocalWorldFavorites,
|
|
||||||
handleFavoriteGroup,
|
|
||||||
showFriendImportDialog,
|
|
||||||
showWorldImportDialog,
|
|
||||||
showAvatarImportDialog
|
|
||||||
} = useFavoriteStore();
|
|
||||||
|
|
||||||
function showBulkUnfavoriteSelectionConfirm() {
|
|
||||||
const elementsTicked = [];
|
|
||||||
// check favorites type
|
|
||||||
for (const ctx of favoriteFriends.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
elementsTicked.push(ctx.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const ctx of favoriteWorlds.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
elementsTicked.push(ctx.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const ctx of favoriteAvatars.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
elementsTicked.push(ctx.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (elementsTicked.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`Are you sure you want to unfavorite ${elementsTicked.length} favorites?
|
|
||||||
This action cannot be undone.`,
|
|
||||||
`Delete ${elementsTicked.length} favorites?`,
|
|
||||||
{
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
bulkUnfavoriteSelection(elementsTicked);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bulkUnfavoriteSelection(elementsTicked) {
|
|
||||||
for (const id of elementsTicked) {
|
|
||||||
favoriteRequest.deleteFavorite({
|
|
||||||
objectId: id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
editFavoritesMode.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeFavoriteGroupName(ctx) {
|
|
||||||
ElMessageBox.prompt(
|
|
||||||
t('prompt.change_favorite_group_name.description'),
|
|
||||||
t('prompt.change_favorite_group_name.header'),
|
|
||||||
{
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
cancelButtonText: t('prompt.change_favorite_group_name.cancel'),
|
|
||||||
confirmButtonText: t('prompt.change_favorite_group_name.change'),
|
|
||||||
inputPlaceholder: t('prompt.change_favorite_group_name.input_placeholder'),
|
|
||||||
inputValue: ctx.displayName,
|
|
||||||
inputPattern: /\S+/,
|
|
||||||
inputErrorMessage: t('prompt.change_favorite_group_name.input_error')
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ value }) => {
|
|
||||||
favoriteRequest
|
|
||||||
.saveFavoriteGroup({
|
|
||||||
type: ctx.type,
|
|
||||||
group: ctx.name,
|
|
||||||
displayName: value
|
|
||||||
})
|
|
||||||
.then((args) => {
|
|
||||||
handleFavoriteGroup({
|
|
||||||
json: args.json,
|
|
||||||
params: {
|
|
||||||
favoriteGroupId: args.json.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ElMessage({
|
|
||||||
message: t('prompt.change_favorite_group_name.message.success'),
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
// load new group name
|
|
||||||
refreshFavoriteGroups();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBulkCopyFavoriteSelection() {
|
|
||||||
let idList = '';
|
|
||||||
switch (currentFavoriteTab.value) {
|
|
||||||
case 'friend':
|
|
||||||
for (const ctx of favoriteFriends.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
idList += `${ctx.id}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
friendImportDialogInput.value = idList;
|
|
||||||
showFriendImportDialog();
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'world':
|
|
||||||
for (const ctx of favoriteWorlds.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
idList += `${ctx.id}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
worldImportDialogInput.value = idList;
|
|
||||||
showWorldImportDialog();
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'avatar':
|
|
||||||
for (const ctx of favoriteAvatars.value) {
|
|
||||||
if (ctx.$selected) {
|
|
||||||
idList += `${ctx.id}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
avatarImportDialogInput.value = idList;
|
|
||||||
showAvatarImportDialog();
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Favorite selection\n', idList);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.header {
|
|
||||||
font-size: 13px;
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
1688
src/views/Favorites/FavoritesAvatar.vue
Normal file
1688
src/views/Favorites/FavoritesAvatar.vue
Normal file
File diff suppressed because it is too large
Load Diff
1040
src/views/Favorites/FavoritesFriend.vue
Normal file
1040
src/views/Favorites/FavoritesFriend.vue
Normal file
File diff suppressed because it is too large
Load Diff
1544
src/views/Favorites/FavoritesWorld.vue
Normal file
1544
src/views/Favorites/FavoritesWorld.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,132 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="$emit('click')">
|
<div :class="cardClasses" @click="$emit('click')">
|
||||||
<div class="x-friend-item">
|
<template v-if="localFavFakeRef">
|
||||||
<template v-if="isLocalFavorite ? favorite.name : favorite.ref">
|
<div class="favorites-search-card__content">
|
||||||
<div class="avatar">
|
<div class="favorites-search-card__avatar" :class="{ 'is-empty': !localFavFakeRef.thumbnailImageUrl }">
|
||||||
<img :src="smallThumbnail" loading="lazy" />
|
<img v-if="localFavFakeRef.thumbnailImageUrl" :src="smallThumbnail" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detail">
|
<div class="favorites-search-card__detail">
|
||||||
<span class="name" v-text="localFavFakeRef.name"></span>
|
<div class="favorites-search-card__title">
|
||||||
<span class="extra" v-text="localFavFakeRef.authorName"></span>
|
<span class="name">{{ localFavFakeRef.name }}</span>
|
||||||
|
<span class="favorites-search-card__badges">
|
||||||
|
<el-tooltip
|
||||||
|
v-if="favorite.deleted"
|
||||||
|
placement="top"
|
||||||
|
:content="t('view.favorite.unavailable_tooltip')">
|
||||||
|
<i class="ri-error-warning-line"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
|
v-if="!isLocalFavorite && favorite.ref?.releaseStatus === 'private'"
|
||||||
|
placement="top"
|
||||||
|
:content="t('view.favorite.private')">
|
||||||
|
<i class="ri-lock-line"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="extra">{{ localFavFakeRef.authorName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editFavoritesMode">
|
</div>
|
||||||
<FavoritesMoveDropdown
|
<div class="favorites-search-card__actions">
|
||||||
:favoriteGroup="favoriteAvatarGroups"
|
<template v-if="editMode">
|
||||||
:currentFavorite="props.favorite"
|
<div
|
||||||
:currentGroup="group"
|
v-if="!isLocalFavorite"
|
||||||
type="avatar" />
|
class="favorites-search-card__action favorites-search-card__action--checkbox"
|
||||||
<el-button v-if="!isLocalFavorite" type="text" size="small" style="margin-left: 5px" @click.stop>
|
@click.stop>
|
||||||
<el-checkbox v-model="isSelected"></el-checkbox>
|
<el-checkbox v-model="isSelected"></el-checkbox>
|
||||||
</el-button>
|
</div>
|
||||||
</div>
|
<div class="favorites-search-card__action-group">
|
||||||
<template v-else-if="!isLocalFavorite">
|
<div class="favorites-search-card__action favorites-search-card__action--full" @click.stop>
|
||||||
<el-tooltip
|
<FavoritesMoveDropdown
|
||||||
v-if="favorite.deleted"
|
:favoriteGroup="favoriteAvatarGroups"
|
||||||
placement="left"
|
:currentFavorite="props.favorite"
|
||||||
:content="t('view.favorite.unavailable_tooltip')"
|
:currentGroup="group"
|
||||||
:teleported="false">
|
class="favorites-search-card__dropdown"
|
||||||
<el-icon><Warning /></el-icon>
|
:is-local-favorite="isLocalFavorite"
|
||||||
</el-tooltip>
|
type="avatar" />
|
||||||
<el-tooltip
|
</div>
|
||||||
v-if="favorite.ref.releaseStatus === 'private'"
|
<div class="favorites-search-card__action">
|
||||||
placement="left"
|
<el-tooltip
|
||||||
:content="t('view.favorite.private')"
|
placement="left"
|
||||||
:teleported="false">
|
:content="
|
||||||
<el-icon><Warning /></el-icon>
|
isLocalFavorite
|
||||||
</el-tooltip>
|
? t('view.favorite.delete_tooltip')
|
||||||
<el-tooltip
|
: t('view.favorite.unfavorite_tooltip')
|
||||||
v-if="favorite.ref.releaseStatus !== 'private' && !favorite.deleted"
|
">
|
||||||
placement="left"
|
<el-button
|
||||||
:content="t('view.favorite.select_avatar_tooltip')"
|
size="small"
|
||||||
:teleported="false">
|
circle
|
||||||
<el-button
|
class="favorites-search-card__action-btn"
|
||||||
:disabled="currentUser.currentAvatar === favorite.id"
|
:type="isLocalFavorite ? 'default' : 'default'"
|
||||||
size="small"
|
@click.stop="handlePrimaryDeleteAction">
|
||||||
:icon="Check"
|
<i class="ri-delete-bin-line"></i>
|
||||||
circle
|
</el-button>
|
||||||
style="margin-left: 5px"
|
</el-tooltip>
|
||||||
@click.stop="selectAvatarWithConfirmation(favorite.id)"></el-button>
|
</div>
|
||||||
</el-tooltip>
|
</div>
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.unfavorite_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
v-if="shiftHeld"
|
|
||||||
size="small"
|
|
||||||
:icon="Close"
|
|
||||||
circle
|
|
||||||
style="color: #f56c6c; margin-left: 5px"
|
|
||||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="default"
|
|
||||||
:icon="Star"
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-tooltip
|
<div class="favorites-search-card__action-group">
|
||||||
placement="left"
|
<div class="favorites-search-card__action" v-if="canSelectAvatar">
|
||||||
:content="t('view.favorite.select_avatar_tooltip')"
|
<el-tooltip placement="top" :content="t('view.favorite.select_avatar_tooltip')">
|
||||||
:teleported="false">
|
<el-button
|
||||||
<el-button
|
:disabled="currentUser.currentAvatar === favorite.id"
|
||||||
:disabled="currentUser.currentAvatar === favorite.id"
|
size="small"
|
||||||
size="small"
|
:icon="Check"
|
||||||
circle
|
circle
|
||||||
style="margin-left: 5px"
|
class="favorites-search-card__action-btn"
|
||||||
:icon="Check"
|
@click.stop="selectAvatarWithConfirmation(favorite.id)" />
|
||||||
@click.stop="selectAvatarWithConfirmation(favorite.id)" />
|
</el-tooltip>
|
||||||
</el-tooltip>
|
</div>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="bottom" :content="t('view.favorite.unfavorite_tooltip')">
|
||||||
|
<el-button
|
||||||
|
v-if="showDangerUnfavorite"
|
||||||
|
size="small"
|
||||||
|
:icon="Close"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
type="danger"
|
||||||
|
@click.stop="handlePrimaryDeleteAction" />
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="default"
|
||||||
|
:icon="Star"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
@click.stop="showFavoriteDialog('avatar', favorite.id)" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-tooltip
|
</div>
|
||||||
v-if="isLocalFavorite"
|
</template>
|
||||||
placement="right"
|
<template v-else>
|
||||||
:content="t('view.favorite.unfavorite_tooltip')"
|
<div class="favorites-search-card__content">
|
||||||
:teleported="false">
|
<div class="favorites-search-card__avatar is-empty"></div>
|
||||||
<el-button
|
<div class="favorites-search-card__detail">
|
||||||
v-if="shiftHeld"
|
<span class="name">{{ favorite.name || favorite.id }}</span>
|
||||||
size="small"
|
|
||||||
:icon="Close"
|
|
||||||
circle
|
|
||||||
style="color: #f56c6c; margin-left: 5px"
|
|
||||||
@click.stop="removeLocalAvatarFavorite(favorite.id, favoriteGroupName)" />
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="default"
|
|
||||||
:icon="Star"
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="showFavoriteDialog('avatar', favorite.id)" />
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="avatar"></div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name" v-text="favorite.name || favorite.id"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
</div>
|
||||||
v-if="isLocalFavorite"
|
<div class="favorites-search-card__actions">
|
||||||
type="text"
|
<div class="favorites-search-card__action">
|
||||||
:icon="Close"
|
<el-button circle type="default" size="small" @click.stop="handlePrimaryDeleteAction">
|
||||||
size="small"
|
<i class="ri-delete-bin-line"></i>
|
||||||
style="margin-left: 5px"
|
</el-button>
|
||||||
@click.stop="removeLocalAvatarFavorite(favorite.id, favoriteGroupName)"></el-button>
|
</div>
|
||||||
<el-button
|
</div>
|
||||||
v-else
|
</template>
|
||||||
type="text"
|
|
||||||
:icon="Close"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Check, Close, Star, Warning } from '@element-plus/icons-vue';
|
import { Check, Close, Star } from '@element-plus/icons-vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -139,35 +133,74 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
favorite: Object,
|
favorite: Object,
|
||||||
group: [Object, String],
|
group: [Object, String],
|
||||||
isLocalFavorite: Boolean
|
isLocalFavorite: Boolean,
|
||||||
|
editMode: { type: Boolean, default: false },
|
||||||
|
selected: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['click', 'handle-select']);
|
const emit = defineEmits(['click', 'toggle-select']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { favoriteAvatarGroups, editFavoritesMode } = storeToRefs(useFavoriteStore());
|
const { favoriteAvatarGroups } = storeToRefs(useFavoriteStore());
|
||||||
const { removeLocalAvatarFavorite, showFavoriteDialog } = useFavoriteStore();
|
const { removeLocalAvatarFavorite, showFavoriteDialog } = useFavoriteStore();
|
||||||
const { selectAvatarWithConfirmation } = useAvatarStore();
|
const { selectAvatarWithConfirmation } = useAvatarStore();
|
||||||
const { shiftHeld } = storeToRefs(useUiStore());
|
const { shiftHeld } = storeToRefs(useUiStore());
|
||||||
const { currentUser } = storeToRefs(useUserStore());
|
const { currentUser } = storeToRefs(useUserStore());
|
||||||
|
|
||||||
const isSelected = computed({
|
const isSelected = computed({
|
||||||
get: () => props.favorite.$selected,
|
get: () => props.selected,
|
||||||
set: (value) => emit('handle-select', value)
|
set: (value) => emit('toggle-select', value)
|
||||||
|
});
|
||||||
|
const localFavFakeRef = computed(() => (props.isLocalFavorite ? props.favorite : props.favorite?.ref));
|
||||||
|
|
||||||
|
const cardClasses = computed(() => [
|
||||||
|
'favorites-search-card',
|
||||||
|
'favorites-search-card--avatar',
|
||||||
|
{
|
||||||
|
'is-selected': props.selected,
|
||||||
|
'is-edit-mode': props.editMode
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const smallThumbnail = computed(() => {
|
||||||
|
if (!localFavFakeRef.value?.thumbnailImageUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return localFavFakeRef.value.thumbnailImageUrl.replace('256', '128');
|
||||||
});
|
});
|
||||||
const localFavFakeRef = computed(() => (props.isLocalFavorite ? props.favorite : props.favorite.ref));
|
|
||||||
|
|
||||||
const smallThumbnail = computed(
|
|
||||||
() => localFavFakeRef.value.thumbnailImageUrl?.replace('256', '128') || localFavFakeRef.value.thumbnailImageUrl
|
|
||||||
);
|
|
||||||
const favoriteGroupName = computed(() => {
|
const favoriteGroupName = computed(() => {
|
||||||
if (typeof props.group === 'string') {
|
if (typeof props.group === 'string') {
|
||||||
return props.group;
|
return props.group;
|
||||||
} else {
|
|
||||||
return props.group?.name;
|
|
||||||
}
|
}
|
||||||
|
return props.group?.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canSelectAvatar = computed(() => {
|
||||||
|
if (props.isLocalFavorite) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.favorite?.deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return props.favorite?.ref?.releaseStatus !== 'private';
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDangerUnfavorite = computed(() => {
|
||||||
|
if (props.isLocalFavorite) {
|
||||||
|
return shiftHeld.value;
|
||||||
|
}
|
||||||
|
return shiftHeld.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handlePrimaryDeleteAction() {
|
||||||
|
if (props.isLocalFavorite) {
|
||||||
|
removeLocalAvatarFavorite(props.favorite.id, favoriteGroupName.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteFavorite(props.favorite.id);
|
||||||
|
}
|
||||||
|
|
||||||
function deleteFavorite(objectId) {
|
function deleteFavorite(objectId) {
|
||||||
favoriteRequest.deleteFavorite({ objectId });
|
favoriteRequest.deleteFavorite({ objectId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="$emit('click')">
|
<div :class="cardClasses" @click="$emit('click')">
|
||||||
<div class="x-friend-item">
|
<div class="favorites-search-card__content">
|
||||||
<div class="avatar">
|
<div class="favorites-search-card__avatar" :class="{ 'is-empty': !favorite.thumbnailImageUrl }">
|
||||||
<img :src="smallThumbnail" loading="lazy" />
|
<img v-if="favorite.thumbnailImageUrl" :src="smallThumbnail" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detail">
|
<div class="favorites-search-card__detail">
|
||||||
<span class="name" v-text="favorite.name"></span>
|
<div class="favorites-search-card__title">
|
||||||
<span class="extra" v-text="favorite.authorName"></span>
|
<span class="name">{{ favorite.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="extra">{{ favorite.authorName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__actions">
|
||||||
|
<div class="favorites-search-card__action-group">
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="top" :content="t('view.favorite.select_avatar_tooltip')">
|
||||||
|
<el-button
|
||||||
|
:disabled="currentUser.currentAvatar === favorite.id"
|
||||||
|
size="small"
|
||||||
|
:icon="Check"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
@click.stop="selectAvatarWithConfirmation(favorite.id)" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="bottom" :content="t('view.favorite.favorite_tooltip')">
|
||||||
|
<el-button
|
||||||
|
type="default"
|
||||||
|
:icon="favoriteExists ? Star : StarFilled"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
@click.stop="showFavoriteDialog('avatar', favorite.id)" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tooltip placement="left" :content="t('view.favorite.select_avatar_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
:disabled="currentUser.currentAvatar === favorite.id"
|
|
||||||
size="small"
|
|
||||||
:icon="Check"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="selectAvatarWithConfirmation(favorite.id)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<template v-if="getCachedFavoritesByObjectId(favorite.id)">
|
|
||||||
<el-tooltip placement="right" content="Favorite" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
type="default"
|
|
||||||
:icon="Star"
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<el-tooltip placement="right" content="Favorite" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
type="default"
|
|
||||||
:icon="StarFilled"
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,7 +63,14 @@
|
|||||||
|
|
||||||
defineEmits(['click']);
|
defineEmits(['click']);
|
||||||
|
|
||||||
|
const favoriteExists = computed(() => Boolean(getCachedFavoritesByObjectId(props.favorite.id)));
|
||||||
|
|
||||||
|
const cardClasses = computed(() => ['favorites-search-card', 'favorites-search-card--avatar']);
|
||||||
|
|
||||||
const smallThumbnail = computed(() => {
|
const smallThumbnail = computed(() => {
|
||||||
return props.favorite.thumbnailImageUrl?.replace('256', '128') || props.favorite.thumbnailImageUrl;
|
if (!props.favorite.thumbnailImageUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return props.favorite.thumbnailImageUrl.replace('256', '128');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,475 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
|
||||||
<div>
|
|
||||||
<el-button size="small" @click="showAvatarExportDialog">
|
|
||||||
{{ t('view.favorite.export') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button size="small" style="margin-left: 5px" @click="showAvatarImportDialog">
|
|
||||||
{{ t('view.favorite.import') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
|
||||||
<span class="name" style="margin-right: 5px; line-height: 10px">
|
|
||||||
{{ t('view.favorite.sort_by') }}
|
|
||||||
</span>
|
|
||||||
<el-radio-group v-model="sortFav" style="margin-right: 12px">
|
|
||||||
<el-radio :label="false">
|
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
|
||||||
</el-radio>
|
|
||||||
<el-radio :label="true">
|
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
|
||||||
</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
<el-input
|
|
||||||
v-model="avatarFavoriteSearch"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
:placeholder="t('view.favorite.avatars.search')"
|
|
||||||
style="width: 200px"
|
|
||||||
@input="searchAvatarFavorites" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<div
|
|
||||||
v-for="favorite in avatarFavoriteSearchResults"
|
|
||||||
:key="favorite.id"
|
|
||||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
|
||||||
@click="showAvatarDialog(favorite.id)">
|
|
||||||
<div class="x-friend-item">
|
|
||||||
<template v-if="favorite.name">
|
|
||||||
<div class="avatar">
|
|
||||||
<img :src="favorite.thumbnailImageUrl" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name" v-text="favorite.name" />
|
|
||||||
<span class="extra" v-text="favorite.authorName" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="avatar"></div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name" v-text="favorite.id" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span style="display: block; margin-top: 20px">
|
|
||||||
{{ t('view.favorite.avatars.vrchat_favorites') }}
|
|
||||||
</span>
|
|
||||||
<el-collapse style="border: 0">
|
|
||||||
<el-collapse-item v-for="group in favoriteAvatarGroups" :key="group.name">
|
|
||||||
<template #title>
|
|
||||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px" v-text="group.displayName" />
|
|
||||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">
|
|
||||||
{{ group.count }}/{{ group.capacity }}
|
|
||||||
</span>
|
|
||||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Edit"
|
|
||||||
circle
|
|
||||||
style="margin-left: 10px"
|
|
||||||
@click.stop="changeFavoriteGroupName(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="clearFavoriteGroup(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<FavoritesAvatarItem
|
|
||||||
v-for="favorite in groupedByGroupKeyFavoriteAvatars[group.key]"
|
|
||||||
:key="favorite.id"
|
|
||||||
:favorite="favorite"
|
|
||||||
:group="group"
|
|
||||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
|
||||||
@handle-select="favorite.$selected = $event"
|
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
style="
|
|
||||||
padding-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(144, 147, 153);
|
|
||||||
">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
<el-collapse-item>
|
|
||||||
<template #title>
|
|
||||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px">Local History</span>
|
|
||||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
|
||||||
>{{ avatarHistory.length }}/100</span
|
|
||||||
>
|
|
||||||
<el-tooltip placement="right" content="Clear" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="promptClearAvatarHistory"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<div v-if="avatarHistory.length" class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<FavoritesAvatarLocalHistoryItem
|
|
||||||
v-for="favorite in avatarHistory"
|
|
||||||
:key="favorite.id"
|
|
||||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
|
||||||
:favorite="favorite"
|
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
style="
|
|
||||||
padding-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(144, 147, 153);
|
|
||||||
">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
<span style="display: block; margin-top: 20px">{{ t('view.favorite.avatars.local_favorites') }}</span>
|
|
||||||
<br />
|
|
||||||
<el-button size="small" :disabled="!isLocalUserVrcPlusSupporter" @click="promptNewLocalAvatarFavoriteGroup">
|
|
||||||
{{ t('view.favorite.avatars.new_group') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="!refreshingLocalFavorites"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click="refreshLocalAvatarFavorites">
|
|
||||||
{{ t('view.favorite.avatars.refresh') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button v-else size="small" style="margin-left: 5px" @click="cancelLocalAvatarRefresh">
|
|
||||||
<el-icon class="is-loading"><Loading /></el-icon>
|
|
||||||
<span>{{ t('view.favorite.avatars.cancel_refresh') }}</span>
|
|
||||||
</el-button>
|
|
||||||
<el-collapse-item v-for="group in localAvatarFavoriteGroups" :key="group">
|
|
||||||
<template #title v-if="localAvatarFavorites[group]">
|
|
||||||
<span :style="{ fontWeight: 'bold', fontSize: '14px', marginLeft: '10px' }">{{ group }}</span>
|
|
||||||
<span :style="{ color: '#909399', fontSize: '12px', marginLeft: '10px' }">{{
|
|
||||||
localAvatarFavGroupLength(group)
|
|
||||||
}}</span>
|
|
||||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Edit"
|
|
||||||
circle
|
|
||||||
:style="{ marginLeft: '5px' }"
|
|
||||||
@click.stop="promptLocalAvatarFavoriteGroupRename(group)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
:style="{ marginLeft: '5px' }"
|
|
||||||
@click.stop="promptLocalAvatarFavoriteGroupDelete(group)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<div v-if="localAvatarFavorites[group]?.length" class="x-friend-list" :style="{ marginTop: '10px' }">
|
|
||||||
<FavoritesAvatarItem
|
|
||||||
v-for="favorite in localAvatarFavorites[group]"
|
|
||||||
:key="favorite.id"
|
|
||||||
is-local-favorite
|
|
||||||
:style="{ display: 'inline-block', width: '300px', marginRight: '15px' }"
|
|
||||||
:favorite="favorite"
|
|
||||||
:group="group"
|
|
||||||
@handle-select="favorite.$selected = $event"
|
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:style="{
|
|
||||||
paddingTop: '25px',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'rgb(144, 147, 153)'
|
|
||||||
}">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Delete, Edit, Loading } from '@element-plus/icons-vue';
|
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
||||||
import { ElMessageBox } from 'element-plus';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
|
|
||||||
import { avatarRequest, favoriteRequest } from '../../../api';
|
|
||||||
|
|
||||||
import AvatarExportDialog from '../dialogs/AvatarExportDialog.vue';
|
|
||||||
import FavoritesAvatarItem from './FavoritesAvatarItem.vue';
|
|
||||||
import FavoritesAvatarLocalHistoryItem from './FavoritesAvatarLocalHistoryItem.vue';
|
|
||||||
|
|
||||||
import * as workerTimers from 'worker-timers';
|
|
||||||
|
|
||||||
const emit = defineEmits(['change-favorite-group-name', 'refresh-local-avatar-favorites']);
|
|
||||||
|
|
||||||
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
|
||||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
|
||||||
const { favoriteAvatars, favoriteAvatarGroups, localAvatarFavorites } = storeToRefs(useFavoriteStore());
|
|
||||||
const {
|
|
||||||
showAvatarImportDialog,
|
|
||||||
localAvatarFavGroupLength,
|
|
||||||
deleteLocalAvatarFavoriteGroup,
|
|
||||||
renameLocalAvatarFavoriteGroup,
|
|
||||||
newLocalAvatarFavoriteGroup,
|
|
||||||
localAvatarFavoritesList,
|
|
||||||
localAvatarFavoriteGroups
|
|
||||||
} = useFavoriteStore();
|
|
||||||
const { avatarHistory } = storeToRefs(useAvatarStore());
|
|
||||||
const { promptClearAvatarHistory, showAvatarDialog, applyAvatar } = useAvatarStore();
|
|
||||||
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const avatarExportDialogVisible = ref(false);
|
|
||||||
const avatarFavoriteSearch = ref('');
|
|
||||||
const avatarFavoriteSearchResults = ref([]);
|
|
||||||
const refreshingLocalFavorites = ref(false);
|
|
||||||
const worker = ref(null);
|
|
||||||
const refreshCancelToken = ref(null);
|
|
||||||
|
|
||||||
const sortFav = computed({
|
|
||||||
get() {
|
|
||||||
return sortFavorites.value;
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
setSortFavorites();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupedByGroupKeyFavoriteAvatars = computed(() => {
|
|
||||||
const groupedByGroupKeyFavoriteAvatars = {};
|
|
||||||
favoriteAvatars.value.forEach((avatar) => {
|
|
||||||
if (avatar.groupKey) {
|
|
||||||
if (!groupedByGroupKeyFavoriteAvatars[avatar.groupKey]) {
|
|
||||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey] = [];
|
|
||||||
}
|
|
||||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey].push(avatar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return groupedByGroupKeyFavoriteAvatars;
|
|
||||||
});
|
|
||||||
|
|
||||||
function searchAvatarFavorites() {
|
|
||||||
let ref = null;
|
|
||||||
const search = avatarFavoriteSearch.value.toLowerCase();
|
|
||||||
if (search.length < 3) {
|
|
||||||
avatarFavoriteSearchResults.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0; i < localAvatarFavoriteGroups.length; ++i) {
|
|
||||||
const group = localAvatarFavoriteGroups[i];
|
|
||||||
if (!localAvatarFavorites.value[group]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let j = 0; j < localAvatarFavorites.value[group].length; ++j) {
|
|
||||||
ref = localAvatarFavorites.value[group][j];
|
|
||||||
if (
|
|
||||||
!ref ||
|
|
||||||
typeof ref.id === 'undefined' ||
|
|
||||||
typeof ref.name === 'undefined' ||
|
|
||||||
typeof ref.authorName === 'undefined'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
|
||||||
if (!results.some((r) => r.id === ref.id)) {
|
|
||||||
results.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < favoriteAvatars.value.length; ++i) {
|
|
||||||
ref = favoriteAvatars.value[i].ref;
|
|
||||||
if (
|
|
||||||
!ref ||
|
|
||||||
typeof ref.id === 'undefined' ||
|
|
||||||
typeof ref.name === 'undefined' ||
|
|
||||||
typeof ref.authorName === 'undefined'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
|
||||||
if (!results.some((r) => r.id === ref.id)) {
|
|
||||||
results.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarFavoriteSearchResults.value = results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFavoriteGroup(ctx) {
|
|
||||||
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
favoriteRequest.clearFavoriteGroup({
|
|
||||||
type: ctx.type,
|
|
||||||
group: ctx.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAvatarExportDialog() {
|
|
||||||
avatarExportDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeFavoriteGroupName(group) {
|
|
||||||
emit('change-favorite-group-name', group);
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptNewLocalAvatarFavoriteGroup() {
|
|
||||||
ElMessageBox.prompt(
|
|
||||||
t('prompt.new_local_favorite_group.description'),
|
|
||||||
t('prompt.new_local_favorite_group.header'),
|
|
||||||
{
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: t('prompt.new_local_favorite_group.ok'),
|
|
||||||
cancelButtonText: t('prompt.new_local_favorite_group.cancel'),
|
|
||||||
inputPattern: /\S+/,
|
|
||||||
inputErrorMessage: t('prompt.new_local_favorite_group.input_error')
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ value }) => {
|
|
||||||
if (value) {
|
|
||||||
newLocalAvatarFavoriteGroup(value);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptLocalAvatarFavoriteGroupRename(group) {
|
|
||||||
ElMessageBox.prompt(
|
|
||||||
t('prompt.local_favorite_group_rename.description'),
|
|
||||||
t('prompt.local_favorite_group_rename.header'),
|
|
||||||
{
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: t('prompt.local_favorite_group_rename.save'),
|
|
||||||
cancelButtonText: t('prompt.local_favorite_group_rename.cancel'),
|
|
||||||
inputPattern: /\S+/,
|
|
||||||
inputErrorMessage: t('prompt.local_favorite_group_rename.input_error'),
|
|
||||||
inputValue: group
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ value }) => {
|
|
||||||
if (value) {
|
|
||||||
renameLocalAvatarFavoriteGroup(value, group);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptLocalAvatarFavoriteGroupDelete(group) {
|
|
||||||
ElMessageBox.confirm(`Delete Group? ${group}`, 'Confirm', {
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
deleteLocalAvatarFavoriteGroup(group);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshLocalAvatarFavorites() {
|
|
||||||
if (refreshingLocalFavorites.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = true;
|
|
||||||
const token = {
|
|
||||||
cancelled: false,
|
|
||||||
resolve: null
|
|
||||||
};
|
|
||||||
refreshCancelToken.value = token;
|
|
||||||
try {
|
|
||||||
for (const avatarId of localAvatarFavoritesList) {
|
|
||||||
if (token.cancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const args = await avatarRequest.getAvatar({
|
|
||||||
avatarId
|
|
||||||
});
|
|
||||||
applyAvatar(args.json);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
if (token.cancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
token.resolve = resolve;
|
|
||||||
worker.value = workerTimers.setTimeout(() => {
|
|
||||||
worker.value = null;
|
|
||||||
resolve();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (worker.value) {
|
|
||||||
workerTimers.clearTimeout(worker.value);
|
|
||||||
worker.value = null;
|
|
||||||
}
|
|
||||||
if (refreshCancelToken.value === token) {
|
|
||||||
refreshCancelToken.value = null;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelLocalAvatarRefresh() {
|
|
||||||
if (!refreshingLocalFavorites.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (refreshCancelToken.value) {
|
|
||||||
refreshCancelToken.value.cancelled = true;
|
|
||||||
if (typeof refreshCancelToken.value.resolve === 'function') {
|
|
||||||
refreshCancelToken.value.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (worker.value) {
|
|
||||||
workerTimers.clearTimeout(worker.value);
|
|
||||||
worker.value = null;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
cancelLocalAvatarRefresh();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,85 +1,134 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="$emit('click')">
|
<div :class="cardClasses" @click="$emit('click')">
|
||||||
<div class="x-friend-item">
|
<template v-if="favorite.ref">
|
||||||
<template v-if="favorite.ref">
|
<div class="favorites-search-card__content">
|
||||||
<div class="avatar" :class="userStatusClass(favorite.ref)">
|
<div class="favorites-search-card__avatar">
|
||||||
<img :src="userImage(favorite.ref, true)" loading="lazy" />
|
<img :src="userImage(favorite.ref, true)" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detail">
|
<div class="favorites-search-card__detail">
|
||||||
<span
|
<div class="favorites-search-card__title">
|
||||||
class="name"
|
<span class="name" :style="displayNameStyle">{{ favorite.ref.displayName }}</span>
|
||||||
:style="{ color: favorite.ref.$userColour }"
|
</div>
|
||||||
v-text="favorite.ref.displayName"></span>
|
<div v-if="favorite.ref.location !== 'offline'" class="favorites-search-card__location">
|
||||||
<Location
|
<Location
|
||||||
class="extra"
|
:location="favorite.ref.location"
|
||||||
v-if="favorite.ref.location !== 'offline'"
|
:traveling="favorite.ref.travelingToLocation"
|
||||||
:location="favorite.ref.location"
|
:link="false" />
|
||||||
:traveling="favorite.ref.travelingToLocation"
|
</div>
|
||||||
:link="false" />
|
<span v-else class="extra">{{ favorite.ref.statusDescription }}</span>
|
||||||
<span v-else v-text="favorite.ref.statusDescription"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editFavoritesMode">
|
</div>
|
||||||
<FavoritesMoveDropdown
|
<div class="favorites-search-card__actions">
|
||||||
:favoriteGroup="favoriteFriendGroups"
|
<template v-if="editMode">
|
||||||
:currentGroup="group"
|
<div class="favorites-search-card__action favorites-search-card__action--checkbox" @click.stop>
|
||||||
:currentFavorite="favorite"
|
<el-checkbox v-model="isSelected"></el-checkbox>
|
||||||
type="friend" />
|
</div>
|
||||||
<el-button type="text" size="small" style="margin-left: 5px" @click.stop>
|
<div class="favorites-search-card__action-group">
|
||||||
<el-checkbox v-model="favorite.$selected"></el-checkbox>
|
<div class="favorites-search-card__action favorites-search-card__action--full" @click.stop>
|
||||||
|
<FavoritesMoveDropdown
|
||||||
|
:favoriteGroup="favoriteFriendGroups"
|
||||||
|
:currentGroup="group"
|
||||||
|
:currentFavorite="favorite"
|
||||||
|
class="favorites-search-card__dropdown"
|
||||||
|
type="friend" />
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="left" :content="t('view.favorite.unfavorite_tooltip')">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
type="default"
|
||||||
|
@click.stop="handleDeleteFavorite">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="right" :content="t('view.favorite.unfavorite_tooltip')">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:icon="Star"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
@click.stop="showFavoriteDialog('friend', favorite.id)" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-search-card__content">
|
||||||
|
<div class="favorites-search-card__avatar is-empty"></div>
|
||||||
|
<div class="favorites-search-card__detail">
|
||||||
|
<span class="name">{{ favorite.name || favorite.id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__actions">
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-button circle type="default" size="small" @click.stop="handleDeleteFavorite">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<template v-else>
|
</template>
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.unfavorite_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
type="default"
|
|
||||||
:icon="Star"
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="showFavoriteDialog('friend', favorite.id)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="avatar"></div>
|
|
||||||
<div class="detail">
|
|
||||||
<span v-text="favorite.name || favorite.id"></span>
|
|
||||||
</div>
|
|
||||||
<el-button
|
|
||||||
type="text"
|
|
||||||
:icon="Close"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Close, Star } from '@element-plus/icons-vue';
|
import { Star } from '@element-plus/icons-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { userImage, userStatusClass } from '../../../shared/utils';
|
|
||||||
import { favoriteRequest } from '../../../api';
|
import { favoriteRequest } from '../../../api';
|
||||||
import { useFavoriteStore } from '../../../stores';
|
import { useFavoriteStore } from '../../../stores';
|
||||||
|
import { userImage } from '../../../shared/utils';
|
||||||
|
|
||||||
import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue';
|
import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
favorite: { type: Object, required: true },
|
favorite: { type: Object, required: true },
|
||||||
group: { type: Object, required: true }
|
group: { type: Object, default: null },
|
||||||
|
editMode: { type: Boolean, default: false },
|
||||||
|
selected: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['click']);
|
const emit = defineEmits(['click', 'toggle-select']);
|
||||||
|
|
||||||
const { favoriteFriendGroups, editFavoritesMode } = storeToRefs(useFavoriteStore());
|
const { favoriteFriendGroups } = storeToRefs(useFavoriteStore());
|
||||||
const { showFavoriteDialog } = useFavoriteStore();
|
const { showFavoriteDialog } = useFavoriteStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
function deleteFavorite(objectId) {
|
const isSelected = computed({
|
||||||
favoriteRequest.deleteFavorite({ objectId });
|
get: () => props.selected,
|
||||||
|
set: (value) => emit('toggle-select', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardClasses = computed(() => [
|
||||||
|
'favorites-search-card',
|
||||||
|
'favorites-search-card--friend',
|
||||||
|
{
|
||||||
|
'is-selected': props.selected,
|
||||||
|
'is-edit-mode': props.editMode
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const displayNameStyle = computed(() => {
|
||||||
|
if (props.favorite?.ref?.$userColour) {
|
||||||
|
return {
|
||||||
|
color: props.favorite.ref.$userColour
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDeleteFavorite() {
|
||||||
|
favoriteRequest.deleteFavorite({
|
||||||
|
objectId: props.favorite.id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
|
||||||
<div>
|
|
||||||
<el-button size="small" @click="showFriendExportDialog">{{ t('view.favorite.export') }}</el-button>
|
|
||||||
<el-button size="small" style="margin-left: 5px" @click="showFriendImportDialog">{{
|
|
||||||
t('view.favorite.import')
|
|
||||||
}}</el-button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
|
||||||
<span class="name" style="margin-right: 5px; line-height: 10px">{{ t('view.favorite.sort_by') }}</span>
|
|
||||||
<el-radio-group v-model="sortFav">
|
|
||||||
<el-radio :label="false">{{
|
|
||||||
t('view.settings.appearance.appearance.sort_favorite_by_name')
|
|
||||||
}}</el-radio>
|
|
||||||
<el-radio :label="true">{{
|
|
||||||
t('view.settings.appearance.appearance.sort_favorite_by_date')
|
|
||||||
}}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span style="display: block; margin-top: 30px">{{ t('view.favorite.avatars.vrchat_favorites') }}</span>
|
|
||||||
<el-collapse style="border: 0">
|
|
||||||
<el-collapse-item v-for="group in favoriteFriendGroups" :key="group.name">
|
|
||||||
<template #title>
|
|
||||||
<span
|
|
||||||
style="font-weight: bold; font-size: 14px; margin-left: 10px"
|
|
||||||
v-text="group.displayName"></span>
|
|
||||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
|
||||||
>{{ group.count }}/{{ group.capacity }}</span
|
|
||||||
>
|
|
||||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Edit"
|
|
||||||
circle
|
|
||||||
style="margin-left: 10px"
|
|
||||||
@click.stop="changeFavoriteGroupName(group)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="clearFavoriteGroup(group)"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<FavoritesFriendItem
|
|
||||||
v-for="favorite in groupedByGroupKeyFavoriteFriends[group.key]"
|
|
||||||
:key="favorite.id"
|
|
||||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
|
||||||
:favorite="favorite"
|
|
||||||
:group="group"
|
|
||||||
@click="showUserDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
style="
|
|
||||||
padding-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(144, 147, 153);
|
|
||||||
">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Delete, Edit } from '@element-plus/icons-vue';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { ElMessageBox } from 'element-plus';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { useAppearanceSettingsStore, useFavoriteStore, useUserStore } from '../../../stores';
|
|
||||||
import { favoriteRequest } from '../../../api';
|
|
||||||
|
|
||||||
import FavoritesFriendItem from './FavoritesFriendItem.vue';
|
|
||||||
import FriendExportDialog from '../dialogs/FriendExportDialog.vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['change-favorite-group-name']);
|
|
||||||
|
|
||||||
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
|
||||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
|
||||||
const { showUserDialog } = useUserStore();
|
|
||||||
const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends } = storeToRefs(useFavoriteStore());
|
|
||||||
const { showFriendImportDialog } = useFavoriteStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const friendExportDialogVisible = ref(false);
|
|
||||||
|
|
||||||
const sortFav = computed({
|
|
||||||
get() {
|
|
||||||
return sortFavorites.value;
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
setSortFavorites();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function showFriendExportDialog() {
|
|
||||||
friendExportDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFavoriteGroup(ctx) {
|
|
||||||
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
favoriteRequest.clearFavoriteGroup({
|
|
||||||
type: ctx.type,
|
|
||||||
group: ctx.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeFavoriteGroupName(group) {
|
|
||||||
emit('change-favorite-group-name', group);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,68 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fav-world-item" @click="$emit('click')">
|
<div :class="cardClasses" @click="$emit('click')">
|
||||||
<div class="x-friend-item">
|
<template v-if="favorite.ref">
|
||||||
<template v-if="favorite.ref">
|
<div class="favorites-search-card__content">
|
||||||
<div class="avatar" v-once>
|
<div
|
||||||
<img :src="smallThumbnail" loading="lazy" decoding="async" fetchpriority="low" />
|
class="favorites-search-card__avatar"
|
||||||
|
:class="{ 'is-empty': !favorite.ref.thumbnailImageUrl }"
|
||||||
|
v-once>
|
||||||
|
<img
|
||||||
|
v-if="favorite.ref.thumbnailImageUrl"
|
||||||
|
:src="smallThumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchpriority="low" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detail" v-once>
|
<div class="favorites-search-card__detail" v-once>
|
||||||
<span class="name">{{ props.favorite.ref.name }}</span>
|
<div class="favorites-search-card__title">
|
||||||
<span v-if="props.favorite.ref.occupants" class="extra">
|
<span class="name">{{ props.favorite.ref.name }}</span>
|
||||||
{{ props.favorite.ref.authorName }} ({{ props.favorite.ref.occupants }})
|
<span
|
||||||
|
v-if="favorite.deleted || favorite.ref.releaseStatus === 'private'"
|
||||||
|
class="favorites-search-card__badges">
|
||||||
|
<i
|
||||||
|
v-if="favorite.deleted"
|
||||||
|
:title="t('view.favorite.unavailable_tooltip')"
|
||||||
|
class="ri-error-warning-line"></i>
|
||||||
|
<i
|
||||||
|
v-if="favorite.ref.releaseStatus === 'private'"
|
||||||
|
:title="t('view.favorite.private')"
|
||||||
|
class="ri-lock-line"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="extra">
|
||||||
|
{{ props.favorite.ref.authorName }}
|
||||||
|
<template v-if="props.favorite.ref.occupants"> ({{ props.favorite.ref.occupants }}) </template>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="extra">{{ props.favorite.ref.authorName }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editFavoritesMode">
|
</div>
|
||||||
<FavoritesMoveDropdown
|
<div class="favorites-search-card__actions">
|
||||||
:favoriteGroup="favoriteWorldGroups"
|
<template v-if="editMode">
|
||||||
:currentFavorite="props.favorite"
|
<div class="favorites-search-card__action favorites-search-card__action--checkbox" @click.stop>
|
||||||
:currentGroup="group"
|
|
||||||
type="world" />
|
|
||||||
<el-button type="text" size="small" @click.stop style="margin-left: 5px">
|
|
||||||
<el-checkbox v-model="isSelected"></el-checkbox>
|
<el-checkbox v-model="isSelected"></el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__action-group">
|
||||||
|
<div class="favorites-search-card__action favorites-search-card__action--full" @click.stop>
|
||||||
|
<FavoritesMoveDropdown
|
||||||
|
:favoriteGroup="favoriteWorldGroups"
|
||||||
|
:currentFavorite="props.favorite"
|
||||||
|
:currentGroup="group"
|
||||||
|
class="favorites-search-card__dropdown"
|
||||||
|
type="world" />
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
type="default"
|
||||||
|
@click.stop="handleDeleteFavorite">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-tooltip placement="top" :content="inviteOrLaunchText">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:icon="Message"
|
||||||
|
class="favorites-search-card__action-btn"
|
||||||
|
@click.stop="newInstanceSelfInvite(favorite.id)"
|
||||||
|
circle />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-search-card__content">
|
||||||
|
<div class="favorites-search-card__avatar is-empty"></div>
|
||||||
|
<div class="favorites-search-card__detail" v-once>
|
||||||
|
<span class="name">{{ favorite.name || favorite.id }}</span>
|
||||||
|
<i
|
||||||
|
v-if="favorite.deleted"
|
||||||
|
:title="t('view.favorite.unavailable_tooltip')"
|
||||||
|
class="ri-error-warning-line"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__actions">
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-button circle type="default" size="small" @click.stop="handleDeleteFavorite">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
</div>
|
||||||
<i
|
</template>
|
||||||
v-if="favorite.deleted"
|
|
||||||
:title="t('view.favorite.unavailable_tooltip')"
|
|
||||||
class="ri-error-warning-line"></i>
|
|
||||||
<i
|
|
||||||
v-if="favorite.ref.releaseStatus === 'private'"
|
|
||||||
:title="t('view.favorite.private')"
|
|
||||||
class="ri-lock-line"></i>
|
|
||||||
<el-tooltip placement="left" :content="inviteOrLaunchText" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Message"
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="newInstanceSelfInvite(favorite.id)"
|
|
||||||
circle></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
type="default"
|
|
||||||
@click.stop="showFavoriteDialog('world', favorite.id)"
|
|
||||||
><i class="ri-delete-bin-line"></i
|
|
||||||
></el-button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="avatar"></div>
|
|
||||||
<div class="detail" v-once>
|
|
||||||
<span>{{ favorite.name || favorite.id }}</span>
|
|
||||||
<i
|
|
||||||
v-if="favorite.deleted"
|
|
||||||
:title="t('view.favorite.unavailable_tooltip')"
|
|
||||||
class="ri-error-warning-line"></i>
|
|
||||||
<el-button type="text" size="small" style="margin-left: 5px" @click.stop="handleDeleteFavorite"
|
|
||||||
><i class="ri-delete-bin-line"></i
|
|
||||||
></el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,21 +111,31 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
group: [Object, String],
|
group: [Object, String],
|
||||||
favorite: Object,
|
favorite: Object,
|
||||||
isLocalFavorite: { type: Boolean, default: false }
|
isLocalFavorite: { type: Boolean, default: false },
|
||||||
|
editMode: { type: Boolean, default: false },
|
||||||
|
selected: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['handle-select', 'remove-local-world-favorite', 'click']);
|
const emit = defineEmits(['toggle-select', 'remove-local-world-favorite', 'click']);
|
||||||
const { favoriteWorldGroups, editFavoritesMode } = storeToRefs(useFavoriteStore());
|
const { favoriteWorldGroups } = storeToRefs(useFavoriteStore());
|
||||||
const { showFavoriteDialog } = useFavoriteStore();
|
|
||||||
const { newInstanceSelfInvite } = useInviteStore();
|
const { newInstanceSelfInvite } = useInviteStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { canOpenInstanceInGame } = useInviteStore();
|
const { canOpenInstanceInGame } = useInviteStore();
|
||||||
|
|
||||||
const isSelected = computed({
|
const isSelected = computed({
|
||||||
get: () => props.favorite.$selected,
|
get: () => props.selected,
|
||||||
set: (value) => emit('handle-select', value)
|
set: (value) => emit('toggle-select', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cardClasses = computed(() => [
|
||||||
|
'favorites-search-card',
|
||||||
|
'favorites-search-card--world',
|
||||||
|
{
|
||||||
|
'is-selected': props.selected,
|
||||||
|
'is-edit-mode': props.editMode
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const smallThumbnail = computed(() => {
|
const smallThumbnail = computed(() => {
|
||||||
const url = props.favorite.ref.thumbnailImageUrl?.replace('256', '128');
|
const url = props.favorite.ref.thumbnailImageUrl?.replace('256', '128');
|
||||||
return url || props.favorite.ref.thumbnailImageUrl;
|
return url || props.favorite.ref.thumbnailImageUrl;
|
||||||
@@ -119,11 +160,4 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.fav-world-item {
|
|
||||||
display: inline-block;
|
|
||||||
width: 300px;
|
|
||||||
margin-right: 15px;
|
|
||||||
height: 53px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,70 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fav-world-item" @click="$emit('click')">
|
<div :class="cardClasses" @click="$emit('click')">
|
||||||
<div class="x-friend-item">
|
<template v-if="favorite.name">
|
||||||
<template v-if="favorite.name">
|
<div class="favorites-search-card__content">
|
||||||
<div class="avatar" v-once>
|
<div class="favorites-search-card__avatar" :class="{ 'is-empty': !favorite.thumbnailImageUrl }" v-once>
|
||||||
<img :src="smallThumbnail" loading="lazy" decoding="async" fetchpriority="low" />
|
<img
|
||||||
|
v-if="favorite.thumbnailImageUrl"
|
||||||
|
:src="smallThumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchpriority="low" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detail" v-once>
|
<div class="favorites-search-card__detail" v-once>
|
||||||
<span class="name">{{ props.favorite.name }}</span>
|
<div class="favorites-search-card__title">
|
||||||
<span v-if="props.favorite.occupants" class="extra">
|
<span class="name">{{ props.favorite.name }}</span>
|
||||||
{{ props.favorite.authorName }} ({{ props.favorite.occupants }})
|
</div>
|
||||||
|
<span class="extra">
|
||||||
|
{{ props.favorite.authorName }}
|
||||||
|
<template v-if="props.favorite.occupants"> ({{ props.favorite.occupants }}) </template>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="extra">{{ props.favorite.authorName }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<FavoritesMoveDropdown
|
</div>
|
||||||
v-if="editFavoritesMode"
|
<div class="favorites-search-card__actions">
|
||||||
:favoriteGroup="favoriteWorldGroups"
|
<template v-if="editMode">
|
||||||
:currentFavorite="props.favorite"
|
<div class="favorites-search-card__action-group">
|
||||||
isLocalFavorite
|
<div class="favorites-search-card__action favorites-search-card__action--full" @click.stop>
|
||||||
type="world" />
|
<FavoritesMoveDropdown
|
||||||
<template v-else>
|
:favoriteGroup="favoriteWorldGroups"
|
||||||
<el-tooltip placement="left" :content="inviteOrLaunchText" :teleported="false">
|
:currentFavorite="props.favorite"
|
||||||
<el-button
|
class="favorites-search-card__dropdown"
|
||||||
size="small"
|
isLocalFavorite
|
||||||
:icon="Message"
|
type="world" />
|
||||||
style="margin-left: 5px"
|
</div>
|
||||||
@click.stop="newInstanceSelfInvite(favorite.id)"
|
<div class="favorites-search-card__action">
|
||||||
circle></el-button>
|
<el-button
|
||||||
</el-tooltip>
|
size="small"
|
||||||
<el-button
|
circle
|
||||||
v-if="shiftHeld"
|
class="favorites-search-card__action-btn"
|
||||||
size="small"
|
:type="deleteButtonType"
|
||||||
:icon="Close"
|
@click.stop="handlePrimaryDeleteAction">
|
||||||
circle
|
<i class="ri-delete-bin-line"></i>
|
||||||
style="color: #f56c6c; margin-left: 5px"
|
</el-button>
|
||||||
@click.stop="$emit('remove-local-world-favorite', favorite.id, group)"
|
</div>
|
||||||
><i class="ri-delete-bin-line"></i
|
</div>
|
||||||
></el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
type="default"
|
|
||||||
@click.stop="showFavoriteDialog('world', favorite.id)"
|
|
||||||
><i class="ri-delete-bin-line"></i
|
|
||||||
></el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
<template v-else>
|
<div class="favorites-search-card__action">
|
||||||
<div class="avatar"></div>
|
<el-tooltip placement="top" :content="inviteOrLaunchText">
|
||||||
<div class="detail" v-once>
|
<el-button
|
||||||
<span>{{ favorite.name || favorite.id }}</span>
|
size="small"
|
||||||
<el-button
|
:icon="Message"
|
||||||
type="text"
|
class="favorites-search-card__action-btn"
|
||||||
:icon="Close"
|
@click.stop="newInstanceSelfInvite(favorite.id)"
|
||||||
size="small"
|
circle />
|
||||||
style="margin-left: 5px"
|
</el-tooltip>
|
||||||
@click.stop="handleDeleteFavorite"></el-button>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-search-card__content">
|
||||||
|
<div class="favorites-search-card__avatar is-empty"></div>
|
||||||
|
<div class="favorites-search-card__detail" v-once>
|
||||||
|
<span class="name">{{ favorite.name || favorite.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
<div class="favorites-search-card__actions">
|
||||||
|
<div class="favorites-search-card__action">
|
||||||
|
<el-button circle type="default" size="small" @click.stop="handleDeleteFavorite">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Close, Message } from '@element-plus/icons-vue';
|
import { Message } from '@element-plus/icons-vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -75,38 +87,50 @@
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
group: [Object, String],
|
group: [Object, String],
|
||||||
favorite: Object
|
favorite: Object,
|
||||||
|
editMode: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['handle-select', 'remove-local-world-favorite', 'click']);
|
const emit = defineEmits(['remove-local-world-favorite', 'click']);
|
||||||
const { favoriteWorldGroups, editFavoritesMode } = storeToRefs(useFavoriteStore());
|
const { favoriteWorldGroups } = storeToRefs(useFavoriteStore());
|
||||||
const { showFavoriteDialog } = useFavoriteStore();
|
const { showFavoriteDialog } = useFavoriteStore();
|
||||||
const { newInstanceSelfInvite } = useInviteStore();
|
const { newInstanceSelfInvite } = useInviteStore();
|
||||||
const { shiftHeld } = storeToRefs(useUiStore());
|
const { shiftHeld } = storeToRefs(useUiStore());
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { canOpenInstanceInGame } = useInviteStore();
|
const { canOpenInstanceInGame } = useInviteStore();
|
||||||
|
|
||||||
|
const cardClasses = computed(() => [
|
||||||
|
'favorites-search-card',
|
||||||
|
'favorites-search-card--world',
|
||||||
|
{
|
||||||
|
'is-edit-mode': props.editMode
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const smallThumbnail = computed(() => {
|
const smallThumbnail = computed(() => {
|
||||||
const url = props.favorite.thumbnailImageUrl?.replace('256', '128');
|
const url = props.favorite.thumbnailImageUrl?.replace('256', '128');
|
||||||
return url || props.favorite.thumbnailImageUrl;
|
return url || props.favorite.thumbnailImageUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteButtonType = computed(() => (shiftHeld.value ? 'danger' : 'default'));
|
||||||
|
|
||||||
const inviteOrLaunchText = computed(() => {
|
const inviteOrLaunchText = computed(() => {
|
||||||
return canOpenInstanceInGame
|
return canOpenInstanceInGame
|
||||||
? t('dialog.world.actions.new_instance_and_open_ingame')
|
? t('dialog.world.actions.new_instance_and_open_ingame')
|
||||||
: t('dialog.world.actions.new_instance_and_self_invite');
|
: t('dialog.world.actions.new_instance_and_self_invite');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handlePrimaryDeleteAction() {
|
||||||
|
if (shiftHeld.value) {
|
||||||
|
emit('remove-local-world-favorite', props.favorite.id, props.group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showFavoriteDialog('world', props.favorite.id);
|
||||||
|
}
|
||||||
|
|
||||||
function handleDeleteFavorite() {
|
function handleDeleteFavorite() {
|
||||||
emit('remove-local-world-favorite', props.favorite.id, props.group);
|
emit('remove-local-world-favorite', props.favorite.id, props.group);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.fav-world-item {
|
|
||||||
display: inline-block;
|
|
||||||
width: 300px;
|
|
||||||
margin-right: 15px;
|
|
||||||
height: 53px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,532 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
|
||||||
<div>
|
|
||||||
<el-button size="small" @click="showExportDialog">{{ t('view.favorite.export') }}</el-button>
|
|
||||||
<el-button size="small" style="margin-left: 5px" @click="showWorldImportDialog">{{
|
|
||||||
t('view.favorite.import')
|
|
||||||
}}</el-button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
|
||||||
<span class="name" style="margin-right: 5px; line-height: 10px">{{ t('view.favorite.sort_by') }}</span>
|
|
||||||
<el-radio-group v-model="sortFav" style="margin-right: 12px">
|
|
||||||
<el-radio :label="false">{{
|
|
||||||
t('view.settings.appearance.appearance.sort_favorite_by_name')
|
|
||||||
}}</el-radio>
|
|
||||||
<el-radio :label="true">{{
|
|
||||||
t('view.settings.appearance.appearance.sort_favorite_by_date')
|
|
||||||
}}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
<el-input
|
|
||||||
v-model="worldFavoriteSearch"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
:placeholder="t('view.favorite.worlds.search')"
|
|
||||||
style="width: 200px"
|
|
||||||
@input="searchWorldFavorites" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<div
|
|
||||||
v-for="favorite in worldFavoriteSearchResults"
|
|
||||||
:key="favorite.id"
|
|
||||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
|
||||||
@click="showWorldDialog(favorite.id)">
|
|
||||||
<div class="x-friend-item">
|
|
||||||
<template v-if="favorite.name">
|
|
||||||
<div class="avatar">
|
|
||||||
<img :src="favorite.thumbnailImageUrl" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name" v-text="favorite.name"></span>
|
|
||||||
<span v-if="favorite.occupants" class="extra"
|
|
||||||
>{{ favorite.authorName }} ({{ favorite.occupants }})</span
|
|
||||||
>
|
|
||||||
<span v-else class="extra" v-text="favorite.authorName"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="avatar"></div>
|
|
||||||
<div class="detail">
|
|
||||||
<span v-text="favorite.id"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span style="display: block; margin-top: 20px">{{ t('view.favorite.worlds.vrchat_favorites') }}</span>
|
|
||||||
<el-collapse style="border: 0">
|
|
||||||
<el-collapse-item v-for="group in favoriteWorldGroups" :key="group.name">
|
|
||||||
<template #title>
|
|
||||||
<div style="display: flex; align-items: center">
|
|
||||||
<span
|
|
||||||
style="font-weight: bold; font-size: 14px; margin-left: 10px"
|
|
||||||
v-text="group.displayName" />
|
|
||||||
<el-tag
|
|
||||||
style="margin: 1px 0 0 5px"
|
|
||||||
size="small"
|
|
||||||
:type="userFavoriteWorldsStatusForFavTab(group.visibility)"
|
|
||||||
effect="plain"
|
|
||||||
>{{ group.visibility.charAt(0).toUpperCase() + group.visibility.slice(1) }}</el-tag
|
|
||||||
>
|
|
||||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
|
||||||
>{{ group.count }}/{{ group.capacity }}</span
|
|
||||||
><el-tooltip
|
|
||||||
placement="top"
|
|
||||||
:content="t('view.favorite.visibility_tooltip')"
|
|
||||||
:teleported="false">
|
|
||||||
<el-dropdown trigger="click" size="small" style="margin-left: 10px" :persistent="false">
|
|
||||||
<el-button type="default" :icon="View" size="small" circle @click.stop />
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<template v-for="visibility in worldGroupVisibilityOptions" :key="visibility">
|
|
||||||
<el-dropdown-item
|
|
||||||
v-if="group.visibility !== visibility"
|
|
||||||
style="display: block; margin: 10px 0"
|
|
||||||
@click="changeWorldGroupVisibility(group.name, visibility)"
|
|
||||||
>{{
|
|
||||||
visibility.charAt(0).toUpperCase() + visibility.slice(1)
|
|
||||||
}}</el-dropdown-item
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Edit"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="changeFavoriteGroupName(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="clearFavoriteGroup(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<el-scrollbar height="700px" @end-reached="worldFavoritesLoadMore">
|
|
||||||
<FavoritesWorldItem
|
|
||||||
v-for="favorite in sliceWorldFavorites(group.key)"
|
|
||||||
:key="favorite.id"
|
|
||||||
:group="group"
|
|
||||||
:favorite="favorite"
|
|
||||||
@click="showWorldDialog(favorite.id)"
|
|
||||||
@handle-select="favorite.$selected = $event" />
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
style="
|
|
||||||
padding-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(144, 147, 153);
|
|
||||||
">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
<span style="display: block; margin-top: 20px">{{ t('view.favorite.worlds.local_favorites') }}</span>
|
|
||||||
<br />
|
|
||||||
<el-button size="small" @click="promptNewLocalWorldFavoriteGroup">{{
|
|
||||||
t('view.favorite.worlds.new_group')
|
|
||||||
}}</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="!refreshingLocalFavorites"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click="refreshLocalWorldFavorites"
|
|
||||||
>{{ t('view.favorite.worlds.refresh') }}</el-button
|
|
||||||
>
|
|
||||||
<el-button v-else size="small" style="margin-left: 5px" @click="cancelLocalWorldRefresh">
|
|
||||||
<el-icon style="margin-right: 5px"><Loading /></el-icon>
|
|
||||||
<span>{{ t('view.favorite.worlds.cancel_refresh') }}</span>
|
|
||||||
</el-button>
|
|
||||||
<el-collapse style="border: 0">
|
|
||||||
<el-collapse-item v-for="group in localWorldFavoriteGroups" :key="group">
|
|
||||||
<template #title>
|
|
||||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px" v-text="group" />
|
|
||||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">{{
|
|
||||||
localWorldFavGroupLength(group)
|
|
||||||
}}</span>
|
|
||||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Edit"
|
|
||||||
circle
|
|
||||||
style="margin-left: 10px"
|
|
||||||
@click.stop="promptLocalWorldFavoriteGroupRename(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')" :teleported="false">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
style="margin-left: 5px"
|
|
||||||
@click.stop="promptLocalWorldFavoriteGroupDelete(group)" />
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
<div v-if="localWorldFavorites[group]?.length" class="x-friend-list" style="margin-top: 10px">
|
|
||||||
<el-scrollbar height="700px" @end-reached="localWorldFavoritesLoadMore">
|
|
||||||
<FavoritesWorldLocalItem
|
|
||||||
v-for="favorite in sliceLocalWorldFavorites(group)"
|
|
||||||
:key="favorite.id"
|
|
||||||
:group="group"
|
|
||||||
:favorite="favorite"
|
|
||||||
@click="showWorldDialog(favorite.id)"
|
|
||||||
@remove-local-world-favorite="removeLocalWorldFavorite"
|
|
||||||
/></el-scrollbar>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
style="
|
|
||||||
padding-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgb(144, 147, 153);
|
|
||||||
">
|
|
||||||
<span>No Data</span>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Delete, Edit, Loading, View } from '@element-plus/icons-vue';
|
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { useAppearanceSettingsStore, useFavoriteStore, useWorldStore } from '../../../stores';
|
|
||||||
import { favoriteRequest, worldRequest } from '../../../api';
|
|
||||||
|
|
||||||
import FavoritesWorldItem from './FavoritesWorldItem.vue';
|
|
||||||
import FavoritesWorldLocalItem from './FavoritesWorldLocalItem.vue';
|
|
||||||
import WorldExportDialog from '../dialogs/WorldExportDialog.vue';
|
|
||||||
|
|
||||||
import * as workerTimers from 'worker-timers';
|
|
||||||
|
|
||||||
const emit = defineEmits([
|
|
||||||
'change-favorite-group-name',
|
|
||||||
'save-sort-favorites-option',
|
|
||||||
'refresh-local-world-favorite'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
|
||||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
|
||||||
const { favoriteWorlds, favoriteWorldGroups, localWorldFavorites } = storeToRefs(useFavoriteStore());
|
|
||||||
const {
|
|
||||||
showWorldImportDialog,
|
|
||||||
localWorldFavGroupLength,
|
|
||||||
deleteLocalWorldFavoriteGroup,
|
|
||||||
renameLocalWorldFavoriteGroup,
|
|
||||||
removeLocalWorldFavorite,
|
|
||||||
newLocalWorldFavoriteGroup,
|
|
||||||
handleFavoriteGroup,
|
|
||||||
localWorldFavoritesList,
|
|
||||||
localWorldFavoriteGroups
|
|
||||||
} = useFavoriteStore();
|
|
||||||
const { showWorldDialog } = useWorldStore();
|
|
||||||
|
|
||||||
const worldGroupVisibilityOptions = ref(['private', 'friends', 'public']);
|
|
||||||
const worldExportDialogVisible = ref(false);
|
|
||||||
const worldFavoriteSearch = ref('');
|
|
||||||
const worldFavoriteSearchResults = ref([]);
|
|
||||||
const sliceLocalWorldFavoritesLoadMoreNumber = ref(60);
|
|
||||||
const sliceWorldFavoritesLoadMoreNumber = ref(60);
|
|
||||||
const refreshingLocalFavorites = ref(false);
|
|
||||||
const worker = ref(null);
|
|
||||||
const refreshCancelToken = ref(null);
|
|
||||||
|
|
||||||
const sliceLocalWorldFavorites = computed(() => {
|
|
||||||
return (group) => {
|
|
||||||
return localWorldFavorites.value[group].slice(0, sliceLocalWorldFavoritesLoadMoreNumber.value);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const sliceWorldFavorites = computed(() => {
|
|
||||||
return (group) => {
|
|
||||||
const groupedByGroupKeyFavoriteWorlds = {};
|
|
||||||
|
|
||||||
favoriteWorlds.value.forEach((world) => {
|
|
||||||
if (world.groupKey) {
|
|
||||||
if (!groupedByGroupKeyFavoriteWorlds[world.groupKey]) {
|
|
||||||
groupedByGroupKeyFavoriteWorlds[world.groupKey] = [];
|
|
||||||
}
|
|
||||||
groupedByGroupKeyFavoriteWorlds[world.groupKey].push(world);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (groupedByGroupKeyFavoriteWorlds[group]) {
|
|
||||||
return groupedByGroupKeyFavoriteWorlds[group].slice(0, sliceWorldFavoritesLoadMoreNumber.value);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortFav = computed({
|
|
||||||
get() {
|
|
||||||
return sortFavorites.value;
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
setSortFavorites();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function localWorldFavoritesLoadMore(direction) {
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
sliceLocalWorldFavoritesLoadMoreNumber.value += 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function worldFavoritesLoadMore(direction) {
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
sliceWorldFavoritesLoadMoreNumber.value += 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showExportDialog() {
|
|
||||||
worldExportDialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function userFavoriteWorldsStatusForFavTab(visibility) {
|
|
||||||
if (visibility === 'public') {
|
|
||||||
return 'primary';
|
|
||||||
}
|
|
||||||
if (visibility === 'friends') {
|
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
return 'info';
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeWorldGroupVisibility(name, visibility) {
|
|
||||||
const params = {
|
|
||||||
type: 'world',
|
|
||||||
group: name,
|
|
||||||
visibility
|
|
||||||
};
|
|
||||||
favoriteRequest.saveFavoriteGroup(params).then((args) => {
|
|
||||||
handleFavoriteGroup({
|
|
||||||
json: args.json,
|
|
||||||
params: {
|
|
||||||
favoriteGroupId: args.json.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ElMessage({
|
|
||||||
message: 'Group visibility changed',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
return args;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptNewLocalWorldFavoriteGroup() {
|
|
||||||
ElMessageBox.prompt(
|
|
||||||
t('prompt.new_local_favorite_group.description'),
|
|
||||||
t('prompt.new_local_favorite_group.header'),
|
|
||||||
{
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: t('prompt.new_local_favorite_group.ok'),
|
|
||||||
cancelButtonText: t('prompt.new_local_favorite_group.cancel'),
|
|
||||||
inputPattern: /\S+/,
|
|
||||||
inputErrorMessage: t('prompt.new_local_favorite_group.input_error')
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ value }) => {
|
|
||||||
if (value) {
|
|
||||||
newLocalWorldFavoriteGroup(value);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptLocalWorldFavoriteGroupRename(group) {
|
|
||||||
ElMessageBox.prompt(
|
|
||||||
t('prompt.local_favorite_group_rename.description'),
|
|
||||||
t('prompt.local_favorite_group_rename.header'),
|
|
||||||
{
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: t('prompt.local_favorite_group_rename.save'),
|
|
||||||
cancelButtonText: t('prompt.local_favorite_group_rename.cancel'),
|
|
||||||
inputPattern: /\S+/,
|
|
||||||
inputErrorMessage: t('prompt.local_favorite_group_rename.input_error'),
|
|
||||||
inputValue: group
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ value }) => {
|
|
||||||
if (value) {
|
|
||||||
renameLocalWorldFavoriteGroup(value, group);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptLocalWorldFavoriteGroupDelete(group) {
|
|
||||||
ElMessageBox.confirm(`Delete Group? ${group}`, 'Confirm', {
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
deleteLocalWorldFavoriteGroup(group);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFavoriteGroup(ctx) {
|
|
||||||
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
|
|
||||||
confirmButtonText: 'Confirm',
|
|
||||||
cancelButtonText: 'Cancel',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
.then((action) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
favoriteRequest.clearFavoriteGroup({
|
|
||||||
type: ctx.type,
|
|
||||||
group: ctx.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchWorldFavorites(worldFavoriteSearch) {
|
|
||||||
let ref = null;
|
|
||||||
const search = worldFavoriteSearch.toLowerCase();
|
|
||||||
if (search.length < 3) {
|
|
||||||
worldFavoriteSearchResults.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0; i < localWorldFavoriteGroups.length; ++i) {
|
|
||||||
const group = localWorldFavoriteGroups[i];
|
|
||||||
if (!localWorldFavorites.value[group]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let j = 0; j < localWorldFavorites.value[group].length; ++j) {
|
|
||||||
ref = localWorldFavorites.value[group][j];
|
|
||||||
if (
|
|
||||||
!ref ||
|
|
||||||
typeof ref.id === 'undefined' ||
|
|
||||||
typeof ref.name === 'undefined' ||
|
|
||||||
typeof ref.authorName === 'undefined'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
|
||||||
if (!results.some((r) => r.id === ref.id)) {
|
|
||||||
results.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < favoriteWorlds.value.length; ++i) {
|
|
||||||
ref = favoriteWorlds.value[i].ref;
|
|
||||||
if (
|
|
||||||
!ref ||
|
|
||||||
typeof ref.id === 'undefined' ||
|
|
||||||
typeof ref.name === 'undefined' ||
|
|
||||||
typeof ref.authorName === 'undefined'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
|
||||||
if (!results.some((r) => r.id === ref.id)) {
|
|
||||||
results.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
worldFavoriteSearchResults.value = results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeFavoriteGroupName(group) {
|
|
||||||
emit('change-favorite-group-name', group);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshLocalWorldFavorites() {
|
|
||||||
if (refreshingLocalFavorites.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = true;
|
|
||||||
const token = {
|
|
||||||
cancelled: false,
|
|
||||||
resolve: null
|
|
||||||
};
|
|
||||||
refreshCancelToken.value = token;
|
|
||||||
try {
|
|
||||||
for (const worldId of localWorldFavoritesList) {
|
|
||||||
if (token.cancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await worldRequest.getWorld({
|
|
||||||
worldId
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
if (token.cancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
token.resolve = resolve;
|
|
||||||
worker.value = workerTimers.setTimeout(() => {
|
|
||||||
worker.value = null;
|
|
||||||
resolve();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (worker.value) {
|
|
||||||
workerTimers.clearTimeout(worker.value);
|
|
||||||
worker.value = null;
|
|
||||||
}
|
|
||||||
if (refreshCancelToken.value === token) {
|
|
||||||
refreshCancelToken.value = null;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelLocalWorldRefresh() {
|
|
||||||
if (!refreshingLocalFavorites.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (refreshCancelToken.value) {
|
|
||||||
refreshCancelToken.value.cancelled = true;
|
|
||||||
if (typeof refreshCancelToken.value.resolve === 'function') {
|
|
||||||
refreshCancelToken.value.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (worker.value) {
|
|
||||||
workerTimers.clearTimeout(worker.value);
|
|
||||||
worker.value = null;
|
|
||||||
}
|
|
||||||
refreshingLocalFavorites.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
cancelLocalWorldRefresh();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user