diff --git a/src/components/dialogs/ChooseFavoriteGroupDialog.vue b/src/components/dialogs/ChooseFavoriteGroupDialog.vue
index 20866e39..41f7dc38 100644
--- a/src/components/dialogs/ChooseFavoriteGroupDialog.vue
+++ b/src/components/dialogs/ChooseFavoriteGroupDialog.vue
@@ -30,6 +30,27 @@
+
+ {{ t('dialog.favorite.local_favorites') }}
+
+
+
+
+
{{ t('dialog.favorite.local_favorites') }}
@@ -99,7 +120,8 @@
favoriteWorldGroups,
favoriteDialog,
localWorldFavoriteGroups,
- localAvatarFavoriteGroups
+ localAvatarFavoriteGroups,
+ localFriendFavoriteGroups
} = storeToRefs(favoriteStore);
const {
localWorldFavGroupLength,
@@ -110,7 +132,11 @@
localAvatarFavGroupLength,
removeLocalAvatarFavorite,
removeLocalWorldFavorite,
- deleteFavoriteNoConfirm
+ deleteFavoriteNoConfirm,
+ localFriendFavGroupLength,
+ addLocalFriendFavorite,
+ hasLocalFriendFavorite,
+ removeLocalFriendFavorite
} = favoriteStore;
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
diff --git a/src/service/database.js b/src/service/database.js
index 0d7cd815..4e6ce59d 100644
--- a/src/service/database.js
+++ b/src/service/database.js
@@ -1,5 +1,6 @@
import { avatarFavorites } from './database/avatarFavorites.js';
import { feed } from './database/feed.js';
+import { friendFavorites } from './database/friendFavorites.js';
import { friendLogCurrent } from './database/friendLogCurrent.js';
import { friendLogHistory } from './database/friendLogHistory.js';
import { gameLog } from './database/gameLog.js';
@@ -30,6 +31,7 @@ const database = {
...friendLogCurrent,
...memos,
...avatarFavorites,
+ ...friendFavorites,
...worldFavorites,
...tableAlter,
...tableFixes,
@@ -126,6 +128,9 @@ const database = {
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS favorite_avatar (id INTEGER PRIMARY KEY, created_at TEXT, avatar_id TEXT, group_name TEXT)`
);
+ await sqliteService.executeNonQuery(
+ `CREATE TABLE IF NOT EXISTS favorite_friend (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, group_name TEXT)`
+ );
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
diff --git a/src/service/database/friendFavorites.js b/src/service/database/friendFavorites.js
new file mode 100644
index 00000000..daa49037
--- /dev/null
+++ b/src/service/database/friendFavorites.js
@@ -0,0 +1,58 @@
+import sqliteService from '../sqlite.js';
+
+const friendFavorites = {
+ addFriendToLocalFavorites(userId, groupName) {
+ sqliteService.executeNonQuery(
+ 'INSERT OR REPLACE INTO favorite_friend (user_id, group_name, created_at) VALUES (@user_id, @group_name, @created_at)',
+ {
+ '@user_id': userId,
+ '@group_name': groupName,
+ '@created_at': new Date().toJSON()
+ }
+ );
+ },
+
+ removeFriendFromLocalFavorites(userId, groupName) {
+ sqliteService.executeNonQuery(
+ `DELETE FROM favorite_friend WHERE user_id = @user_id AND group_name = @group_name`,
+ {
+ '@user_id': userId,
+ '@group_name': groupName
+ }
+ );
+ },
+
+ renameFriendFavoriteGroup(newGroupName, groupName) {
+ sqliteService.executeNonQuery(
+ `UPDATE favorite_friend SET group_name = @new_group_name WHERE group_name = @group_name`,
+ {
+ '@new_group_name': newGroupName,
+ '@group_name': groupName
+ }
+ );
+ },
+
+ deleteFriendFavoriteGroup(groupName) {
+ sqliteService.executeNonQuery(
+ `DELETE FROM favorite_friend WHERE group_name = @group_name`,
+ {
+ '@group_name': groupName
+ }
+ );
+ },
+
+ async getFriendFavorites() {
+ const data = [];
+ await sqliteService.execute((dbRow) => {
+ const row = {
+ created_at: dbRow[1],
+ userId: dbRow[2],
+ groupName: dbRow[3]
+ };
+ data.push(row);
+ }, 'SELECT * FROM favorite_friend');
+ return data;
+ }
+};
+
+export { friendFavorites };
diff --git a/src/stores/favorite.js b/src/stores/favorite.js
index 3a925961..1299981b 100644
--- a/src/stores/favorite.js
+++ b/src/stores/favorite.js
@@ -81,6 +81,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
const localAvatarFavorites = reactive({});
+ const localFriendFavorites = reactive({});
+
const selectedFavoriteFriends = ref([]);
const selectedFavoriteWorlds = ref([]);
const selectedFavoriteAvatars = ref([]);
@@ -189,6 +191,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return favoriteGroup.length;
});
+ const localFriendFavoriteGroups = computed(() =>
+ Object.keys(localFriendFavorites).sort()
+ );
+
+ const localFriendFavGroupLength = computed(() => (group) => {
+ const favoriteGroup = localFriendFavorites[group];
+ if (!favoriteGroup) {
+ return 0;
+ }
+ return favoriteGroup.length;
+ });
+
function syncFavoriteSelection(list, selectionRef) {
if (!Array.isArray(list)) {
selectionRef.value = [];
@@ -1514,6 +1528,157 @@ export const useFavoriteStore = defineStore('Favorite', () => {
sortLocalWorldFavorites();
}
+ /**
+ * @param {string} userId
+ * @param {string} group
+ */
+ function addLocalFriendFavorite(userId, group) {
+ if (hasLocalFriendFavorite(userId, group)) {
+ return;
+ }
+ if (!localFriendFavorites[group]) {
+ localFriendFavorites[group] = [];
+ }
+ localFriendFavorites[group].unshift(userId);
+ database.addFriendToLocalFavorites(userId, group);
+ if (
+ favoriteDialog.value.visible &&
+ favoriteDialog.value.objectId === userId
+ ) {
+ updateFavoriteDialog(userId);
+ }
+ if (
+ generalSettingsStore.localFavoriteFriendsGroups.includes(
+ `local:${group}`
+ )
+ ) {
+ friendStore.updateLocalFavoriteFriends();
+ }
+ }
+
+ /**
+ * @param {string} userId
+ * @param {string} group
+ * @returns {boolean}
+ */
+ function hasLocalFriendFavorite(userId, group) {
+ const favoriteGroup = localFriendFavorites[group];
+ if (!favoriteGroup) {
+ return false;
+ }
+ return favoriteGroup.includes(userId);
+ }
+
+ /**
+ * @param {string} userId
+ * @param {string} group
+ */
+ function removeLocalFriendFavorite(userId, group) {
+ const favoriteGroup = localFriendFavorites[group];
+ if (favoriteGroup) {
+ const idx = favoriteGroup.indexOf(userId);
+ if (idx !== -1) {
+ favoriteGroup.splice(idx, 1);
+ }
+ }
+ database.removeFriendFromLocalFavorites(userId, group);
+ if (
+ favoriteDialog.value.visible &&
+ favoriteDialog.value.objectId === userId
+ ) {
+ updateFavoriteDialog(userId);
+ }
+ if (
+ generalSettingsStore.localFavoriteFriendsGroups.includes(
+ `local:${group}`
+ )
+ ) {
+ friendStore.updateLocalFavoriteFriends();
+ }
+ }
+
+ /**
+ * @param {string} group
+ */
+ function deleteLocalFriendFavoriteGroup(group) {
+ delete localFriendFavorites[group];
+ database.deleteFriendFavoriteGroup(group);
+ if (
+ generalSettingsStore.localFavoriteFriendsGroups.includes(
+ `local:${group}`
+ )
+ ) {
+ friendStore.updateLocalFavoriteFriends();
+ }
+ }
+
+ /**
+ * @param {string} newName
+ * @param {string} group
+ */
+ function renameLocalFriendFavoriteGroup(newName, group) {
+ if (localFriendFavoriteGroups.value.includes(newName)) {
+ toast.error(
+ t('prompt.local_favorite_group_rename.message.error', {
+ name: newName
+ })
+ );
+ return;
+ }
+ localFriendFavorites[newName] = localFriendFavorites[group];
+ delete localFriendFavorites[group];
+ database.renameFriendFavoriteGroup(newName, group);
+ const oldKey = `local:${group}`;
+ const idx =
+ generalSettingsStore.localFavoriteFriendsGroups.indexOf(oldKey);
+ if (idx !== -1) {
+ const updated = [
+ ...generalSettingsStore.localFavoriteFriendsGroups
+ ];
+ updated[idx] = `local:${newName}`;
+ generalSettingsStore.setLocalFavoriteFriendsGroups(updated);
+ }
+ }
+
+ /**
+ * @param {string} group
+ */
+ function newLocalFriendFavoriteGroup(group) {
+ if (localFriendFavoriteGroups.value.includes(group)) {
+ toast.error(
+ t('prompt.new_local_favorite_group.message.error', {
+ name: group
+ })
+ );
+ return;
+ }
+ if (!localFriendFavorites[group]) {
+ localFriendFavorites[group] = [];
+ }
+ }
+
+ /**
+ * @returns {Promise}
+ */
+ async function getLocalFriendFavorites() {
+ const localFavorites = Object.create(null);
+
+ const favorites = await database.getFriendFavorites();
+ for (let i = 0; i < favorites.length; ++i) {
+ const favorite = favorites[i];
+ if (!localFavorites[favorite.groupName]) {
+ localFavorites[favorite.groupName] = [];
+ }
+ localFavorites[favorite.groupName].unshift(favorite.userId);
+ }
+
+ if (Object.keys(localFavorites).length === 0) {
+ localFavorites.Favorites = [];
+ }
+
+ replaceReactiveObject(localFriendFavorites, localFavorites);
+ }
+
/**
*
* @param {string} objectId
@@ -1545,6 +1710,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
async function saveSortFavoritesOption() {
getLocalWorldFavorites();
+ getLocalFriendFavorites();
appearanceSettingsStore.setSortFavorites();
}
@@ -1552,6 +1718,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
refreshFavorites();
getLocalWorldFavorites();
getLocalAvatarFavorites();
+ getLocalFriendFavorites();
}
function compareByFavoriteSortOrder(a, b) {
@@ -1588,6 +1755,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
localWorldFavoritesList,
localWorldFavoriteGroups,
+ localFriendFavorites,
+ localFriendFavoriteGroups,
+
+ localFriendFavGroupLength,
groupedByGroupKeyFavoriteFriends,
selectedFavoriteFriends,
selectedFavoriteWorlds,
@@ -1632,6 +1803,13 @@ export const useFavoriteStore = defineStore('Favorite', () => {
getCachedFavoritesByObjectId,
checkInvalidLocalAvatars,
removeInvalidLocalAvatars,
- getCachedFavoriteGroupsByTypeName
+ getCachedFavoriteGroupsByTypeName,
+ addLocalFriendFavorite,
+ hasLocalFriendFavorite,
+ removeLocalFriendFavorite,
+ deleteLocalFriendFavoriteGroup,
+ renameLocalFriendFavoriteGroup,
+ newLocalFriendFavoriteGroup,
+ getLocalFriendFavorites
};
});
diff --git a/src/stores/friend.js b/src/stores/friend.js
index 526ff130..83acbc87 100644
--- a/src/stores/friend.js
+++ b/src/stores/friend.js
@@ -317,6 +317,17 @@ export const useFriendStore = defineStore('Friend', () => {
localFavoriteFriends.add(ref.favoriteId);
}
}
+ for (const selectedKey of generalSettingsStore.localFavoriteFriendsGroups) {
+ if (selectedKey.startsWith('local:')) {
+ const groupName = selectedKey.slice(6);
+ const userIds = favoriteStore.localFriendFavorites[groupName];
+ if (userIds) {
+ for (let i = 0; i < userIds.length; ++i) {
+ localFavoriteFriends.add(userIds[i]);
+ }
+ }
+ }
+ }
updateSidebarFavorites();
}
diff --git a/src/views/Favorites/FavoritesFriend.vue b/src/views/Favorites/FavoritesFriend.vue
index c28d4bec..d1e11ded 100644
--- a/src/views/Favorites/FavoritesFriend.vue
+++ b/src/views/Favorites/FavoritesFriend.vue
@@ -183,6 +183,86 @@
+
+
+
+
+
+
+
{{ group }}
+
+
{{
+ localFriendFavGroupLength(group)
+ }}
+
+
+
+
+
+
+
+ {{ t('view.favorite.rename_tooltip') }}
+
+
+ {{ t('view.favorite.delete_tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('view.favorite.worlds.new_group') }}
+
+
+
+
@@ -197,11 +277,17 @@
{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}
+
+ {{ activeLocalGroupName }}
+ {{ activeLocalGroupCount }}
+
No Group Selected
{{ t('view.favorite.edit_mode') }}
-
+
@@ -259,6 +345,28 @@
+
+
+
No Group Selected
@@ -309,11 +417,11 @@